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
2 changes: 1 addition & 1 deletion docs/Rules/MA0075.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ _ = "abc" + -1.ToString(CultureInfo.InvariantCulture); // compliant

A type is culture-sensitive when it implements `System.IFormattable` or `System.ISpanFormattable`, or exposes a provider-aware `ToString` overload. A type is culture-insensitive when it is known to be invariant, is marked with `CultureInsensitiveTypeAttribute`, or is sealed and does not support culture-aware formatting. Values typed as `object`, an interface, or an unconstrained type parameter are opaque runtime types. Non-sealed classes are tracked separately. These two categories are not reported by default; set `MA0075.treat_opaque_runtime_types_as_culture_sensitive` or `MA0075.treat_unsealed_types_as_culture_sensitive` to `true` to report them. A union type is culture-sensitive when at least one of its case types is culture-sensitive.<br/>
Known culture-invariant types include:
* Any enum
* Any enum, including values typed as `System.Enum`
* System.Byte
* System.Char
* System.Guid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ public CultureSensitivity GetCultureSensitivity(IOperation operation, CultureSen
return CultureSensitivity.CultureSensitive;
}

// "value?.ToString()" formats the value when it is not null, so the culture sensitivity is the one of the accessed value
if (operation is IConditionalAccessOperation conditionalAccess)
return GetCultureSensitivity(conditionalAccess.WhenNotNull, options);

if (operation is IInterpolatedStringHandlerCreationOperation handler)
return GetCultureSensitivity(handler.Content, options);

Expand Down Expand Up @@ -427,6 +431,11 @@ private CultureSensitivity GetCultureSensitivity(ITypeSymbol? typeSymbol, Cultur
if (typeSymbol.IsEnum())
return CultureSensitivity.CultureInsensitive;

// The ToString overloads of an enum are declared on System.Enum, so the containing type of an invocation
// such as 'enumValue.ToString("G")' is System.Enum. They ignore the format provider, so they are culture-insensitive.
if (typeSymbol.IsEqualTo(EnumSymbol))
return CultureSensitivity.CultureInsensitive;

if (typeSymbol.SpecialType == SpecialType.System_Boolean)
return CultureSensitivity.CultureInsensitive;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,215 @@ await CreateProjectBuilder()
.ValidateAsync();
}

[Theory]
[InlineData("value")]
[InlineData("value.ToString()")]
[InlineData("value.ToString(\"G\")")]
[InlineData("value.ToString(\"g\")")]
[InlineData("value.ToString(\"F\")")]
[InlineData("value.ToString(\"f\")")]
[InlineData("value.ToString(\"D\")")]
[InlineData("value.ToString(\"d\")")]
[InlineData("value.ToString(\"X\")")]
[InlineData("value.ToString(\"x\")")]
[InlineData("value.ToString(default(string))")]
[InlineData("value.ToString(format)")]
public async Task Concat_Enum_NoDiagnostic(string expression)
{
var sourceCode = $$"""
using System;
class Test
{
void A(StringComparison value, string format) { _ = "abc" + {{expression}}; }
}
""";
await CreateProjectBuilder()
.WithSourceCode(sourceCode)
.ValidateAsync();
}

[Theory]
[InlineData("value")]
[InlineData("value.ToString()")]
[InlineData("value?.ToString(\"G\")")]
[InlineData("value.Value.ToString(\"G\")")]
public async Task Concat_NullableEnum_NoDiagnostic(string expression)
{
var sourceCode = $$"""
using System;
class Test
{
void A(StringComparison? value) { _ = "abc" + {{expression}}; }
}
""";
await CreateProjectBuilder()
.WithSourceCode(sourceCode)
.ValidateAsync();
}

[Theory]
[InlineData("value.ToString()")]
[InlineData("value.ToString(\"G\")")]
public async Task Concat_SystemEnum_NoDiagnostic(string expression)
{
var sourceCode = $$"""
using System;
class Test
{
void A(Enum value) { _ = "abc" + {{expression}}; }
}
""";
await CreateProjectBuilder()
.WithSourceCode(sourceCode)
.ValidateAsync();
}

[Theory]
[InlineData("value.ToString(\"G\")")]
[InlineData("value.ToString(\"F\")")]
public async Task Concat_UserDefinedEnum_NoDiagnostic(string expression)
{
var sourceCode = $$"""
class Test
{
void A(Sample value) { _ = "abc" + {{expression}}; }
}

enum Sample { A }
""";
await CreateProjectBuilder()
.WithSourceCode(sourceCode)
.ValidateAsync();
}

[Theory]
[InlineData("{value}")]
[InlineData("{value:G}")]
[InlineData("{value:F}")]
[InlineData("{value.ToString()}")]
[InlineData("{value.ToString(\"G\")}")]
public async Task InterpolatedString_Enum_NoDiagnostic(string content)
{
var sourceCode = $$"""
using System;
class Test
{
void A(StringComparison value) { _ = $"abc{{content}}"; }
}
""";
await CreateProjectBuilder()
.WithSourceCode(sourceCode)
.ValidateAsync();
}

[Theory]
[InlineData("value.ToString()")]
[InlineData("value.ToString(\"G\")")]
public async Task Concat_EnumInGenericMethod_NoDiagnostic(string expression)
{
var sourceCode = $$"""
using System;
class Test
{
void A<T>(T value) where T : struct, Enum { _ = "abc" + {{expression}}; }
}
""";
await CreateProjectBuilder()
.WithSourceCode(sourceCode)
.ValidateAsync();
}

[Theory]
[InlineData("value?.ToString()")]
[InlineData("value?.ToString(\"F\")")]
[InlineData("value?.ToString(format)")]
[InlineData("value?.Date.ToString(\"F\")")]
[InlineData("value?.Ticks.ToString()")]
public async Task Concat_ConditionalAccess_Diagnostic(string expression)
{
var sourceCode = $$"""
using System;
class Test
{
void A(DateTime? value, string format) { _ = "abc" + [|{{expression}}|]; }
}
""";
await CreateProjectBuilder()
.WithSourceCode(sourceCode)
.ValidateAsync();
}

[Theory]
[InlineData("value?.ToString(\"o\")")]
[InlineData("value?.ToString(System.Globalization.CultureInfo.InvariantCulture)")]
[InlineData("value?.Ticks.ToString(\"X\")")]
[InlineData("value?.Kind.ToString(\"G\")")]
public async Task Concat_ConditionalAccess_NoDiagnostic(string expression)
{
var sourceCode = $$"""
using System;
class Test
{
void A(DateTime? value) { _ = "abc" + {{expression}}; }
}
""";
await CreateProjectBuilder()
.WithSourceCode(sourceCode)
.ValidateAsync();
}

[Fact]
public async Task Concat_NestedConditionalAccess_Diagnostic()
{
const string SourceCode = """
using System;
class Test
{
void A(Test value) { _ = "abc" + [|value?.Child?.Value.ToString("F")|]; }

Test Child { get; }
DateTime Value { get; }
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task Concat_ConditionalAccessToCultureSensitiveMember_Diagnostic()
{
const string SourceCode = """
using System;
class Test
{
void A(Test value) { _ = "abc" + [|value?.Value|]; }

DateTime Value { get; }
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task InterpolatedString_ConditionalAccess_Diagnostic()
{
const string SourceCode = """
using System;
class Test
{
void A(Test value) { _ = $"abc[|{value?.Value}|]"; }

DateTime Value { get; }
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

#if ROSLYN_5_9_OR_GREATER
private static ProjectBuilder CreateUnionProjectBuilder()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -714,4 +714,42 @@ await CreateProjectBuilder()
.ShouldFixCodeWith(Fix)
.ValidateAsync();
}

[Theory]
[InlineData("{value}")]
[InlineData("{value:G}")]
[InlineData("{value.ToString()}")]
[InlineData("{value.ToString(\"G\")}")]
public async Task StringCreateWithInvariantCulture_Enum_ShouldReport(string content)
{
var sourceCode = $$"""
using System;
using System.Globalization;

class TypeName
{
public void Test(StringComparison value)
{
var x = [|string.Create(CultureInfo.InvariantCulture, $"abc{{content}}")|];
}
}
""";

var fix = $$"""
using System;
using System.Globalization;

class TypeName
{
public void Test(StringComparison value)
{
var x = $"abc{{content}}";
}
}
""";
await CreateProjectBuilder()
.WithSourceCode(sourceCode)
.ShouldFixCodeWith(fix)
.ValidateAsync();
}
}
Loading