Skip to content

Commit 9d78580

Browse files
committed
feat(analyzers): smart Span fixes, char optimizations,NRT updatesand add Documentation as per suggestion
1 parent df54546 commit 9d78580

13 files changed

Lines changed: 701 additions & 170 deletions

Directory.Packages.props

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
<RoslynAnalyzerPackageVersion>4.14.0</RoslynAnalyzerPackageVersion>
2828
</PropertyGroup>
2929
<ItemGroup Label="NuGet Package Reference Versions">
30-
<PackageVersion Include="J2N" Version="2.1.0" />
3130
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.8.118" />
3231
<PackageVersion Include="NUnit" Version="4.4.0" />
3332
<PackageVersion Include="NUnit3TestAdapter" Version="5.2.0" />

src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs

Lines changed: 155 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,9 @@
11
/*
22
* Licensed to the Apache Software Foundation (ASF) under one
3-
* or more contributor license agreements. See the NOTICE file
4-
* distributed with this work for additional information
5-
* regarding copyright ownership. The ASF licenses this file
6-
* to you under the Apache License, Version 2.0 (the
7-
* "License"); you may not use this file except in compliance
8-
* with the License. You may obtain a copy of the License at
9-
*
10-
* http://www.apache.org/licenses/LICENSE-2.0
11-
*
12-
* Unless required by applicable law or agreed to in writing, software
13-
* distributed under the License is distributed on an "AS IS" BASIS,
14-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15-
* See the License for the specific language governing permissions and
16-
* limitations under the License.
3+
* or more contributor license agreements. See the NOTICE file for additional information.
4+
* The ASF licenses this file under the Apache License, Version 2.0.
175
*/
6+
187
using System.Collections.Immutable;
198
using System.Composition;
209
using System.Linq;
@@ -24,14 +13,16 @@
2413
using Microsoft.CodeAnalysis;
2514
using Microsoft.CodeAnalysis.CodeActions;
2615
using Microsoft.CodeAnalysis.CodeFixes;
27-
using Microsoft.CodeAnalysis.CSharp;
2816
using Microsoft.CodeAnalysis.CSharp.Syntax;
17+
using Microsoft.CodeAnalysis.CSharp;
2918

3019
namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx
3120
{
3221
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6001_StringComparisonCodeFixProvider)), Shared]
3322
public sealed class LuceneDev6001_StringComparisonCodeFixProvider : CodeFixProvider
3423
{
24+
private const string Ordinal = "Ordinal";
25+
private const string OrdinalIgnoreCase = "OrdinalIgnoreCase";
3526
private const string TitleOrdinal = "Use StringComparison.Ordinal";
3627
private const string TitleOrdinalIgnoreCase = "Use StringComparison.OrdinalIgnoreCase";
3728

@@ -42,67 +33,186 @@ public sealed class LuceneDev6001_StringComparisonCodeFixProvider : CodeFixProvi
4233

4334
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
4435

36+
/// <summary>
37+
/// Registers available code fixes for all diagnostics in the context.
38+
/// </summary>
4539
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
4640
{
4741
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
4842
if (root == null) return;
4943

50-
var diagnostic = context.Diagnostics.First();
51-
var diagnosticSpan = diagnostic.Location.SourceSpan;
44+
// Iterate over ALL diagnostics in the context to ensure all issues are offered a fix.
45+
foreach (var diagnostic in context.Diagnostics)
46+
{
47+
var invocation = root.FindToken(diagnostic.Location.SourceSpan.Start)
48+
.Parent?
49+
.AncestorsAndSelf()
50+
.OfType<InvocationExpressionSyntax>()
51+
.FirstOrDefault();
52+
if (invocation == null) continue;
53+
54+
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
55+
if (semanticModel == null) continue;
56+
57+
//Double check to Skip char literals and single-character string literals when safe ---
58+
var firstArgExpr = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression;
59+
if (firstArgExpr is LiteralExpressionSyntax lit)
60+
{
61+
if (lit.IsKind(SyntaxKind.CharacterLiteralExpression))
62+
return; // already char overload; no diagnostic
63+
64+
if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1)
65+
{
66+
// Check if a StringComparison argument is present
67+
bool hasStringComparisonArgForLiteral = invocation.ArgumentList.Arguments.Any(arg =>
68+
semanticModel.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t &&
69+
t.ToDisplayString() == "System.StringComparison"
70+
|| (semanticModel.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f &&
71+
f.ContainingType?.ToDisplayString() == "System.StringComparison"));
72+
73+
if (!hasStringComparisonArgForLiteral)
74+
{
75+
// safe to convert to char (6003), so skip 6001 reporting
76+
return;
77+
}
78+
// else: has StringComparison -> do not skip; let codefix handle it
79+
}
80+
}
5281

53-
var invocation = root.FindToken(diagnosticSpan.Start)
54-
.Parent?
55-
.AncestorsAndSelf()
56-
.OfType<InvocationExpressionSyntax>()
57-
.FirstOrDefault();
58-
if (invocation == null) return;
82+
// --- Fix Registration Logic ---
5983

60-
// Offer both Ordinal and OrdinalIgnoreCase fixes
61-
context.RegisterCodeFix(CodeAction.Create(
62-
title: TitleOrdinal,
63-
createChangedDocument: c => FixInvocationAsync(context.Document, invocation, "Ordinal", c),
64-
equivalenceKey: TitleOrdinal),
65-
diagnostic);
84+
if (diagnostic.Id == Descriptors.LuceneDev6001_MissingStringComparison.Id)
85+
{
86+
// Case 1: Argument is missing. Only offer Ordinal as the safe, conservative default.
87+
RegisterFix(context, invocation, Ordinal, TitleOrdinal, diagnostic);
88+
}
89+
else if (diagnostic.Id == Descriptors.LuceneDev6001_InvalidStringComparison.Id)
90+
{
91+
// Case 2: Invalid argument is present. Determine the best replacement.
92+
if (TryDetermineReplacement(invocation, semanticModel, out string? targetComparison))
93+
{
94+
var title = (targetComparison!) == Ordinal ? TitleOrdinal : TitleOrdinalIgnoreCase;
95+
RegisterFix(context, invocation, targetComparison!, title, diagnostic);
96+
}
97+
// If TryDetermineReplacement returns false, the argument is an invalid non-constant
98+
// expression (e.g., a variable). We skip the fix to avoid arbitrary changes.
99+
}
100+
}
101+
}
66102

103+
private static void RegisterFix(
104+
CodeFixContext context,
105+
InvocationExpressionSyntax invocation,
106+
string comparisonMember,
107+
string title,
108+
Diagnostic diagnostic)
109+
{
67110
context.RegisterCodeFix(CodeAction.Create(
68-
title: TitleOrdinalIgnoreCase,
69-
createChangedDocument: c => FixInvocationAsync(context.Document, invocation, "OrdinalIgnoreCase", c),
70-
equivalenceKey: TitleOrdinalIgnoreCase),
111+
title: title,
112+
createChangedDocument: c => FixInvocationAsync(context.Document, invocation, comparisonMember, c),
113+
equivalenceKey: title),
71114
diagnostic);
72115
}
73116

117+
/// <summary>
118+
/// Determines the appropriate ordinal replacement (Ordinal or OrdinalIgnoreCase)
119+
/// for an existing culture-sensitive StringComparison argument.
120+
/// Only operates on constant argument values.
121+
/// </summary>
122+
/// <returns>True if a valid replacement was determined, false otherwise (e.g., if argument is non-constant).</returns>
123+
private static bool TryDetermineReplacement(InvocationExpressionSyntax invocation, SemanticModel semanticModel, out string? targetComparison)
124+
{
125+
targetComparison = null;
126+
var stringComparisonType = semanticModel.Compilation.GetTypeByMetadataName("System.StringComparison");
127+
var existingArg = invocation.ArgumentList.Arguments.FirstOrDefault(arg =>
128+
SymbolEqualityComparer.Default.Equals(
129+
semanticModel.GetTypeInfo(arg.Expression).Type, stringComparisonType));
130+
131+
if (existingArg != null)
132+
{
133+
var constVal = semanticModel.GetConstantValue(existingArg.Expression);
134+
if (constVal.HasValue && constVal.Value is int intVal)
135+
{
136+
// Map original comparison to corresponding ordinal variant for constant values
137+
switch ((System.StringComparison)intVal)
138+
{
139+
case System.StringComparison.CurrentCulture:
140+
case System.StringComparison.InvariantCulture:
141+
targetComparison = Ordinal;
142+
return true;
143+
case System.StringComparison.CurrentCultureIgnoreCase:
144+
case System.StringComparison.InvariantCultureIgnoreCase:
145+
targetComparison = OrdinalIgnoreCase;
146+
return true;
147+
case System.StringComparison.Ordinal:
148+
case System.StringComparison.OrdinalIgnoreCase:
149+
return false; // Already correct
150+
}
151+
}
152+
// Argument exists, but is not a constant value (e.g., a variable). We skip the fix.
153+
return false;
154+
}
155+
156+
// Should not be called for missing arguments by the caller.
157+
return false;
158+
}
159+
160+
/// <summary>
161+
/// Creates the new document by either replacing an existing StringComparison argument
162+
/// or adding a new one, based on the fix action.
163+
/// </summary>
74164
private static async Task<Document> FixInvocationAsync(Document document, InvocationExpressionSyntax invocation, string comparisonMember, CancellationToken cancellationToken)
75165
{
76166
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
77167
if (root == null) return document;
78168

79-
// Create the StringComparison expression
169+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
170+
var stringComparisonType = semanticModel?.Compilation.GetTypeByMetadataName("System.StringComparison");
171+
172+
// 1. Create the new StringComparison argument expression
80173
var stringComparisonExpr = SyntaxFactory.MemberAccessExpression(
81174
SyntaxKind.SimpleMemberAccessExpression,
82175
SyntaxFactory.IdentifierName("StringComparison"),
83176
SyntaxFactory.IdentifierName(comparisonMember));
84177

85178
var newArg = SyntaxFactory.Argument(stringComparisonExpr);
86179

87-
// Check if a StringComparison argument already exists
88-
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
89-
var stringComparisonType = semanticModel?.Compilation.GetTypeByMetadataName("System.StringComparison");
180+
// 2. Find existing argument for replacement/addition check
90181
var existingArg = invocation.ArgumentList.Arguments.FirstOrDefault(arg =>
91182
semanticModel != null &&
92-
(SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arg.Expression).Type, stringComparisonType) ||
93-
(semanticModel.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && SymbolEqualityComparer.Default.Equals(f.ContainingType, stringComparisonType))));
183+
SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arg.Expression).Type, stringComparisonType));
94184

95-
// Replace existing argument or add new one
96-
var newInvocation = existingArg != null
97-
? invocation.ReplaceNode(existingArg, newArg)
98-
: invocation.WithArgumentList(invocation.ArgumentList.AddArguments(newArg));
185+
// 3. Perform the syntax replacement/addition
186+
InvocationExpressionSyntax newInvocation;
187+
if (existingArg != null)
188+
{
189+
// Argument exists (Replacement case: InvalidComparison)
190+
// Preserve leading/trailing trivia (spaces/comma) from the expression being replaced
191+
var newExprWithTrivia = stringComparisonExpr
192+
.WithLeadingTrivia(existingArg.Expression.GetLeadingTrivia())
193+
.WithTrailingTrivia(existingArg.Expression.GetTrailingTrivia());
99194

100-
// Combine adding 'using System;' and replacing invocation in a single root
101-
var newRoot = EnsureSystemUsing(root).ReplaceNode(invocation, newInvocation);
195+
var newArgWithTrivia = existingArg.WithExpression(newExprWithTrivia);
102196

197+
newInvocation = invocation.ReplaceNode(existingArg, newArgWithTrivia);
198+
}
199+
else
200+
{
201+
// Argument is missing (Addition case: MissingComparison)
202+
// Use AddArguments, relying on Roslyn to correctly handle comma/spacing trivia.
203+
newInvocation = invocation.WithArgumentList(
204+
invocation.ArgumentList.AddArguments(newArg)
205+
);
206+
}
207+
208+
// 4. Update the document root (Ensure using statement is present and replace invocation)
209+
var newRoot = EnsureSystemUsing(root).ReplaceNode(invocation, newInvocation);
103210
return document.WithSyntaxRoot(newRoot);
104211
}
105212

213+
/// <summary>
214+
/// Ensures a 'using System;' directive is present in the document.
215+
/// </summary>
106216
private static SyntaxNode EnsureSystemUsing(SyntaxNode root)
107217
{
108218
if (root is CompilationUnitSyntax compilationUnit)
@@ -113,7 +223,7 @@ private static SyntaxNode EnsureSystemUsing(SyntaxNode root)
113223
if (!hasSystemUsing)
114224
{
115225
var systemUsing = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("System"))
116-
.WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed);
226+
.WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed);
117227
return compilationUnit.AddUsings(systemUsing);
118228
}
119229
}

0 commit comments

Comments
 (0)