Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental Source Generator #431

Merged
merged 4 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,10 @@ Generates Feature Files in `Corvus.Json.Specs` for the JSON Patch tests.

Benchmark suites for various components.

### Corvus.Json.SourceGenerator

The Source Generator which generates types from Json Schema.

## V4.0 Updates

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

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.

### New Source Generator

We now have a source generator that can generate types at compile time, rather than using the `generatejsonschematypes` tool.

### Using the source generator

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.]
Add your JSON schema file(s) as _AdditionalFiles_.C

```xml
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Corvus.Json.ExtendedTypes\Corvus.Json.ExtendedTypes.csproj" />
<ProjectReference
Include="..\Corvus.Json.SourceGenerator\Corvus.Json.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
SetTargetFramework="TargetFramework=netstandard2.0" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="test.json" />
</ItemGroup>

</Project>
```

Now, create a `readonly partial struct` as a placeholder for your root generated type, and attribute it with
`[JsonSchemaTypeGenerator]`. The path to the schema file is relative to the file containing the attribute. You can
provide a pointer fragment in the usual way, if you need to e.g. `"./somefile.json#/components/schema/mySchema"`

```csharp
namespace SourceGenTest2.Model;

using Corvus.Json;

[JsonSchemaTypeGenerator("../test.json")]
public readonly partial struct FlimFlam
{
}
```

The source generator will now automatically emit code for your schema, and you can use the generated types in your code.

```
using Corvus.Json;
using SourceGenTest2.Model;

FlimFlam flimFlam = JsonAny.ParseValue("[1,2,3]"u8);
Console.WriteLine(flimFlam);
JsonArray array = flimFlam.As<JsonArray>();
Console.WriteLine(array);
```

You can find an example project here: [Sandbox.SourceGenerator](./Solutions/Sandbox.SourceGenerator)

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.

### New dynamic schema validation

There is a new `Corvus.Json.Validator` assembly, containing a `JsonSchema` type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ public static CodeGenerator AppendSeparatorLine(this CodeGenerator generator)
return generator;
}

if ((generator.ScopeType == ScopeType.Type && generator.EndsWith($";{Environment.NewLine}")) ||
generator.EndsWith($"}}{Environment.NewLine}") ||
generator.EndsWith($"#endif{Environment.NewLine}"))
if ((generator.ScopeType == ScopeType.Type && generator.EndsWith($";\n")) ||
generator.EndsWith($"}}\n") ||
generator.EndsWith($"#endif\n"))
{
// Append a blank line
generator.AppendLine();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// <copyright file="ValueStringBuilder.AppendSpanFormattable.cs" company="Endjin Limited">
// Copyright (c) Endjin Limited. All rights reserved.
// </copyright>
// <license>
// Derived from code Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See https://github.com/dotnet/runtime/blob/c1049390d5b33483203f058b0e1457d2a1f62bf4/src/libraries/Common/src/System/Text/ValueStringBuilder.cs
// </license>

#pragma warning disable // Currently this is a straight copy of the original code, so we disable warnings to avoid diagnostic problems.

#if NET8_0_OR_GREATER

namespace Corvus.HighPerformance;

public ref partial struct ValueStringBuilder
{
public void AppendSpanFormattable<T>(T value, string? format = null, IFormatProvider? provider = null) where T : ISpanFormattable
{
if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider))
{
_pos += charsWritten;
}
else
{
Append(value.ToString(format, provider));
}
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// <copyright file="ValueStringBuilder.Replace.cs" company="Endjin Limited">
// Copyright (c) Endjin Limited. All rights reserved.
// </copyright>
// <license>
// Derived from code Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See https://github.com/dotnet/runtime/blob/c1049390d5b33483203f058b0e1457d2a1f62bf4/src/libraries/Common/src/System/Text/ValueStringBuilder.cs
// </license>

#pragma warning disable // Currently this is a straight copy of the original code, so we disable warnings to avoid diagnostic problems.

#if !NET8_0_OR_GREATER
using System.Runtime.CompilerServices;
#endif

namespace Corvus.HighPerformance;

public ref partial struct ValueStringBuilder
{
#if !NET8_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Replace(
string oldValue,
string newValue,
int startIndex,
int count)
=> Replace(oldValue.AsSpan(), newValue.AsSpan(), startIndex, count);
#endif

public void Replace(
ReadOnlySpan<char> oldValue,
ReadOnlySpan<char> newValue,
int startIndex,
int count)
{
if (startIndex < 0 || (startIndex + count) > _pos)
{
throw new ArgumentOutOfRangeException(nameof(startIndex));
}

if (count == 0)
{
return;
}

Span<char> rangeBuffer = _chars.Slice(startIndex, count);

int diff = newValue.Length - oldValue.Length;
if (diff == 0)
{
int matchIndex = rangeBuffer.IndexOf(oldValue);
if (matchIndex == -1)
{
return;
}

Span<char> remainingBuffer = rangeBuffer;
do
{
remainingBuffer = remainingBuffer[matchIndex..];
newValue.CopyTo(remainingBuffer);
remainingBuffer = remainingBuffer[oldValue.Length..];

matchIndex = remainingBuffer.IndexOf(oldValue);
} while (matchIndex != -1);

return;
}

if (diff < 0)
{
int matchIndex = rangeBuffer.IndexOf(oldValue);
if (matchIndex == -1)
{
return;
}

// We will never need to grow the buffer, but we might need to shift characters
// down.
Span<char> remainingTargetBuffer = _chars[(startIndex + matchIndex)..this._pos];
Span<char> remainingSourceBuffer = remainingTargetBuffer;
int endOfSearchRangeRelativeToRemainingSourceBuffer = count - matchIndex;
do
{
this._pos += diff;

newValue.CopyTo(remainingTargetBuffer);

remainingSourceBuffer = remainingSourceBuffer[oldValue.Length..];
endOfSearchRangeRelativeToRemainingSourceBuffer -= oldValue.Length;
remainingTargetBuffer = remainingTargetBuffer[newValue.Length..];

matchIndex = remainingSourceBuffer[..endOfSearchRangeRelativeToRemainingSourceBuffer]
.IndexOf(oldValue);

int lengthOfChunkToRelocate = matchIndex == -1
? remainingSourceBuffer.Length
: matchIndex;
remainingSourceBuffer[..lengthOfChunkToRelocate].CopyTo(remainingTargetBuffer);

remainingSourceBuffer = remainingSourceBuffer[lengthOfChunkToRelocate..];
endOfSearchRangeRelativeToRemainingSourceBuffer -= lengthOfChunkToRelocate;
remainingTargetBuffer = remainingTargetBuffer[lengthOfChunkToRelocate..];
} while (matchIndex != -1);

return;
}
else
{
int matchIndex = rangeBuffer.IndexOf(oldValue);
if (matchIndex == -1)
{
return;
}

Span<int> matchIndexes = stackalloc int[(rangeBuffer.Length + oldValue.Length - 1) / oldValue.Length];

int matchCount = 0;
int currentRelocationDistance = 0;
while (matchIndex != -1)
{
matchIndexes[matchCount++] = matchIndex;
currentRelocationDistance += diff;

int nextIndex = rangeBuffer[(matchIndex + oldValue.Length)..].IndexOf(oldValue);
matchIndex = nextIndex == -1 ? -1 : matchIndex + nextIndex + oldValue.Length;
}

int relocationRangeEndIndex = this._pos;

int growBy = (this._pos + currentRelocationDistance) - _chars.Length;
if (growBy > 0)
{
Grow(growBy);
}
this._pos += currentRelocationDistance;


// We work from the back of the string when growing to avoid having to
// shift anything more than once.
do
{
matchIndex = matchIndexes[matchCount - 1];

int relocationTargetStart = startIndex + matchIndex + oldValue.Length + currentRelocationDistance;
int relocationSourceStart = startIndex + matchIndex + oldValue.Length;
int endOfSearchRangeRelativeToRemainingSourceBuffer = count - matchIndex;

Span<char> relocationTargetBuffer = _chars[relocationTargetStart..];
Span<char> sourceBuffer = _chars[relocationSourceStart..relocationRangeEndIndex];

sourceBuffer.CopyTo(relocationTargetBuffer);

currentRelocationDistance -= diff;
Span<char> replaceTargetBuffer = this._chars.Slice(startIndex + matchIndex + currentRelocationDistance);
newValue.CopyTo(replaceTargetBuffer);

relocationRangeEndIndex = matchIndex + startIndex;
matchIndex = rangeBuffer[..matchIndex].LastIndexOf(oldValue);

matchCount -= 1;
} while (matchCount > 0);
}
}
}
Loading
Loading