diff --git a/ChangeLog.md b/ChangeLog.md index 400de8f52b..99b0217e96 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -24,6 +24,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [TestFramework] Bump `xunit.assert` to `2.6.2` ([PR](https://github.com/dotnet/roslynator/pull/1332)) - Bump Roslyn to 4.7.0 ([PR](https://github.com/dotnet/roslynator/pull/1325)) +### Fixed + +- Fix analyzer [RCS1262](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1262) ([PR](https://github.com/dotnet/roslynator/pull/1339)) + ## [4.7.0] - 2023-12-03 ### Added diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/UnnecessaryRawStringLiteralCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/UnnecessaryRawStringLiteralCodeFixProvider.cs index 0c6964325e..49242198e9 100644 --- a/src/Analyzers.CodeFixes/CSharp/CodeFixes/UnnecessaryRawStringLiteralCodeFixProvider.cs +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/UnnecessaryRawStringLiteralCodeFixProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Composition; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -71,9 +72,26 @@ private static Task RefactorAsync( InterpolatedStringExpressionSyntax interpolatedString, CancellationToken cancellationToken) { - string newText = interpolatedString.ToString(); - int startIndex = interpolatedString.StringStartToken.Text.Length; - newText = "$\"" + newText.Substring(startIndex, newText.Length - startIndex - interpolatedString.StringEndToken.Text.Length) + "\""; + InterpolatedStringExpressionSyntax newInterpolatedString = interpolatedString.ReplaceTokens( + interpolatedString + .Contents + .OfType() + .SelectMany(interpolation => new SyntaxToken[] { interpolation.OpenBraceToken, interpolation.CloseBraceToken }), + (token, _) => + { + if (token.IsKind(SyntaxKind.OpenBraceToken)) + { + return SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithTriviaFrom(token); + } + else + { + return SyntaxFactory.Token(SyntaxKind.CloseBraceToken).WithTriviaFrom(token); + } + }); + + string text = newInterpolatedString.ToString(); + int startIndex = newInterpolatedString.StringStartToken.Text.Length; + string newText = "$\"" + text.Substring(startIndex, text.Length - startIndex - newInterpolatedString.StringEndToken.Text.Length) + "\""; return document.WithTextChangeAsync(interpolatedString.Span, newText, cancellationToken); } diff --git a/src/Analyzers/CSharp/Analysis/UnnecessaryRawStringLiteralAnalyzer.cs b/src/Analyzers/CSharp/Analysis/UnnecessaryRawStringLiteralAnalyzer.cs index 6bd89abb5c..8da47483a8 100644 --- a/src/Analyzers/CSharp/Analysis/UnnecessaryRawStringLiteralAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/UnnecessaryRawStringLiteralAnalyzer.cs @@ -48,7 +48,7 @@ private static void AnalyzeStringLiteralExpression(SyntaxNodeAnalysisContext con string text = info.Text; - if (ContainsBackSlashQuote(text, info.QuoteCount, text.Length - (info.QuoteCount * 2))) + if (ContainsBackSlashOrQuote(text, info.QuoteCount, text.Length - (info.QuoteCount * 2))) return; DiagnosticHelpers.ReportDiagnostic( @@ -72,18 +72,35 @@ private static void AnalyzeInterpolatedStringExpression(SyntaxNodeAnalysisContex { string text = interpolatedStringText.TextToken.Text; - if (ContainsBackSlashQuote(text, 0, text.Length)) + if (ContainsBackSlashOrQuoteOrOpenBrace(text, 0, text.Length)) return; } } + int offset = startToken.ValueText.LastIndexOf('$') + 2; + DiagnosticHelpers.ReportDiagnostic( context, DiagnosticRules.UnnecessaryRawStringLiteral, - Location.Create(interpolatedString.SyntaxTree, new TextSpan(startToken.SpanStart + 2, startToken.Span.Length - 2))); + Location.Create(interpolatedString.SyntaxTree, new TextSpan(startToken.SpanStart + offset, startToken.Span.Length - offset))); + } + + private static bool ContainsBackSlashOrQuote(string text, int start, int length) + { + for (int pos = start; pos < start + length; pos++) + { + switch (text[pos]) + { + case '\\': + case '"': + return true; + } + } + + return false; } - private static bool ContainsBackSlashQuote(string text, int start, int length) + private static bool ContainsBackSlashOrQuoteOrOpenBrace(string text, int start, int length) { for (int pos = start; pos < start + length; pos++) { @@ -91,6 +108,7 @@ private static bool ContainsBackSlashQuote(string text, int start, int length) { case '\\': case '"': + case '{': return true; } } diff --git a/src/Tests/Analyzers.Tests/RCS1262UnnecessaryRawStringLiteralTests.cs b/src/Tests/Analyzers.Tests/RCS1262UnnecessaryRawStringLiteralTests.cs index c4c7f82295..04ad8d8111 100644 --- a/src/Tests/Analyzers.Tests/RCS1262UnnecessaryRawStringLiteralTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1262UnnecessaryRawStringLiteralTests.cs @@ -58,6 +58,32 @@ void M() "); } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnnecessaryRawStringLiteral)] + public async Task Test_InterpolatedString_MultipleDollarSigns() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + void M() + { + string s1 = """"; + string s2 = """"; + string s3 = $$$""[|""""|] {{{s1}}} foo {{{s2}}} """"""; + } +} +", @" +class C +{ + void M() + { + string s1 = """"; + string s2 = """"; + string s3 = $"" {s1} foo {s2} ""; + } +} +"); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnnecessaryRawStringLiteral)] public async Task TestNoDiagnostic_ContainsQuote() { @@ -111,6 +137,21 @@ void M() string s = $"""""" {""""} \t """"""; } } +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnnecessaryRawStringLiteral)] + public async Task TestNoDiagnostic_MultipleDollarSigns() + { + await VerifyNoDiagnosticAsync(@" +class C +{ + void M() + { + string s = string.Empty; + s = $$""""""{{s}}{s}""""""; + } +} "); } }