Skip to content

Commit 6c575e3

Browse files
feat: add SourceWriter (#1966)
1 parent 367838e commit 6c575e3

10 files changed

+169
-28
lines changed

Diff for: InterfaceStubGenerator.Roslyn38/InterfaceStubGenerator.Roslyn38.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<IsRoslynComponent>true</IsRoslynComponent>
1111
<Nullable>enable</Nullable>
1212
<MsCACSharpVersion>3.8.0</MsCACSharpVersion>
13+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
1314
</PropertyGroup>
1415

1516
<ItemGroup>

Diff for: InterfaceStubGenerator.Roslyn41/InterfaceStubGenerator.Roslyn41.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<Nullable>enable</Nullable>
1212
<DefineConstants>$(DefineConstants);ROSLYN_4</DefineConstants>
1313
<MsCACSharpVersion>4.1.0</MsCACSharpVersion>
14+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
1415
</PropertyGroup>
1516

1617
<ItemGroup>

Diff for: InterfaceStubGenerator.Shared/Emitter.cs

+19-23
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,18 @@ public static void Initialize()
6969
addSource("Generated.g.cs", SourceText.From(generatedClassText, Encoding.UTF8));
7070
}
7171

72-
public static string EmitInterface(InterfaceModel model)
72+
public static SourceText EmitInterface(InterfaceModel model)
7373
{
74-
var source = new StringBuilder();
74+
var source = new SourceWriter();
7575

7676
// if nullability is supported emit the nullable directive
7777
if (model.Nullability != Nullability.None)
7878
{
79-
source.Append("#nullable ");
80-
source.Append(model.Nullability == Nullability.Enabled ? "enable" : "disable");
79+
source.WriteLine("#nullable " + (model.Nullability == Nullability.Enabled ? "enable" : "disable"));
8180
}
8281

83-
source.Append(
84-
$@"
85-
#pragma warning disable
82+
source.WriteLine(
83+
$@"#pragma warning disable
8684
namespace Refit.Implementation
8785
{{
8886
@@ -108,8 +106,7 @@ partial class {model.Ns}{model.ClassDeclaration}
108106
{{
109107
Client = client;
110108
this.requestBuilder = requestBuilder;
111-
}}
112-
"
109+
}}"
113110
);
114111

115112
var uniqueNames = new UniqueNameBuilder();
@@ -138,16 +135,15 @@ partial class {model.Ns}{model.ClassDeclaration}
138135
WriteDisposableMethod(source);
139136
}
140137

141-
source.Append(
138+
source.WriteLine(
142139
@"
143140
}
144141
}
145142
}
146143
147-
#pragma warning restore
148-
"
144+
#pragma warning restore"
149145
);
150-
return source.ToString();
146+
return source.ToSourceText();
151147
}
152148

153149
/// <summary>
@@ -158,7 +154,7 @@ partial class {model.Ns}{model.ClassDeclaration}
158154
/// <param name="isTopLevel">True if directly from the type we're generating for, false for methods found on base interfaces</param>
159155
/// <param name="uniqueNames">Contains the unique member names in the interface scope.</param>
160156
private static void WriteRefitMethod(
161-
StringBuilder source,
157+
SourceWriter source,
162158
MethodModel methodModel,
163159
bool isTopLevel,
164160
UniqueNameBuilder uniqueNames
@@ -220,23 +216,23 @@ UniqueNameBuilder uniqueNames
220216
WriteMethodClosing(source);
221217
}
222218

223-
private static void WriteNonRefitMethod(StringBuilder source, MethodModel methodModel)
219+
private static void WriteNonRefitMethod(SourceWriter source, MethodModel methodModel)
224220
{
225221
WriteMethodOpening(source, methodModel, true);
226222

227-
source.Append(
223+
source.WriteLine(
228224
@"
229-
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."");
230-
"
231-
);
225+
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."");");
232226

227+
source.Indentation += 1;
233228
WriteMethodClosing(source);
229+
source.Indentation -= 1;
234230
}
235231

236232
// TODO: This assumes that the Dispose method is a void that takes no parameters.
237233
// The previous version did not.
238234
// Does the bool overload cause an issue here.
239-
private static void WriteDisposableMethod(StringBuilder source)
235+
private static void WriteDisposableMethod(SourceWriter source)
240236
{
241237
source.Append(
242238
"""
@@ -252,7 +248,7 @@ private static void WriteDisposableMethod(StringBuilder source)
252248
}
253249

254250
private static string GenerateTypeParameterExpression(
255-
StringBuilder source,
251+
SourceWriter source,
256252
MethodModel methodModel,
257253
UniqueNameBuilder uniqueNames
258254
)
@@ -283,7 +279,7 @@ UniqueNameBuilder uniqueNames
283279
}
284280

285281
private static void WriteMethodOpening(
286-
StringBuilder source,
282+
SourceWriter source,
287283
MethodModel methodModel,
288284
bool isExplicitInterface,
289285
bool isAsync = false
@@ -324,7 +320,7 @@ private static void WriteMethodOpening(
324320
);
325321
}
326322

327-
private static void WriteMethodClosing(StringBuilder source) => source.Append(@" }");
323+
private static void WriteMethodClosing(SourceWriter source) => source.Append(@" }");
328324

329325
private static string GenerateConstraints(
330326
ImmutableEquatableArray<TypeConstraint> typeParameters,

Diff for: InterfaceStubGenerator.Shared/IncrementalValuesProviderExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ IncrementalValuesProvider<InterfaceModel> model
6060
static (spc, model) =>
6161
{
6262
var mapperText = Emitter.EmitInterface(model);
63-
spc.AddSource(model.FileName, SourceText.From(mapperText, Encoding.UTF8));
63+
spc.AddSource(model.FileName, mapperText);
6464
}
6565
);
6666
}

Diff for: InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.projitems

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<Compile Include="$(MSBuildThisFileDirectory)Models\TypeConstraint.cs" />
2424
<Compile Include="$(MSBuildThisFileDirectory)Models\WellKnownTYpes.cs" />
2525
<Compile Include="$(MSBuildThisFileDirectory)Parser.cs" />
26+
<Compile Include="$(MSBuildThisFileDirectory)SourceWriter.cs" />
2627
<Compile Include="$(MSBuildThisFileDirectory)UniqueNameBuilder.cs" />
2728
</ItemGroup>
2829
</Project>

Diff for: InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ out var refitInternalNamespace
6363
var interfaceText = Emitter.EmitInterface(interfaceModel);
6464
context.AddSource(
6565
interfaceModel.FileName,
66-
SourceText.From(interfaceText, Encoding.UTF8)
66+
interfaceText
6767
);
6868
}
6969

Diff for: InterfaceStubGenerator.Shared/SourceWriter.cs

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using System.Diagnostics;
2+
using System.Text;
3+
4+
using Microsoft.CodeAnalysis.Text;
5+
6+
namespace Refit.Generator;
7+
8+
// From https://github.com/dotnet/runtime/blob/233826c88d2100263fb9e9535d96f75824ba0aea/src/libraries/Common/src/SourceGenerators/SourceWriter.cs#L11
9+
internal sealed class SourceWriter
10+
{
11+
private const char IndentationChar = ' ';
12+
private const int CharsPerIndentation = 4;
13+
14+
private readonly StringBuilder _sb = new();
15+
private int _indentation;
16+
17+
public int Indentation
18+
{
19+
get => _indentation;
20+
set
21+
{
22+
if (value < 0)
23+
{
24+
Throw();
25+
static void Throw() => throw new ArgumentOutOfRangeException(nameof(value));
26+
}
27+
28+
_indentation = value;
29+
}
30+
}
31+
32+
public void Append(string text)
33+
{
34+
if (_indentation == 0)
35+
{
36+
_sb.Append(text);
37+
return;
38+
}
39+
40+
bool isFinalLine;
41+
ReadOnlySpan<char> remainingText = text.AsSpan();
42+
do
43+
{
44+
ReadOnlySpan<char> nextLine = GetNextLine(ref remainingText, out isFinalLine);
45+
46+
AddIndentation();
47+
AppendSpan(_sb, nextLine);
48+
if (!isFinalLine)
49+
{
50+
_sb.AppendLine();
51+
}
52+
}
53+
while (!isFinalLine);
54+
}
55+
56+
public void WriteLine(char value)
57+
{
58+
AddIndentation();
59+
_sb.Append(value);
60+
_sb.AppendLine();
61+
}
62+
63+
public void WriteLine(string text)
64+
{
65+
if (_indentation == 0)
66+
{
67+
_sb.AppendLine(text);
68+
return;
69+
}
70+
71+
bool isFinalLine;
72+
ReadOnlySpan<char> remainingText = text.AsSpan();
73+
do
74+
{
75+
ReadOnlySpan<char> nextLine = GetNextLine(ref remainingText, out isFinalLine);
76+
77+
AddIndentation();
78+
AppendSpan(_sb, nextLine);
79+
_sb.AppendLine();
80+
}
81+
while (!isFinalLine);
82+
}
83+
84+
public void WriteLine() => _sb.AppendLine();
85+
86+
public SourceText ToSourceText()
87+
{
88+
Debug.Assert(_indentation == 0 && _sb.Length > 0);
89+
return SourceText.From(_sb.ToString(), Encoding.UTF8);
90+
}
91+
92+
public void Reset()
93+
{
94+
_sb.Clear();
95+
_indentation = 0;
96+
}
97+
98+
private void AddIndentation()
99+
=> _sb.Append(IndentationChar, CharsPerIndentation * _indentation);
100+
101+
private static ReadOnlySpan<char> GetNextLine(ref ReadOnlySpan<char> remainingText, out bool isFinalLine)
102+
{
103+
if (remainingText.IsEmpty)
104+
{
105+
isFinalLine = true;
106+
return default;
107+
}
108+
109+
ReadOnlySpan<char> next;
110+
ReadOnlySpan<char> rest;
111+
112+
int lineLength = remainingText.IndexOf('\n');
113+
if (lineLength == -1)
114+
{
115+
lineLength = remainingText.Length;
116+
isFinalLine = true;
117+
rest = default;
118+
}
119+
else
120+
{
121+
rest = remainingText.Slice(lineLength + 1);
122+
isFinalLine = false;
123+
}
124+
125+
if ((uint)lineLength > 0 && remainingText[lineLength - 1] == '\r')
126+
{
127+
lineLength--;
128+
}
129+
130+
next = remainingText.Slice(0, lineLength);
131+
remainingText = rest;
132+
return next;
133+
}
134+
135+
private static unsafe void AppendSpan(StringBuilder builder, ReadOnlySpan<char> span)
136+
{
137+
fixed (char* ptr = span)
138+
{
139+
builder.Append(ptr, span.Length);
140+
}
141+
}
142+
}

Diff for: Refit.GeneratorTests/_snapshots/InterfaceTests.NonRefitMethodShouldRaiseDiagnostic#IGeneratedClient.g.verified.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public RefitGeneratorTestIGeneratedClient(global::System.Net.Http.HttpClient cli
5050
/// <inheritdoc />
5151
void global::RefitGeneratorTest.IGeneratedClient.NonRefitMethod()
5252
{
53-
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.");
53+
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.");
5454
}
5555
}
5656
}

Diff for: Refit.GeneratorTests/_snapshots/InterfaceTests.RefitInterfaceDerivedFromBaseTest#IGeneratedInterface.g.verified.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public RefitGeneratorTestIGeneratedInterface(global::System.Net.Http.HttpClient
5050
/// <inheritdoc />
5151
void global::RefitGeneratorTest.IBaseInterface.NonRefitMethod()
5252
{
53-
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.");
53+
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.");
5454
}
5555
}
5656
}

Diff for: Refit.GeneratorTests/_snapshots/MethodTests.MethodsWithGenericConstraints#IGeneratedClient.g.verified.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public RefitGeneratorTestIGeneratedClient(global::System.Net.Http.HttpClient cli
6161
where T3 : struct
6262
where T5 : class
6363
{
64-
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.");
64+
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.");
6565
}
6666
}
6767
}

0 commit comments

Comments
 (0)