Handle C# 15 unions in culture-sensitive formatting analysis - #1293
Merged
Merged
Conversation
The culture sensitivity of a union type must be derived from its case
types: a union is culture-sensitive when at least one of its case types
is culture-sensitive, and culture-insensitive when none of them is.
Without this, a union was reported as culture-insensitive because the
generated type is a sealed struct that doesn't implement IFormattable.
Roslyn exposes no public API to get the case types, so they are read
from the union creation members defined by the specification: the public
constructors with a single by-value or `in` parameter, and the `Create`
methods of the nested `IUnionMembers` interface for union member
providers. Nested unions are expanded inline using a set of visited
types as case types may reference each other.
The case types are also evaluated against the format string, so
`$"{value:o}"` stays culture-insensitive when the case types support
this invariant format.
This was referenced Aug 17, 2026
Bump Meziantou.Analyzer from 3.0.103 to 3.0.163
Analogy-LogViewer/Analogy.LogViewer.OpenTelemetry#90
Closed
Closed
Closed
Closed
Bump Meziantou.Analyzer from 3.0.139 to 3.0.163
Analogy-LogViewer/Analogy.LogViewer.NLog.Targets#549
Closed
Closed
Bump Meziantou.Analyzer from 3.0.139 to 3.0.163
Analogy-LogViewer/Analogy.AspNetCore.LogProvider#535
Closed
This was referenced Sep 14, 2026
Closed
Open
Open
Open
Bump Meziantou.Analyzer from 3.0.139 to 3.0.258
Analogy-LogViewer/Analogy.LogViewer.NLog.Targets#573
Open
Open
Bump Meziantou.Analyzer from 3.0.139 to 3.0.258
Analogy-LogViewer/Analogy.AspNetCore.LogProvider#556
Open
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
CultureSensitiveFormattingContextnow understands C# 15uniontypes: a union is culture-sensitive when at least one of its case types is culture-sensitive, and culture-insensitive when none of them is.This affects every rule built on that context: MA0011, MA0075, MA0076, MA0077 and MA0111.
Why
The type generated for a
uniondeclaration is a sealed struct that doesn't implementIFormattableand has no provider-awareToString, so the context classified every union as culture-insensitive — includingunion Sample(bool, double).How
Roslyn exposes
ITypeSymbol.IsUnion(Roslyn 5.9, still experimental) but no public API for the case types, so they are read from the union creation members as defined by the specification:inparameter;Createmethods of the nestedIUnionMembersinterface, for union member providers (whose constructor is private).Both helpers live in
TypeSymbolExtensionsunder#if ROSLYN_5_9_OR_GREATER.In the context itself:
CultureInsensitiveTypeAttributecheck, so annotating a union still wins;$"{value:o}"on a union with aDateTimecase stays insensitive);union A(B); union B(A);compiles, so cycles are real — and nullable case types are unwrapped whenconsider_nullable_typesis enabled, which is where a cycle could otherwise slip through;Notes for the reviewer
The compiler-generated union struct does not override
ToString()(verified on the emitted metadata), so$"{pet}"on a plainunion Pet(int, string)prints the type name and is culture-insensitive in practice. The new logic matters for unions that do overrideToString— a union body with aToStringoverride, or a custom[Union]type — which is why the interpolation tests declare one. MA0077 (UsesObjectToString) consequently fires on plain unions in interpolated strings; that behavior is unchanged and out of scope here.Tests
10 tests in
DoNotUseImplicitCultureSensitiveToStringAnalyzerTestsunder#if CSHARP15_OR_GREATER: insensitive/sensitive case types, nested unions, mutually-referencing unions, nullable case type,CultureInsensitiveTypeAttribute, custom[Union]type, union member provider, and the two format cases. 6 of them fail without the change (verified).dotnet run --project src/DocumentationGeneratorreports no pending change.