Skip to content

Commit 4812886

Browse files
committed
Add codefixer to convert a string to an interpolated string
1 parent 3b050d1 commit 4812886

File tree

4 files changed

+221
-0
lines changed

4 files changed

+221
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.Composition;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CodeActions;
8+
using Microsoft.CodeAnalysis.CodeFixes;
9+
using Microsoft.CodeAnalysis.CSharp;
10+
using Microsoft.CodeAnalysis.CSharp.Syntax;
11+
using Microsoft.CodeAnalysis.Editing;
12+
using Microsoft.CodeAnalysis.Formatting;
13+
14+
namespace Meziantou.Analyzer.Rules;
15+
16+
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
17+
public sealed class MakeInterpolatedStringFixer : CodeFixProvider
18+
{
19+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.MakeInterpolatedString);
20+
21+
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
22+
23+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
24+
{
25+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
26+
if (root?.FindNode(context.Span, getInnermostNodeForTie: true) is not LiteralExpressionSyntax nodeToFix)
27+
return;
28+
29+
var title = "Convert to interpolated string";
30+
var codeAction = CodeAction.Create(
31+
title,
32+
cancellationToken => MakeInterpolatedString(context.Document, nodeToFix, cancellationToken),
33+
equivalenceKey: title);
34+
35+
context.RegisterCodeFix(codeAction, context.Diagnostics);
36+
}
37+
38+
private static async Task<Document> MakeInterpolatedString(Document document, LiteralExpressionSyntax nodeToFix, CancellationToken cancellationToken)
39+
{
40+
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
41+
var newNode = SyntaxFactory.ParseExpression("$" + nodeToFix.Token.Text);
42+
editor.ReplaceNode(nodeToFix, newNode);
43+
return editor.GetChangedDocument();
44+
}
45+
}

src/Meziantou.Analyzer/RuleIdentifiers.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ internal static class RuleIdentifiers
167167
public const string UseProcessStartOverload = "MA0162";
168168
public const string UseShellExecuteMustBeFalse = "MA0163";
169169
public const string NotPatternShouldBeParenthesized = "MA0164";
170+
public const string MakeInterpolatedString = "MA0165";
170171

171172
public static string GetHelpUri(string identifier)
172173
{
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
using Microsoft.CodeAnalysis.Operations;
8+
9+
namespace Meziantou.Analyzer.Rules;
10+
11+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
12+
public sealed class MakeInterpolatedStringAnalyzer : DiagnosticAnalyzer
13+
{
14+
private static readonly DiagnosticDescriptor Rule = new(
15+
RuleIdentifiers.MakeInterpolatedString,
16+
title: "Make interpolated string",
17+
messageFormat: "Make interpolated string",
18+
RuleCategories.Usage,
19+
DiagnosticSeverity.Hidden,
20+
isEnabledByDefault: true,
21+
description: "",
22+
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.MakeInterpolatedString));
23+
24+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
25+
26+
public override void Initialize(AnalysisContext context)
27+
{
28+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
29+
context.EnableConcurrentExecution();
30+
context.RegisterSyntaxNodeAction(AnalyzeString, SyntaxKind.StringLiteralExpression);
31+
}
32+
33+
private void AnalyzeString(SyntaxNodeAnalysisContext context)
34+
{
35+
var node = (LiteralExpressionSyntax)context.Node;
36+
if (IsInterpolatedString(node))
37+
return;
38+
39+
if (IsRawString(node))
40+
return;
41+
42+
context.ReportDiagnostic(Rule, node);
43+
}
44+
45+
private static bool IsRawString(LiteralExpressionSyntax node)
46+
{
47+
var token = node.Token.Text;
48+
return token.Contains("\"\"\"", StringComparison.Ordinal);
49+
}
50+
51+
private static bool IsInterpolatedString(LiteralExpressionSyntax node)
52+
{
53+
var token = node.Token.Text;
54+
foreach (var c in token)
55+
{
56+
if (c == '"')
57+
return false;
58+
59+
if (c == '$')
60+
return true;
61+
}
62+
63+
return false;
64+
}
65+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using System.Threading.Tasks;
2+
using Meziantou.Analyzer.Rules;
3+
using TestHelper;
4+
using Xunit;
5+
6+
namespace Meziantou.Analyzer.Test.Rules;
7+
public sealed class MakeInterpolatedStringAnalyzerTests
8+
{
9+
private static ProjectBuilder CreateProjectBuilder()
10+
=> new ProjectBuilder()
11+
.WithAnalyzer<MakeInterpolatedStringAnalyzer>()
12+
.WithCodeFixProvider<MakeInterpolatedStringFixer>()
13+
.WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview)
14+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication);
15+
16+
[Fact]
17+
public Task SimpleString()
18+
=> CreateProjectBuilder()
19+
.WithSourceCode("""
20+
_ = [|"test"|];
21+
""")
22+
.ShouldFixCodeWith("""
23+
_ = $"test";
24+
""")
25+
.ValidateAsync();
26+
27+
[Fact]
28+
public Task VerbatimString()
29+
=> CreateProjectBuilder()
30+
.WithSourceCode("""
31+
_ = [|@"test"|];
32+
""")
33+
.ShouldFixCodeWith("""
34+
_ = $@"test";
35+
""")
36+
.ValidateAsync();
37+
38+
[Fact]
39+
public Task InterpolatedString()
40+
=> CreateProjectBuilder()
41+
.WithSourceCode("""
42+
_ = $"test{42}";
43+
""")
44+
.ValidateAsync();
45+
46+
[Fact]
47+
public Task InterpolatedVerbatimString()
48+
=> CreateProjectBuilder()
49+
.WithSourceCode("""
50+
_ = $@"test{42}";
51+
""")
52+
.ValidateAsync();
53+
54+
#if CSHARP10_OR_GREATER
55+
[Fact]
56+
public Task RawString()
57+
=> CreateProjectBuilder()
58+
.WithSourceCode("""""
59+
_ = """test{42}""";
60+
""""")
61+
.ValidateAsync();
62+
#endif
63+
64+
[Fact]
65+
public Task SimpleStringWithOpenAndCloseCurlyBraces()
66+
=> CreateProjectBuilder()
67+
.WithSourceCode("""
68+
_ = [|"test{0}"|];
69+
""")
70+
.ShouldFixCodeWith("""
71+
_ = $"test{0}";
72+
""")
73+
.ValidateAsync();
74+
75+
[Fact]
76+
public Task SimpleStringWithOpenCurlyBrace()
77+
=> CreateProjectBuilder()
78+
.WithSourceCode("""
79+
_ = [|"test{0"|];
80+
""")
81+
.ShouldFixCodeWith("""
82+
_ = $"test{0";
83+
""")
84+
.WithNoFixCompilation()
85+
.ValidateAsync();
86+
87+
[Fact]
88+
public Task VerbatimStringWithOpenAndCloseCurlyBraces()
89+
=> CreateProjectBuilder()
90+
.WithSourceCode("""
91+
_ = [|@"test{0}"|];
92+
""")
93+
.ShouldFixCodeWith("""
94+
_ = $@"test{0}";
95+
""")
96+
.ValidateAsync();
97+
98+
[Fact]
99+
public Task VerbatimStringWithOpenCurlyBrace()
100+
=> CreateProjectBuilder()
101+
.WithSourceCode("""
102+
_ = [|@"test{0"|];
103+
""")
104+
.ShouldFixCodeWith("""
105+
_ = $@"test{0";
106+
""")
107+
.WithNoFixCompilation()
108+
.ValidateAsync();
109+
110+
}

0 commit comments

Comments
 (0)