forked from apache/lucenenet-codeanalysis-dev
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLuceneDev6001_6002_StringComparisonCodeFixProvider.cs
More file actions
248 lines (219 loc) · 12.1 KB
/
LuceneDev6001_6002_StringComparisonCodeFixProvider.cs
File metadata and controls
248 lines (219 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file for additional information.
* The ASF licenses this file under the Apache License, Version 2.0.
*/
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Lucene.Net.CodeAnalysis.Dev.Utility;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp;
namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6001_6002_StringComparisonCodeFixProvider)), Shared]
public sealed class LuceneDev6001_6002_StringComparisonCodeFixProvider : CodeFixProvider
{
private const string Ordinal = "Ordinal";
private const string OrdinalIgnoreCase = "OrdinalIgnoreCase";
private const string TitleOrdinal = "Use StringComparison.Ordinal";
private const string TitleOrdinalIgnoreCase = "Use StringComparison.OrdinalIgnoreCase";
public override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(
Descriptors.LuceneDev6001_MissingStringComparison.Id,
Descriptors.LuceneDev6002_InvalidStringComparison.Id);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
/// <summary>
/// Registers available code fixes for all diagnostics in the context.
/// </summary>
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root == null) return;
// Iterate over ALL diagnostics in the context to ensure all issues are offered a fix.
foreach (var diagnostic in context.Diagnostics)
{
var invocation = root.FindToken(diagnostic.Location.SourceSpan.Start)
.Parent?
.AncestorsAndSelf()
.OfType<InvocationExpressionSyntax>()
.FirstOrDefault();
if (invocation == null) continue;
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (semanticModel == null) continue;
// Skip char literals and single-character string literals when safe (LuceneDev6005 handles conversion).
var firstArgExpr = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression;
if (firstArgExpr is LiteralExpressionSyntax lit)
{
if (lit.IsKind(SyntaxKind.CharacterLiteralExpression))
continue;
if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1)
{
bool hasStringComparisonArg = invocation.ArgumentList.Arguments.Any(arg =>
(semanticModel.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t &&
t.ToDisplayString() == "System.StringComparison")
|| (semanticModel.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f &&
f.ContainingType?.ToDisplayString() == "System.StringComparison"));
if (!hasStringComparisonArg)
continue;
}
}
// --- Fix Registration Logic ---
if (diagnostic.Id == Descriptors.LuceneDev6001_MissingStringComparison.Id)
{
// Case 1: Argument is missing. Only offer Ordinal as the safe, conservative default.
RegisterFix(context, invocation, Ordinal, TitleOrdinal, diagnostic);
}
else if (diagnostic.Id == Descriptors.LuceneDev6002_InvalidStringComparison.Id)
{
// Case 2: Invalid argument is present. Determine the best replacement.
if (TryDetermineReplacement(invocation, semanticModel, out string? targetComparison))
{
var title = (targetComparison!) == Ordinal ? TitleOrdinal : TitleOrdinalIgnoreCase;
RegisterFix(context, invocation, targetComparison!, title, diagnostic);
}
// If TryDetermineReplacement returns false, the argument is an invalid non-constant
// expression (e.g., a variable). We skip the fix to avoid arbitrary changes.
}
}
}
private static void RegisterFix(
CodeFixContext context,
InvocationExpressionSyntax invocation,
string comparisonMember,
string title,
Diagnostic diagnostic)
{
context.RegisterCodeFix(CodeAction.Create(
title: title,
createChangedDocument: c => FixInvocationAsync(context.Document, invocation, comparisonMember, c),
equivalenceKey: title),
diagnostic);
}
/// <summary>
/// Determines the appropriate ordinal replacement (Ordinal or OrdinalIgnoreCase)
/// for an existing culture-sensitive StringComparison argument.
/// Only operates on constant argument values.
/// </summary>
/// <returns>True if a valid replacement was determined, false otherwise (e.g., if argument is non-constant).</returns>
private static bool TryDetermineReplacement(InvocationExpressionSyntax invocation, SemanticModel semanticModel, out string? targetComparison)
{
targetComparison = null;
var stringComparisonType = semanticModel.Compilation.GetTypeByMetadataName("System.StringComparison");
var existingArg = invocation.ArgumentList.Arguments.FirstOrDefault(arg =>
SymbolEqualityComparer.Default.Equals(
semanticModel.GetTypeInfo(arg.Expression).Type, stringComparisonType));
if (existingArg != null)
{
var constVal = semanticModel.GetConstantValue(existingArg.Expression);
if (constVal.HasValue && constVal.Value is int intVal)
{
// Map original comparison to corresponding ordinal variant for constant values
switch ((System.StringComparison)intVal)
{
case System.StringComparison.CurrentCulture:
case System.StringComparison.InvariantCulture:
targetComparison = Ordinal;
return true;
case System.StringComparison.CurrentCultureIgnoreCase:
case System.StringComparison.InvariantCultureIgnoreCase:
targetComparison = OrdinalIgnoreCase;
return true;
case System.StringComparison.Ordinal:
case System.StringComparison.OrdinalIgnoreCase:
return false; // Already correct
}
}
// Argument exists, but is not a constant value (e.g., a variable). We skip the fix.
return false;
}
// Should not be called for missing arguments by the caller.
return false;
}
/// <summary>
/// Creates the new document by either replacing an existing StringComparison argument
/// or adding a new one, based on the fix action.
/// </summary>
private static async Task<Document> FixInvocationAsync(Document document, InvocationExpressionSyntax invocation, string comparisonMember, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root == null) return document;
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var stringComparisonType = semanticModel?.Compilation.GetTypeByMetadataName("System.StringComparison");
// 1. Create the new StringComparison argument expression
var stringComparisonExpr = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("StringComparison"),
SyntaxFactory.IdentifierName(comparisonMember));
var newArg = SyntaxFactory.Argument(stringComparisonExpr);
// 2. Find existing argument for replacement/addition check
var existingArg = invocation.ArgumentList.Arguments.FirstOrDefault(arg =>
semanticModel != null &&
SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arg.Expression).Type, stringComparisonType));
// 3. Perform the syntax replacement/addition
InvocationExpressionSyntax newInvocation;
if (existingArg != null)
{
// Argument exists (Replacement case: InvalidComparison)
// Preserve leading/trailing trivia (spaces/comma) from the expression being replaced
var newExprWithTrivia = stringComparisonExpr
.WithLeadingTrivia(existingArg.Expression.GetLeadingTrivia())
.WithTrailingTrivia(existingArg.Expression.GetTrailingTrivia());
var newArgWithTrivia = existingArg.WithExpression(newExprWithTrivia);
newInvocation = invocation.ReplaceNode(existingArg, newArgWithTrivia);
}
else
{
// Argument is missing (Addition case: MissingComparison)
// Use AddArguments, relying on Roslyn to correctly handle comma/spacing trivia.
newInvocation = invocation.WithArgumentList(
invocation.ArgumentList.AddArguments(newArg)
);
}
// 4. Update the document root (Ensure using statement is present and replace invocation)
var newRoot = EnsureSystemUsing(root).ReplaceNode(invocation, newInvocation);
return document.WithSyntaxRoot(newRoot);
}
/// <summary>
/// Ensures a 'using System;' directive is present in the document.
/// </summary>
private static SyntaxNode EnsureSystemUsing(SyntaxNode root)
{
if (root is CompilationUnitSyntax compilationUnit)
{
var hasSystemUsing = compilationUnit.Usings.Any(u =>
u.Name is IdentifierNameSyntax id && id.Identifier.ValueText == "System");
if (!hasSystemUsing)
{
var systemUsing = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("System"))
.WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed);
return compilationUnit.AddUsings(systemUsing);
}
}
return root;
}
}
}