blob: 2489c87a594705d472b785e6c2b1203a7297d1ae [file]
/*
* 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;
}
}
}