diff --git a/docs/Rules/MA0075.md b/docs/Rules/MA0075.md index 214fede1f..21955540b 100644 --- a/docs/Rules/MA0075.md +++ b/docs/Rules/MA0075.md @@ -41,3 +41,5 @@ MA0075.exclude_tostring_methods=true # Report Nullable.ToString when T is culture-sensitive MA0075.consider_nullable_types=true ```` + +You can also annotate a type with `[Meziantou.Analyzer.Annotations.CultureInsensitiveTypeAttribute]` to disable the rule for this type. diff --git a/docs/Rules/MA0076.md b/docs/Rules/MA0076.md index c897bfaea..38bd1f162 100644 --- a/docs/Rules/MA0076.md +++ b/docs/Rules/MA0076.md @@ -17,3 +17,5 @@ MA0076.exclude_tostring_methods=true # Report Nullable.ToString when T is culture-sensitive MA0076.consider_nullable_types=true ```` + +You can also annotate a type with `[Meziantou.Analyzer.Annotations.CultureInsensitiveTypeAttribute]` to disable the rule for this type. diff --git a/docs/Rules/MA0107.md b/docs/Rules/MA0107.md index 3b6f47dc2..2dd57b0b7 100644 --- a/docs/Rules/MA0107.md +++ b/docs/Rules/MA0107.md @@ -13,3 +13,5 @@ _ = FormattableString.Invariant($"{a}"); // compliant # Exclude ToString methods from analysis MA0107.exclude_tostring_methods=true ```` + +You can also annotate a type with `[Meziantou.Analyzer.Annotations.CultureInsensitiveTypeAttribute]` to disable the rule for this type. diff --git a/src/Meziantou.Analyzer.Annotations/CultureInsensitiveTypeAttribute.cs b/src/Meziantou.Analyzer.Annotations/CultureInsensitiveTypeAttribute.cs new file mode 100644 index 000000000..0284ffe95 --- /dev/null +++ b/src/Meziantou.Analyzer.Annotations/CultureInsensitiveTypeAttribute.cs @@ -0,0 +1,20 @@ +#pragma warning disable CS1591 + +using System; + +namespace Meziantou.Analyzer.Annotations; + +/// +/// Indicates that the type is culture insensitive. This can be used to suppress rules such as MA0075, MA0076 or MA0107. +/// [CultureInsensitiveType]class Foo { } +/// [assembly: CultureInsensitiveType(typeof(Foo))] +/// +[System.Diagnostics.Conditional("MEZIANTOU_ANALYZER_ANNOTATIONS")] +[System.AttributeUsage(System.AttributeTargets.Assembly | System.AttributeTargets.Struct | System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public sealed class CultureInsensitiveTypeAttribute : System.Attribute +{ + public CultureInsensitiveTypeAttribute() { } + public CultureInsensitiveTypeAttribute(System.Type type) => Type = type; + + public Type? Type { get; } +} diff --git a/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs b/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs index 7a5276c6f..3cae22484 100755 --- a/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs +++ b/src/Meziantou.Analyzer/Internals/CultureSensitiveFormattingContext.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Operations; @@ -6,6 +6,7 @@ namespace Meziantou.Analyzer.Internals; internal sealed class CultureSensitiveFormattingContext(Compilation compilation) { + public INamedTypeSymbol? CultureInsensitiveTypeAttributeSymbol { get; } = compilation.GetBestTypeByMetadataName("Meziantou.Analyzer.Annotations.CultureInsensitiveTypeAttribute"); public INamedTypeSymbol? FormatProviderSymbol { get; } = compilation.GetBestTypeByMetadataName("System.IFormatProvider"); public INamedTypeSymbol? CultureInfoSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Globalization.CultureInfo"); public INamedTypeSymbol? NumberStyleSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Globalization.NumberStyles"); @@ -252,7 +253,28 @@ private bool IsCultureSensitiveType(ITypeSymbol? typeSymbol, CultureSensitiveOpt if (typeSymbol.IsOrInheritFrom(SystemWindowsMediaBrushSymbol)) return false; - return typeSymbol.Implements(SystemIFormattableSymbol); + if (!typeSymbol.Implements(SystemIFormattableSymbol)) + return false; + + if (typeSymbol.HasAttribute(CultureInsensitiveTypeAttributeSymbol)) + return false; + + foreach (var attribute in compilation.Assembly.GetAttributes()) + { + if (attribute.ConstructorArguments.Length != 1) + continue; + + if (!attribute.AttributeClass.IsEqualTo(CultureInsensitiveTypeAttributeSymbol)) + continue; + + if (attribute.ConstructorArguments[0].Value is INamedTypeSymbol attributeType && attributeType.IsEqualTo(typeSymbol)) + return false; + } + + if (compilation.Assembly.HasAttribute(CultureInsensitiveTypeAttributeSymbol)) + return false; + + return true; } private bool IsCultureSensitiveType(ITypeSymbol? symbol, IOperation? format, IOperation? instance, CultureSensitiveOptions options) diff --git a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs index 927f4ef0d..c9e0cb6d6 100755 --- a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs +++ b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs @@ -11,6 +11,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +using Meziantou.Analyzer.Annotations; using Meziantou.Analyzer.Rules; using Meziantou.Analyzer.Test.Helpers; using Microsoft.CodeAnalysis; @@ -191,6 +192,13 @@ public ProjectBuilder AddAsyncInterfaceApi() => public ProjectBuilder AddSystemTextJson() => AddNuGetReference("System.Text.Json", "4.7.2", "lib/netstandard2.0/"); + public ProjectBuilder AddMeziantouAttributes() + { + var location = typeof(RequireNamedArgumentAttribute).Assembly.Location; + References.Add(MetadataReference.CreateFromFile(location)); + return this; + } + public ProjectBuilder WithOutputKind(OutputKind outputKind) { OutputKind = outputKind; diff --git a/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj b/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj index 8470adba6..f1280f6bd 100644 --- a/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj +++ b/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj @@ -26,6 +26,7 @@ + diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzerTests.cs index e4a0af045..5818d39db 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseImplicitCultureSensitiveToStringAnalyzerTests.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Meziantou.Analyzer.Rules; using Meziantou.Analyzer.Test.Helpers; using TestHelper; @@ -12,6 +12,7 @@ private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() .WithAnalyzer() + .AddMeziantouAttributes() .WithTargetFramework(TargetFramework.NetLatest); } @@ -352,6 +353,76 @@ void A() _ = "=" + (value == true); } } +"""; + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task IgnoreTypeUsingAssemblyAttribute() + { + var sourceCode = """ +[assembly: Meziantou.Analyzer.Annotations.CultureInsensitiveTypeAttribute(typeof(System.DateTime))] + +class Test +{ + void A() + { + _ = "abc" + new System.DateTime(); + } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task IgnoreTypeUsingAttribute() + { + var sourceCode = """ +class Test +{ + void A() + { + _ = "abc" + new Sample(); + } +} + +[Meziantou.Analyzer.Annotations.CultureInsensitiveTypeAttribute] +class Sample : System.IFormattable +{ + public string ToString(string? format, System.IFormatProvider? formatProvider) + { + return "abc"; + } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task CustomTypeImplementingIFormattable() + { + var sourceCode = """ +class Test +{ + void A() + { + _ = "abc" + [|new Sample()|]; + } +} + +class Sample : System.IFormattable +{ + public string ToString(string? format, System.IFormatProvider? formatProvider) + { + return "abc"; + } +} """; await CreateProjectBuilder() .WithSourceCode(sourceCode)