From 11b060a89e416d22fe75e4d3b87c599a00e56722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Sun, 4 Jun 2023 18:10:05 -0400 Subject: [PATCH] Add CodeFixer for MA0089 (#542) --- .../Rules/OptimizeStartsWithAnalyzer.cs | 30 +-- .../Rules/OptimizeStartsWithFixer.cs | 63 +++++ .../Rules/OptimizeStartsWithAnalyzerTests.cs | 226 ++++++++++++------ 3 files changed, 234 insertions(+), 85 deletions(-) create mode 100644 src/Meziantou.Analyzer/Rules/OptimizeStartsWithFixer.cs diff --git a/src/Meziantou.Analyzer/Rules/OptimizeStartsWithAnalyzer.cs b/src/Meziantou.Analyzer/Rules/OptimizeStartsWithAnalyzer.cs index 40b5dc41..6399a1a4 100644 --- a/src/Meziantou.Analyzer/Rules/OptimizeStartsWithAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/OptimizeStartsWithAnalyzer.cs @@ -191,7 +191,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) if (operation.Arguments[0].Value is { Type.SpecialType: SpecialType.System_String, ConstantValue: { HasValue: true, Value: string { Length: 1 } } } && operation.Arguments[1].Value is { ConstantValue: { HasValue: true, Value: (int)StringComparison.Ordinal } }) { - context.ReportDiagnostic(s_rule, operation.Arguments[0]); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); } } } @@ -205,7 +205,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) if (operation.Arguments[0].Value is { Type.SpecialType: SpecialType.System_String, ConstantValue: { HasValue: true, Value: string { Length: 1 } } } && operation.Arguments[1].Value is { ConstantValue: { HasValue: true, Value: (int)StringComparison.Ordinal } }) { - context.ReportDiagnostic(s_rule, operation.Arguments[0]); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); } } } @@ -241,7 +241,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) if (operation.Arguments[0].Value is { Type.SpecialType: SpecialType.System_String, ConstantValue: { HasValue: true, Value: string { Length: 1 } } } && operation.Arguments[1].Value is { ConstantValue: { HasValue: true, Value: (int)StringComparison.Ordinal } }) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); return; } } @@ -251,7 +251,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) if (operation.Arguments[0].Value is { Type.SpecialType: SpecialType.System_String, ConstantValue: { HasValue: true, Value: string { Length: 1 } } } && operation.Arguments[1].Value.Type.IsEqualTo(StringComparisonSymbol)) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); return; } } @@ -265,7 +265,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) operation.Arguments[1].Value.Type.IsInt32() && operation.Arguments[2].Value is { ConstantValue: { HasValue: true, Value: (int)StringComparison.Ordinal } }) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); } } else if (operation.Arguments.Length == 4) @@ -279,7 +279,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) operation.Arguments[2].Value.Type.IsInt32() && operation.Arguments[3].Value is { ConstantValue: { HasValue: true, Value: (int)StringComparison.Ordinal } }) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); } } } @@ -292,7 +292,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) if (operation.Arguments[0].Value is { Type.SpecialType: SpecialType.System_String, ConstantValue: { HasValue: true, Value: string { Length: 1 } } } && operation.Arguments[1].Value is { ConstantValue: { HasValue: true, Value: (int)StringComparison.Ordinal } }) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); return; } } @@ -302,7 +302,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) if (operation.Arguments[0].Value is { Type.SpecialType: SpecialType.System_String, ConstantValue: { HasValue: true, Value: string { Length: 1 } } } && operation.Arguments[1].Value.Type.IsEqualTo(StringComparisonSymbol)) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); return; } } @@ -317,7 +317,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) operation.Arguments[1].Value.Type.IsInt32() && operation.Arguments[2].Value is { ConstantValue: { HasValue: true, Value: (int)StringComparison.Ordinal } }) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); } } else if (operation.Arguments.Length == 4) @@ -331,7 +331,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) operation.Arguments[2].Value.Type.IsInt32() && operation.Arguments[3].Value is { ConstantValue: { HasValue: true, Value: (int)StringComparison.Ordinal } }) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); } } } @@ -347,19 +347,19 @@ public void AnalyzeInvocation(OperationAnalysisContext context) case 2: if (Join_Char_ObjectArray != null && secondParameterType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Object }) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); return; } if (Join_Char_StringArray != null && secondParameterType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_String }) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); return; } if (Join_Char_IEnumerableT != null && secondParameterType is INamedTypeSymbol symbol && symbol.ConstructedFrom.IsEqualTo(EnumerableOfTSymbol)) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); return; } @@ -368,7 +368,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) case 4: if (Join_Char_StringArray_Int32_Int32 != null) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); return; } @@ -382,7 +382,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) { if (operation.Arguments[0].Value is { Type.SpecialType: SpecialType.System_String, ConstantValue: { HasValue: true, Value: string { Length: 1 } } }) { - context.ReportDiagnostic(s_rule, operation); + context.ReportDiagnostic(s_rule, operation.Arguments[0].Value); return; } } diff --git a/src/Meziantou.Analyzer/Rules/OptimizeStartsWithFixer.cs b/src/Meziantou.Analyzer/Rules/OptimizeStartsWithFixer.cs new file mode 100644 index 00000000..72839556 --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/OptimizeStartsWithFixer.cs @@ -0,0 +1,63 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Operations; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public class OptimizeStartsWithFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.OptimizeStartsWith); + + public override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix == null) + return; + + var title = "Optimize arguments"; + var codeAction = CodeAction.Create( + title, + ct => Fix(context.Document, nodeToFix, ct), + equivalenceKey: title); + context.RegisterCodeFix(codeAction, context.Diagnostics); + } + + private static async Task Fix(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var operation = editor.SemanticModel.GetOperation(nodeToFix, cancellationToken); + if (operation is ILiteralOperation literalOperation && literalOperation.ConstantValue.Value is string { Length: 1 } literalValue) + { + editor.ReplaceNode(literalOperation.Syntax, editor.Generator.LiteralExpression(literalValue[0])); + } + else if (operation is IArgumentOperation argumentOperation && argumentOperation.Value.ConstantValue.Value is string { Length: 1 } argumentValue) + { + editor.ReplaceNode(argumentOperation.Value.Syntax, editor.Generator.LiteralExpression(argumentValue[0])); + } + else if (operation is IInvocationOperation invocation && invocation.TargetMethod.Name == "Replace" && invocation.Arguments.Length >= 2) + { + for (var i = 0; i < 2; i++) + { + if (invocation.Arguments[i].Value.ConstantValue.Value is string { Length: 1 } arg) + { + editor.ReplaceNode(invocation.Arguments[i].Value.Syntax, editor.Generator.LiteralExpression(arg[0])); + } + } + } + + return editor.GetChangedDocument(); + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/OptimizeStartsWithAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/OptimizeStartsWithAnalyzerTests.cs index b1c4e95d..05d1826c 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/OptimizeStartsWithAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/OptimizeStartsWithAnalyzerTests.cs @@ -11,6 +11,7 @@ private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() .WithAnalyzer() + .WithCodeFixProvider() .WithTargetFramework(TargetFramework.Net7_0); } @@ -24,22 +25,49 @@ private static ProjectBuilder CreateProjectBuilder() [InlineData(@"""a"", StringComparison.CurrentCultureIgnoreCase")] [InlineData(@"""a"", StringComparison.InvariantCultureIgnoreCase")] [InlineData(@"""a""")] - [InlineData(@"[|""a""|], StringComparison.Ordinal")] [InlineData(@"""a"", StringComparison.CurrentCulture")] [InlineData(@"""a"", StringComparison.InvariantCulture")] - public async Task StartsWith(string method) + public async Task StartsWith_NoReport(string method) { - var sourceCode = @" -using System; -class Test -{ - void A(string str) - { - _ = str.StartsWith(" + method + @"); + await CreateProjectBuilder() + .WithSourceCode($$""" + using System; + class Test + { + void A(string str) + { + _ = str.StartsWith({{method}}); + } + } + """) + .ValidateAsync(); } -}"; + + [Theory] + [InlineData("""[|"a"|], StringComparison.Ordinal""", """'a', StringComparison.Ordinal""")] + public async Task StartsWith_Report(string method, string fix) + { await CreateProjectBuilder() - .WithSourceCode(sourceCode) + .WithSourceCode($$""" + using System; + class Test + { + void A(string str) + { + _ = str.StartsWith({{method}}); + } + } + """) + .ShouldFixCodeWith($$""" + using System; + class Test + { + void A(string str) + { + _ = str.StartsWith({{fix}}); + } + } + """) .ValidateAsync(); } @@ -53,43 +81,80 @@ await CreateProjectBuilder() [InlineData(@"""a"", StringComparison.CurrentCultureIgnoreCase")] [InlineData(@"""a"", StringComparison.InvariantCultureIgnoreCase")] [InlineData(@"""a""")] - [InlineData(@"[|""a""|], StringComparison.Ordinal")] [InlineData(@"""a"", StringComparison.CurrentCulture")] [InlineData(@"""a"", StringComparison.InvariantCulture")] - public async Task EndsWith(string method) - { - var sourceCode = @" -using System; -class Test -{ - void A(string str) + public async Task EndsWith_NoReport(string method) { - _ = str.EndsWith(" + method + @"); - } -}"; await CreateProjectBuilder() - .WithSourceCode(sourceCode) + .WithSourceCode($$""" + using System; + class Test + { + void A(string str) + { + _ = str.EndsWith({{method}}); + } + } + """) .ValidateAsync(); } [Theory] - [InlineData(@"""a"", StringComparison.Ordinal")] - [InlineData(@"""a"", StringComparison.CurrentCulture")] - [InlineData(@"""a"", 1, 2, StringComparison.Ordinal")] - [InlineData(@"""a"", 1, StringComparison.Ordinal")] - public async Task IndexOf_Report(string method) + [InlineData("""[|"a"|], StringComparison.Ordinal""", """'a', StringComparison.Ordinal""")] + public async Task EndsWith_Report(string method, string fix) { - var sourceCode = @" -using System; -class Test -{ - void A(string str) - { - _ = [||]str.IndexOf(" + method + @"); + await CreateProjectBuilder() + .WithSourceCode($$""" + using System; + class Test + { + void A(string str) + { + _ = str.EndsWith({{method}}); + } + } + """) + .ShouldFixCodeWith($$""" + using System; + class Test + { + void A(string str) + { + _ = str.EndsWith({{fix}}); + } + } + """) + .ValidateAsync(); } -}"; + + [Theory] + [InlineData(@"[|""a""|], StringComparison.Ordinal", @"'a', StringComparison.Ordinal")] + [InlineData(@"[|""a""|], StringComparison.CurrentCulture", @"'a', StringComparison.CurrentCulture")] + [InlineData(@"[|""a""|], 1, 2, StringComparison.Ordinal", @"'a', 1, 2, StringComparison.Ordinal")] + [InlineData(@"[|""a""|], 1, StringComparison.Ordinal", @"'a', 1, StringComparison.Ordinal")] + public async Task IndexOf_Report(string method, string fix) + { await CreateProjectBuilder() - .WithSourceCode(sourceCode) + .WithSourceCode($$""" + using System; + class Test + { + void A(string str) + { + _ = str.IndexOf({{method}}); + } + } + """) + .ShouldFixCodeWith($$""" + using System; + class Test + { + void A(string str) + { + _ = str.IndexOf({{fix}}); + } + } + """) .ValidateAsync(); } @@ -139,22 +204,32 @@ await CreateProjectBuilder() } [Theory] - [InlineData(@"""a"", StringComparison.Ordinal")] - [InlineData(@"""a"", 1, 2, StringComparison.Ordinal")] - [InlineData(@"""a"", 1, StringComparison.Ordinal")] - public async Task LastIndexOf_Report(string method) + [InlineData(@"[|""a""|], StringComparison.Ordinal", @"'a', StringComparison.Ordinal")] + [InlineData(@"[|""a""|], 1, 2, StringComparison.Ordinal", @"'a', 1, 2, StringComparison.Ordinal")] + [InlineData(@"[|""a""|], 1, StringComparison.Ordinal", @"'a', 1, StringComparison.Ordinal")] + public async Task LastIndexOf_Report(string method, string fix) { - var sourceCode = @" -using System; -class Test -{ - void A(string str) - { - _ = [||]str.LastIndexOf(" + method + @"); - } -}"; await CreateProjectBuilder() - .WithSourceCode(sourceCode) + .WithSourceCode($$""" + using System; + class Test + { + void A(string str) + { + _ = str.LastIndexOf({{method}}); + } + } + """) + .ShouldFixCodeWith($$""" + using System; + class Test + { + void A(string str) + { + _ = str.LastIndexOf({{fix}}); + } + } + """) .ValidateAsync(); } @@ -228,30 +303,41 @@ await CreateProjectBuilder() } [Theory] - [InlineData(@"""a"", ""b""")] - [InlineData(@"""a"", ""b"", StringComparison.Ordinal")] - public async Task Replace_Report(string method) + [InlineData(@"""a"", ""b""", @"'a', 'b'")] + [InlineData(@"""a"", ""b"", StringComparison.Ordinal", @"'a', 'b', StringComparison.Ordinal")] + public async Task Replace_Report(string method, string fix) { - var sourceCode = @" -using System; -class Test -{ - void A(string str) - { - _ = [||]str.Replace(" + method + @"); - } -}"; await CreateProjectBuilder() - .WithSourceCode(sourceCode) + .WithSourceCode($$""" + using System; + class Test + { + void A(string str) + { + _ = [||]str.Replace({{method}}); + } + } + """) + .ShouldFixCodeWith($$""" + using System; + class Test + { + void A(string str) + { + _ = str.Replace({{fix}}); + } + } + """) .ValidateAsync(); } [Theory] - [InlineData(@""","", new object[0]")] - [InlineData(@""","", new string[0]")] - [InlineData(@""","", new string[0], 0, 1")] - [InlineData(@""","", Enumerable.Empty()")] - [InlineData(@""","", Enumerable.Empty()")] + [InlineData(@"separator: [|"",""|], new object[0]")] + [InlineData(@"[|"",""|], new object[0]")] + [InlineData(@"[|"",""|], new string[0]")] + [InlineData(@"[|"",""|], new string[0], 0, 1")] + [InlineData(@"[|"",""|], Enumerable.Empty()")] + [InlineData(@"[|"",""|], Enumerable.Empty()")] public async Task Join_Report(string method) { var sourceCode = @" @@ -263,7 +349,7 @@ class Test { void A() { - _ = [||]string.Join(" + method + @"); + _ = string.Join(" + method + @"); } }"; await CreateProjectBuilder()