From 1c762ff06db54f54ac0e1047b0ad774d82a999e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Thu, 23 Oct 2025 23:51:36 -0400 Subject: [PATCH] Add support for culture-insensitive default formats --- .../CultureInsensitiveTypeAttribute.cs | 4 ++ .../Meziantou.Analyzer.Annotations.csproj | 2 +- .../CultureSensitiveFormattingContext.cs | 42 +++++++++++++++++-- ...itCultureSensitiveToStringAnalyzerTests.cs | 42 +++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/Meziantou.Analyzer.Annotations/CultureInsensitiveTypeAttribute.cs b/src/Meziantou.Analyzer.Annotations/CultureInsensitiveTypeAttribute.cs index ef777ee8..513c5525 100644 --- a/src/Meziantou.Analyzer.Annotations/CultureInsensitiveTypeAttribute.cs +++ b/src/Meziantou.Analyzer.Annotations/CultureInsensitiveTypeAttribute.cs @@ -12,10 +12,14 @@ public sealed class CultureInsensitiveTypeAttribute : System.Attribute { public CultureInsensitiveTypeAttribute() { } public CultureInsensitiveTypeAttribute(string? format) => Format = format; + public CultureInsensitiveTypeAttribute(bool isDefaultFormatCultureInsensitive) => IsDefaultFormatCultureInsensitive = isDefaultFormatCultureInsensitive; public CultureInsensitiveTypeAttribute(System.Type type) => Type = type; public CultureInsensitiveTypeAttribute(System.Type type, string? format) => (Type, Format) = (type, format); + public CultureInsensitiveTypeAttribute(System.Type type, bool isDefaultFormatCultureInsensitive) + => (Type, IsDefaultFormatCultureInsensitive) = (type, isDefaultFormatCultureInsensitive); public Type? Type { get; } public string? Format { get; } + public bool IsDefaultFormatCultureInsensitive { get; } } diff --git a/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj b/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj index 88155ab0..f9dd6ba7 100644 --- a/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj +++ b/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 1.1.1 + 1.2.0 Annotations to configure Meziantou.Analyzer Meziantou.Analyzer, analyzers True diff --git a/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs b/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs index ff015a02..0f96fd00 100755 --- a/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs +++ b/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs @@ -286,12 +286,36 @@ private bool IsCultureSensitiveType(ITypeSymbol? typeSymbol, CultureSensitiveOpt if (!typeSymbol.Implements(SystemIFormattableSymbol)) return false; - if (!IsCultureSensitiveTypeUsingAttribute(typeSymbol, hasFormat: false, format: null)) + if (!IsCultureSensitiveTypeUsingAttribute(typeSymbol)) return false; return true; } + private bool IsCultureSensitiveTypeUsingAttribute(ITypeSymbol typeSymbol) + { + var attributes = typeSymbol.GetAttributes(CultureInsensitiveTypeAttributeSymbol); + foreach (var attr in attributes) + { + if (attr.ConstructorArguments.IsEmpty) + return false; // no format is set, so the type is culture insensitive + } + + foreach (var attribute in compilation.Assembly.GetAttributes(CultureInsensitiveTypeAttributeSymbol)) + { + if (attribute.ConstructorArguments.IsEmpty) + continue; + + if (attribute.ConstructorArguments[0].Value is INamedTypeSymbol attributeType && attributeType.IsEqualTo(typeSymbol)) + { + if (attribute.ConstructorArguments.Length == 1) + return false; + } + } + + return true; + } + private bool IsCultureSensitiveTypeUsingAttribute(ITypeSymbol typeSymbol, bool hasFormat, string? format) { var attributes = typeSymbol.GetAttributes(CultureInsensitiveTypeAttributeSymbol); @@ -300,10 +324,16 @@ private bool IsCultureSensitiveTypeUsingAttribute(ITypeSymbol typeSymbol, bool h if (attr.ConstructorArguments.IsEmpty) return false; // no format is set, so the type is culture insensitive + var attrValue = attr.ConstructorArguments[0].Value; if (!hasFormat) + { + if (attrValue is bool isDefaultFormatCultureInsensitive && isDefaultFormatCultureInsensitive) + return false; + continue; + } - var attrFormat = attr.ConstructorArguments[0].Value as string; + var attrFormat = attrValue as string; if (attrFormat == format) return false; // no format is set, so the type is culture insensitive } @@ -318,10 +348,16 @@ private bool IsCultureSensitiveTypeUsingAttribute(ITypeSymbol typeSymbol, bool h if (attribute.ConstructorArguments.Length == 1) return false; + var attrValue = attribute.ConstructorArguments[1].Value; if (!hasFormat) + { + if (attrValue is bool isDefaultFormatCultureInsensitive && isDefaultFormatCultureInsensitive) + return false; + continue; + } - var attrFormat = attribute.ConstructorArguments[1].Value as string; + var attrFormat = attrValue as string; if (attrFormat == format) return false; // no format is set, so the type is culture insensitive } diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzerTests.cs index ddb80038..ed0a0274 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzerTests.cs @@ -416,6 +416,48 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Theory] + [InlineData(""" $"abc{new System.DateTime()}" """)] + [InlineData(""" $"abc[|{new System.DateTime():a}|]" """)] + public async Task IgnoreTypeUsingAssemblyAttribute_WithFormat_DefaultFormatInvariant(string content) + { + var sourceCode = $$""" +[assembly: Meziantou.Analyzer.Annotations.CultureInsensitiveTypeAttribute(typeof(System.DateTime), isDefaultFormatCultureInsensitive: true)] + +class Test +{ + void A() + { + _ = {{content}}; + } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Theory] + [InlineData(""" $"abc[|{new System.DateTime()}|]" """)] + [InlineData(""" $"abc[|{new System.DateTime():a}|]" """)] + public async Task IgnoreTypeUsingAssemblyAttribute_WithFormat_DefaultFormatCultureSensitive(string content) + { + var sourceCode = $$""" +[assembly: Meziantou.Analyzer.Annotations.CultureInsensitiveTypeAttribute(typeof(System.DateTime), isDefaultFormatCultureInsensitive: false)] + +class Test +{ + void A() + { + _ = {{content}}; + } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + [Fact] public async Task IgnoreTypeUsingAssemblyAttribute_WithFormatNotMatchingAttribute() {