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 @@ -199,6 +199,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[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|ℹ️|❌|✔️|
|[MA0185](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0185.md)|Performance|Simplify string.Create when all parameters are culture invariant|ℹ️|✔️|✔️|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
|[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>|❌|✔️|
|[MA0185](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0185.md)|Performance|Simplify string.Create when all parameters are culture invariant|<span title='Info'>ℹ️</span>|✔️|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -747,6 +748,9 @@ dotnet_diagnostic.MA0183.severity = warning

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

# MA0185: Simplify string.Create when all parameters are culture invariant
dotnet_diagnostic.MA0185.severity = suggestion
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1297,4 +1301,7 @@ dotnet_diagnostic.MA0183.severity = none

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

# MA0185: Simplify string.Create when all parameters are culture invariant
dotnet_diagnostic.MA0185.severity = none
```
28 changes: 28 additions & 0 deletions docs/Rules/MA0185.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# MA0185 - Simplify string.Create when all parameters are culture invariant
<!-- sources -->
Sources: [SimplifyStringCreateWhenAllParametersAreCultureInvariantAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/SimplifyStringCreateWhenAllParametersAreCultureInvariantAnalyzer.cs), [SimplifyStringCreateWhenAllParametersAreCultureInvariantFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/SimplifyStringCreateWhenAllParametersAreCultureInvariantFixer.cs)
<!-- sources -->

When using `string.Create(CultureInfo.InvariantCulture, ...)` with an interpolated string where all parameters are culture-invariant, you can simplify the code by using a simple interpolated string instead.

````c#
// ❌ Unnecessary use of string.Create with culture-invariant parameters
var x = string.Create(CultureInfo.InvariantCulture, $"Current time is {DateTime.Now:O}.");

// ✅ Simplified version
var y = $"Current time is {DateTime.Now:O}.";
````

The analyzer detects when all interpolated values are culture-invariant, such as:
- Strings
- GUIDs
- DateTime with invariant formats (O, o, R, r, s, u)
- TimeSpan with invariant formats (c, t, T)
- Unsigned integers
- Enum values
- Boolean values
- and other culture-insensitive types

The analyzer will NOT suggest simplification when:
- Any parameter is culture-sensitive (e.g., `double`, `DateTime` with culture-sensitive format)
- The culture is not `CultureInfo.InvariantCulture`
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules;

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

public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

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

var title = "Simplify to interpolated string";
var codeAction = CodeAction.Create(
title,
ct => Fix(context.Document, nodeToFix, ct),
equivalenceKey: title);

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

private static async Task<Document> Fix(Document document, InvocationExpressionSyntax nodeToFix, CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

if (editor.SemanticModel.GetOperation(nodeToFix, cancellationToken) is not IInvocationOperation op)
return document;

// Get the interpolated string from the second argument
if (op.Arguments.Length != 2)
return document;

var interpolatedStringArgument = op.Arguments[1];

#if CSHARP10_OR_GREATER
if (interpolatedStringArgument.Value is IInterpolatedStringHandlerCreationOperation handlerCreation)
{
// The Content property contains the interpolated string operation
if (handlerCreation.Content is IInterpolatedStringOperation interpolatedString)
{
// Get the syntax of the interpolated string
var interpolatedStringSyntax = interpolatedString.Syntax as InterpolatedStringExpressionSyntax;
if (interpolatedStringSyntax is not null)
{
editor.ReplaceNode(nodeToFix, interpolatedStringSyntax);
}
}
}
#endif

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

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

# MA0185: Simplify string.Create when all parameters are culture invariant
dotnet_diagnostic.MA0185.severity = suggestion
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 @@ -547,3 +547,6 @@ dotnet_diagnostic.MA0183.severity = none

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

# MA0185: Simplify string.Create when all parameters are culture invariant
dotnet_diagnostic.MA0185.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 @@ -184,6 +184,7 @@ internal static class RuleIdentifiers
public const string AvoidUnusedInternalTypes = "MA0182";
public const string StringFormatShouldBeConstant = "MA0183";
public const string DoNotUseInterpolatedStringWithoutParameters = "MA0184";
public const string SimplifyStringCreateWhenAllParametersAreCultureInvariant = "MA0185";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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 SimplifyStringCreateWhenAllParametersAreCultureInvariantAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.SimplifyStringCreateWhenAllParametersAreCultureInvariant,
title: "Simplify string.Create when all parameters are culture invariant",
messageFormat: "Simplify string.Create when all parameters are culture invariant",
RuleCategories.Performance,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.SimplifyStringCreateWhenAllParametersAreCultureInvariant));

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

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

context.RegisterCompilationStartAction(ctx =>
{
if (!ctx.Compilation.GetCSharpLanguageVersion().IsCSharp10OrAbove())
return;

var formatProviderSymbol = ctx.Compilation.GetBestTypeByMetadataName("System.IFormatProvider");
var cultureInfoSymbol = ctx.Compilation.GetBestTypeByMetadataName("System.Globalization.CultureInfo");
var defaultInterpolatedStringHandlerSymbol = ctx.Compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.DefaultInterpolatedStringHandler");

var stringCreateSymbol = ctx.Compilation.GetSpecialType(SpecialType.System_String)
.GetMembers("Create")
.OfType<IMethodSymbol>()
.FirstOrDefault(m => m.ReturnType.IsString() && m.Parameters.Length == 2 && m.Parameters[0].Type.IsEqualTo(formatProviderSymbol) && m.Parameters[1].Type.IsEqualTo(defaultInterpolatedStringHandlerSymbol));

if (stringCreateSymbol is null || cultureInfoSymbol is null)
return;

var cultureInfoInvariantCultureProperty = cultureInfoSymbol.GetMembers("InvariantCulture").OfType<IPropertySymbol>().FirstOrDefault();
if (cultureInfoInvariantCultureProperty is null)
return;

var cultureSensitiveContext = new CultureSensitiveFormattingContext(ctx.Compilation);

ctx.RegisterOperationAction(context => AnalyzeInvocation(context, stringCreateSymbol, cultureInfoInvariantCultureProperty, cultureSensitiveContext), OperationKind.Invocation);
});
}

private static void AnalyzeInvocation(OperationAnalysisContext context, IMethodSymbol stringCreateSymbol, IPropertySymbol cultureInfoInvariantCultureProperty, CultureSensitiveFormattingContext cultureSensitiveContext)
{
var operation = (IInvocationOperation)context.Operation;

if (!operation.TargetMethod.IsEqualTo(stringCreateSymbol))
return;

// Check if the first argument is CultureInfo.InvariantCulture
if (operation.Arguments.Length != 2)
return;

var cultureArgument = operation.Arguments[0].Value;
if (!IsCultureInfoInvariantCulture(cultureArgument, cultureInfoInvariantCultureProperty))
return;

// Check if the second argument (interpolated string handler) has only culture-invariant parameters
var interpolatedStringArgument = operation.Arguments[1].Value;

#if CSHARP10_OR_GREATER
if (interpolatedStringArgument is IInterpolatedStringHandlerCreationOperation handlerCreation)
{
var interpolatedStringContent = handlerCreation.Content;
if (!cultureSensitiveContext.IsCultureSensitiveOperation(interpolatedStringContent, CultureSensitiveOptions.None))
{
context.ReportDiagnostic(Rule, operation);
}
}
#endif
}

private static bool IsCultureInfoInvariantCulture(IOperation operation, IPropertySymbol cultureInfoInvariantCultureProperty)
{
operation = operation.UnwrapImplicitConversionOperations();

if (operation is IPropertyReferenceOperation propertyReference)
{
return SymbolEqualityComparer.Default.Equals(propertyReference.Property, cultureInfoInvariantCultureProperty);
}

return false;
}
}
Loading