diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4891cb8c..d7c1e8c6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -67,12 +67,14 @@ jobs:
- run: dotnet build src/Meziantou.Analyzer/Meziantou.Analyzer.csproj --configuration Release /p:RoslynVersion=roslyn4.4 /p:Version=${{ needs.compute_package_version.outputs.package_version }} /bl:Meziantou.Analyzer.roslyn4.4.binlog
- run: dotnet build src/Meziantou.Analyzer/Meziantou.Analyzer.csproj --configuration Release /p:RoslynVersion=roslyn4.6 /p:Version=${{ needs.compute_package_version.outputs.package_version }} /bl:Meziantou.Analyzer.roslyn4.6.binlog
- run: dotnet build src/Meziantou.Analyzer/Meziantou.Analyzer.csproj --configuration Release /p:RoslynVersion=roslyn4.8 /p:Version=${{ needs.compute_package_version.outputs.package_version }} /bl:Meziantou.Analyzer.roslyn4.8.binlog
+ - run: dotnet build src/Meziantou.Analyzer/Meziantou.Analyzer.csproj --configuration Release /p:RoslynVersion=roslyn4.14 /p:Version=${{ needs.compute_package_version.outputs.package_version }} /bl:Meziantou.Analyzer.roslyn4.14.binlog
- run: dotnet build src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj --configuration Release /p:RoslynVersion=roslyn3.8 /p:Version=${{ needs.compute_package_version.outputs.package_version }} /bl:Meziantou.Analyzer.CodeFixers.roslyn3.8.binlog
- run: dotnet build src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj --configuration Release /p:RoslynVersion=roslyn4.2 /p:Version=${{ needs.compute_package_version.outputs.package_version }} /bl:Meziantou.Analyzer.CodeFixers.roslyn4.2.binlog
- run: dotnet build src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj --configuration Release /p:RoslynVersion=roslyn4.4 /p:Version=${{ needs.compute_package_version.outputs.package_version }} /bl:Meziantou.Analyzer.CodeFixers.roslyn4.4.binlog
- run: dotnet build src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj --configuration Release /p:RoslynVersion=roslyn4.6 /p:Version=${{ needs.compute_package_version.outputs.package_version }} /bl:Meziantou.Analyzer.CodeFixers.roslyn4.6.binlog
- run: dotnet build src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj --configuration Release /p:RoslynVersion=roslyn4.8 /p:Version=${{ needs.compute_package_version.outputs.package_version }} /bl:Meziantou.Analyzer.CodeFixers.roslyn4.8.binlog
+ - run: dotnet build src/Meziantou.Analyzer.CodeFixers/Meziantou.Analyzer.CodeFixers.csproj --configuration Release /p:RoslynVersion=roslyn4.14 /p:Version=${{ needs.compute_package_version.outputs.package_version }} /bl:Meziantou.Analyzer.CodeFixers.roslyn4.14.binlog
- run: dotnet restore src/Meziantou.Analyzer.Pack/Meziantou.Analyzer.Pack.csproj -bl:Meziantou.Analyzer.Pack.restore.binlog
- run: dotnet pack src/Meziantou.Analyzer.Pack/Meziantou.Analyzer.Pack.csproj --configuration Release --no-build /p:Version=${{ needs.compute_package_version.outputs.package_version }} /bl:Meziantou.Analyzer.Pack.pack.binlog
@@ -127,7 +129,7 @@ jobs:
matrix:
runs-on: [ ubuntu-latest ]
configuration: [ Release ]
- roslyn-version: [ 'roslyn3.8', 'roslyn4.2', 'roslyn4.4', 'roslyn4.6', 'roslyn4.8', 'default' ]
+ roslyn-version: [ 'roslyn3.8', 'roslyn4.2', 'roslyn4.4', 'roslyn4.6', 'roslyn4.8', 'roslyn4.14', 'default' ]
fail-fast: false
steps:
- uses: actions/checkout@v4
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 45198f25..3561832b 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -72,6 +72,19 @@
+
+
+
+
+
+
+
+ $(DefineConstants);ROSLYN_4_8;ROSLYN_4_2_OR_GREATER;ROSLYN_4_4_OR_GREATER;ROSLYN_4_6_OR_GREATER;ROSLYN_4_8_OR_GREATER;ROSLYN_4_14_OR_GREATER
+ $(DefineConstants);CSHARP9_OR_GREATER;CSHARP10_OR_GREATER;CSHARP11_OR_GREATER;CSHARP12_OR_GREATER;CSHARP13_OR_GREATER
+ $(NoWarn);nullable
+
+
+
@@ -79,7 +92,7 @@
- $(DefineConstants);ROSLYN5_0;ROSLYN_4_2_OR_GREATER;ROSLYN_4_4_OR_GREATER;ROSLYN_4_5_OR_GREATER;ROSLYN_4_6_OR_GREATER;ROSLYN_4_8_OR_GREATER;ROSLYN_4_10_OR_GREATER;ROSLYN_5_0_OR_GREATER
+ $(DefineConstants);ROSLYN5_0;ROSLYN_4_2_OR_GREATER;ROSLYN_4_4_OR_GREATER;ROSLYN_4_5_OR_GREATER;ROSLYN_4_6_OR_GREATER;ROSLYN_4_8_OR_GREATER;ROSLYN_4_10_OR_GREATER;ROSLYN_4_14_OR_GREATER;ROSLYN_5_0_OR_GREATER
$(DefineConstants);CSHARP9_OR_GREATER;CSHARP10_OR_GREATER;CSHARP11_OR_GREATER;CSHARP12_OR_GREATER;CSHARP13_OR_GREATER;CSHARP14_OR_GREATER
$(NoWarn);CS0618
diff --git a/src/Meziantou.Analyzer.Pack/Meziantou.Analyzer.Pack.csproj b/src/Meziantou.Analyzer.Pack/Meziantou.Analyzer.Pack.csproj
index 3bdaf071..11518327 100644
--- a/src/Meziantou.Analyzer.Pack/Meziantou.Analyzer.Pack.csproj
+++ b/src/Meziantou.Analyzer.Pack/Meziantou.Analyzer.Pack.csproj
@@ -37,5 +37,8 @@
+
+
+
\ No newline at end of file
diff --git a/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs b/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs
index cdf10d05..8a8d6f1a 100755
--- a/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs
+++ b/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs
@@ -26,6 +26,8 @@ internal sealed class CultureSensitiveFormattingContext(Compilation compilation)
public INamedTypeSymbol? SystemWindowsFontStretchSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Windows.FontStretch");
public INamedTypeSymbol? SystemWindowsMediaBrushSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Windows.Media.Brush");
public INamedTypeSymbol? NuGetVersioningSemanticVersionSymbol { get; } = compilation.GetBestTypeByMetadataName("NuGet.Versioning.SemanticVersion");
+ public INamedTypeSymbol? FormattableStringSymbol { get; } = compilation.GetBestTypeByMetadataName("System.FormattableString");
+ public INamedTypeSymbol? InterpolatedStringHandlerAttributeSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute");
private static HashSet CreateExcludedMethods(Compilation compilation)
{
@@ -127,12 +129,22 @@ public bool IsCultureSensitiveOperation(IOperation operation, CultureSensitiveOp
return initializer.ElementValues.Any(arg => IsCultureSensitiveOperation(arg.UnwrapImplicitConversionOperations(), options));
}
+#if ROSLYN_4_14_OR_GREATER
+ else if (invocation.TargetMethod.Parameters.Length == 2 && invocation.Arguments[1].Value is ICollectionExpressionOperation collectionExpression)
+ {
+ return collectionExpression.Elements.Any(arg => IsCultureSensitiveOperation(arg.UnwrapImplicitConversionOperations(), options));
+ }
+#endif
else
{
return invocation.Arguments.Skip(1).Any(arg => IsCultureSensitiveOperation(arg.Value.UnwrapImplicitConversionOperations(), options));
}
}
+ // Check if all interpolated string arguments are culture-invariant
+ if (HasOnlyCultureInvariantInterpolatedStringArguments(invocation, options))
+ return false;
+
if ((options & CultureSensitiveOptions.UseInvocationReturnType) == CultureSensitiveOptions.UseInvocationReturnType)
return IsCultureSensitiveType(invocation.Type, options);
@@ -175,6 +187,9 @@ public bool IsCultureSensitiveOperation(IOperation operation, CultureSensitiveOp
}
#endif
+ if (operation is IConversionOperation interpolatedConversion && interpolatedConversion.Type.IsEqualTo(FormattableStringSymbol))
+ return IsCultureSensitiveOperation(interpolatedConversion.Operand, options);
+
if (operation is IInterpolatedStringOperation interpolatedString)
{
if (interpolatedString.Parts.Length == 0)
@@ -482,4 +497,39 @@ private static bool IsConstantPositiveNumber(IOperation operation)
return false;
}
+
+ private bool HasOnlyCultureInvariantInterpolatedStringArguments(IInvocationOperation invocation, CultureSensitiveOptions options)
+ {
+ var hasInterpolatedStringParam = false;
+
+ foreach (var argument in invocation.Arguments)
+ {
+ var argumentType = argument.Value.Type;
+ if (argumentType is null)
+ continue;
+
+ if (IsInterpolatedStringType(argumentType))
+ {
+ hasInterpolatedStringParam = true;
+
+ // If any interpolated string argument is culture-sensitive, return false
+ if (IsCultureSensitiveOperation(argument.Value, options))
+ return false;
+ }
+ }
+
+ // Return true only if we found interpolated string parameters and all were culture-invariant
+ return hasInterpolatedStringParam;
+ }
+
+ private bool IsInterpolatedStringType(ITypeSymbol typeSymbol)
+ {
+ if (typeSymbol.IsEqualTo(FormattableStringSymbol))
+ return true;
+
+ if (InterpolatedStringHandlerAttributeSymbol is not null && typeSymbol.HasAttribute(InterpolatedStringHandlerAttributeSymbol))
+ return true;
+
+ return false;
+ }
}
diff --git a/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs
index e17245c9..493d5e1c 100644
--- a/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs
+++ b/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs
@@ -1,4 +1,4 @@
-using System.Collections.Immutable;
+using System.Collections.Immutable;
using Meziantou.Analyzer.Configurations;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseIFormatProviderAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseIFormatProviderAnalyzerTests.cs
index 752da8f3..a8900552 100755
--- a/tests/Meziantou.Analyzer.Test/Rules/UseIFormatProviderAnalyzerTests.cs
+++ b/tests/Meziantou.Analyzer.Test/Rules/UseIFormatProviderAnalyzerTests.cs
@@ -11,6 +11,7 @@ private static ProjectBuilder CreateProjectBuilder()
return new ProjectBuilder()
.WithAnalyzer()
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication)
+ .WithTargetFramework(TargetFramework.NetLatest)
.AddMeziantouAttributes();
}
@@ -314,6 +315,199 @@ class Location
public override string ToString() => throw null;
public string ToString(System.IFormatProvider formatProvider) => throw null;
}
+""";
+ await CreateProjectBuilder()
+ .WithSourceCode(sourceCode)
+ .ValidateAsync();
+ }
+
+#if CSHARP10_OR_GREATER
+ [Fact]
+ public async Task InterpolatedStringHandler_CultureSensitiveFormat_ShouldReport()
+ {
+ var sourceCode = """
+using System;
+using System.Runtime.CompilerServices;
+
+[||]A.Print($"{DateTime.Now:D}");
+
+class A
+{
+ public static void Print(ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+ public static void Print(IFormatProvider provider, ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+}
+""";
+ await CreateProjectBuilder()
+ .WithSourceCode(sourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InterpolatedStringHandler_CultureInvariantFormat_ShouldNotReport()
+ {
+ var sourceCode = """
+using System;
+using System.Runtime.CompilerServices;
+
+A.Print($"{DateTime.Now:o}");
+
+class A
+{
+ public static void Print(ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+ public static void Print(IFormatProvider provider, ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+}
+""";
+ await CreateProjectBuilder()
+ .WithSourceCode(sourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InterpolatedStringHandler_NoFormattableTypes_ShouldNotReport()
+ {
+ var sourceCode = """
+using System;
+using System.Runtime.CompilerServices;
+
+A.Print($"XXX");
+
+class A
+{
+ public static void Print(ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+ public static void Print(IFormatProvider provider, ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+}
+""";
+ await CreateProjectBuilder()
+ .WithSourceCode(sourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InterpolatedStringHandler_MixedFormats_ShouldReport()
+ {
+ var sourceCode = """
+using System;
+using System.Runtime.CompilerServices;
+
+[||]A.Print($"{DateTime.Now:o} | {DateTime.Now:D}");
+
+class A
+{
+ public static void Print(ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+ public static void Print(IFormatProvider provider, ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+}
+""";
+ await CreateProjectBuilder()
+ .WithSourceCode(sourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InterpolatedStringHandler_CustomTypeWithAttribute_CultureInvariantFormat_ShouldNotReport()
+ {
+ var sourceCode = """
+using System;
+using System.Runtime.CompilerServices;
+using Meziantou.Analyzer.Annotations;
+
+A.Print($"{new Bar():o}");
+
+class A
+{
+ public static void Print(ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+ public static void Print(IFormatProvider provider, ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+}
+
+[CultureInsensitiveType(format: "o")]
+sealed class Bar : IFormattable
+{
+ public string ToString(string? format, IFormatProvider? formatProvider) => string.Empty;
+}
+""";
+ await CreateProjectBuilder()
+ .WithSourceCode(sourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InterpolatedStringHandler_CustomTypeWithAttribute_CultureSensitiveFormat_ShouldReport()
+ {
+ var sourceCode = """
+using System;
+using System.Runtime.CompilerServices;
+using Meziantou.Analyzer.Annotations;
+
+[||]A.Print($"{new Bar():D}");
+
+class A
+{
+ public static void Print(ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+ public static void Print(IFormatProvider provider, ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+}
+
+[CultureInsensitiveType(format: "o")]
+sealed class Bar : IFormattable
+{
+ public string ToString(string? format, IFormatProvider? formatProvider) => string.Empty;
+}
+""";
+ await CreateProjectBuilder()
+ .WithSourceCode(sourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task InterpolatedStringHandler_NoOverload_ShouldNotReport()
+ {
+ var sourceCode = """
+using System;
+using System.Runtime.CompilerServices;
+
+A.Print($"{DateTime.Now:D}");
+
+class A
+{
+ public static void Print(ref DefaultInterpolatedStringHandler interpolatedStringHandler) => throw null;
+}
+""";
+ await CreateProjectBuilder()
+ .WithSourceCode(sourceCode)
+ .ValidateAsync();
+ }
+#endif
+
+ [Fact]
+ public async Task FormattableString_CultureSensitiveFormat_ShouldReport()
+ {
+ var sourceCode = """
+using System;
+
+[||]A.Sample($"{DateTime.Now:D}");
+
+class A
+{
+ public static void Sample(FormattableString value) => throw null;
+ public static void Sample(IFormatProvider format, FormattableString value) => throw null;
+}
+""";
+ await CreateProjectBuilder()
+ .WithSourceCode(sourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task FormattableString_CultureInvariantFormat_ShouldNotReport()
+ {
+ var sourceCode = """
+using System;
+
+A.Sample($"{DateTime.Now:o}");
+
+class A
+{
+ public static void Sample(FormattableString value) => throw null;
+ public static void Sample(IFormatProvider format, FormattableString value) => throw null;
+}
""";
await CreateProjectBuilder()
.WithSourceCode(sourceCode)