-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathLuceneDev6001_StringComparisonCodeFixProvider.cs
More file actions
234 lines (206 loc) · 11.6 KB
/
LuceneDev6001_StringComparisonCodeFixProvider.cs
File metadata and controls
234 lines (206 loc) · 11.6 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
/*
* 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_StringComparisonCodeFixProvider)), Shared]
public sealed class LuceneDev6001_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.LuceneDev6001_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;
//Double check to Skip char literals and single-character string literals when safe ---
var firstArgExpr = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression;
if (firstArgExpr is LiteralExpressionSyntax lit)
{
if (lit.IsKind(SyntaxKind.CharacterLiteralExpression))
return; // already char overload; no diagnostic
if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1)
{
// Check if a StringComparison argument is present
bool hasStringComparisonArgForLiteral = 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 (!hasStringComparisonArgForLiteral)
{
// safe to convert to char (6003), so skip 6001 reporting
return;
}
// else: has StringComparison -> do not skip; let codefix handle it
}
}
// --- 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.LuceneDev6001_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;
}
}
}