feat(discriminated-union): closed-DU source generator - #115
Conversation
Adds ANcpLua.Analyzers.DiscriminatedUnion as a sibling to ExtensibleEnumMirror
and AotReflection. The generator turns a [DiscriminatedUnion]-marked partial
record root and its nested partial record cases into a closed F#-style
discriminated union:
- private parameterless base ctor on the root → only nested cases can derive
- sealed case records generated as partial declarations of the user's records
- exhaustive Match<TResult>(Func<Case1,TResult>, …) and Switch(Action<Case1>, …)
dispatchers — adding a case without updating Match is a Build-error, not a
runtime default arm
Userland:
[DiscriminatedUnion]
public partial record Msg
{
public partial record AddPoint(double X, double Y);
public partial record Undo;
public partial record Redo;
}
Generator emits a separate partial declaration that injects `abstract`,
`private Msg() { }`, the sealed `: Msg` cases, and the dispatchers.
Diagnostics: AL0300 (root must be partial record), AL0301 (no cases),
AL0302 (case must be nested partial record), AL0303 (root must not have a
primary constructor — would conflict with the generated parameterless base).
Generic union roots are supported (`Result<T> { Ok(T Value); Err(string Reason); }`)
— TypeParameterList flows through into the case base list.
Tests: 8 cases covering the happy path, output compilation, closed-hierarchy
enforcement, all four diagnostics, and the generic-root scenario.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (12)
Cache: Disabled due to data retention organization setting Knowledge base: Disabled due to data retention organization setting 📝 WalkthroughWalkthroughThis PR introduces a discriminated-union source generator for Roslyn-based code generation. It adds a new analyzer project ( Comment |
|
@coderabbitai autofix |
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Autofix skipped. No unresolved CodeRabbit review comments with fix instructions found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ca88da33a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var caseSyntaxes = rootSyntax.Members | ||
| .OfType<RecordDeclarationSyntax>() | ||
| .Where(r => r.Kind() is SyntaxKind.RecordDeclaration) | ||
| .ToArray(); |
There was a problem hiding this comment.
Collect union cases from all partial declarations
This extractor only reads rootSyntax.Members from the single declaration that carries [DiscriminatedUnion], so valid cases declared in another partial part of the same root are ignored. In that layout, the generator can emit AL0301 (“no cases”) or generate incomplete Match/Switch signatures that omit real cases, which breaks otherwise-correct partial union definitions split across files.
Useful? React with 👍 / 👎.
| sb.AppendLine(); | ||
| } | ||
|
|
||
| sb.AppendLine($"abstract partial record {model.RootName}{model.TypeParameterList}"); |
There was a problem hiding this comment.
Emit nested declaration chain for nested union roots
The generator always emits the root as a namespace-level type (abstract partial record {RootName}), but ForAttributeWithMetadataName can target nested records too. For a nested union root, this output no longer matches the original containing type chain, so generation produces the wrong type shape (or a conflicting new top-level type) instead of extending the annotated root.
Useful? React with 👍 / 👎.
| private static string GetHintName(UnionModel model) => | ||
| string.IsNullOrEmpty(model.RootNamespace) | ||
| ? $"{model.RootName}.DiscriminatedUnion.g.cs" | ||
| : $"{model.RootNamespace}.{model.RootName}.DiscriminatedUnion.g.cs"; |
There was a problem hiding this comment.
Make generated hint names unique across containing types
The hint name is based only on namespace + root name, so two annotated roots with the same name in the same namespace (for example under different containing types, or different arity) produce identical hint names. AddSource requires uniqueness per generator invocation, so this can throw and stop generation for one or both unions.
Useful? React with 👍 / 👎.
Summary
New analyzer package
ANcpLua.Analyzers.DiscriminatedUnion, sibling toExtensibleEnumMirrorandAotReflection. Turns a `[DiscriminatedUnion]`-marked partial record root + nested partial record cases into a closed F#-style discriminated union — private base ctor that locks inheritance to the nested cases, sealed case records, and exhaustive `Match` / `Switch` dispatchers that the compiler enforces.What gets generated
```csharp
[DiscriminatedUnion]
public partial record Msg
{
public partial record AddPoint(double X, double Y);
public partial record Undo;
public partial record Redo;
}
```
— gets a separate partial declaration that injects:
Generic roots like `Result` flow type parameters through to the case base list.
Diagnostics
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes