Skip to content

Commit 7036cba

Browse files
authored
Share SourceWriter between JSON & config binding generators (#89150)
* Share SourceWriter between JSON & config binding generators * Fix failing test, clean up impl, and address feedback
1 parent 50ba405 commit 7036cba

32 files changed

+298
-286
lines changed

src/libraries/System.Text.Json/gen/Helpers/SourceWriter.cs renamed to src/libraries/Common/src/SourceGenerators/SourceWriter.cs

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,21 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using Microsoft.CodeAnalysis.Text;
4+
using System;
5+
using System.Text;
56
using System.Diagnostics;
7+
using Microsoft.CodeAnalysis.Text;
68

7-
namespace System.Text.Json.SourceGeneration
9+
namespace SourceGenerators
810
{
911
internal sealed class SourceWriter
1012
{
13+
private const char IndentationChar = ' ';
14+
private const int CharsPerIndentation = 4;
15+
1116
private readonly StringBuilder _sb = new();
1217
private int _indentation;
1318

14-
public SourceWriter()
15-
{
16-
IndentationChar = ' ';
17-
CharsPerIndentation = 4;
18-
}
19-
20-
public SourceWriter(char indentationChar, int charsPerIndentation)
21-
{
22-
if (!char.IsWhiteSpace(indentationChar))
23-
{
24-
throw new ArgumentOutOfRangeException(nameof(indentationChar));
25-
}
26-
27-
if (charsPerIndentation < 1)
28-
{
29-
throw new ArgumentOutOfRangeException(nameof(charsPerIndentation));
30-
}
31-
32-
IndentationChar = indentationChar;
33-
CharsPerIndentation = charsPerIndentation;
34-
}
35-
36-
public char IndentationChar { get; }
37-
public int CharsPerIndentation { get; }
38-
39-
public int Length => _sb.Length;
4019
public int Indentation
4120
{
4221
get => _indentation;
@@ -88,6 +67,12 @@ public SourceText ToSourceText()
8867
return SourceText.From(_sb.ToString(), Encoding.UTF8);
8968
}
9069

70+
public void Reset()
71+
{
72+
_sb.Clear();
73+
_indentation = 0;
74+
}
75+
9176
private void AddIndentation()
9277
=> _sb.Append(IndentationChar, CharsPerIndentation * _indentation);
9378

src/libraries/Common/tests/Common.Tests.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
Link="Common\Interop\Linux\Interop.ProcFsStat.TryReadStatusFile.cs" />
1919
<Compile Include="$(CommonPath)Interop\Linux\os-release\Interop.OSReleaseFile.cs"
2020
Link="Common\Interop\Linux\os-release\Interop.OSReleaseFile.cs" />
21+
<Compile Include="$(CommonPath)SourceGenerators\SourceWriter.cs"
22+
Link="Common\SourceGenerators\SourceWriter.cs" />
2123
<Compile Include="$(CommonPath)System\CharArrayHelpers.cs"
2224
Link="Common\System\CharArrayHelpers.cs" />
2325
<Compile Include="$(CommonPath)System\StringExtensions.cs"
@@ -81,6 +83,7 @@
8183
<Compile Include="Tests\Interop\cgroupsTests.cs" />
8284
<Compile Include="Tests\Interop\procfsTests.cs" />
8385
<Compile Include="Tests\Interop\OSReleaseTests.cs" />
86+
<Compile Include="Tests\SourceGenerators\SourceWriterTests.cs" />
8487
<Compile Include="Tests\System\IO\PathInternal.Tests.cs" />
8588
<Compile Include="Tests\System\IO\StringParserTests.cs" />
8689
<Compile Include="Tests\System\Net\HttpDateParserTests.cs" />
@@ -155,6 +158,9 @@
155158
<Folder Include="System\Net\Sockets\" />
156159
<Folder Include="System\Net\VirtualNetwork\" />
157160
</ItemGroup>
161+
<ItemGroup>
162+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisVersion_LatestVS)" PrivateAssets="all" />
163+
</ItemGroup>
158164
<ItemGroup>
159165
<ProjectReference Include="$(CommonTestPath)StreamConformanceTests\StreamConformanceTests.csproj" />
160166
</ItemGroup>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using SourceGenerators;
5+
using Xunit;
6+
7+
namespace Common.Tests
8+
{
9+
public sealed class SourceWriterTests
10+
{
11+
[Fact]
12+
public void CanHandleVariousLineEndings()
13+
{
14+
string testTemplate = "public static void Main(){0}{{{1}\tConsole.WriteLine(\"Hello, world\");{2}}}";
15+
SourceWriter writer = new();
16+
17+
CheckCanWrite(string.Format(testTemplate, "\n", "\n", "\n"));
18+
CheckCanWrite(string.Format(testTemplate, "\r\n", "\r\n", "\r\n"));
19+
CheckCanWrite(string.Format(testTemplate, "\n", "\r\n", "\n"));
20+
21+
// Regression test for https://github.com/dotnet/runtime/issues/88918.
22+
CheckCanWrite(" global::Microsoft.Extensions.DependencyInjection.OptionsServiceCollectionExtensions.AddOptions(services);\r\n global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton<global::Microsoft.Extensions.Options.IOptionsChangeTokenSource<TOptions>>(services, new global::Microsoft.Extensions.Options.ConfigurationChangeTokenSource<TOptions>(name, configuration));\r\n return global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton<global::Microsoft.Extensions.Options.IConfigureOptions<TOptions>>(services, new global::Microsoft.Extensions.Options.ConfigureNamedOptions<TOptions>(name, obj => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.CoreBindingHelper.BindCoreUntyped(configuration, obj, typeof(TOptions), configureOptions)));\r\n}");
23+
24+
void CheckCanWrite(string source)
25+
{
26+
// No exception expected.
27+
writer.WriteLine(source);
28+
writer.Reset();
29+
}
30+
}
31+
}
32+
}

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.Text.RegularExpressions;
77
using Microsoft.CodeAnalysis;
8+
using SourceGenerators;
89

910
namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
1011
{
@@ -36,17 +37,17 @@ public void Emit()
3637
return;
3738
}
3839

39-
_writer.WriteBlock("""
40+
_writer.WriteLine("""
4041
// <auto-generated/>
4142
#nullable enable
4243
#pragma warning disable CS0612, CS0618 // Suppress warnings about [Obsolete] member usage in generated code.
4344
""");
44-
_writer.WriteBlankLine();
45+
_writer.WriteLine();
4546

4647
_useFullyQualifiedNames = true;
47-
EmitBinder_ConfigurationBinder();
48+
EmitBinder_Extensions_IConfiguration();
4849
EmitBinder_Extensions_OptionsBuilder();
49-
EmitBinder_Extensions_ServiceCollection();
50+
EmitBinder_Extensions_IServiceCollection();
5051

5152
_useFullyQualifiedNames = false;
5253
Emit_CoreBindingHelper();
@@ -131,14 +132,16 @@ private void EmitBindLogicFromString(
131132

132133
if (!checkForNullSectionValue)
133134
{
134-
writeOnSuccess?.Invoke(parsedValueExpr);
135+
InvokeWriteOnSuccess();
135136
}
136137
else
137138
{
138-
_writer.WriteBlockStart($"if ({sectionValueExpr} is string {nonNull_StringValue_Identifier})");
139-
writeOnSuccess?.Invoke(parsedValueExpr);
140-
_writer.WriteBlockEnd();
139+
EmitStartBlock($"if ({sectionValueExpr} is string {nonNull_StringValue_Identifier})");
140+
InvokeWriteOnSuccess();
141+
EmitEndBlock();
141142
}
143+
144+
void InvokeWriteOnSuccess() => writeOnSuccess?.Invoke(parsedValueExpr);
142145
}
143146

144147
private bool EmitObjectInit(TypeSpec type, string memberAccessExpr, InitializationKind initKind, string configArgExpr)
@@ -222,7 +225,7 @@ private void EmitCastToIConfigurationSection()
222225
exceptionTypeDisplayString = nameof(InvalidOperationException);
223226
}
224227

225-
_writer.WriteBlock($$"""
228+
_writer.WriteLine($$"""
226229
if ({{Identifier.configuration}} is not {{sectionTypeDisplayString}} {{Identifier.section}})
227230
{
228231
throw new {{exceptionTypeDisplayString}}();
@@ -235,13 +238,13 @@ private void EmitIConfigurationHasValueOrChildrenCheck(bool voidReturn)
235238
string returnPostfix = voidReturn ? string.Empty : " null";
236239
string methodDisplayString = GetHelperMethodDisplayString(Identifier.HasValueOrChildren);
237240

238-
_writer.WriteBlock($$"""
241+
_writer.WriteLine($$"""
239242
if (!{{methodDisplayString}}({{Identifier.configuration}}))
240243
{
241244
return{{returnPostfix}};
242245
}
243246
""");
244-
_writer.WriteBlankLine();
247+
_writer.WriteLine();
245248
}
246249
}
247250
}

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/ConfigurationBinder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ private sealed partial class Emitter
1414
{
1515
private bool ShouldEmitMethods(MethodsToGen_ConfigurationBinder methods) => (_sourceGenSpec.MethodsToGen_ConfigurationBinder & methods) != 0;
1616

17-
private void EmitBinder_ConfigurationBinder()
17+
private void EmitBinder_Extensions_IConfiguration()
1818
{
1919
Debug.Assert(_sourceGenSpec.TypesForGen_ConfigurationBinder_BindMethods.Count <= 3 &&
2020
!_sourceGenSpec.TypesForGen_ConfigurationBinder_BindMethods.Keys.Any(overload => (overload & MethodsToGen_ConfigurationBinder.Bind) is 0));
@@ -25,13 +25,13 @@ private void EmitBinder_ConfigurationBinder()
2525
}
2626

2727
_emitBlankLineBeforeNextStatement = false;
28-
EmitRootBindingClassBlockStart(Identifier.GeneratedConfigurationBinder);
28+
EmitRootBindingClassStartBlock(Identifier.GeneratedConfigurationBinder);
2929

3030
EmitGetMethods();
3131
EmitGetValueMethods();
3232
EmitBindMethods_ConfigurationBinder();
3333

34-
_writer.WriteBlockEnd();
34+
EmitEndBlock();
3535
_emitBlankLineBeforeNextStatement = true;
3636
}
3737

0 commit comments

Comments
 (0)