diff --git a/src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj b/src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj index c94d010ed..d6ff52599 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj +++ b/src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj @@ -22,6 +22,9 @@ + + + diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringComparisonFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringComparisonFixer.cs index 1e8a3ef22..d4610be67 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringComparisonFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringComparisonFixer.cs @@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Operations; namespace Meziantou.Analyzer.Rules; @@ -59,7 +60,38 @@ private static async Task AddStringComparison(Document document, Synta generator.TypeExpression(stringComparison, addImport: true), stringComparisonMode)); - editor.ReplaceNode(invocationExpression, invocationExpression.AddArgumentListArguments(newArgument)); + var insertionIndex = FindStringComparisonParameterIndex(semanticModel, invocationExpression, stringComparison, cancellationToken); + SyntaxNode newInvocation; + if (insertionIndex >= 0 && insertionIndex < invocationExpression.ArgumentList.Arguments.Count) + { + var newArgList = invocationExpression.ArgumentList.Arguments.Insert(insertionIndex, newArgument); + newInvocation = invocationExpression.WithArgumentList(invocationExpression.ArgumentList.WithArguments(newArgList)); + } + else + { + newInvocation = invocationExpression.AddArgumentListArguments(newArgument); + } + + editor.ReplaceNode(invocationExpression, newInvocation); return editor.GetChangedDocument(); } + + private static int FindStringComparisonParameterIndex(SemanticModel semanticModel, SyntaxNode node, INamedTypeSymbol stringComparison, CancellationToken cancellationToken) + { + if (semanticModel.GetOperation(node, cancellationToken) is not IInvocationOperation operation) + return -1; + + var overloadFinder = new OverloadFinder(semanticModel.Compilation); + var overload = overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, options: default, [stringComparison]); + if (overload is null) + return -1; + + for (var i = 0; i < overload.Parameters.Length; i++) + { + if (overload.Parameters[i].Type.IsEqualTo(stringComparison)) + return i; + } + + return -1; + } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseStringComparisonAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseStringComparisonAnalyzerTests.cs index 77f7dd7aa..e18a2c52a 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseStringComparisonAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseStringComparisonAnalyzerTests.cs @@ -248,4 +248,43 @@ await CreateProjectBuilder() .WithSourceCode(SourceCode) .ValidateAsync(); } + + [Fact] + public async Task Contains_WithTrailingMessageParameter_ShouldInsertStringComparisonBeforeMessage() + { + const string SourceCode = """ + class TypeName + { + public void Test() + { + [||]MyAssert.Contains("a", "b", "message"); + } + } + + static class MyAssert + { + public static void Contains(string value, string substring, string message) { } + public static void Contains(string value, string substring, System.StringComparison comparison, string message) { } + } + """; + const string CodeFix = """ + class TypeName + { + public void Test() + { + MyAssert.Contains("a", "b", System.StringComparison.Ordinal, "message"); + } + } + + static class MyAssert + { + public static void Contains(string value, string substring, string message) { } + public static void Contains(string value, string substring, System.StringComparison comparison, string message) { } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(CodeFix) + .ValidateAsync(); + } }