diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs index 3fd7785a1b27a9..965bf7fc210751 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -43,45 +44,58 @@ public void Initialize(GeneratorInitializationContext context) /// public void Execute(GeneratorExecutionContext executionContext) { - if (executionContext.SyntaxContextReceiver is not SyntaxContextReceiver receiver || receiver.ContextClassDeclarations == null) + // Ensure the source generator parses and emits using invariant culture. + // This prevents issues such as locale-specific negative signs (e.g., U+2212 in fi-FI) + // from being written to generated source files. + // Note: RS1035 is already disabled at the file level for this Roslyn version. + CultureInfo originalCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + try { - // nothing to do yet - return; - } + if (executionContext.SyntaxContextReceiver is not SyntaxContextReceiver receiver || receiver.ContextClassDeclarations == null) + { + // nothing to do yet + return; + } - // Stage 1. Parse the identified JsonSerializerContext classes and store the model types. - KnownTypeSymbols knownSymbols = new(executionContext.Compilation); - Parser parser = new(knownSymbols); + // Stage 1. Parse the identified JsonSerializerContext classes and store the model types. + KnownTypeSymbols knownSymbols = new(executionContext.Compilation); + Parser parser = new(knownSymbols); - List? contextGenerationSpecs = null; - foreach ((ClassDeclarationSyntax? contextClassDeclaration, SemanticModel semanticModel) in receiver.ContextClassDeclarations) - { - ContextGenerationSpec? contextGenerationSpec = parser.ParseContextGenerationSpec(contextClassDeclaration, semanticModel, executionContext.CancellationToken); - if (contextGenerationSpec is null) + List? contextGenerationSpecs = null; + foreach ((ClassDeclarationSyntax? contextClassDeclaration, SemanticModel semanticModel) in receiver.ContextClassDeclarations) { - continue; + ContextGenerationSpec? contextGenerationSpec = parser.ParseContextGenerationSpec(contextClassDeclaration, semanticModel, executionContext.CancellationToken); + if (contextGenerationSpec is null) + { + continue; + } + + (contextGenerationSpecs ??= new()).Add(contextGenerationSpec); } - (contextGenerationSpecs ??= new()).Add(contextGenerationSpec); - } + // Stage 2. Report any diagnostics gathered by the parser. + foreach (DiagnosticInfo diagnosticInfo in parser.Diagnostics) + { + executionContext.ReportDiagnostic(diagnosticInfo.CreateDiagnostic()); + } - // Stage 2. Report any diagnostics gathered by the parser. - foreach (DiagnosticInfo diagnosticInfo in parser.Diagnostics) - { - executionContext.ReportDiagnostic(diagnosticInfo.CreateDiagnostic()); - } + if (contextGenerationSpecs is null) + { + return; + } - if (contextGenerationSpecs is null) - { - return; + // Stage 3. Emit source code from the spec models. + OnSourceEmitting?.Invoke(contextGenerationSpecs.ToImmutableArray()); + Emitter emitter = new(executionContext); + foreach (ContextGenerationSpec contextGenerationSpec in contextGenerationSpecs) + { + emitter.Emit(contextGenerationSpec); + } } - - // Stage 3. Emit source code from the spec models. - OnSourceEmitting?.Invoke(contextGenerationSpecs.ToImmutableArray()); - Emitter emitter = new(executionContext); - foreach (ContextGenerationSpec contextGenerationSpec in contextGenerationSpecs) + finally { - emitter.Emit(contextGenerationSpec); + CultureInfo.CurrentCulture = originalCulture; } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs index 447f54c7f07821..75f1f51269e7a6 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; +using System.Globalization; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -42,10 +43,26 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Combine(knownTypeSymbols) .Select(static (tuple, cancellationToken) => { - Parser parser = new(tuple.Right); - ContextGenerationSpec? contextGenerationSpec = parser.ParseContextGenerationSpec(tuple.Left.ContextClass, tuple.Left.SemanticModel, cancellationToken); - ImmutableEquatableArray diagnostics = parser.Diagnostics.ToImmutableEquatableArray(); - return (contextGenerationSpec, diagnostics); + // Ensure the source generator parses using invariant culture. + // This prevents issues such as locale-specific negative signs (e.g., U+2212 in fi-FI) + // from being written to generated source files. +#pragma warning disable RS1035 // CultureInfo.CurrentCulture is banned in analyzers + CultureInfo originalCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + try + { +#pragma warning restore RS1035 + Parser parser = new(tuple.Right); + ContextGenerationSpec? contextGenerationSpec = parser.ParseContextGenerationSpec(tuple.Left.ContextClass, tuple.Left.SemanticModel, cancellationToken); + ImmutableEquatableArray diagnostics = parser.Diagnostics.ToImmutableEquatableArray(); + return (contextGenerationSpec, diagnostics); +#pragma warning disable RS1035 + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } +#pragma warning restore RS1035 }) #if ROSLYN4_4_OR_GREATER .WithTrackingName(SourceGenerationSpecTrackingName) @@ -69,8 +86,23 @@ private void ReportDiagnosticsAndEmitSource(SourceProductionContext sourceProduc } OnSourceEmitting?.Invoke(ImmutableArray.Create(input.ContextGenerationSpec)); - Emitter emitter = new(sourceProductionContext); - emitter.Emit(input.ContextGenerationSpec); + + // Ensure the source generator emits number literals using invariant culture. + // This prevents issues such as locale-specific negative signs (e.g., U+2212 in fi-FI) + // from being written to generated source files. +#pragma warning disable RS1035 // CultureInfo.CurrentCulture is banned in analyzers + CultureInfo originalCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + try + { + Emitter emitter = new(sourceProductionContext); + emitter.Emit(input.ContextGenerationSpec); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } +#pragma warning restore RS1035 } /// diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs index 9faf0472700dd5..ba940f04a2fe00 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; +using System.Tests; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Xunit; @@ -965,5 +966,51 @@ public partial class MyContext : JsonSerializerContext Assert.NotEmpty(result.NewCompilation.GetDiagnostics().Where(d => d.Id == "EXP001")); } #endif + + [Fact] + public void NegativeJsonPropertyOrderGeneratesValidCode() + { + // Test for https://github.com/dotnet/runtime/issues/121277 + // Verify that negative JsonPropertyOrder values generate compilable code + // even on locales that use non-ASCII minus signs (e.g., fi_FI uses U+2212) + string source = """ + using System.Text.Json.Serialization; + + namespace Test + { + public class MyClass + { + [JsonPropertyOrder(-1)] + public int FirstProperty { get; set; } + + [JsonPropertyOrder(0)] + public int SecondProperty { get; set; } + + [JsonPropertyOrder(-100)] + public int ThirdProperty { get; set; } + } + + [JsonSerializable(typeof(MyClass))] + public partial class MyContext : JsonSerializerContext + { + } + } + """; + + // Test with fi_FI culture which uses U+2212 minus sign for negative numbers + using (new ThreadCultureChange("fi-FI")) + { + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, logger: logger); + + // The generated code should compile without errors + // If the bug exists, we'd see CS1525, CS1002, CS1056, or CS0201 errors + var errors = result.NewCompilation.GetDiagnostics() + .Where(d => d.Severity == DiagnosticSeverity.Error) + .ToList(); + + Assert.Empty(errors); + } + } } }