From 207f7acf35174341b50e1f0909e68bc4100af1d4 Mon Sep 17 00:00:00 2001 From: Chenfeng Bao Date: Sun, 5 Apr 2026 19:51:01 -0700 Subject: [PATCH 1/2] respect end_of_line in editorconfig --- .../Async/Generator/FileCollector.cs | 23 +++++-- .../Async/Generator/SyncGenerator.cs | 16 ++++- .../D2L.CodeStyle.Analyzers.csproj | 2 +- .../Async/Generator/FileCollectorTests.cs | 62 +++++++++++++++++-- 4 files changed, 89 insertions(+), 14 deletions(-) diff --git a/src/D2L.CodeStyle.Analyzers/Async/Generator/FileCollector.cs b/src/D2L.CodeStyle.Analyzers/Async/Generator/FileCollector.cs index e70ecbc5..f33329f7 100644 --- a/src/D2L.CodeStyle.Analyzers/Async/Generator/FileCollector.cs +++ b/src/D2L.CodeStyle.Analyzers/Async/Generator/FileCollector.cs @@ -56,18 +56,22 @@ public static implicit operator PieceOfSyntax( SyntaxList private readonly LinkedList m_preambles = new(); private readonly StringBuilder m_out = new(); + private readonly string? m_endOfLine; private FileCollector( CompilationUnitSyntax root, - Dictionary> methods + Dictionary> methods, + string? endOfLine ) { m_root = root; m_methods = methods; + m_endOfLine = endOfLine; } public static FileCollector Create( CompilationUnitSyntax root, - ImmutableArray<(TypeDeclarationSyntax Parent, string Source)> methods + ImmutableArray<(TypeDeclarationSyntax Parent, string Source)> methods, + string? endOfLine ) { var groupedMethods = methods .GroupBy( static m => m.Parent ) @@ -76,15 +80,24 @@ public static FileCollector Create( static g => g.Select( static m => m.Source ) ); - return new FileCollector( root, groupedMethods ); + return new FileCollector( root, groupedMethods, endOfLine ); } public string CollectSource() { // TODO: Remove this and modify XML param elements in generator when changed/removed - m_out.AppendLine( "#pragma warning disable CS1572" ); + if( m_endOfLine != null ) { + m_out.Append( "#pragma warning disable CS1572" + m_endOfLine ); + } else { + m_out.AppendLine( "#pragma warning disable CS1572" ); + } // This allows us to copy+paste annotations but otherwise does not emit diagnostics // otherwise we get "CS8669: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source." - m_out.AppendLine( "#nullable enable annotations" ); + if( m_endOfLine != null ) { + m_out.Append( "#nullable enable annotations" + m_endOfLine ); + } else { + m_out.AppendLine( "#nullable enable annotations" ); + } + // File-scoped usings: m_out.Append( m_root.Usings.ToFullString() ); diff --git a/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.cs b/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.cs index c2deb806..23da8ba5 100644 --- a/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.cs +++ b/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.cs @@ -38,7 +38,8 @@ public void Initialize( IncrementalGeneratorInitializationContext context ) { // that when any file with [GenerateSync] has changed. IncrementalValuesProvider generatedFiles = generatedMethods.Collect() - .SelectMany( GenerateFiles ); + .Combine( context.AnalyzerConfigOptionsProvider ) + .SelectMany( ( x, ct ) => GenerateFiles( x.Left, x.Right, ct ) ); context.RegisterSourceOutput( generatedFiles, WriteFiles ); } @@ -132,6 +133,7 @@ CancellationToken cancellationToken private static IEnumerable GenerateFiles( ImmutableArray methodResults, + AnalyzerConfigOptionsProvider configOptionsProvider, CancellationToken cancellationToken ) { var methodsByFile = methodResults @@ -161,9 +163,19 @@ CancellationToken cancellationToken generatedMethods.Add( ((method.Original.Parent as TypeDeclarationSyntax)!, method.GeneratedSyntax ) ); } + var fileOptions = configOptionsProvider.GetOptions( file.Data.Key.SyntaxTree ); + fileOptions.TryGetValue( "end_of_line", out var endOfLineValue ); + var endOfLine = endOfLineValue?.Trim() switch { + "crlf" => "\r\n", + "lf" => "\n", + "cr" => "\r", + _ => null, + }; + var collector = FileCollector.Create( file.Data.Key, - generatedMethods.ToImmutable() + generatedMethods.ToImmutable(), + endOfLine ); var generatedFile = collector.CollectSource(); diff --git a/src/D2L.CodeStyle.Analyzers/D2L.CodeStyle.Analyzers.csproj b/src/D2L.CodeStyle.Analyzers/D2L.CodeStyle.Analyzers.csproj index 5d7da8b6..4bf1089b 100644 --- a/src/D2L.CodeStyle.Analyzers/D2L.CodeStyle.Analyzers.csproj +++ b/src/D2L.CodeStyle.Analyzers/D2L.CodeStyle.Analyzers.csproj @@ -6,7 +6,7 @@ D2L.CodeStyle.Analyzers D2L.CodeStyle D2L.CodeStyle analyzers - 0.220.0 + 0.221.0 Apache-2.0 https://github.com/Brightspace/D2L.CodeStyle D2L diff --git a/tests/D2L.CodeStyle.Analyzers.Test/Async/Generator/FileCollectorTests.cs b/tests/D2L.CodeStyle.Analyzers.Test/Async/Generator/FileCollectorTests.cs index 01b992e4..94b7687a 100644 --- a/tests/D2L.CodeStyle.Analyzers.Test/Async/Generator/FileCollectorTests.cs +++ b/tests/D2L.CodeStyle.Analyzers.Test/Async/Generator/FileCollectorTests.cs @@ -20,7 +20,8 @@ void SomeMethod() {} var collector = FileCollector.Create( root, - ImmutableArray<(TypeDeclarationSyntax, string)>.Empty + ImmutableArray<(TypeDeclarationSyntax, string)>.Empty, + endOfLine: null ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -28,6 +29,50 @@ void SomeMethod() {} ", collector.CollectSource() ); } + [TestCase( "\n" )] + [TestCase( "\r\n" )] + public void EndOfLine( string endOfLine ) { + var source = string.Join( endOfLine, [ + "", + "using Foo;", + "", + "namespace X;", + "", + "public sealed class Y {", + " void MyMethodBefore() {", + " Console.WriteLine( \"Hello\" );", + " }", + "}", + ] ); + + var root = CSharpSyntaxTree.ParseText( source ).GetCompilationUnitRoot(); + + SyntaxNode myMethodBefore = root.DescendantNodes().OfType().Single(); + + var collector = FileCollector.Create( + root, + ImmutableArray.Create( + ((TypeDeclarationSyntax)myMethodBefore.Parent, "\tany text" + endOfLine) + ), + endOfLine + ); + + var expected = string.Join( endOfLine, [ + "#pragma warning disable CS1572", + "#nullable enable annotations", + "", + "using Foo;", + "", + "namespace X;", + "", + "partial class Y {", + " any text", + "}", + ] ); + + Assert.AreEqual( expected, collector.CollectSource() ); + } + [Test] public void Basic() { var root = CSharpSyntaxTree.ParseText( @" @@ -49,7 +94,8 @@ void MyMethodBefore() { root, ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\tany text\r\n") - ) + ), + endOfLine: null ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -87,7 +133,8 @@ void MyMethodBefore() { root, ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\tany text\r\n") - ) + ), + endOfLine: null ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -123,7 +170,8 @@ void MyMethodBefore() {{ root, ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\tany text\r\n") - ) + ), + endOfLine: null ); Assert.AreEqual( @$"#pragma warning disable CS1572 @@ -163,7 +211,8 @@ class Ignored2 {} root, ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\t\t\t\tany text\r\n") - ) + ), + endOfLine: null ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -245,7 +294,8 @@ void MyBeforeMethod4() { } var collector = FileCollector.Create( root, - myMethodsBefore.ToImmutableArray() + myMethodsBefore.ToImmutableArray(), + endOfLine: null ); Assert.AreEqual( @"#pragma warning disable CS1572 From c380fa23de1976daf82a2e482dd992482493ed49 Mon Sep 17 00:00:00 2001 From: Chenfeng Bao Date: Fri, 10 Apr 2026 20:48:04 -0700 Subject: [PATCH 2/2] support charset too --- .../Async/Generator/FileCollector.cs | 15 +++++--- .../Async/Generator/SyncGenerator.Results.cs | 3 +- .../Async/Generator/SyncGenerator.cs | 19 ++++++++-- .../Async/Generator/FileCollectorTests.cs | 36 +++++++++++-------- 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/src/D2L.CodeStyle.Analyzers/Async/Generator/FileCollector.cs b/src/D2L.CodeStyle.Analyzers/Async/Generator/FileCollector.cs index f33329f7..487ca243 100644 --- a/src/D2L.CodeStyle.Analyzers/Async/Generator/FileCollector.cs +++ b/src/D2L.CodeStyle.Analyzers/Async/Generator/FileCollector.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; namespace D2L.CodeStyle.Analyzers.Async.Generator; @@ -57,21 +58,25 @@ public static implicit operator PieceOfSyntax( SyntaxList private readonly StringBuilder m_out = new(); private readonly string? m_endOfLine; + private readonly Encoding m_encoding; private FileCollector( CompilationUnitSyntax root, Dictionary> methods, - string? endOfLine + string? endOfLine, + Encoding encoding ) { m_root = root; m_methods = methods; m_endOfLine = endOfLine; + m_encoding = encoding; } public static FileCollector Create( CompilationUnitSyntax root, ImmutableArray<(TypeDeclarationSyntax Parent, string Source)> methods, - string? endOfLine + string? endOfLine, + Encoding encoding ) { var groupedMethods = methods .GroupBy( static m => m.Parent ) @@ -80,10 +85,10 @@ public static FileCollector Create( static g => g.Select( static m => m.Source ) ); - return new FileCollector( root, groupedMethods, endOfLine ); + return new FileCollector( root, groupedMethods, endOfLine, encoding ); } - public string CollectSource() { + public SourceText CollectSource() { // TODO: Remove this and modify XML param elements in generator when changed/removed if( m_endOfLine != null ) { m_out.Append( "#pragma warning disable CS1572" + m_endOfLine ); @@ -111,7 +116,7 @@ public string CollectSource() { throw new BugException( "left over methods" ); } - return m_out.ToString(); + return SourceText.From( m_out.ToString(), m_encoding ); } private bool WriteChildren( SyntaxNode node ) { diff --git a/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.Results.cs b/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.Results.cs index 43db239b..31924bb8 100644 --- a/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.Results.cs +++ b/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.Results.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; namespace D2L.CodeStyle.Analyzers.Async.Generator; @@ -23,7 +24,7 @@ ImmutableArray Diagnostics /// private readonly record struct FileGenerationResult( string HintName, - string GeneratedSource, + SourceText GeneratedSource, ImmutableArray Diagnostics ); } diff --git a/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.cs b/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.cs index 23da8ba5..2f351754 100644 --- a/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.cs +++ b/src/D2L.CodeStyle.Analyzers/Async/Generator/SyncGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -8,6 +9,8 @@ namespace D2L.CodeStyle.Analyzers.Async.Generator; [Generator] internal sealed partial class SyncGenerator : IIncrementalGenerator { + private static readonly Encoding UTF8WithoutBom = new UTF8Encoding( encoderShouldEmitUTF8Identifier: false ); + public void Initialize( IncrementalGeneratorInitializationContext context ) { var options = context.AnalyzerConfigOptionsProvider .Select( ParseConfig ); @@ -164,6 +167,7 @@ CancellationToken cancellationToken } var fileOptions = configOptionsProvider.GetOptions( file.Data.Key.SyntaxTree ); + fileOptions.TryGetValue( "end_of_line", out var endOfLineValue ); var endOfLine = endOfLineValue?.Trim() switch { "crlf" => "\r\n", @@ -172,10 +176,21 @@ CancellationToken cancellationToken _ => null, }; + fileOptions.TryGetValue( "charset", out var charsetValue ); + var encoding = charsetValue?.Trim().ToLower() switch { + "latin-1" => Encoding.GetEncoding( "iso-8859-1" ), + "utf-8" => UTF8WithoutBom, + "utf-8-bom" => Encoding.UTF8, + "utf-16le" => Encoding.Unicode, + "utf-16be" => Encoding.BigEndianUnicode, + _ => Encoding.Default, + }; + var collector = FileCollector.Create( file.Data.Key, generatedMethods.ToImmutable(), - endOfLine + endOfLine, + encoding ); var generatedFile = collector.CollectSource(); @@ -198,7 +213,7 @@ FileGenerationResult result context.AddSource( hintName: result.HintName, - source: result.GeneratedSource + sourceText: result.GeneratedSource ); } diff --git a/tests/D2L.CodeStyle.Analyzers.Test/Async/Generator/FileCollectorTests.cs b/tests/D2L.CodeStyle.Analyzers.Test/Async/Generator/FileCollectorTests.cs index 94b7687a..11f7a96a 100644 --- a/tests/D2L.CodeStyle.Analyzers.Test/Async/Generator/FileCollectorTests.cs +++ b/tests/D2L.CodeStyle.Analyzers.Test/Async/Generator/FileCollectorTests.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -21,12 +22,13 @@ void SomeMethod() {} var collector = FileCollector.Create( root, ImmutableArray<(TypeDeclarationSyntax, string)>.Empty, - endOfLine: null + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @"#pragma warning disable CS1572 #nullable enable annotations -", collector.CollectSource() ); +", collector.CollectSource().ToString() ); } [TestCase( "\n" )] @@ -54,7 +56,8 @@ public void EndOfLine( string endOfLine ) { ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\tany text" + endOfLine) ), - endOfLine + endOfLine: endOfLine, + encoding: Encoding.Default ); var expected = string.Join( endOfLine, [ @@ -70,7 +73,7 @@ public void EndOfLine( string endOfLine ) { "}", ] ); - Assert.AreEqual( expected, collector.CollectSource() ); + Assert.AreEqual( expected, collector.CollectSource().ToString() ); } [Test] @@ -95,7 +98,8 @@ void MyMethodBefore() { ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\tany text\r\n") ), - endOfLine: null + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -108,7 +112,7 @@ namespace X; partial class Y { any text }", - collector.CollectSource() + collector.CollectSource().ToString() ); } @@ -134,7 +138,8 @@ void MyMethodBefore() { ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\tany text\r\n") ), - endOfLine: null + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -147,7 +152,7 @@ namespace X; partial class Y { any text }", - collector.CollectSource() + collector.CollectSource().ToString() ); } [TestCase( "class" )] // static/selaed come before partial and don't need to show up in the other partials @@ -171,7 +176,8 @@ void MyMethodBefore() {{ ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\tany text\r\n") ), - endOfLine: null + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @$"#pragma warning disable CS1572 @@ -180,7 +186,7 @@ void MyMethodBefore() {{ partial {kind} X {{ any text }}", - collector.CollectSource() + collector.CollectSource().ToString() ); } @@ -212,7 +218,8 @@ class Ignored2 {} ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\t\t\t\tany text\r\n") ), - endOfLine: null + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -230,7 +237,7 @@ any text } } }", - collector.CollectSource() + collector.CollectSource().ToString() ); } @@ -295,7 +302,8 @@ void MyBeforeMethod4() { } var collector = FileCollector.Create( root, myMethodsBefore.ToImmutableArray(), - endOfLine: null + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -334,7 +342,7 @@ any text3 } } ", - collector.CollectSource() + collector.CollectSource().ToString() ); } }