Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add analyzer 'Unnecessary raw string literal' #1293

Merged
merged 15 commits into from
Nov 28, 2023
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add analyzer "Dispose resource asynchronously" ([RCS1261](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1261)) ([PR](https://github.com/dotnet/roslynator/pull/1285)).
- Add analyzer "Unnecessary raw string literal" ([RCS1262](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1262)) ([PR](https://github.com/dotnet/roslynator/pull/1293)).

### Changed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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.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(UnnecessaryRawStringLiteralCodeFixProvider))]
[Shared]
public sealed class UnnecessaryRawStringLiteralCodeFixProvider : BaseCodeFixProvider
{
private const string Title = "Unnecessary raw string literal";

public override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(DiagnosticIdentifiers.UnnecessaryRawStringLiteral); }
}

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)
{
CodeAction codeAction = CodeAction.Create(
Title,
ct => RefactorAsync(document, literalExpression, ct),
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
}
else if (node is InterpolatedStringExpressionSyntax interpolatedString)
{
CodeAction codeAction = CodeAction.Create(
Title,
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)
{
string newText = interpolatedString.ToString();
int startIndex = interpolatedString.StringStartToken.Text.Length;
newText = "$\"" + newText.Substring(startIndex, newText.Length - startIndex - interpolatedString.StringEndToken.Text.Length) + "\"";

return document.WithTextChangeAsync(interpolatedString.Span, newText, cancellationToken);
}
}
21 changes: 21 additions & 0 deletions src/Analyzers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7490,6 +7490,27 @@ void M()
</Sample>
</Samples>
</Analyzer>
<Analyzer>
<Id>RCS1262</Id>
<Identifier>UnnecessaryRawStringLiteral</Identifier>
<Title>Unnecessary raw string literal.</Title>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>true</IsEnabledByDefault>
<SupportsFadeOut>true</SupportsFadeOut>
<MinLanguageVersion>11.0</MinLanguageVersion>
<Samples>
<Sample>
<Before><![CDATA[string s = """foo""";]]></Before>
<After><![CDATA[string s = "foo";]]></After>
</Sample>
</Samples>
<Links>
<Link>
<Url>https://learn.microsoft.com/dotnet/csharp/language-reference/tokens/raw-string</Url>
<Text>Raw string literal text - """ in string literals</Text>
</Link>
</Links>
</Analyzer>
<Analyzer>
<Id>RCS9001</Id>
<Identifier>UsePatternMatching</Identifier>
Expand Down
100 changes: 100 additions & 0 deletions src/Analyzers/CSharp/Analysis/UnnecessaryRawStringLiteralAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;

namespace Roslynator.CSharp.Analysis;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UnnecessaryRawStringLiteralAnalyzer : BaseDiagnosticAnalyzer
{
private static ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics;

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
if (_supportedDiagnostics.IsDefault)
Immutable.InterlockedInitialize(ref _supportedDiagnostics, DiagnosticRules.UnnecessaryRawStringLiteral);

return _supportedDiagnostics;
}
}

public override void Initialize(AnalysisContext context)
{
base.Initialize(context);

context.RegisterCompilationStartAction(startContext =>
{
if (((CSharpCompilation)startContext.Compilation).LanguageVersion >= LanguageVersion.CSharp11)
{
startContext.RegisterSyntaxNodeAction(f => AnalyzeStringLiteralExpression(f), SyntaxKind.StringLiteralExpression);
startContext.RegisterSyntaxNodeAction(f => AnalyzeInterpolatedStringExpression(f), SyntaxKind.InterpolatedStringExpression);
}
});
}

private static void AnalyzeStringLiteralExpression(SyntaxNodeAnalysisContext context)
{
var literalExpression = (LiteralExpressionSyntax)context.Node;

if (!RawStringLiteralInfo.TryCreate(literalExpression, out RawStringLiteralInfo info))
return;

string text = info.Text;

if (ContainsBackSlashQuote(text, info.QuoteCount, text.Length - (info.QuoteCount * 2)))
return;

DiagnosticHelpers.ReportDiagnostic(
context,
DiagnosticRules.UnnecessaryRawStringLiteral,
Location.Create(literalExpression.SyntaxTree, new TextSpan(literalExpression.SpanStart + 1, info.QuoteCount - 1)));
}

private static void AnalyzeInterpolatedStringExpression(SyntaxNodeAnalysisContext context)
{
var interpolatedString = (InterpolatedStringExpressionSyntax)context.Node;

SyntaxToken startToken = interpolatedString.StringStartToken;

if (!startToken.IsKind(SyntaxKind.InterpolatedSingleLineRawStringStartToken))
return;

foreach (InterpolatedStringContentSyntax content in interpolatedString.Contents)
{
if (content is InterpolatedStringTextSyntax interpolatedStringText)
{
string text = interpolatedStringText.TextToken.Text;

if (ContainsBackSlashQuote(text, 0, text.Length))
return;
}
}

DiagnosticHelpers.ReportDiagnostic(
context,
DiagnosticRules.UnnecessaryRawStringLiteral,
Location.Create(interpolatedString.SyntaxTree, new TextSpan(startToken.SpanStart + 2, startToken.Span.Length - 2)));
}

private static bool ContainsBackSlashQuote(string text, int start, int length)
{
for (int pos = start; pos < start + length; pos++)
{
switch (text[pos])
{
case '\\':
case '"':
return true;
}
}

return false;
}
}
1 change: 1 addition & 0 deletions src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,5 +218,6 @@ public static partial class DiagnosticIdentifiers
public const string RemoveEmptySyntax = "RCS1259";
public const string AddOrRemoveTrailingComma = "RCS1260";
public const string DisposeResourceAsynchronously = "RCS1261";
public const string UnnecessaryRawStringLiteral = "RCS1262";
}
}
12 changes: 12 additions & 0 deletions src/Analyzers/CSharp/DiagnosticRules.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2581,5 +2581,17 @@ public static partial class DiagnosticRules
helpLinkUri: DiagnosticIdentifiers.DisposeResourceAsynchronously,
customTags: Array.Empty<string>());

/// <summary>RCS1262</summary>
public static readonly DiagnosticDescriptor UnnecessaryRawStringLiteral = DiagnosticDescriptorFactory.Create(
id: DiagnosticIdentifiers.UnnecessaryRawStringLiteral,
title: "Unnecessary raw string literal.",
messageFormat: "Unnecessary raw string literal.",
category: DiagnosticCategories.Roslynator,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: null,
helpLinkUri: DiagnosticIdentifiers.UnnecessaryRawStringLiteral,
customTags: WellKnownDiagnosticTags.Unnecessary);

}
}
75 changes: 75 additions & 0 deletions src/Analyzers/CSharp/RawStringLiteralInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Roslynator.CSharp;

internal readonly struct RawStringLiteralInfo
{
public RawStringLiteralInfo(LiteralExpressionSyntax expression, int quoteCount)
{
Expression = expression;
QuoteCount = quoteCount;
}

public LiteralExpressionSyntax Expression { get; }

public string Text => Expression.Token.Text;

public int QuoteCount { get; }

public bool IsDefault => Expression is null;

public static bool TryCreate(LiteralExpressionSyntax literalExpression, out RawStringLiteralInfo info)
{
info = default;

if (!literalExpression.IsKind(SyntaxKind.StringLiteralExpression))
return false;

SyntaxToken token = literalExpression.Token;

if (!token.IsKind(SyntaxKind.SingleLineRawStringLiteralToken))
return false;

string text = token.Text;
int startCount = 0;
int endCount = 0;

int i = 0;
while (i < text.Length
&& text[i] == '"')
{
startCount++;
i++;
}

i = text.Length - 1;
while (i >= startCount
&& text[i] == '"')
{
endCount++;
i--;
}

if (startCount < 3
|| startCount != endCount)
{
return false;
}

info = new RawStringLiteralInfo(literalExpression, startCount);
return true;
}

public static RawStringLiteralInfo Create(LiteralExpressionSyntax literalExpression)
{
if (TryCreate(literalExpression, out RawStringLiteralInfo info))
return info;

throw new InvalidOperationException();
}
}
Loading