Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0181](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0181.md)|Style|Do not use cast|ℹ️|❌|❌|
|[MA0182](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0182.md)|Design|Avoid unused internal types|ℹ️|✔️|✔️|
|[MA0183](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0183.md)|Usage|string.Format should use a format string with placeholders|⚠️|✔️|❌|
|[MA0184](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0184.md)|Style|Do not use interpolated string without parameters|ℹ️|❌|✔️|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
|[MA0181](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0181.md)|Style|Do not use cast|<span title='Info'>ℹ️</span>|❌|❌|
|[MA0182](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0182.md)|Design|Avoid unused internal types|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0183](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0183.md)|Usage|string.Format should use a format string with placeholders|<span title='Warning'>⚠️</span>|✔️|❌|
|[MA0184](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0184.md)|Style|Do not use interpolated string without parameters|<span title='Info'>ℹ️</span>|❌|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -743,6 +744,9 @@ dotnet_diagnostic.MA0182.severity = suggestion

# MA0183: string.Format should use a format string with placeholders
dotnet_diagnostic.MA0183.severity = warning

# MA0184: Do not use interpolated string without parameters
dotnet_diagnostic.MA0184.severity = none
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1290,4 +1294,7 @@ dotnet_diagnostic.MA0182.severity = none

# MA0183: string.Format should use a format string with placeholders
dotnet_diagnostic.MA0183.severity = none

# MA0184: Do not use interpolated string without parameters
dotnet_diagnostic.MA0184.severity = none
```
43 changes: 43 additions & 0 deletions docs/Rules/MA0184.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# MA0184 - Do not use interpolated string without parameters
<!-- sources -->
Sources: [DoNotUseInterpolatedStringWithoutParametersAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/DoNotUseInterpolatedStringWithoutParametersAnalyzer.cs), [DoNotUseInterpolatedStringWithoutParametersFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseInterpolatedStringWithoutParametersFixer.cs)
<!-- sources -->

## Description

An interpolated string without any parameters is unnecessary and can be converted to a regular string literal for better clarity and consistency.

## Example

```csharp
// ❌ Bad: Using interpolated string without parameters
var message = $"Required attribute 'output' not found.";

// ✅ Good: Use a regular string literal
var message = "Required attribute 'output' not found.";
```

## Exceptions

This rule does not report diagnostics when:

1. The interpolated string is assigned to or converted to `FormattableString`:
```csharp
FormattableString fs = $"Hello"; // OK
```

2. The interpolated string is used with a custom `InterpolatedStringHandler`:
```csharp
void Method(CustomInterpolatedStringHandler handler) { }
Method($"Hello"); // OK if CustomInterpolatedStringHandler has InterpolatedStringHandlerAttribute
```

## Configuration

This rule is **disabled by default** as it's a stylistic preference that doesn't affect runtime behavior.

To enable it, add the following to your `.editorconfig`:

```ini
dotnet_diagnostic.MA0184.severity = suggestion
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;

namespace Meziantou.Analyzer.Rules;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class DoNotUseInterpolatedStringWithoutParametersFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.DoNotUseInterpolatedStringWithoutParameters);

public override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true);
if (nodeToFix is not InterpolatedStringExpressionSyntax interpolatedString)
return;

context.RegisterCodeFix(
CodeAction.Create(
"Convert to regular string",
ct => ConvertToRegularString(context.Document, interpolatedString, ct),
equivalenceKey: "Convert to regular string"),
context.Diagnostics);
}

private static async Task<Document> ConvertToRegularString(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+)
#if CSHARP10_OR_GREATER
var isRawString = interpolatedString.StringStartToken.IsKind(SyntaxKind.InterpolatedMultiLineRawStringStartToken) ||
interpolatedString.StringStartToken.IsKind(SyntaxKind.InterpolatedSingleLineRawStringStartToken);
#else
var isRawString = false;
#endif

if (isRawString)
{
// For raw strings, simply remove the $ prefix from the start token
// $""" text """ -> """ text """
var originalText = interpolatedString.ToFullString();

// 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);

editor.ReplaceNode(interpolatedString, newNode.WithTriviaFrom(interpolatedString));
}
}
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));

editor.ReplaceNode(interpolatedString, regularString.WithTriviaFrom(interpolatedString));
}

return editor.GetChangedDocument();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -544,3 +544,6 @@ dotnet_diagnostic.MA0182.severity = suggestion

# MA0183: string.Format should use a format string with placeholders
dotnet_diagnostic.MA0183.severity = warning

# MA0184: Do not use interpolated string without parameters
dotnet_diagnostic.MA0184.severity = none
3 changes: 3 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -544,3 +544,6 @@ dotnet_diagnostic.MA0182.severity = none

# MA0183: string.Format should use a format string with placeholders
dotnet_diagnostic.MA0183.severity = none

# MA0184: Do not use interpolated string without parameters
dotnet_diagnostic.MA0184.severity = none
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ internal static class RuleIdentifiers
public const string DoNotUseCast = "MA0181";
public const string AvoidUnusedInternalTypes = "MA0182";
public const string StringFormatShouldBeConstant = "MA0183";
public const string DoNotUseInterpolatedStringWithoutParameters = "MA0184";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Collections.Immutable;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DoNotUseInterpolatedStringWithoutParametersAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.DoNotUseInterpolatedStringWithoutParameters,
title: "Do not use interpolated string without parameters",
messageFormat: "Do not use interpolated string without parameters",
RuleCategories.Style,
DiagnosticSeverity.Info,
isEnabledByDefault: false,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.DoNotUseInterpolatedStringWithoutParameters));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterCompilationStartAction(ctx =>
{
var formattableStringSymbol = ctx.Compilation.GetBestTypeByMetadataName("System.FormattableString");
var interpolatedStringHandlerAttributeSymbol = ctx.Compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute");

ctx.RegisterOperationAction(context => AnalyzeInterpolatedString(context, formattableStringSymbol, interpolatedStringHandlerAttributeSymbol), OperationKind.InterpolatedString);
});
}

private static void AnalyzeInterpolatedString(OperationAnalysisContext context, INamedTypeSymbol? formattableStringSymbol, INamedTypeSymbol? interpolatedStringHandlerAttributeSymbol)
{
var operation = (IInterpolatedStringOperation)context.Operation;

// Only report if there are no interpolations (no parameters)
if (operation.Parts.Any(part => part is IInterpolationOperation))
return;

#if CSHARP10_OR_GREATER
// If there are IInterpolatedStringAppendOperation parts, it means a custom handler is being used
if (operation.Parts.Any(part => part is IInterpolatedStringAppendOperation))
return;
#endif

// Check if the target type is FormattableString
var parent = operation.Parent;
if (parent is IConversionOperation conversionOperation)
{
// If converting to FormattableString, don't report
if (conversionOperation.Type?.IsEqualTo(formattableStringSymbol) == true)
return;

// If converting to a custom InterpolatedStringHandler, don't report
if (IsInterpolatedStringHandler(conversionOperation.Type, interpolatedStringHandlerAttributeSymbol))
return;
}

// If assigned to FormattableString, don't report
if (parent is IVariableInitializerOperation variableInitializer)
{
if (variableInitializer.Parent is IVariableDeclaratorOperation declarator)
{
if (declarator.Symbol?.Type?.IsEqualTo(formattableStringSymbol) == true)
return;

if (IsInterpolatedStringHandler(declarator.Symbol?.Type, interpolatedStringHandlerAttributeSymbol))
return;
}
}

// Report diagnostic as a suggestion (Hidden severity with unnecessary tag)
var diagnostic = Diagnostic.Create(Rule, operation.Syntax.GetLocation());
context.ReportDiagnostic(diagnostic);
}

private static bool IsInterpolatedStringHandler(ITypeSymbol? typeSymbol, INamedTypeSymbol? interpolatedStringHandlerAttributeSymbol)
{
if (typeSymbol is null || interpolatedStringHandlerAttributeSymbol is null)
return false;

return typeSymbol.HasAttribute(interpolatedStringHandlerAttributeSymbol);
}
}
Loading