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+
187using System . Collections . Immutable ;
198using System . Composition ;
209using System . Linq ;
2413using Microsoft . CodeAnalysis ;
2514using Microsoft . CodeAnalysis . CodeActions ;
2615using Microsoft . CodeAnalysis . CodeFixes ;
27- using Microsoft . CodeAnalysis . CSharp ;
2816using Microsoft . CodeAnalysis . CSharp . Syntax ;
17+ using Microsoft . CodeAnalysis . CSharp ;
2918
3019namespace 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