diff --git a/src/D2L.CodeStyle.Analyzers/Async/Generator/FileCollector.cs b/src/D2L.CodeStyle.Analyzers/Async/Generator/FileCollector.cs index e70ecbc5..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; @@ -56,18 +57,26 @@ public static implicit operator PieceOfSyntax( SyntaxList private readonly LinkedList m_preambles = new(); private readonly StringBuilder m_out = new(); + private readonly string? m_endOfLine; + private readonly Encoding m_encoding; private FileCollector( CompilationUnitSyntax root, - Dictionary> methods + Dictionary> methods, + 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 + ImmutableArray<(TypeDeclarationSyntax Parent, string Source)> methods, + string? endOfLine, + Encoding encoding ) { var groupedMethods = methods .GroupBy( static m => m.Parent ) @@ -76,15 +85,24 @@ public static FileCollector Create( static g => g.Select( static m => m.Source ) ); - return new FileCollector( root, groupedMethods ); + 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 - 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() ); @@ -98,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 c2deb806..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 ); @@ -38,7 +41,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 +136,7 @@ CancellationToken cancellationToken private static IEnumerable GenerateFiles( ImmutableArray methodResults, + AnalyzerConfigOptionsProvider configOptionsProvider, CancellationToken cancellationToken ) { var methodsByFile = methodResults @@ -161,9 +166,31 @@ 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, + }; + + 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() + generatedMethods.ToImmutable(), + endOfLine, + encoding ); var generatedFile = collector.CollectSource(); @@ -186,7 +213,7 @@ FileGenerationResult result context.AddSource( hintName: result.HintName, - source: result.GeneratedSource + sourceText: result.GeneratedSource ); } 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..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; @@ -20,12 +21,59 @@ void SomeMethod() {} var collector = FileCollector.Create( root, - ImmutableArray<(TypeDeclarationSyntax, string)>.Empty + ImmutableArray<(TypeDeclarationSyntax, string)>.Empty, + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @"#pragma warning disable CS1572 #nullable enable annotations -", collector.CollectSource() ); +", collector.CollectSource().ToString() ); + } + + [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: endOfLine, + encoding: Encoding.Default + ); + + 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().ToString() ); } [Test] @@ -49,7 +97,9 @@ void MyMethodBefore() { root, ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\tany text\r\n") - ) + ), + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -62,7 +112,7 @@ namespace X; partial class Y { any text }", - collector.CollectSource() + collector.CollectSource().ToString() ); } @@ -87,7 +137,9 @@ void MyMethodBefore() { root, ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\tany text\r\n") - ) + ), + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -100,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 @@ -123,7 +175,9 @@ void MyMethodBefore() {{ root, ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\tany text\r\n") - ) + ), + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @$"#pragma warning disable CS1572 @@ -132,7 +186,7 @@ void MyMethodBefore() {{ partial {kind} X {{ any text }}", - collector.CollectSource() + collector.CollectSource().ToString() ); } @@ -163,7 +217,9 @@ class Ignored2 {} root, ImmutableArray.Create( ((TypeDeclarationSyntax)myMethodBefore.Parent, "\t\t\t\tany text\r\n") - ) + ), + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -181,7 +237,7 @@ any text } } }", - collector.CollectSource() + collector.CollectSource().ToString() ); } @@ -245,7 +301,9 @@ void MyBeforeMethod4() { } var collector = FileCollector.Create( root, - myMethodsBefore.ToImmutableArray() + myMethodsBefore.ToImmutableArray(), + endOfLine: null, + encoding: Encoding.Default ); Assert.AreEqual( @"#pragma warning disable CS1572 @@ -284,7 +342,7 @@ any text3 } } ", - collector.CollectSource() + collector.CollectSource().ToString() ); } }