diff --git a/README.md b/README.md
index a0a3c87b..5b6ff092 100755
--- a/README.md
+++ b/README.md
@@ -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|ℹ️|✔️|✔️|
diff --git a/docs/README.md b/docs/README.md
index db87f4f8..2dbecba3 100755
--- a/docs/README.md
+++ b/docs/README.md
@@ -183,6 +183,7 @@
|[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|ℹ️|✔️|✔️|
|Id|Suppressed rule|Justification|
|--|---------------|-------------|
@@ -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
@@ -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
```
diff --git a/docs/Rules/MA0185.md b/docs/Rules/MA0185.md
new file mode 100644
index 00000000..b25ca178
--- /dev/null
+++ b/docs/Rules/MA0185.md
@@ -0,0 +1,28 @@
+# MA0185 - Simplify string.Create when all parameters are culture invariant
+
+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)
+
+
+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`
diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/SimplifyStringCreateWhenAllParametersAreCultureInvariantFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/SimplifyStringCreateWhenAllParametersAreCultureInvariantFixer.cs
new file mode 100644
index 00000000..1b2337be
--- /dev/null
+++ b/src/Meziantou.Analyzer.CodeFixers/Rules/SimplifyStringCreateWhenAllParametersAreCultureInvariantFixer.cs
@@ -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 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 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();
+ }
+}
diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
index 93c78851..0649345a 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
@@ -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
diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
index 7508a853..bb651f75 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
@@ -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
diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs
index 8c7e3b08..22d72ced 100755
--- a/src/Meziantou.Analyzer/RuleIdentifiers.cs
+++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs
@@ -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)
{
diff --git a/src/Meziantou.Analyzer/Rules/SimplifyStringCreateWhenAllParametersAreCultureInvariantAnalyzer.cs b/src/Meziantou.Analyzer/Rules/SimplifyStringCreateWhenAllParametersAreCultureInvariantAnalyzer.cs
new file mode 100644
index 00000000..15ee26ee
--- /dev/null
+++ b/src/Meziantou.Analyzer/Rules/SimplifyStringCreateWhenAllParametersAreCultureInvariantAnalyzer.cs
@@ -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 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()
+ .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().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;
+ }
+}
diff --git a/tests/Meziantou.Analyzer.Test/Rules/SimplifyStringCreateWhenAllParametersAreCultureInvariantAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/SimplifyStringCreateWhenAllParametersAreCultureInvariantAnalyzerTests.cs
new file mode 100644
index 00000000..94ff1265
--- /dev/null
+++ b/tests/Meziantou.Analyzer.Test/Rules/SimplifyStringCreateWhenAllParametersAreCultureInvariantAnalyzerTests.cs
@@ -0,0 +1,294 @@
+using Meziantou.Analyzer.Rules;
+using Meziantou.Analyzer.Test.Helpers;
+using TestHelper;
+
+namespace Meziantou.Analyzer.Test.Rules;
+
+public sealed class SimplifyStringCreateWhenAllParametersAreCultureInvariantAnalyzerTests
+{
+ private static ProjectBuilder CreateProjectBuilder()
+ {
+ return new ProjectBuilder()
+ .WithAnalyzer()
+ .WithCodeFixProvider()
+ .WithTargetFramework(TargetFramework.Net6_0);
+ }
+
+#if CSHARP10_OR_GREATER
+ [Fact]
+ public async Task StringCreateWithInvariantCulture_OnlyCultureInvariantParameters_ShouldReport()
+ {
+ const string SourceCode = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var x = [|string.Create(CultureInfo.InvariantCulture, $"Current time is {DateTime.Now:O}.")|];
+ }
+}
+""";
+
+ const string Fix = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var x = $"Current time is {DateTime.Now:O}.";
+ }
+}
+""";
+ await CreateProjectBuilder()
+ .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp10)
+ .WithTargetFramework(TargetFramework.Net6_0)
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(Fix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task StringCreateWithInvariantCulture_WithString_ShouldReport()
+ {
+ const string SourceCode = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var name = "test";
+ var x = [|string.Create(CultureInfo.InvariantCulture, $"Name: {name}")|];
+ }
+}
+""";
+
+ const string Fix = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var name = "test";
+ var x = $"Name: {name}";
+ }
+}
+""";
+ await CreateProjectBuilder()
+ .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp10)
+ .WithTargetFramework(TargetFramework.Net6_0)
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(Fix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task StringCreateWithInvariantCulture_WithGuid_ShouldReport()
+ {
+ const string SourceCode = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var id = Guid.NewGuid();
+ var x = [|string.Create(CultureInfo.InvariantCulture, $"ID: {id}")|];
+ }
+}
+""";
+
+ const string Fix = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var id = Guid.NewGuid();
+ var x = $"ID: {id}";
+ }
+}
+""";
+ await CreateProjectBuilder()
+ .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp10)
+ .WithTargetFramework(TargetFramework.Net6_0)
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(Fix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task StringCreateWithInvariantCulture_WithTimeSpanInvariantFormat_ShouldReport()
+ {
+ const string SourceCode = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var duration = TimeSpan.FromSeconds(42);
+ var x = [|string.Create(CultureInfo.InvariantCulture, $"Duration: {duration:c}")|];
+ }
+}
+""";
+
+ const string Fix = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var duration = TimeSpan.FromSeconds(42);
+ var x = $"Duration: {duration:c}";
+ }
+}
+""";
+ await CreateProjectBuilder()
+ .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp10)
+ .WithTargetFramework(TargetFramework.Net6_0)
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(Fix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task StringCreateWithInvariantCulture_WithCultureSensitiveParameter_NoDiagnostic()
+ {
+ const string SourceCode = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var price = 42.5;
+ var x = string.Create(CultureInfo.InvariantCulture, $"Price: {price}");
+ }
+}
+""";
+ await CreateProjectBuilder()
+ .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp10)
+ .WithTargetFramework(TargetFramework.Net6_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task StringCreateWithInvariantCulture_WithDateTimeNonInvariantFormat_NoDiagnostic()
+ {
+ const string SourceCode = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var x = string.Create(CultureInfo.InvariantCulture, $"Current time is {DateTime.Now:d}.");
+ }
+}
+""";
+ await CreateProjectBuilder()
+ .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp10)
+ .WithTargetFramework(TargetFramework.Net6_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task StringCreateWithCurrentCulture_NoDiagnostic()
+ {
+ const string SourceCode = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var x = string.Create(CultureInfo.CurrentCulture, $"Current time is {DateTime.Now:O}.");
+ }
+}
+""";
+ await CreateProjectBuilder()
+ .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp10)
+ .WithTargetFramework(TargetFramework.Net6_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task StringCreateWithInvariantCulture_LiteralOnly_ShouldReport()
+ {
+ const string SourceCode = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var x = [|string.Create(CultureInfo.InvariantCulture, $"Hello World")|];
+ }
+}
+""";
+
+ const string Fix = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var x = $"Hello World";
+ }
+}
+""";
+ await CreateProjectBuilder()
+ .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp10)
+ .WithTargetFramework(TargetFramework.Net6_0)
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(Fix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task StringCreateWithInvariantCulture_WithInteger_NoDiagnostic()
+ {
+ const string SourceCode = """
+using System;
+using System.Globalization;
+
+class TypeName
+{
+ public void Test()
+ {
+ var count = 42;
+ var x = string.Create(CultureInfo.InvariantCulture, $"Count: {count}");
+ }
+}
+""";
+ await CreateProjectBuilder()
+ .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp10)
+ .WithTargetFramework(TargetFramework.Net6_0)
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+#endif
+}