Skip to content

Commit e126c90

Browse files
committed
Experimental Source Generator (#431)
* Added packable and locally-referenceable source builder. * Added local Sandbox project that consumes the generator. * Added SourceGenerator info to the README.
1 parent 3275d22 commit e126c90

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2912
-13
lines changed

README.md

+70
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,10 @@ Generates Feature Files in `Corvus.Json.Specs` for the JSON Patch tests.
400400

401401
Benchmark suites for various components.
402402

403+
### Corvus.Json.SourceGenerator
404+
405+
The Source Generator which generates types from Json Schema.
406+
403407
## V4.0 Updates
404408

405409
There are a number of significant changes in this release
@@ -427,6 +431,72 @@ If so, you can now use the `--useImplicitOperatorString` command line switch to
427431

428432
Note: this means you will never use the built-in `Corvus.Json` types for your string-like types. This could increase the amount of code generated for your schema.
429433

434+
### New Source Generator
435+
436+
We now have a source generator that can generate types at compile time, rather than using the `generatejsonschematypes` tool.
437+
438+
### Using the source generator
439+
440+
Add a reference to the `Corvus.Json.SourceGenerator` nuget package in addition to `Corvus.Json.ExtendedTypes`. [Note, you may need to restart Visual Studio once you have done this.]
441+
Add your JSON schema file(s) as _AdditionalFiles_.C
442+
443+
```xml
444+
<Project Sdk="Microsoft.NET.Sdk">
445+
446+
<PropertyGroup>
447+
<TargetFramework>net8.0</TargetFramework>
448+
<ImplicitUsings>enable</ImplicitUsings>
449+
<Nullable>enable</Nullable>
450+
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
451+
</PropertyGroup>
452+
453+
<ItemGroup>
454+
<ProjectReference Include="..\Corvus.Json.ExtendedTypes\Corvus.Json.ExtendedTypes.csproj" />
455+
<ProjectReference
456+
Include="..\Corvus.Json.SourceGenerator\Corvus.Json.SourceGenerator.csproj"
457+
OutputItemType="Analyzer"
458+
ReferenceOutputAssembly="false"
459+
SetTargetFramework="TargetFramework=netstandard2.0" />
460+
</ItemGroup>
461+
462+
<ItemGroup>
463+
<AdditionalFiles Include="test.json" />
464+
</ItemGroup>
465+
466+
</Project>
467+
```
468+
469+
Now, create a `readonly partial struct` as a placeholder for your root generated type, and attribute it with
470+
`[JsonSchemaTypeGenerator]`. The path to the schema file is relative to the file containing the attribute. You can
471+
provide a pointer fragment in the usual way, if you need to e.g. `"./somefile.json#/components/schema/mySchema"`
472+
473+
```csharp
474+
namespace SourceGenTest2.Model;
475+
476+
using Corvus.Json;
477+
478+
[JsonSchemaTypeGenerator("../test.json")]
479+
public readonly partial struct FlimFlam
480+
{
481+
}
482+
```
483+
484+
The source generator will now automatically emit code for your schema, and you can use the generated types in your code.
485+
486+
```
487+
using Corvus.Json;
488+
using SourceGenTest2.Model;
489+
490+
FlimFlam flimFlam = JsonAny.ParseValue("[1,2,3]"u8);
491+
Console.WriteLine(flimFlam);
492+
JsonArray array = flimFlam.As<JsonArray>();
493+
Console.WriteLine(array);
494+
```
495+
496+
You can find an example project here: [Sandbox.SourceGenerator](./Solutions/Sandbox.SourceGenerator)
497+
498+
We'd like to credit our Google Summer of Code 2024 contributor, [Pranay Joshi](https://github.com/pranayjoshi) and mentor [Greg Dennis](https://github.com/gregsdennis) for their work on this tool.
499+
430500
### New dynamic schema validation
431501

432502
There is a new `Corvus.Json.Validator` assembly, containing a `JsonSchema` type.

Solutions/Corvus.Json.CodeGeneration.CSharp/Corvus.Json.CodeGeneration/CSharp/PublicCodeGeneratorExtensions.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,9 @@ public static CodeGenerator AppendSeparatorLine(this CodeGenerator generator)
243243
return generator;
244244
}
245245

246-
if ((generator.ScopeType == ScopeType.Type && generator.EndsWith($";{Environment.NewLine}")) ||
247-
generator.EndsWith($"}}{Environment.NewLine}") ||
248-
generator.EndsWith($"#endif{Environment.NewLine}"))
246+
if ((generator.ScopeType == ScopeType.Type && generator.EndsWith($";\n")) ||
247+
generator.EndsWith($"}}\n") ||
248+
generator.EndsWith($"#endif\n"))
249249
{
250250
// Append a blank line
251251
generator.AppendLine();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// <copyright file="ValueStringBuilder.AppendSpanFormattable.cs" company="Endjin Limited">
2+
// Copyright (c) Endjin Limited. All rights reserved.
3+
// </copyright>
4+
// <license>
5+
// Derived from code Licensed to the .NET Foundation under one or more agreements.
6+
// The .NET Foundation licenses this file to you under the MIT license.
7+
// See https://github.com/dotnet/runtime/blob/c1049390d5b33483203f058b0e1457d2a1f62bf4/src/libraries/Common/src/System/Text/ValueStringBuilder.cs
8+
// </license>
9+
10+
#pragma warning disable // Currently this is a straight copy of the original code, so we disable warnings to avoid diagnostic problems.
11+
12+
#if NET8_0_OR_GREATER
13+
14+
namespace Corvus.HighPerformance;
15+
16+
public ref partial struct ValueStringBuilder
17+
{
18+
public void AppendSpanFormattable<T>(T value, string? format = null, IFormatProvider? provider = null) where T : ISpanFormattable
19+
{
20+
if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider))
21+
{
22+
_pos += charsWritten;
23+
}
24+
else
25+
{
26+
Append(value.ToString(format, provider));
27+
}
28+
}
29+
}
30+
31+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// <copyright file="ValueStringBuilder.Replace.cs" company="Endjin Limited">
2+
// Copyright (c) Endjin Limited. All rights reserved.
3+
// </copyright>
4+
// <license>
5+
// Derived from code Licensed to the .NET Foundation under one or more agreements.
6+
// The .NET Foundation licenses this file to you under the MIT license.
7+
// See https://github.com/dotnet/runtime/blob/c1049390d5b33483203f058b0e1457d2a1f62bf4/src/libraries/Common/src/System/Text/ValueStringBuilder.cs
8+
// </license>
9+
10+
#pragma warning disable // Currently this is a straight copy of the original code, so we disable warnings to avoid diagnostic problems.
11+
12+
#if !NET8_0_OR_GREATER
13+
using System.Runtime.CompilerServices;
14+
#endif
15+
16+
namespace Corvus.HighPerformance;
17+
18+
public ref partial struct ValueStringBuilder
19+
{
20+
#if !NET8_0_OR_GREATER
21+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
22+
public void Replace(
23+
string oldValue,
24+
string newValue,
25+
int startIndex,
26+
int count)
27+
=> Replace(oldValue.AsSpan(), newValue.AsSpan(), startIndex, count);
28+
#endif
29+
30+
public void Replace(
31+
ReadOnlySpan<char> oldValue,
32+
ReadOnlySpan<char> newValue,
33+
int startIndex,
34+
int count)
35+
{
36+
if (startIndex < 0 || (startIndex + count) > _pos)
37+
{
38+
throw new ArgumentOutOfRangeException(nameof(startIndex));
39+
}
40+
41+
if (count == 0)
42+
{
43+
return;
44+
}
45+
46+
Span<char> rangeBuffer = _chars.Slice(startIndex, count);
47+
48+
int diff = newValue.Length - oldValue.Length;
49+
if (diff == 0)
50+
{
51+
int matchIndex = rangeBuffer.IndexOf(oldValue);
52+
if (matchIndex == -1)
53+
{
54+
return;
55+
}
56+
57+
Span<char> remainingBuffer = rangeBuffer;
58+
do
59+
{
60+
remainingBuffer = remainingBuffer[matchIndex..];
61+
newValue.CopyTo(remainingBuffer);
62+
remainingBuffer = remainingBuffer[oldValue.Length..];
63+
64+
matchIndex = remainingBuffer.IndexOf(oldValue);
65+
} while (matchIndex != -1);
66+
67+
return;
68+
}
69+
70+
if (diff < 0)
71+
{
72+
int matchIndex = rangeBuffer.IndexOf(oldValue);
73+
if (matchIndex == -1)
74+
{
75+
return;
76+
}
77+
78+
// We will never need to grow the buffer, but we might need to shift characters
79+
// down.
80+
Span<char> remainingTargetBuffer = _chars[(startIndex + matchIndex)..this._pos];
81+
Span<char> remainingSourceBuffer = remainingTargetBuffer;
82+
int endOfSearchRangeRelativeToRemainingSourceBuffer = count - matchIndex;
83+
do
84+
{
85+
this._pos += diff;
86+
87+
newValue.CopyTo(remainingTargetBuffer);
88+
89+
remainingSourceBuffer = remainingSourceBuffer[oldValue.Length..];
90+
endOfSearchRangeRelativeToRemainingSourceBuffer -= oldValue.Length;
91+
remainingTargetBuffer = remainingTargetBuffer[newValue.Length..];
92+
93+
matchIndex = remainingSourceBuffer[..endOfSearchRangeRelativeToRemainingSourceBuffer]
94+
.IndexOf(oldValue);
95+
96+
int lengthOfChunkToRelocate = matchIndex == -1
97+
? remainingSourceBuffer.Length
98+
: matchIndex;
99+
remainingSourceBuffer[..lengthOfChunkToRelocate].CopyTo(remainingTargetBuffer);
100+
101+
remainingSourceBuffer = remainingSourceBuffer[lengthOfChunkToRelocate..];
102+
endOfSearchRangeRelativeToRemainingSourceBuffer -= lengthOfChunkToRelocate;
103+
remainingTargetBuffer = remainingTargetBuffer[lengthOfChunkToRelocate..];
104+
} while (matchIndex != -1);
105+
106+
return;
107+
}
108+
else
109+
{
110+
int matchIndex = rangeBuffer.IndexOf(oldValue);
111+
if (matchIndex == -1)
112+
{
113+
return;
114+
}
115+
116+
Span<int> matchIndexes = stackalloc int[(rangeBuffer.Length + oldValue.Length - 1) / oldValue.Length];
117+
118+
int matchCount = 0;
119+
int currentRelocationDistance = 0;
120+
while (matchIndex != -1)
121+
{
122+
matchIndexes[matchCount++] = matchIndex;
123+
currentRelocationDistance += diff;
124+
125+
int nextIndex = rangeBuffer[(matchIndex + oldValue.Length)..].IndexOf(oldValue);
126+
matchIndex = nextIndex == -1 ? -1 : matchIndex + nextIndex + oldValue.Length;
127+
}
128+
129+
int relocationRangeEndIndex = this._pos;
130+
131+
int growBy = (this._pos + currentRelocationDistance) - _chars.Length;
132+
if (growBy > 0)
133+
{
134+
Grow(growBy);
135+
}
136+
this._pos += currentRelocationDistance;
137+
138+
139+
// We work from the back of the string when growing to avoid having to
140+
// shift anything more than once.
141+
do
142+
{
143+
matchIndex = matchIndexes[matchCount - 1];
144+
145+
int relocationTargetStart = startIndex + matchIndex + oldValue.Length + currentRelocationDistance;
146+
int relocationSourceStart = startIndex + matchIndex + oldValue.Length;
147+
int endOfSearchRangeRelativeToRemainingSourceBuffer = count - matchIndex;
148+
149+
Span<char> relocationTargetBuffer = _chars[relocationTargetStart..];
150+
Span<char> sourceBuffer = _chars[relocationSourceStart..relocationRangeEndIndex];
151+
152+
sourceBuffer.CopyTo(relocationTargetBuffer);
153+
154+
currentRelocationDistance -= diff;
155+
Span<char> replaceTargetBuffer = this._chars.Slice(startIndex + matchIndex + currentRelocationDistance);
156+
newValue.CopyTo(replaceTargetBuffer);
157+
158+
relocationRangeEndIndex = matchIndex + startIndex;
159+
matchIndex = rangeBuffer[..matchIndex].LastIndexOf(oldValue);
160+
161+
matchCount -= 1;
162+
} while (matchCount > 0);
163+
}
164+
}
165+
}

0 commit comments

Comments
 (0)