From b013deff6101330b38cdec8f9d9b72e4ec14fd51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Fri, 11 Sep 2026 16:18:05 -0400 Subject: [PATCH] Fix MA0184 code fix keeping doubled braces as literal text The code fix copied the token text of the interpolated string into a regular string literal. That text still contains the escaped braces ("{{" and "}}"), so $"{{text}}" was converted to "{{text}}" instead of "{text}", changing the value of the string. The fix now uses the value computed by the compiler (the constant values of the IInterpolatedStringTextOperation parts), and is only registered when that value is available. --- ...nterpolatedStringWithoutParametersFixer.cs | 63 ++++++++++--------- ...tedStringWithoutParametersAnalyzerTests.cs | 60 ++++++++++++++++++ 2 files changed, 93 insertions(+), 30 deletions(-) diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseInterpolatedStringWithoutParametersFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseInterpolatedStringWithoutParametersFixer.cs index 0612b264d..fc1dc1404 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseInterpolatedStringWithoutParametersFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseInterpolatedStringWithoutParametersFixer.cs @@ -1,3 +1,5 @@ +using System.Text; + namespace Meziantou.Analyzer.Rules; [ExportCodeFixProvider(LanguageNames.CSharp), Shared] @@ -17,22 +19,22 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (nodeToFix is not InterpolatedStringExpressionSyntax interpolatedString) return; + var regularString = await CreateRegularStringAsync(context.Document, interpolatedString, context.CancellationToken).ConfigureAwait(false); + if (regularString is null) + return; + context.RegisterCodeFix( CodeAction.Create( "Convert to regular string", - ct => ConvertToRegularString(context.Document, interpolatedString, ct), + ct => ConvertToRegularString(context.Document, interpolatedString, regularString, ct), equivalenceKey: "Convert to regular string"), context.Diagnostics); } - private static async Task ConvertToRegularString(Document document, InterpolatedStringExpressionSyntax interpolatedString, CancellationToken cancellationToken) + private static async Task CreateRegularStringAsync(Document document, InterpolatedStringExpressionSyntax interpolatedString, CancellationToken cancellationToken) { - var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - // Check if this is a raw string literal (C# 11+) - var isRawString = interpolatedString.StringStartToken.IsKind(SyntaxKind.InterpolatedMultiLineRawStringStartToken) || - interpolatedString.StringStartToken.IsKind(SyntaxKind.InterpolatedSingleLineRawStringStartToken); - if (isRawString) + if (interpolatedString.StringStartToken.Kind() is SyntaxKind.InterpolatedMultiLineRawStringStartToken or SyntaxKind.InterpolatedSingleLineRawStringStartToken) { // For raw strings, simply remove the $ prefix from the start token // $""" text """ -> """ text """ @@ -40,35 +42,36 @@ private static async Task ConvertToRegularString(Document document, In // Find the position of $ in the start token and remove it var dollarIndex = originalText.IndexOf('$', StringComparison.Ordinal); - if (dollarIndex >= 0) - { - var newText = originalText.Remove(dollarIndex, 1); - var newNode = SyntaxFactory.ParseExpression(newText); + if (dollarIndex < 0) + return null; - editor.ReplaceNode(interpolatedString, newNode.WithTriviaFrom(interpolatedString)); - } + return SyntaxFactory.ParseExpression(originalText.Remove(dollarIndex, 1)); } - else - { - // Extract the string content from the interpolated string - var stringContent = string.Empty; - foreach (var content in interpolatedString.Contents) - { - if (content is InterpolatedStringTextSyntax textSyntax) - { - // Use the ValueText which contains the actual string value (not escaped) - stringContent += textSyntax.TextToken.ValueText; - } - } - // Create a regular string literal with the same content - var regularString = SyntaxFactory.LiteralExpression( - SyntaxKind.StringLiteralExpression, - SyntaxFactory.Literal(stringContent)); + // The text of the tokens still contains the escaped braces ("{{" and "}}"), + // so use the value computed by the compiler + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (semanticModel?.GetOperation(interpolatedString, cancellationToken) is not IInterpolatedStringOperation operation) + return null; - editor.ReplaceNode(interpolatedString, regularString.WithTriviaFrom(interpolatedString)); + var stringContent = new StringBuilder(); + foreach (var part in operation.Parts) + { + if (part is not IInterpolatedStringTextOperation { Text.ConstantValue: { HasValue: true, Value: string text } }) + return null; + + stringContent.Append(text); } + return SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(stringContent.ToString())); + } + + private static async Task ConvertToRegularString(Document document, InterpolatedStringExpressionSyntax interpolatedString, ExpressionSyntax regularString, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(interpolatedString, regularString.WithTriviaFrom(interpolatedString)); return editor.GetChangedDocument(); } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseInterpolatedStringWithoutParametersAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseInterpolatedStringWithoutParametersAnalyzerTests.cs index 62a8c1392..a29902633 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseInterpolatedStringWithoutParametersAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseInterpolatedStringWithoutParametersAnalyzerTests.cs @@ -277,6 +277,66 @@ public void Test() return test.RunAsync(); } + [Fact] + public Task CodeFix_ShouldUnescapeBraces() + { + var test = CreateTest(); + test.TestCode = """ + class TypeName + { + public string Test() => {|MA0184:$"{{text}}"|}; + } + """; + test.FixedCode = """ + class TypeName + { + public string Test() => "{text}"; + } + """; + + return test.RunAsync(); + } + + [Fact] + public Task CodeFix_Verbatim_ShouldUnescapeBracesAndQuotes() + { + var test = CreateTest(); + test.TestCode = """ + class TypeName + { + public string Test() => {|MA0184:$@"{{""text"": ""\""}}"|}; + } + """; + test.FixedCode = """ + class TypeName + { + public string Test() => "{\"text\": \"\\\"}"; + } + """; + + return test.RunAsync(); + } + + [Fact] + public Task CodeFix_EmptyString() + { + var test = CreateTest(); + test.TestCode = """ + class TypeName + { + public string Test() => {|MA0184:$""|}; + } + """; + test.FixedCode = """ + class TypeName + { + public string Test() => ""; + } + """; + + return test.RunAsync(); + } + [Fact] public Task RawInterpolatedStringWithoutParameters_ShouldReportDiagnostic() {