| /* |
| * 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. |
| */ |
| |
| 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; |
| using Microsoft.CodeAnalysis.CSharp.Syntax; |
| |
| namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx |
| { |
| [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6003_6004_SpanComparisonCodeFixProvider)), Shared] |
| public sealed class LuceneDev6003_6004_SpanComparisonCodeFixProvider : CodeFixProvider |
| { |
| private const string TitleRemoveOrdinal = "Remove redundant StringComparison.Ordinal"; |
| private const string TitleOptimizeToDefaultOrdinal = "Optimize to default Ordinal comparison (remove argument)"; |
| private const string TitleReplaceWithOrdinalIgnoreCase = "Use StringComparison.OrdinalIgnoreCase"; |
| |
| // Integer values for StringComparison Enum members (used for semantic analysis) |
| private const int CurrentCulture = 0; |
| private const int CurrentCultureIgnoreCase = 1; |
| private const int InvariantCulture = 2; |
| private const int InvariantCultureIgnoreCase = 3; |
| private const int Ordinal = 4; |
| private const int OrdinalIgnoreCase = 5; |
| |
| public override ImmutableArray<string> FixableDiagnosticIds => |
| ImmutableArray.Create( |
| Descriptors.LuceneDev6003_RedundantOrdinal.Id, |
| Descriptors.LuceneDev6004_InvalidComparison.Id); |
| |
| public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; |
| |
| public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) |
| { |
| var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); |
| if (root == null) |
| return; |
| |
| var diagnostic = context.Diagnostics.First(); |
| var diagnosticSpan = diagnostic.Location.SourceSpan; |
| var invocation = root.FindNode(diagnosticSpan).FirstAncestorOrSelf<InvocationExpressionSyntax>(); |
| if (invocation == null) |
| return; |
| |
| // 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)) |
| return; |
| |
| if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1) |
| { |
| var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); |
| if (semanticModel == null) |
| return; |
| |
| 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) |
| return; |
| } |
| } |
| switch (diagnostic.Id) |
| { |
| case var id when id == Descriptors.LuceneDev6003_RedundantOrdinal.Id: |
| context.RegisterCodeFix( |
| CodeAction.Create( |
| title: TitleRemoveOrdinal, |
| createChangedDocument: c => RemoveStringComparisonArgumentAsync(context.Document, invocation, c), |
| equivalenceKey: "RemoveRedundantOrdinal"), |
| diagnostic); |
| break; |
| |
| case var id when id == Descriptors.LuceneDev6004_InvalidComparison.Id: |
| var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); |
| if (semanticModel == null) |
| return; |
| |
| var comparisonArg = invocation.ArgumentList.Arguments.FirstOrDefault(arg => |
| semanticModel.GetTypeInfo(arg.Expression).Type?.ToDisplayString() == "System.StringComparison"); |
| |
| if (comparisonArg == null) |
| return; |
| |
| var originalComparisonValue = semanticModel.GetConstantValue(comparisonArg.Expression); |
| |
| if (originalComparisonValue.HasValue && originalComparisonValue.Value is int intValue) |
| { |
| // Check if the original comparison was case-insensitive |
| bool wasCaseInsensitive = intValue == CurrentCultureIgnoreCase || |
| intValue == InvariantCultureIgnoreCase; |
| |
| if (wasCaseInsensitive) |
| { |
| // Fix 1: Case-Insensitive Invalid -> OrdinalIgnoreCase (Single, targeted fix) |
| context.RegisterCodeFix( |
| CodeAction.Create( |
| title: TitleReplaceWithOrdinalIgnoreCase, |
| createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "OrdinalIgnoreCase", c), |
| equivalenceKey: "ReplaceWithOrdinalIgnoreCase"), |
| diagnostic); |
| |
| // Optionally, still offer the case-sensitive fix for completeness |
| context.RegisterCodeFix( |
| CodeAction.Create( |
| title: "Use StringComparison.Ordinal", // Offer Ordinal as second choice |
| createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "Ordinal", c), |
| equivalenceKey: "ReplaceWithOrdinal"), |
| diagnostic); |
| } |
| else |
| { |
| // Fix 1: Case-Sensitive Invalid (CurrentCulture/InvariantCulture) -> Optimal Default (Remove argument) |
| // This skips the redundant intermediate step (Ordinal) |
| context.RegisterCodeFix( |
| CodeAction.Create( |
| title: TitleOptimizeToDefaultOrdinal, |
| createChangedDocument: c => RemoveStringComparisonArgumentAsync(context.Document, invocation, c), |
| equivalenceKey: "OptimizeToDefaultOrdinal"), |
| diagnostic); |
| |
| // Optionally, still offer the case-insensitive fix for completeness |
| context.RegisterCodeFix( |
| CodeAction.Create( |
| title: TitleReplaceWithOrdinalIgnoreCase, |
| createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "OrdinalIgnoreCase", c), |
| equivalenceKey: "ReplaceWithOrdinalIgnoreCase"), |
| diagnostic); |
| } |
| } |
| break; |
| } |
| } |
| |
| private static async Task<Document> RemoveStringComparisonArgumentAsync( |
| Document document, |
| InvocationExpressionSyntax invocation, |
| CancellationToken cancellationToken) |
| { |
| var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); |
| if (root == null) |
| return document; |
| |
| var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); |
| if (semanticModel == null) |
| return document; |
| |
| var compilation = semanticModel.Compilation; |
| var stringComparisonType = compilation.GetTypeByMetadataName("System.StringComparison"); |
| if (stringComparisonType == null) |
| return document; |
| |
| // Find the StringComparison argument |
| ArgumentSyntax? argumentToRemove = null; |
| foreach (var arg in invocation.ArgumentList.Arguments) |
| { |
| var argType = semanticModel.GetTypeInfo(arg.Expression, cancellationToken).Type; |
| if (argType != null && SymbolEqualityComparer.Default.Equals(argType, stringComparisonType)) |
| { |
| argumentToRemove = arg; |
| break; |
| } |
| |
| // fallback: check if it's a member access of StringComparison.* |
| if (argumentToRemove == null && arg.Expression is MemberAccessExpressionSyntax member && |
| member.Expression is IdentifierNameSyntax idName && |
| idName.Identifier.ValueText == "StringComparison") |
| { |
| argumentToRemove = arg; |
| break; |
| } |
| |
| } |
| |
| if (argumentToRemove == null) |
| return document; |
| |
| // Remove the argument |
| var newArguments = invocation.ArgumentList.Arguments.Remove(argumentToRemove); |
| var newArgumentList = invocation.ArgumentList.WithArguments(newArguments); |
| |
| // CRITICAL FIX: Removed NormalizeWhitespace() which causes test instability |
| var newInvocation = invocation.WithArgumentList(newArgumentList) |
| .WithTriviaFrom(invocation); // Preserving trivia on the outer node is usually fine |
| |
| var newRoot = root.ReplaceNode(invocation, newInvocation); |
| return document.WithSyntaxRoot(newRoot); |
| } |
| |
| private static async Task<Document> ReplaceWithStringComparisonAsync( |
| 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); |
| if (semanticModel == null) |
| return document; |
| |
| var compilation = semanticModel.Compilation; |
| var stringComparisonType = compilation.GetTypeByMetadataName("System.StringComparison"); |
| if (stringComparisonType == null) |
| return document; |
| |
| // Find the StringComparison argument |
| ArgumentSyntax? argumentToReplace = null; |
| foreach (var arg in invocation.ArgumentList.Arguments) |
| { |
| var argType = semanticModel.GetTypeInfo(arg.Expression, cancellationToken).Type; |
| if (argType != null && SymbolEqualityComparer.Default.Equals(argType, stringComparisonType)) |
| { |
| argumentToReplace = arg; |
| break; |
| } |
| |
| // fallback: check if it's a member access of StringComparison.* |
| if (argumentToReplace == null && arg.Expression is MemberAccessExpressionSyntax member && |
| member.Expression is IdentifierNameSyntax idName && |
| idName.Identifier.ValueText == "StringComparison") |
| { |
| argumentToReplace = arg; |
| break; |
| } |
| |
| } |
| |
| if (argumentToReplace == null) |
| return document; |
| |
| // Check if argument already uses System.StringComparison |
| bool isFullyQualified = argumentToReplace.Expression.ToString().StartsWith("System.StringComparison"); |
| |
| // Create new StringComparison expression |
| var baseExpression = isFullyQualified |
| ? (ExpressionSyntax)SyntaxFactory.MemberAccessExpression( |
| SyntaxKind.SimpleMemberAccessExpression, |
| SyntaxFactory.IdentifierName("System"), |
| SyntaxFactory.IdentifierName("StringComparison")) |
| : SyntaxFactory.IdentifierName("StringComparison"); |
| |
| var newExpression = SyntaxFactory.MemberAccessExpression( |
| SyntaxKind.SimpleMemberAccessExpression, |
| baseExpression, |
| SyntaxFactory.IdentifierName(comparisonMember)); |
| |
| |
| var newArgument = argumentToReplace.WithExpression(newExpression); |
| |
| // CRITICAL FIX: Removed WithTriviaFrom(invocation) and NormalizeWhitespace() which cause test instability |
| var newInvocation = invocation.ReplaceNode(argumentToReplace, newArgument); |
| |
| var newRoot = root; |
| if (!isFullyQualified) |
| { |
| newRoot = EnsureSystemUsing(newRoot); |
| } |
| newRoot = newRoot.ReplaceNode(invocation, newInvocation); |
| return document.WithSyntaxRoot(newRoot); |
| } |
| |
| // EnsureSystemUsing remains unchanged as it looks correct for adding a using directive |
| 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"); |
| |
| // only add if missing |
| if (!hasSystemUsing) |
| { |
| var systemUsing = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("System")) |
| .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed); |
| |
| return compilationUnit.AddUsings(systemUsing); |
| } |
| } |
| |
| return root; |
| } |
| } |
| } |