From 481288666302979bbf2439cdbb219fc84784440f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Wed, 23 Oct 2024 19:43:40 -0400 Subject: [PATCH] Add codefixer to convert a string to an interpolated string --- .../Rules/MakeInterpolatedStringFixer.cs | 45 +++++++ src/Meziantou.Analyzer/RuleIdentifiers.cs | 1 + .../Rules/MakeInterpolatedStringAnalyzer.cs | 65 +++++++++++ .../MakeInterpolatedStringAnalyzerTests.cs | 110 ++++++++++++++++++ 4 files changed, 221 insertions(+) create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/MakeInterpolatedStringFixer.cs create mode 100644 src/Meziantou.Analyzer/Rules/MakeInterpolatedStringAnalyzer.cs create mode 100644 tests/Meziantou.Analyzer.Test/Rules/MakeInterpolatedStringAnalyzerTests.cs diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/MakeInterpolatedStringFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/MakeInterpolatedStringFixer.cs new file mode 100644 index 00000000..686320ab --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/MakeInterpolatedStringFixer.cs @@ -0,0 +1,45 @@ +using System; +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.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class MakeInterpolatedStringFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.MakeInterpolatedString); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root?.FindNode(context.Span, getInnermostNodeForTie: true) is not LiteralExpressionSyntax nodeToFix) + return; + + var title = "Convert to interpolated string"; + var codeAction = CodeAction.Create( + title, + cancellationToken => MakeInterpolatedString(context.Document, nodeToFix, cancellationToken), + equivalenceKey: title); + + context.RegisterCodeFix(codeAction, context.Diagnostics); + } + + private static async Task MakeInterpolatedString(Document document, LiteralExpressionSyntax nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var newNode = SyntaxFactory.ParseExpression("$" + nodeToFix.Token.Text); + editor.ReplaceNode(nodeToFix, newNode); + return editor.GetChangedDocument(); + } +} diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index fcb95b97..4794fffd 100755 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -167,6 +167,7 @@ internal static class RuleIdentifiers public const string UseProcessStartOverload = "MA0162"; public const string UseShellExecuteMustBeFalse = "MA0163"; public const string NotPatternShouldBeParenthesized = "MA0164"; + public const string MakeInterpolatedString = "MA0165"; public static string GetHelpUri(string identifier) { diff --git a/src/Meziantou.Analyzer/Rules/MakeInterpolatedStringAnalyzer.cs b/src/Meziantou.Analyzer/Rules/MakeInterpolatedStringAnalyzer.cs new file mode 100644 index 00000000..adc4fa09 --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/MakeInterpolatedStringAnalyzer.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Meziantou.Analyzer.Rules; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class MakeInterpolatedStringAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor Rule = new( + RuleIdentifiers.MakeInterpolatedString, + title: "Make interpolated string", + messageFormat: "Make interpolated string", + RuleCategories.Usage, + DiagnosticSeverity.Hidden, + isEnabledByDefault: true, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.MakeInterpolatedString)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeString, SyntaxKind.StringLiteralExpression); + } + + private void AnalyzeString(SyntaxNodeAnalysisContext context) + { + var node = (LiteralExpressionSyntax)context.Node; + if (IsInterpolatedString(node)) + return; + + if (IsRawString(node)) + return; + + context.ReportDiagnostic(Rule, node); + } + + private static bool IsRawString(LiteralExpressionSyntax node) + { + var token = node.Token.Text; + return token.Contains("\"\"\"", StringComparison.Ordinal); + } + + private static bool IsInterpolatedString(LiteralExpressionSyntax node) + { + var token = node.Token.Text; + foreach (var c in token) + { + if (c == '"') + return false; + + if (c == '$') + return true; + } + + return false; + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/MakeInterpolatedStringAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/MakeInterpolatedStringAnalyzerTests.cs new file mode 100644 index 00000000..cf5cbd5e --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Rules/MakeInterpolatedStringAnalyzerTests.cs @@ -0,0 +1,110 @@ +using System.Threading.Tasks; +using Meziantou.Analyzer.Rules; +using TestHelper; +using Xunit; + +namespace Meziantou.Analyzer.Test.Rules; +public sealed class MakeInterpolatedStringAnalyzerTests +{ + private static ProjectBuilder CreateProjectBuilder() + => new ProjectBuilder() + .WithAnalyzer() + .WithCodeFixProvider() + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview) + .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication); + + [Fact] + public Task SimpleString() + => CreateProjectBuilder() + .WithSourceCode(""" + _ = [|"test"|]; + """) + .ShouldFixCodeWith(""" + _ = $"test"; + """) + .ValidateAsync(); + + [Fact] + public Task VerbatimString() + => CreateProjectBuilder() + .WithSourceCode(""" + _ = [|@"test"|]; + """) + .ShouldFixCodeWith(""" + _ = $@"test"; + """) + .ValidateAsync(); + + [Fact] + public Task InterpolatedString() + => CreateProjectBuilder() + .WithSourceCode(""" + _ = $"test{42}"; + """) + .ValidateAsync(); + + [Fact] + public Task InterpolatedVerbatimString() + => CreateProjectBuilder() + .WithSourceCode(""" + _ = $@"test{42}"; + """) + .ValidateAsync(); + +#if CSHARP10_OR_GREATER + [Fact] + public Task RawString() + => CreateProjectBuilder() + .WithSourceCode(""""" + _ = """test{42}"""; + """"") + .ValidateAsync(); +#endif + + [Fact] + public Task SimpleStringWithOpenAndCloseCurlyBraces() + => CreateProjectBuilder() + .WithSourceCode(""" + _ = [|"test{0}"|]; + """) + .ShouldFixCodeWith(""" + _ = $"test{0}"; + """) + .ValidateAsync(); + + [Fact] + public Task SimpleStringWithOpenCurlyBrace() + => CreateProjectBuilder() + .WithSourceCode(""" + _ = [|"test{0"|]; + """) + .ShouldFixCodeWith(""" + _ = $"test{0"; + """) + .WithNoFixCompilation() + .ValidateAsync(); + + [Fact] + public Task VerbatimStringWithOpenAndCloseCurlyBraces() + => CreateProjectBuilder() + .WithSourceCode(""" + _ = [|@"test{0}"|]; + """) + .ShouldFixCodeWith(""" + _ = $@"test{0}"; + """) + .ValidateAsync(); + + [Fact] + public Task VerbatimStringWithOpenCurlyBrace() + => CreateProjectBuilder() + .WithSourceCode(""" + _ = [|@"test{0"|]; + """) + .ShouldFixCodeWith(""" + _ = $@"test{0"; + """) + .WithNoFixCompilation() + .ValidateAsync(); + +}