Skip to content

Commit df54546

Browse files
committed
Add analyzers, codefixes, sample and test files for 6001, 6002, 6003
1 parent d17a264 commit df54546

20 files changed

Lines changed: 3237 additions & 22 deletions

Directory.Packages.props

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,11 @@
2323
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
2424
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
2525
</PropertyGroup>
26-
2726
<PropertyGroup Label="Shared NuGet Package Reference Versions">
2827
<RoslynAnalyzerPackageVersion>4.14.0</RoslynAnalyzerPackageVersion>
2928
</PropertyGroup>
30-
3129
<ItemGroup Label="NuGet Package Reference Versions">
30+
<PackageVersion Include="J2N" Version="2.1.0" />
3231
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.8.118" />
3332
<PackageVersion Include="NUnit" Version="4.4.0" />
3433
<PackageVersion Include="NUnit3TestAdapter" Version="5.2.0" />
@@ -46,5 +45,4 @@
4645
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
4746
<PackageVersion Include="Microsoft.VSSDK.BuildTools" Version="17.14.2094" />
4847
</ItemGroup>
49-
5048
</Project>
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* 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.
17+
*/
18+
using System.Collections.Immutable;
19+
using System.Composition;
20+
using System.Linq;
21+
using System.Threading;
22+
using System.Threading.Tasks;
23+
using Lucene.Net.CodeAnalysis.Dev.Utility;
24+
using Microsoft.CodeAnalysis;
25+
using Microsoft.CodeAnalysis.CodeActions;
26+
using Microsoft.CodeAnalysis.CodeFixes;
27+
using Microsoft.CodeAnalysis.CSharp;
28+
using Microsoft.CodeAnalysis.CSharp.Syntax;
29+
30+
namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx
31+
{
32+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6001_StringComparisonCodeFixProvider)), Shared]
33+
public sealed class LuceneDev6001_StringComparisonCodeFixProvider : CodeFixProvider
34+
{
35+
private const string TitleOrdinal = "Use StringComparison.Ordinal";
36+
private const string TitleOrdinalIgnoreCase = "Use StringComparison.OrdinalIgnoreCase";
37+
38+
public override ImmutableArray<string> FixableDiagnosticIds =>
39+
ImmutableArray.Create(
40+
Descriptors.LuceneDev6001_MissingStringComparison.Id,
41+
Descriptors.LuceneDev6001_InvalidStringComparison.Id);
42+
43+
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
44+
45+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
46+
{
47+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
48+
if (root == null) return;
49+
50+
var diagnostic = context.Diagnostics.First();
51+
var diagnosticSpan = diagnostic.Location.SourceSpan;
52+
53+
var invocation = root.FindToken(diagnosticSpan.Start)
54+
.Parent?
55+
.AncestorsAndSelf()
56+
.OfType<InvocationExpressionSyntax>()
57+
.FirstOrDefault();
58+
if (invocation == null) return;
59+
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);
66+
67+
context.RegisterCodeFix(CodeAction.Create(
68+
title: TitleOrdinalIgnoreCase,
69+
createChangedDocument: c => FixInvocationAsync(context.Document, invocation, "OrdinalIgnoreCase", c),
70+
equivalenceKey: TitleOrdinalIgnoreCase),
71+
diagnostic);
72+
}
73+
74+
private static async Task<Document> FixInvocationAsync(Document document, InvocationExpressionSyntax invocation, string comparisonMember, CancellationToken cancellationToken)
75+
{
76+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
77+
if (root == null) return document;
78+
79+
// Create the StringComparison expression
80+
var stringComparisonExpr = SyntaxFactory.MemberAccessExpression(
81+
SyntaxKind.SimpleMemberAccessExpression,
82+
SyntaxFactory.IdentifierName("StringComparison"),
83+
SyntaxFactory.IdentifierName(comparisonMember));
84+
85+
var newArg = SyntaxFactory.Argument(stringComparisonExpr);
86+
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");
90+
var existingArg = invocation.ArgumentList.Arguments.FirstOrDefault(arg =>
91+
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))));
94+
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));
99+
100+
// Combine adding 'using System;' and replacing invocation in a single root
101+
var newRoot = EnsureSystemUsing(root).ReplaceNode(invocation, newInvocation);
102+
103+
return document.WithSyntaxRoot(newRoot);
104+
}
105+
106+
private static SyntaxNode EnsureSystemUsing(SyntaxNode root)
107+
{
108+
if (root is CompilationUnitSyntax compilationUnit)
109+
{
110+
var hasSystemUsing = compilationUnit.Usings.Any(u =>
111+
u.Name is IdentifierNameSyntax id && id.Identifier.ValueText == "System");
112+
113+
if (!hasSystemUsing)
114+
{
115+
var systemUsing = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("System"))
116+
.WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed);
117+
return compilationUnit.AddUsings(systemUsing);
118+
}
119+
}
120+
121+
return root;
122+
}
123+
}
124+
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
* 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.
17+
*/
18+
19+
using System.Collections.Immutable;
20+
using System.Composition;
21+
using System.Linq;
22+
using System.Threading;
23+
using System.Threading.Tasks;
24+
using Lucene.Net.CodeAnalysis.Dev.Utility;
25+
using Microsoft.CodeAnalysis;
26+
using Microsoft.CodeAnalysis.CodeActions;
27+
using Microsoft.CodeAnalysis.CodeFixes;
28+
using Microsoft.CodeAnalysis.CSharp;
29+
using Microsoft.CodeAnalysis.CSharp.Syntax;
30+
31+
namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx
32+
{
33+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6002_SpanComparisonCodeFixProvider)), Shared]
34+
public sealed class LuceneDev6002_SpanComparisonCodeFixProvider : CodeFixProvider
35+
{
36+
private const string TitleRemoveOrdinal = "Remove redundant StringComparison.Ordinal";
37+
private const string TitleReplaceWithOrdinal = "Replace with StringComparison.Ordinal";
38+
private const string TitleReplaceWithOrdinalIgnoreCase = "Replace with StringComparison.OrdinalIgnoreCase";
39+
40+
public override ImmutableArray<string> FixableDiagnosticIds =>
41+
ImmutableArray.Create(
42+
Descriptors.LuceneDev6002_RedundantOrdinal.Id,
43+
Descriptors.LuceneDev6002_InvalidComparison.Id);
44+
45+
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
46+
47+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
48+
{
49+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
50+
if (root == null)
51+
return;
52+
53+
var diagnostic = context.Diagnostics.First();
54+
var diagnosticSpan = diagnostic.Location.SourceSpan;
55+
var invocation = root.FindNode(diagnosticSpan).FirstAncestorOrSelf<InvocationExpressionSyntax>();
56+
if (invocation == null)
57+
return;
58+
59+
switch (diagnostic.Id)
60+
{
61+
case var id when id == Descriptors.LuceneDev6002_RedundantOrdinal.Id:
62+
context.RegisterCodeFix(
63+
CodeAction.Create(
64+
title: "Remove redundant StringComparison.Ordinal",
65+
createChangedDocument: c => RemoveStringComparisonArgumentAsync(context.Document, invocation, c),
66+
equivalenceKey: "RemoveRedundantOrdinal"),
67+
diagnostic);
68+
break;
69+
70+
case var id when id == Descriptors.LuceneDev6002_InvalidComparison.Id:
71+
context.RegisterCodeFix(
72+
CodeAction.Create(
73+
title: "Use StringComparison.Ordinal",
74+
createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "Ordinal", c),
75+
equivalenceKey: "ReplaceWithOrdinal"),
76+
diagnostic);
77+
78+
context.RegisterCodeFix(
79+
CodeAction.Create(
80+
title: "Use StringComparison.OrdinalIgnoreCase",
81+
createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "OrdinalIgnoreCase", c),
82+
equivalenceKey: "ReplaceWithOrdinalIgnoreCase"),
83+
diagnostic);
84+
break;
85+
}
86+
}
87+
88+
private static async Task<Document> RemoveStringComparisonArgumentAsync(
89+
Document document,
90+
InvocationExpressionSyntax invocation,
91+
CancellationToken cancellationToken)
92+
{
93+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
94+
if (root == null)
95+
return document;
96+
97+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
98+
if (semanticModel == null)
99+
return document;
100+
101+
var compilation = semanticModel.Compilation;
102+
var stringComparisonType = compilation.GetTypeByMetadataName("System.StringComparison");
103+
if (stringComparisonType == null)
104+
return document;
105+
106+
// Find the StringComparison argument
107+
ArgumentSyntax? argumentToRemove = null;
108+
foreach (var arg in invocation.ArgumentList.Arguments)
109+
{
110+
var argType = semanticModel.GetTypeInfo(arg.Expression, cancellationToken).Type;
111+
if (argType != null && SymbolEqualityComparer.Default.Equals(argType, stringComparisonType))
112+
{
113+
argumentToRemove = arg;
114+
break;
115+
}
116+
117+
// fallback: check if it's a member access of StringComparison.*
118+
if (argumentToRemove == null && arg.Expression is MemberAccessExpressionSyntax member &&
119+
member.Expression is IdentifierNameSyntax idName &&
120+
idName.Identifier.ValueText == "StringComparison")
121+
{
122+
argumentToRemove = arg;
123+
break;
124+
}
125+
126+
}
127+
128+
if (argumentToRemove == null)
129+
return document;
130+
131+
// Remove the argument and normalize formatting
132+
var newArguments = invocation.ArgumentList.Arguments.Remove(argumentToRemove);
133+
var newArgumentList = invocation.ArgumentList.WithArguments(newArguments);
134+
var newInvocation = invocation.WithArgumentList(newArgumentList)
135+
.WithTriviaFrom(invocation) // preserve trivia
136+
.NormalizeWhitespace(); // clean formatting
137+
138+
var newRoot = root.ReplaceNode(invocation, newInvocation);
139+
return document.WithSyntaxRoot(newRoot);
140+
}
141+
142+
private static async Task<Document> ReplaceWithStringComparisonAsync(
143+
Document document,
144+
InvocationExpressionSyntax invocation,
145+
string comparisonMember,
146+
CancellationToken cancellationToken)
147+
{
148+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
149+
if (root == null)
150+
return document;
151+
152+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
153+
if (semanticModel == null)
154+
return document;
155+
156+
var compilation = semanticModel.Compilation;
157+
var stringComparisonType = compilation.GetTypeByMetadataName("System.StringComparison");
158+
if (stringComparisonType == null)
159+
return document;
160+
161+
// Find the StringComparison argument
162+
ArgumentSyntax? argumentToReplace = null;
163+
foreach (var arg in invocation.ArgumentList.Arguments)
164+
{
165+
var argType = semanticModel.GetTypeInfo(arg.Expression, cancellationToken).Type;
166+
if (argType != null && SymbolEqualityComparer.Default.Equals(argType, stringComparisonType))
167+
{
168+
argumentToReplace = arg;
169+
break;
170+
}
171+
172+
// fallback: check if it's a member access of StringComparison.*
173+
if (argumentToReplace == null && arg.Expression is MemberAccessExpressionSyntax member &&
174+
member.Expression is IdentifierNameSyntax idName &&
175+
idName.Identifier.ValueText == "StringComparison")
176+
{
177+
argumentToReplace = arg;
178+
break;
179+
}
180+
181+
}
182+
183+
if (argumentToReplace == null)
184+
return document;
185+
186+
// Check if argument already uses System.StringComparison
187+
bool isFullyQualified = argumentToReplace.Expression.ToString().StartsWith("System.StringComparison");
188+
189+
// Create new StringComparison expression
190+
var baseExpression = isFullyQualified
191+
? (ExpressionSyntax)SyntaxFactory.MemberAccessExpression(
192+
SyntaxKind.SimpleMemberAccessExpression,
193+
SyntaxFactory.IdentifierName("System"),
194+
SyntaxFactory.IdentifierName("StringComparison"))
195+
: SyntaxFactory.IdentifierName("StringComparison");
196+
197+
var newExpression = SyntaxFactory.MemberAccessExpression(
198+
SyntaxKind.SimpleMemberAccessExpression,
199+
baseExpression,
200+
SyntaxFactory.IdentifierName(comparisonMember));
201+
202+
203+
var newArgument = argumentToReplace.WithExpression(newExpression);
204+
var newInvocation = invocation.ReplaceNode(argumentToReplace, newArgument)
205+
.WithTriviaFrom(invocation)
206+
.NormalizeWhitespace();
207+
208+
var newRoot = root;
209+
if (!isFullyQualified)
210+
{
211+
newRoot = EnsureSystemUsing(newRoot);
212+
}
213+
newRoot = newRoot.ReplaceNode(invocation, newInvocation);
214+
return document.WithSyntaxRoot(newRoot);
215+
}
216+
217+
private static SyntaxNode EnsureSystemUsing(SyntaxNode root)
218+
{
219+
if (root is CompilationUnitSyntax compilationUnit)
220+
{
221+
var hasSystemUsing = compilationUnit.Usings.Any(u =>
222+
u.Name is IdentifierNameSyntax id && id.Identifier.ValueText == "System");
223+
224+
// only add if missing
225+
if (!hasSystemUsing)
226+
{
227+
var systemUsing = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("System"))
228+
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
229+
230+
return compilationUnit.AddUsings(systemUsing);
231+
}
232+
}
233+
234+
return root;
235+
}
236+
}
237+
}

0 commit comments

Comments
 (0)