Skip to content

Commit a82a697

Browse files
committed
feat: incremental generation tests
1 parent a6e673d commit a82a697

File tree

6 files changed

+345
-6
lines changed

6 files changed

+345
-6
lines changed

NewType.Generator/NUGET.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
# `newtype` (Distinct Type Aliases for C#)
1+
# `newtype` *(Distinct Type Aliases for C#)*
22

33
![logo, a stylized N with a red and Blue half](https://raw.githubusercontent.com/outfox/newtype/main/logo.svg)
44

5-
A source generator that creates distinct type aliases with full operator forwarding. Inspired by Haskell's `newtype` and
6-
F#'s type abbreviations. `newtype` works for a healthy number of types - many primitives, structs, records, classes work out of the box.
5+
This package is a source generator that creates distinct type aliases with full operator and constructor forwarding. Inspired by Haskell's `newtype` and F#'s type abbreviations. `newtype` works for a healthy number of types - many primitives, structs, records, classes work out of the box.
76

87
## Installation
98

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using Basic.Reference.Assemblies;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using newtype.generator;
5+
using Xunit;
6+
7+
namespace newtype.tests;
8+
9+
public class GeneratorOutputTests
10+
{
11+
[Fact]
12+
public void Generates_Output_For_Int_Alias()
13+
{
14+
const string source = """
15+
using newtype;
16+
17+
[newtype<int>]
18+
public readonly partial struct TestId;
19+
""";
20+
21+
var result = GeneratorTestHelper.RunGenerator(source);
22+
23+
// Attribute source + alias source
24+
Assert.Equal(2, result.GeneratedTrees.Length);
25+
26+
var generatedSources = result.Results[0].GeneratedSources;
27+
28+
var aliasSource = generatedSources.Single(s => s.HintName.EndsWith("TestId.g.cs"));
29+
var text = aliasSource.SourceText.ToString();
30+
31+
Assert.Contains("private readonly int _value;", text);
32+
Assert.Contains("public TestId(int value)", text);
33+
Assert.Contains("operator +", text);
34+
Assert.Contains("operator ==", text);
35+
Assert.Contains("IComparable<TestId>", text);
36+
Assert.Contains("IEquatable<TestId>", text);
37+
}
38+
39+
[Fact]
40+
public void Generates_Output_For_String_Alias()
41+
{
42+
const string source = """
43+
using newtype;
44+
45+
[newtype<string>]
46+
public readonly partial struct Label;
47+
""";
48+
49+
var result = GeneratorTestHelper.RunGenerator(source);
50+
51+
Assert.Equal(2, result.GeneratedTrees.Length);
52+
53+
var generatedSources = result.Results[0].GeneratedSources;
54+
55+
var aliasSource = generatedSources.Single(s => s.HintName.EndsWith("Label.g.cs"));
56+
var text = aliasSource.SourceText.ToString();
57+
58+
Assert.Contains("private readonly string _value;", text);
59+
Assert.Contains(@"_value?.ToString() ?? """"", text);
60+
Assert.Contains("IComparable<Label>", text);
61+
}
62+
63+
[Fact]
64+
public void Generates_Output_For_Custom_Class_Alias()
65+
{
66+
const string source = """
67+
using newtype;
68+
using System;
69+
70+
public class Widget
71+
{
72+
public string Name { get; }
73+
74+
public Widget(string name) => Name = name;
75+
76+
public static Widget operator +(Widget a, Widget b) => new(a.Name + b.Name);
77+
}
78+
79+
[newtype<Widget>]
80+
public readonly partial struct MyWidget;
81+
""";
82+
83+
var result = GeneratorTestHelper.RunGenerator(source);
84+
85+
Assert.Equal(2, result.GeneratedTrees.Length);
86+
87+
var generatedSources = result.Results[0].GeneratedSources;
88+
89+
var aliasSource = generatedSources.Single(s => s.HintName.EndsWith("MyWidget.g.cs"));
90+
var text = aliasSource.SourceText.ToString();
91+
92+
// Operator forwarding
93+
Assert.Contains("operator +", text);
94+
// Property forwarding
95+
Assert.Contains("Name", text);
96+
// Constructor forwarding
97+
Assert.Contains("public MyWidget(string name)", text);
98+
}
99+
100+
[Fact]
101+
public void Non_Partial_Struct_Produces_Compilation_Error()
102+
{
103+
const string source = """
104+
using newtype;
105+
106+
[newtype<int>]
107+
public struct Bad;
108+
""";
109+
110+
// Run the generator directly — we expect a compilation error (CS0260),
111+
// so we can't use RunGenerator which asserts no errors.
112+
var syntaxTree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Preview));
113+
114+
var compilation = CSharpCompilation.Create(
115+
"Tests",
116+
[syntaxTree],
117+
Net80.References.All,
118+
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
119+
120+
var generator = new AliasGenerator().AsSourceGenerator();
121+
122+
GeneratorDriver driver = CSharpGeneratorDriver.Create(
123+
generators: [generator],
124+
parseOptions: new CSharpParseOptions(LanguageVersion.Preview),
125+
driverOptions: new GeneratorDriverOptions(
126+
disabledOutputs: IncrementalGeneratorOutputKind.None,
127+
trackIncrementalGeneratorSteps: true));
128+
129+
driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _);
130+
131+
// The generated partial declaration conflicts with the non-partial user declaration
132+
var errors = outputCompilation.GetDiagnostics()
133+
.Where(d => d.Severity == DiagnosticSeverity.Error)
134+
.ToArray();
135+
136+
Assert.Contains(errors, e => e.Id == "CS0260");
137+
}
138+
139+
[Fact]
140+
public void Generates_Separate_Outputs_Per_Type()
141+
{
142+
const string source = """
143+
using newtype;
144+
145+
[newtype<int>]
146+
public readonly partial struct IdA;
147+
148+
[newtype<float>]
149+
public readonly partial struct IdB;
150+
""";
151+
152+
var result = GeneratorTestHelper.RunGenerator(source);
153+
154+
// Attribute + 2 aliases
155+
Assert.Equal(3, result.GeneratedTrees.Length);
156+
157+
var generatedSources = result.Results[0].GeneratedSources;
158+
159+
Assert.Single(generatedSources.Where(s => s.HintName.EndsWith("IdA.g.cs")));
160+
Assert.Single(generatedSources.Where(s => s.HintName.EndsWith("IdB.g.cs")));
161+
}
162+
163+
[Fact]
164+
public void NonGeneric_Attribute_Works()
165+
{
166+
const string source = """
167+
using newtype;
168+
169+
[newtype(typeof(int))]
170+
public readonly partial struct TestId;
171+
""";
172+
173+
var result = GeneratorTestHelper.RunGenerator(source);
174+
175+
Assert.Equal(2, result.GeneratedTrees.Length);
176+
177+
var generatedSources = result.Results[0].GeneratedSources;
178+
179+
var aliasSource = generatedSources.Single(s => s.HintName.EndsWith("TestId.g.cs"));
180+
var text = aliasSource.SourceText.ToString();
181+
182+
Assert.Contains("private readonly int _value;", text);
183+
Assert.Contains("public TestId(int value)", text);
184+
}
185+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using Basic.Reference.Assemblies;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using newtype.generator;
5+
using Xunit;
6+
7+
namespace newtype.tests;
8+
9+
internal static class GeneratorTestHelper
10+
{
11+
public static GeneratorDriverRunResult RunGenerator(string source)
12+
{
13+
var syntaxTree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Preview));
14+
15+
var compilation = CSharpCompilation.Create(
16+
"Tests",
17+
[syntaxTree],
18+
Net80.References.All,
19+
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
20+
21+
var generator = new AliasGenerator().AsSourceGenerator();
22+
23+
GeneratorDriver driver = CSharpGeneratorDriver.Create(
24+
generators: [generator],
25+
parseOptions: new CSharpParseOptions(LanguageVersion.Preview),
26+
driverOptions: new GeneratorDriverOptions(
27+
disabledOutputs: IncrementalGeneratorOutputKind.None,
28+
trackIncrementalGeneratorSteps: true));
29+
30+
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics);
31+
32+
// No generator diagnostics
33+
Assert.Empty(diagnostics);
34+
35+
// No errors in the output compilation
36+
var errors = outputCompilation.GetDiagnostics()
37+
.Where(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning)
38+
.ToArray();
39+
Assert.Empty(errors);
40+
41+
return driver.GetRunResult();
42+
}
43+
44+
public static void VerifyIncrementality(string source)
45+
{
46+
var syntaxTree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Preview));
47+
48+
var compilation = CSharpCompilation.Create(
49+
"Tests",
50+
[syntaxTree],
51+
Net80.References.All,
52+
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
53+
54+
var generator = new AliasGenerator().AsSourceGenerator();
55+
56+
GeneratorDriver driver = CSharpGeneratorDriver.Create(
57+
generators: [generator],
58+
parseOptions: new CSharpParseOptions(LanguageVersion.Preview),
59+
driverOptions: new GeneratorDriverOptions(
60+
disabledOutputs: IncrementalGeneratorOutputKind.None,
61+
trackIncrementalGeneratorSteps: true));
62+
63+
// First run
64+
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _);
65+
66+
// Second run with an unrelated change (add a dummy syntax tree)
67+
var dummyTree = CSharpSyntaxTree.ParseText("// dummy", new CSharpParseOptions(LanguageVersion.Preview));
68+
var modifiedCompilation = compilation.AddSyntaxTrees(dummyTree);
69+
70+
driver = driver.RunGeneratorsAndUpdateCompilation(modifiedCompilation, out _, out _);
71+
72+
var result = driver.GetRunResult();
73+
74+
// All output steps should be Cached or Unchanged on the second run
75+
foreach (var generatorResult in result.Results)
76+
{
77+
foreach (var (_, steps) in generatorResult.TrackedOutputSteps)
78+
{
79+
foreach (var step in steps)
80+
{
81+
foreach (var output in step.Outputs)
82+
{
83+
Assert.True(
84+
output.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged,
85+
$"Expected Cached or Unchanged but got {output.Reason}");
86+
}
87+
}
88+
}
89+
}
90+
}
91+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using Xunit;
2+
3+
namespace newtype.tests;
4+
5+
public class IncrementalityTests
6+
{
7+
[Fact]
8+
public void Generator_Is_Incremental_For_Unrelated_Change()
9+
{
10+
const string source = """
11+
using newtype;
12+
13+
[newtype<int>]
14+
public readonly partial struct TestId;
15+
""";
16+
17+
GeneratorTestHelper.VerifyIncrementality(source);
18+
}
19+
20+
[Fact]
21+
public void Generator_Is_Incremental_For_Multiple_Types()
22+
{
23+
const string source = """
24+
using newtype;
25+
26+
[newtype<int>]
27+
public readonly partial struct TestId;
28+
29+
[newtype<string>]
30+
public readonly partial struct TestName;
31+
""";
32+
33+
GeneratorTestHelper.VerifyIncrementality(source);
34+
}
35+
}

NewType.Tests/NewType.Tests.csproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,20 @@
1717
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*"/>
1818
<PackageReference Include="xunit" Version="2.*"/>
1919
<PackageReference Include="xunit.runner.visualstudio" Version="2.*"/>
20+
21+
<!-- Roslyn APIs for in-memory compilation in generator tests -->
22+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0"/>
23+
24+
<!-- Reference assemblies for in-memory compilation (includes System.Numerics etc.) -->
25+
<PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.8.4"/>
2026
</ItemGroup>
2127

2228
<ItemGroup>
29+
<!-- Analyzer reference: feeds generator to the build-time Roslyn host (powers integration tests) -->
2330
<ProjectReference Include="..\NewType.Generator\NewType.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
31+
32+
<!-- Assembly reference: lets test code instantiate AliasGenerator directly -->
33+
<ProjectReference Include="..\NewType.Generator\NewType.Generator.csproj"/>
2434
</ItemGroup>
2535

2636
</Project>

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# `newtype` - Type Aliases for C#
1+
# `newtype` *(Distinct Type Aliases for C#)*
22

33
<p align="center">
44
<img src="logo.svg" alt="logo, a stylized N with a red and Blue half" width="30%">
@@ -8,7 +8,7 @@
88
[![NuGet](https://img.shields.io/nuget/v/newtype?color=blue)](https://www.nuget.org/packages/newtype/)
99
[![Build Status](https://github.com/outfox/newtype/actions/workflows/dotnet.yml/badge.svg)](https://github.com/outfox/newtype/actions/workflows/dotnet.yml)
1010

11-
A source generator that creates distinct type aliases with full operator forwarding. Inspired by Haskell's `newtype` and F#'s type abbreviations. `newtype` works for a healthy number of types - many primitives, structs, records, classes work out of the box.
11+
This package is a source generator that creates distinct type aliases with full operator and constructor forwarding. Inspired by Haskell's `newtype` and F#'s type abbreviations. `newtype` works for a healthy number of types - many primitives, structs, records, classes work out of the box.
1212

1313
## Installation
1414

@@ -22,11 +22,30 @@ dotnet add package newtype
2222
```csharp
2323
using newtype;
2424

25+
[newtype<string>]
26+
public readonly partial struct TableId;
27+
2528
[newtype<int>]
26-
public readonly partial struct Pizzas;
29+
public readonly partial struct PizzasEaten;
2730

2831
[newtype<double>]
2932
public readonly partial struct Fullness;
33+
34+
class Guest
35+
{
36+
TableId table = "Table 1";
37+
PizzasEaten pizzasEaten;
38+
Fullness fullness;
39+
40+
public void fillEmUp(Fullness threshold)
41+
{
42+
while (fullness < threshold)
43+
{
44+
pizzasEaten++;
45+
fullness += 0.1;
46+
}
47+
}
48+
}
3049
```
3150

3251
#### Typical: quantities backed by the same data type.

0 commit comments

Comments
 (0)