Skip to content

Commit

Permalink
Add codefixer to convert a string to an interpolated string
Browse files Browse the repository at this point in the history
  • Loading branch information
meziantou committed Oct 23, 2024
1 parent 3b050d1 commit 4812886
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<string> 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<Document> 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();
}
}
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
65 changes: 65 additions & 0 deletions src/Meziantou.Analyzer/Rules/MakeInterpolatedStringAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<MakeInterpolatedStringAnalyzer>()
.WithCodeFixProvider<MakeInterpolatedStringFixer>()
.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();

}

0 comments on commit 4812886

Please # to comment.