Skip to content

Commit

Permalink
Add analyzer "Use string interpolation instead of 'string.Concat'" (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
josefpihrt authored Jan 29, 2024
1 parent e96a13a commit 774b83d
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 25 deletions.
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 "Use raw string literal" [RCS1266](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1266) ([PR](https://github.com/dotnet/roslynator/pull/1375))
- Add analyzer "Convert 'string.Concat' to interpolated string" [RCS1267](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1267) ([PR](https://github.com/dotnet/roslynator/pull/1379))

## [4.10.0] - 2024-01-24

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// 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 System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
Expand All @@ -13,6 +15,7 @@
using Roslynator.CodeFixes;
using Roslynator.CSharp.Refactorings;
using Roslynator.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Roslynator.CSharp.CodeFixes;

Expand All @@ -30,7 +33,8 @@ public override ImmutableArray<string> FixableDiagnosticIds
DiagnosticIdentifiers.RemoveRedundantStringToCharArrayCall,
DiagnosticIdentifiers.CombineEnumerableWhereMethodChain,
DiagnosticIdentifiers.CallExtensionMethodAsInstanceMethod,
DiagnosticIdentifiers.CallThenByInsteadOfOrderBy);
DiagnosticIdentifiers.CallThenByInsteadOfOrderBy,
DiagnosticIdentifiers.UseStringInterpolationInsteadOfStringConcat);
}
}

Expand Down Expand Up @@ -109,7 +113,17 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)

CodeAction codeAction = CodeAction.Create(
$"Call '{newName}' instead of '{oldName}'",
ct => CallThenByInsteadOfOrderByRefactoring.RefactorAsync(context.Document, invocation, newName, ct),
ct => CallThenByInsteadOfOrderByAsync(context.Document, invocation, newName, ct),
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
break;
}
case DiagnosticIdentifiers.UseStringInterpolationInsteadOfStringConcat:
{
CodeAction codeAction = CodeAction.Create(
"Use string interpolation",
ct => UseStringInterpolationInsteadOfStringConcatAsync(context.Document, invocation, ct),
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
Expand Down Expand Up @@ -137,4 +151,81 @@ private static ExpressionSyntax RemoveInvocation(InvocationExpressionSyntax invo
.EmptyIfWhitespace()
.AddRange(closeParen.TrailingTrivia));
}

private static Task<Document> CallThenByInsteadOfOrderByAsync(
Document document,
InvocationExpressionSyntax invocationExpression,
string newName,
CancellationToken cancellationToken)
{
InvocationExpressionSyntax newInvocationExpression = SyntaxRefactorings.ChangeInvokedMethodName(invocationExpression, newName);

return document.ReplaceNodeAsync(invocationExpression, newInvocationExpression, cancellationToken);
}

private static Task<Document> UseStringInterpolationInsteadOfStringConcatAsync(
Document document,
InvocationExpressionSyntax invocationExpression,
CancellationToken cancellationToken)
{
var contents = new List<InterpolatedStringContentSyntax>();
var isVerbatim = false;

foreach (ArgumentSyntax argument in invocationExpression.ArgumentList.Arguments)
{
ExpressionSyntax expression = argument.Expression;

if (expression.IsKind(SyntaxKind.StringLiteralExpression))
{
var literal = (LiteralExpressionSyntax)expression;

SyntaxToken token = literal.Token;
string text = token.Text;

if (text.StartsWith("@"))
isVerbatim = true;

text = (isVerbatim)
? text.Substring(2, text.Length - 3)
: text.Substring(1, text.Length - 2);

contents.Add(
InterpolatedStringText(
Token(
token.LeadingTrivia,
SyntaxKind.InterpolatedStringTextToken,
text,
token.ValueText,
token.TrailingTrivia)));
}
else
{
contents.Add(Interpolation(expression.Parenthesize()));
}
}

string startTokenText = (isVerbatim) ? "@$\"" : "$\"";

SyntaxToken startToken = Token(
SyntaxTriviaList.Empty,
SyntaxKind.InterpolatedStringStartToken,
startTokenText,
startTokenText,
SyntaxTriviaList.Empty);

SyntaxToken endToken = Token(
SyntaxTriviaList.Empty,
SyntaxKind.InterpolatedStringEndToken,
"\"",
"\"",
SyntaxTriviaList.Empty);

InterpolatedStringExpressionSyntax interpolatedString = InterpolatedStringExpression(
startToken,
contents.ToSyntaxList(),
endToken)
.WithTriviaFrom(invocationExpression);

return document.ReplaceNodeAsync(invocationExpression, interpolatedString, cancellationToken);
}
}

This file was deleted.

13 changes: 13 additions & 0 deletions src/Analyzers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7714,6 +7714,19 @@ string s = """
</Sample>
</Samples>
</Analyzer>
<Analyzer>
<Id>RCS1267</Id>
<Identifier>UseStringInterpolationInsteadOfStringConcat</Identifier>
<Title>Use string interpolation instead of 'string.Concat'</Title>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>true</IsEnabledByDefault>
<Samples>
<Sample>
<Before><![CDATA[var s = string.Concat("Id: ", id, ", Value: ", value);]]></Before>
<After><![CDATA[var s = $"Id: {id}, Value: {value}";]]></After>
</Sample>
</Samples>
</Analyzer>
<Analyzer>
<Id>RCS9001</Id>
<Identifier>UsePatternMatching</Identifier>
Expand Down
13 changes: 12 additions & 1 deletion src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
DiagnosticRules.RemoveRedundantCast,
DiagnosticRules.SimplifyLogicalNegation,
DiagnosticRules.UseCoalesceExpression,
DiagnosticRules.OptimizeMethodCall);
DiagnosticRules.OptimizeMethodCall,
DiagnosticRules.UseStringInterpolationInsteadOfStringConcat);
}

return _supportedDiagnostics;
Expand Down Expand Up @@ -451,6 +452,16 @@ private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext contex
OptimizeMethodCallAnalysis.OptimizeStringJoin(context, invocationInfo);
}

break;
}
case "Concat":
{
if (DiagnosticRules.UseStringInterpolationInsteadOfStringConcat.IsEffective(context)
&& argumentCount > 1)
{
UseStringInterpolationInsteadOfStringConcatAnalysis.Analyze(context, invocationInfo);
}

break;
}
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Roslynator.CSharp.Syntax;

namespace Roslynator.CSharp.Analysis;

internal static class UseStringInterpolationInsteadOfStringConcatAnalysis
{
internal static void Analyze(SyntaxNodeAnalysisContext context, SimpleMemberInvocationExpressionInfo invocationInfo)
{
ISymbol symbol = context.SemanticModel.GetSymbol(invocationInfo.InvocationExpression, context.CancellationToken);

if (symbol?.Name == "Concat"
&& symbol.IsStatic
&& symbol.ContainingType.IsString())
{
bool? isVerbatim = null;

foreach (ArgumentSyntax argument in invocationInfo.Arguments)
{
ExpressionSyntax expression = argument.Expression;

if (expression.IsKind(SyntaxKind.InterpolatedStringExpression))
return;

if (expression.IsKind(SyntaxKind.StringLiteralExpression))
{
var literalExpression = (LiteralExpressionSyntax)expression;

if (literalExpression.Token.Text.StartsWith("@"))
{
if (isVerbatim is null)
{
isVerbatim = true;
}
else if (isVerbatim == false)
{
return;
}
}
else if (isVerbatim is null)
{
isVerbatim = false;
}
else if (isVerbatim == true)
{
return;
}
}
}

if (isVerbatim is not null
&& invocationInfo.ArgumentList.IsSingleLine())
{
DiagnosticHelpers.ReportDiagnostic(context, DiagnosticRules.UseStringInterpolationInsteadOfStringConcat, invocationInfo.InvocationExpression);
}
}
}
}
1 change: 1 addition & 0 deletions src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,5 +223,6 @@ public static partial class DiagnosticIdentifiers
public const string UseVarOrExplicitType = "RCS1264";
public const string RemoveRedundantCatchBlock = "RCS1265";
public const string UseRawStringLiteral = "RCS1266";
public const string UseStringInterpolationInsteadOfStringConcat = "RCS1267";
}
}
12 changes: 12 additions & 0 deletions src/Analyzers/CSharp/DiagnosticRules.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2639,5 +2639,17 @@ public static partial class DiagnosticRules
helpLinkUri: DiagnosticIdentifiers.UseRawStringLiteral,
customTags: Array.Empty<string>());

/// <summary>RCS1267</summary>
public static readonly DiagnosticDescriptor UseStringInterpolationInsteadOfStringConcat = DiagnosticDescriptorFactory.Create(
id: DiagnosticIdentifiers.UseStringInterpolationInsteadOfStringConcat,
title: "Use string interpolation instead of 'string.Concat'",
messageFormat: "Use string interpolation instead of 'string.Concat'",
category: DiagnosticCategories.Roslynator,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: null,
helpLinkUri: DiagnosticIdentifiers.UseStringInterpolationInsteadOfStringConcat,
customTags: Array.Empty<string>());

}
}
Loading

0 comments on commit 774b83d

Please sign in to comment.