From 439953915a51ccd0ca98a6f0b1094b4e633a532c Mon Sep 17 00:00:00 2001 From: Timothy Makkison Date: Thu, 3 Apr 2025 22:37:53 +0100 Subject: [PATCH] feat: add `SourceWriter` --- .../InterfaceStubGenerator.Roslyn38.csproj | 1 + .../InterfaceStubGenerator.Roslyn41.csproj | 1 + InterfaceStubGenerator.Shared/Emitter.cs | 42 +++--- .../IncrementalValuesProviderExtensions.cs | 2 +- .../InterfaceStubGenerator.Shared.projitems | 1 + .../InterfaceStubGenerator.cs | 2 +- InterfaceStubGenerator.Shared/SourceWriter.cs | 142 ++++++++++++++++++ ...eDiagnostic#IGeneratedClient.g.verified.cs | 2 +- ...BaseTest#IGeneratedInterface.g.verified.cs | 2 +- ...Constraints#IGeneratedClient.g.verified.cs | 2 +- 10 files changed, 169 insertions(+), 28 deletions(-) create mode 100644 InterfaceStubGenerator.Shared/SourceWriter.cs diff --git a/InterfaceStubGenerator.Roslyn38/InterfaceStubGenerator.Roslyn38.csproj b/InterfaceStubGenerator.Roslyn38/InterfaceStubGenerator.Roslyn38.csproj index fa3e54b88..1a3a97d7f 100644 --- a/InterfaceStubGenerator.Roslyn38/InterfaceStubGenerator.Roslyn38.csproj +++ b/InterfaceStubGenerator.Roslyn38/InterfaceStubGenerator.Roslyn38.csproj @@ -10,6 +10,7 @@ true enable 3.8.0 + true diff --git a/InterfaceStubGenerator.Roslyn41/InterfaceStubGenerator.Roslyn41.csproj b/InterfaceStubGenerator.Roslyn41/InterfaceStubGenerator.Roslyn41.csproj index 8911bd287..192b8ebb1 100644 --- a/InterfaceStubGenerator.Roslyn41/InterfaceStubGenerator.Roslyn41.csproj +++ b/InterfaceStubGenerator.Roslyn41/InterfaceStubGenerator.Roslyn41.csproj @@ -11,6 +11,7 @@ enable $(DefineConstants);ROSLYN_4 4.1.0 + true diff --git a/InterfaceStubGenerator.Shared/Emitter.cs b/InterfaceStubGenerator.Shared/Emitter.cs index a1305fb48..cc6280128 100644 --- a/InterfaceStubGenerator.Shared/Emitter.cs +++ b/InterfaceStubGenerator.Shared/Emitter.cs @@ -69,20 +69,18 @@ public static void Initialize() addSource("Generated.g.cs", SourceText.From(generatedClassText, Encoding.UTF8)); } - public static string EmitInterface(InterfaceModel model) + public static SourceText EmitInterface(InterfaceModel model) { - var source = new StringBuilder(); + var source = new SourceWriter(); // if nullability is supported emit the nullable directive if (model.Nullability != Nullability.None) { - source.Append("#nullable "); - source.Append(model.Nullability == Nullability.Enabled ? "enable" : "disable"); + source.WriteLine("#nullable " + (model.Nullability == Nullability.Enabled ? "enable" : "disable")); } - source.Append( - $@" -#pragma warning disable + source.WriteLine( + $@"#pragma warning disable namespace Refit.Implementation {{ @@ -108,8 +106,7 @@ partial class {model.Ns}{model.ClassDeclaration} {{ Client = client; this.requestBuilder = requestBuilder; - }} -" + }}" ); var uniqueNames = new UniqueNameBuilder(); @@ -138,16 +135,15 @@ partial class {model.Ns}{model.ClassDeclaration} WriteDisposableMethod(source); } - source.Append( + source.WriteLine( @" } } } -#pragma warning restore -" +#pragma warning restore" ); - return source.ToString(); + return source.ToSourceText(); } /// @@ -158,7 +154,7 @@ partial class {model.Ns}{model.ClassDeclaration} /// True if directly from the type we're generating for, false for methods found on base interfaces /// Contains the unique member names in the interface scope. private static void WriteRefitMethod( - StringBuilder source, + SourceWriter source, MethodModel methodModel, bool isTopLevel, UniqueNameBuilder uniqueNames @@ -220,23 +216,23 @@ UniqueNameBuilder uniqueNames WriteMethodClosing(source); } - private static void WriteNonRefitMethod(StringBuilder source, MethodModel methodModel) + private static void WriteNonRefitMethod(SourceWriter source, MethodModel methodModel) { WriteMethodOpening(source, methodModel, true); - source.Append( + source.WriteLine( @" - throw new global::System.NotImplementedException(""Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument.""); - " - ); + throw new global::System.NotImplementedException(""Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument."");"); + source.Indentation += 1; WriteMethodClosing(source); + source.Indentation -= 1; } // TODO: This assumes that the Dispose method is a void that takes no parameters. // The previous version did not. // Does the bool overload cause an issue here. - private static void WriteDisposableMethod(StringBuilder source) + private static void WriteDisposableMethod(SourceWriter source) { source.Append( """ @@ -252,7 +248,7 @@ private static void WriteDisposableMethod(StringBuilder source) } private static string GenerateTypeParameterExpression( - StringBuilder source, + SourceWriter source, MethodModel methodModel, UniqueNameBuilder uniqueNames ) @@ -283,7 +279,7 @@ UniqueNameBuilder uniqueNames } private static void WriteMethodOpening( - StringBuilder source, + SourceWriter source, MethodModel methodModel, bool isExplicitInterface, bool isAsync = false @@ -324,7 +320,7 @@ private static void WriteMethodOpening( ); } - private static void WriteMethodClosing(StringBuilder source) => source.Append(@" }"); + private static void WriteMethodClosing(SourceWriter source) => source.Append(@" }"); private static string GenerateConstraints( ImmutableEquatableArray typeParameters, diff --git a/InterfaceStubGenerator.Shared/IncrementalValuesProviderExtensions.cs b/InterfaceStubGenerator.Shared/IncrementalValuesProviderExtensions.cs index 0766c4c93..ef4bd070b 100644 --- a/InterfaceStubGenerator.Shared/IncrementalValuesProviderExtensions.cs +++ b/InterfaceStubGenerator.Shared/IncrementalValuesProviderExtensions.cs @@ -60,7 +60,7 @@ IncrementalValuesProvider model static (spc, model) => { var mapperText = Emitter.EmitInterface(model); - spc.AddSource(model.FileName, SourceText.From(mapperText, Encoding.UTF8)); + spc.AddSource(model.FileName, mapperText); } ); } diff --git a/InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.projitems b/InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.projitems index f4cfc6944..7e8f0ebb5 100644 --- a/InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.projitems +++ b/InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.projitems @@ -23,6 +23,7 @@ + \ No newline at end of file diff --git a/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs b/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs index d33ffc17d..bd6e91cde 100644 --- a/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs +++ b/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs @@ -63,7 +63,7 @@ out var refitInternalNamespace var interfaceText = Emitter.EmitInterface(interfaceModel); context.AddSource( interfaceModel.FileName, - SourceText.From(interfaceText, Encoding.UTF8) + interfaceText ); } diff --git a/InterfaceStubGenerator.Shared/SourceWriter.cs b/InterfaceStubGenerator.Shared/SourceWriter.cs new file mode 100644 index 000000000..ac93b28e9 --- /dev/null +++ b/InterfaceStubGenerator.Shared/SourceWriter.cs @@ -0,0 +1,142 @@ +using System.Diagnostics; +using System.Text; + +using Microsoft.CodeAnalysis.Text; + +namespace Refit.Generator; + +// From https://github.com/dotnet/runtime/blob/233826c88d2100263fb9e9535d96f75824ba0aea/src/libraries/Common/src/SourceGenerators/SourceWriter.cs#L11 +internal sealed class SourceWriter +{ + private const char IndentationChar = ' '; + private const int CharsPerIndentation = 4; + + private readonly StringBuilder _sb = new(); + private int _indentation; + + public int Indentation + { + get => _indentation; + set + { + if (value < 0) + { + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(value)); + } + + _indentation = value; + } + } + + public void Append(string text) + { + if (_indentation == 0) + { + _sb.Append(text); + return; + } + + bool isFinalLine; + ReadOnlySpan remainingText = text.AsSpan(); + do + { + ReadOnlySpan nextLine = GetNextLine(ref remainingText, out isFinalLine); + + AddIndentation(); + AppendSpan(_sb, nextLine); + if (!isFinalLine) + { + _sb.AppendLine(); + } + } + while (!isFinalLine); + } + + public void WriteLine(char value) + { + AddIndentation(); + _sb.Append(value); + _sb.AppendLine(); + } + + public void WriteLine(string text) + { + if (_indentation == 0) + { + _sb.AppendLine(text); + return; + } + + bool isFinalLine; + ReadOnlySpan remainingText = text.AsSpan(); + do + { + ReadOnlySpan nextLine = GetNextLine(ref remainingText, out isFinalLine); + + AddIndentation(); + AppendSpan(_sb, nextLine); + _sb.AppendLine(); + } + while (!isFinalLine); + } + + public void WriteLine() => _sb.AppendLine(); + + public SourceText ToSourceText() + { + Debug.Assert(_indentation == 0 && _sb.Length > 0); + return SourceText.From(_sb.ToString(), Encoding.UTF8); + } + + public void Reset() + { + _sb.Clear(); + _indentation = 0; + } + + private void AddIndentation() + => _sb.Append(IndentationChar, CharsPerIndentation * _indentation); + + private static ReadOnlySpan GetNextLine(ref ReadOnlySpan remainingText, out bool isFinalLine) + { + if (remainingText.IsEmpty) + { + isFinalLine = true; + return default; + } + + ReadOnlySpan next; + ReadOnlySpan rest; + + int lineLength = remainingText.IndexOf('\n'); + if (lineLength == -1) + { + lineLength = remainingText.Length; + isFinalLine = true; + rest = default; + } + else + { + rest = remainingText.Slice(lineLength + 1); + isFinalLine = false; + } + + if ((uint)lineLength > 0 && remainingText[lineLength - 1] == '\r') + { + lineLength--; + } + + next = remainingText.Slice(0, lineLength); + remainingText = rest; + return next; + } + + private static unsafe void AppendSpan(StringBuilder builder, ReadOnlySpan span) + { + fixed (char* ptr = span) + { + builder.Append(ptr, span.Length); + } + } +} diff --git a/Refit.GeneratorTests/_snapshots/InterfaceTests.NonRefitMethodShouldRaiseDiagnostic#IGeneratedClient.g.verified.cs b/Refit.GeneratorTests/_snapshots/InterfaceTests.NonRefitMethodShouldRaiseDiagnostic#IGeneratedClient.g.verified.cs index 1b01c0762..bd40b5712 100644 --- a/Refit.GeneratorTests/_snapshots/InterfaceTests.NonRefitMethodShouldRaiseDiagnostic#IGeneratedClient.g.verified.cs +++ b/Refit.GeneratorTests/_snapshots/InterfaceTests.NonRefitMethodShouldRaiseDiagnostic#IGeneratedClient.g.verified.cs @@ -50,7 +50,7 @@ public RefitGeneratorTestIGeneratedClient(global::System.Net.Http.HttpClient cli /// void global::RefitGeneratorTest.IGeneratedClient.NonRefitMethod() { - throw new global::System.NotImplementedException("Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument."); + throw new global::System.NotImplementedException("Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument."); } } } diff --git a/Refit.GeneratorTests/_snapshots/InterfaceTests.RefitInterfaceDerivedFromBaseTest#IGeneratedInterface.g.verified.cs b/Refit.GeneratorTests/_snapshots/InterfaceTests.RefitInterfaceDerivedFromBaseTest#IGeneratedInterface.g.verified.cs index e4f43fc5c..ad553c934 100644 --- a/Refit.GeneratorTests/_snapshots/InterfaceTests.RefitInterfaceDerivedFromBaseTest#IGeneratedInterface.g.verified.cs +++ b/Refit.GeneratorTests/_snapshots/InterfaceTests.RefitInterfaceDerivedFromBaseTest#IGeneratedInterface.g.verified.cs @@ -50,7 +50,7 @@ public RefitGeneratorTestIGeneratedInterface(global::System.Net.Http.HttpClient /// void global::RefitGeneratorTest.IBaseInterface.NonRefitMethod() { - throw new global::System.NotImplementedException("Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument."); + throw new global::System.NotImplementedException("Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument."); } } } diff --git a/Refit.GeneratorTests/_snapshots/MethodTests.MethodsWithGenericConstraints#IGeneratedClient.g.verified.cs b/Refit.GeneratorTests/_snapshots/MethodTests.MethodsWithGenericConstraints#IGeneratedClient.g.verified.cs index 3b8ce9976..349e3867c 100644 --- a/Refit.GeneratorTests/_snapshots/MethodTests.MethodsWithGenericConstraints#IGeneratedClient.g.verified.cs +++ b/Refit.GeneratorTests/_snapshots/MethodTests.MethodsWithGenericConstraints#IGeneratedClient.g.verified.cs @@ -61,7 +61,7 @@ public RefitGeneratorTestIGeneratedClient(global::System.Net.Http.HttpClient cli where T3 : struct where T5 : class { - throw new global::System.NotImplementedException("Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument."); + throw new global::System.NotImplementedException("Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument."); } } }