Skip to content

Commit

Permalink
Add analyzer 'Use raw string literal' (#1375)
Browse files Browse the repository at this point in the history
  • Loading branch information
josefpihrt committed Jan 29, 2024
1 parent 58d3f25 commit e96a13a
Show file tree
Hide file tree
Showing 10 changed files with 635 additions and 101 deletions.
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add analyzer "Use raw string literal" [RCS1266](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1266) ([PR](https://github.com/dotnet/roslynator/pull/1375))

## [4.10.0] - 2024-01-24

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
#if ROSLYN_4_2
// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
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 Roslynator.CodeFixes;

namespace Roslynator.CSharp.CodeFixes;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RawStringLiteralCodeFixProvider))]
[Shared]
public sealed class RawStringLiteralCodeFixProvider : BaseCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
{
get
{
return ImmutableArray.Create(
DiagnosticIdentifiers.UnnecessaryRawStringLiteral,
DiagnosticIdentifiers.UseRawStringLiteral);
}
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode root = await context.GetSyntaxRootAsync().ConfigureAwait(false);

if (!TryFindFirstAncestorOrSelf(root, context.Span, out SyntaxNode node, predicate: f => f.IsKind(SyntaxKind.StringLiteralExpression, SyntaxKind.InterpolatedStringExpression)))
return;

Diagnostic diagnostic = context.Diagnostics[0];
Document document = context.Document;

if (node is LiteralExpressionSyntax literalExpression)
{
if (literalExpression.Token.IsKind(SyntaxKind.StringLiteralToken))
{
CodeAction codeAction = CodeAction.Create(
"Use raw string literal",
ct => UseRawStringLiteralAsync(document, literalExpression, ct),
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
}
else
{
CodeAction codeAction = CodeAction.Create(
"Unnecessary raw string literal",
ct => RefactorAsync(document, literalExpression, ct),
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
}
}
else if (node is InterpolatedStringExpressionSyntax interpolatedString)
{
if (interpolatedString.StringStartToken.IsKind(SyntaxKind.InterpolatedVerbatimStringStartToken))
{
CodeAction codeAction = CodeAction.Create(
"Use raw string literal",
ct => UseRawStringLiteralAsync(document, interpolatedString, ct),
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
}
else
{
CodeAction codeAction = CodeAction.Create(
"Unnecessary raw string literal",
ct => RefactorAsync(document, interpolatedString, ct),
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
}
}
}

private static Task<Document> RefactorAsync(
Document document,
LiteralExpressionSyntax literalExpression,
CancellationToken cancellationToken)
{
RawStringLiteralInfo info = RawStringLiteralInfo.Create(literalExpression);

string newText = info.Text.Substring(info.QuoteCount - 1, info.Text.Length - ((info.QuoteCount * 2) - 2));

return document.WithTextChangeAsync(literalExpression.Span, newText, cancellationToken);
}

private static Task<Document> RefactorAsync(
Document document,
InterpolatedStringExpressionSyntax interpolatedString,
CancellationToken cancellationToken)
{
InterpolatedStringExpressionSyntax newInterpolatedString = interpolatedString.ReplaceTokens(
interpolatedString
.Contents
.OfType<InterpolationSyntax>()
.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);
}

private static Task<Document> UseRawStringLiteralAsync(
Document document,
LiteralExpressionSyntax literalExpression,
CancellationToken cancellationToken)
{
string text = literalExpression.Token.Text;
int quoteCount = 0;
Match match = Regex.Match(text, @"""+");

while (match.Success)
{
if (match.Length > quoteCount)
quoteCount = match.Length / 2;

match = match.NextMatch();
}

quoteCount = (quoteCount > 2) ? quoteCount + 1 : 3;
var quotes = new string('"', quoteCount);

var sb = new StringBuilder();
sb.AppendLine(quotes);
sb.Append(text, 2, text.Length - 3);
sb.Replace("\"\"", "\"", quoteCount, sb.Length - quoteCount);
sb.AppendLine();
sb.Append(quotes);

return document.WithTextChangeAsync(literalExpression.Span, sb.ToString(), cancellationToken);
}

private static Task<Document> UseRawStringLiteralAsync(
Document document,
InterpolatedStringExpressionSyntax interpolatedString,
CancellationToken cancellationToken)
{
int quoteCount = 0;
int braceCount = 0;

foreach (InterpolatedStringContentSyntax content in interpolatedString.Contents)
{
if (content is InterpolatedStringTextSyntax interpolatedText)
{
Match match = Regex.Match(interpolatedText.TextToken.Text, @"""+");

while (match.Success)
{
if (match.Length > quoteCount)
quoteCount = match.Length / 2;

match = match.NextMatch();
}

match = Regex.Match(interpolatedText.TextToken.Text, @"\{+");

while (match.Success)
{
if (match.Length > braceCount)
braceCount = match.Length / 2;

match = match.NextMatch();
}
}
}

quoteCount = (quoteCount > 2) ? quoteCount + 1 : 3;
var quotes = new string('"', quoteCount);

var sb = new StringBuilder();
sb.Append('$', braceCount + 1);
sb.AppendLine(quotes);

foreach (InterpolatedStringContentSyntax content in interpolatedString.Contents)
{
if (content is InterpolatedStringTextSyntax interpolatedText)
{
string text = interpolatedText.TextToken.Text;
int startIndex = sb.Length;
sb.Append(text);
sb.Replace("\"\"", "\"", startIndex, sb.Length - startIndex);
sb.Replace("{{", "{", startIndex, sb.Length - startIndex);
sb.Replace("}}", "}", startIndex, sb.Length - startIndex);
}
else
{
sb.Append('{', braceCount);
sb.Append(content.ToString());
sb.Append('}', braceCount);
}
}

sb.AppendLine();
sb.Append(quotes);

return document.WithTextChangeAsync(interpolatedString.Span, sb.ToString(), cancellationToken);
}
}
#endif

This file was deleted.

23 changes: 23 additions & 0 deletions src/Analyzers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7687,6 +7687,29 @@ finally
{
DoSomethingElse();
}
]]></After>
</Sample>
</Samples>
</Analyzer>
<Analyzer>
<Id>RCS1266</Id>
<Identifier>UseRawStringLiteral</Identifier>
<Title>Use raw string literal</Title>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>true</IsEnabledByDefault>
<Samples>
<Sample>
<Before><![CDATA[
string s = @"
""foo""
";
]]></Before>
<After><![CDATA[
string s = """
"foo"
""";
]]></After>
</Sample>
</Samples>
Expand Down

0 comments on commit e96a13a

Please sign in to comment.