Skip to content

Commit 60c70d8

Browse files
committed
Add unit tests, CLAUDE doc, fix EquatableArray
Add a CLAUDE.md contributor/AI guidance document and a suite of new unit tests under src/ReactiveUI.SourceGenerator.Tests/UnitTests (AttributeDataExtensionTests, EquatableArrayTests, FieldSyntaxExtensionTests, ImmutableArrayBuilderTests, SymbolExtensionTests). Also apply minor updates to EquatableArray{T}.cs in the Analyzers.CodeFixes and Roslyn helper projects and update ReactiveUI.SourceGenerators.Roslyn4120.csproj. These changes add test coverage for core helpers/extensions and include documentation for contributors/AI assistants.
1 parent a3d01cb commit 60c70d8

9 files changed

Lines changed: 1907 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# CLAUDE.md — ReactiveUI.SourceGenerators
2+
3+
This document provides guidance for AI assistants and contributors working in this repository.
4+
5+
## Overview
6+
7+
ReactiveUI.SourceGenerators is a Roslyn incremental source-generator package that automates ReactiveUI boilerplate at compile-time. It generates reactive properties, observable-as-property helpers, reactive commands, IViewFor registrations, bindable derived lists, reactive collections, and full reactive-object scaffolding — all with zero runtime reflection, making generated code fully AOT-compatible.
8+
9+
**Minimum consumer requirements:** C# 12.0 · Visual Studio 17.8.0 · ReactiveUI 19.5.31+
10+
11+
## Architecture Overview
12+
13+
The repository ships **three versioned generator assemblies** built from a single shared source folder:
14+
15+
| Project | Roslyn version | Preprocessor constant | Extra features |
16+
|---------|---------------|-----------------------|----------------|
17+
| `ReactiveUI.SourceGenerators.Roslyn480` | 4.8.x (baseline) | _(none)_ | Field-based `[Reactive]`, `[ObservableAsProperty]`, `[ReactiveCommand]`, etc. |
18+
| `ReactiveUI.SourceGenerators.Roslyn4120` | 4.12.0 | `ROSYLN_412` | + partial-property `[Reactive]` and `[ObservableAsProperty]` |
19+
| `ReactiveUI.SourceGenerators.Roslyn5000` | 5.0.0 | `ROSYLN_500` | + same partial-property support on Roslyn 5 |
20+
21+
Each versioned project links all `.cs` files from `ReactiveUI.SourceGenerators.Roslyn/` via:
22+
23+
```xml
24+
<Compile Include="..\ReactiveUI.SourceGenerators.Roslyn\**\*.cs" LinkBase="Shared" />
25+
```
26+
27+
`#if ROSYLN_412 || ROSYLN_500` guards inside the shared source enable partial-property pipelines only on the newer Roslyn builds.
28+
29+
The `ReactiveUI.SourceGenerators` NuGet project packages all three DLLs under separate `analyzers/dotnet/roslyn4.8/cs`, `analyzers/dotnet/roslyn4.12/cs`, and `analyzers/dotnet/roslyn5.0/cs` paths, so NuGet/MSBuild automatically selects the right build based on the host compiler.
30+
31+
Diagnostics are **not** reported by generators. All `RXUISG*` diagnostics live in the separate `ReactiveUI.SourceGenerators.Analyzers.CodeFixes` project.
32+
33+
## Project Structure
34+
35+
```
36+
src/
37+
├── ReactiveUI.SourceGenerators.Roslyn/ # Shared source (linked into all versioned projects)
38+
│ ├── AttributeDefinitions.cs # Injected attribute source texts
39+
│ ├── Reactive/ # [Reactive] generator + Execute + models
40+
│ ├── ReactiveCommand/ # [ReactiveCommand] generator + Execute + models
41+
│ ├── ObservableAsProperty/ # [ObservableAsProperty] generator + Execute + models
42+
│ ├── IViewFor/ # [IViewFor<T>] generator + Execute + models
43+
│ ├── RoutedControlHost/ # [RoutedControlHost] generator
44+
│ ├── ViewModelControlHost/ # [ViewModelControlHost] generator
45+
│ ├── BindableDerivedList/ # [BindableDerivedList] generator
46+
│ ├── ReactiveCollection/ # [ReactiveCollection] generator
47+
│ ├── ReactiveObject/ # [IReactiveObject] generator
48+
│ ├── Diagnostics/ # DiagnosticDescriptors, SuppressionDescriptors
49+
│ └── Core/
50+
│ ├── Extensions/ # ISymbol*, ITypeSymbol*, INamedTypeSymbol*, AttributeData extensions
51+
│ ├── Helpers/ # ImmutableArrayBuilder<T>, EquatableArray<T>, HashCode, etc.
52+
│ └── Models/ # Result<T>, DiagnosticInfo, TargetInfo, etc.
53+
├── ReactiveUI.SourceGenerators.Roslyn480/ # Roslyn 4.8 build (no define)
54+
├── ReactiveUI.SourceGenerators.Roslyn4120/ # Roslyn 4.12 build (ROSYLN_412)
55+
├── ReactiveUI.SourceGenerators.Roslyn5000/ # Roslyn 5.0 build (ROSYLN_500)
56+
├── ReactiveUI.SourceGenerators.Analyzers.CodeFixes/ # Analyzers + code fixers
57+
├── ReactiveUI.SourceGenerators/ # NuGet packaging project (bundles all three DLLs)
58+
├── ReactiveUI.SourceGenerator.Tests/ # TUnit + Verify snapshot tests
59+
├── ReactiveUI.SourceGenerators.Execute*/ # Compile-time execution verification projects
60+
└── TestApps/ # Manual test applications (WPF, WinForms, MAUI, Avalonia)
61+
```
62+
63+
## Code Generation Strategy
64+
65+
All generated C# source is produced using **raw string literals** (`$$"""..."""`). Do **not** use `StringBuilder` or `SyntaxFactory` for code generation.
66+
67+
```csharp
68+
// CORRECT — raw string literal with $$ interpolation
69+
internal static string GenerateProperty(string name, string type) => $$"""
70+
public {{type}} {{name}}
71+
{
72+
get => _{{char.ToLower(name[0])}{{name.Substring(1)}}};
73+
set => this.RaiseAndSetIfChanged(ref _{{char.ToLower(name[0])}{{name.Substring(1)}}}, value);
74+
}
75+
""";
76+
77+
// WRONG — do not use StringBuilder
78+
var sb = new StringBuilder();
79+
sb.AppendLine($"public {type} {name}");
80+
// ...
81+
82+
// WRONG — do not use SyntaxFactory
83+
SyntaxFactory.PropertyDeclaration(...)
84+
```
85+
86+
Raw string literals preserve formatting intent, are trivially diffable in code review, and do not require the overhead of SyntaxFactory node construction.
87+
88+
The injected attribute source texts (in `AttributeDefinitions.cs`) also use `$$"""..."""` raw string literals.
89+
90+
## Roslyn Incremental Pipeline Pattern
91+
92+
Each generator follows this structure:
93+
94+
1. **`Initialize`** — registers post-initialization output (inject attribute source), then calls one or more `Run*` methods.
95+
2. **`Run*`** — builds the `IncrementalValuesProvider` using `ForAttributeWithMetadataName` + a syntax predicate + a semantic extraction function.
96+
3. **`Get*Info` (Execute file)** — stateless extraction function. Returns `Result<TModel?>` with embedded diagnostics. Must be pure; must not capture any `ISymbol` or `SyntaxNode` beyond this call.
97+
4. **`GenerateSource` (Execute file)** — pure function that converts model → raw string source text. No Roslyn symbols allowed here.
98+
99+
```
100+
Initialize()
101+
├─ RegisterPostInitializationOutput → inject attribute definitions
102+
└─ SyntaxProvider.ForAttributeWithMetadataName
103+
├─ syntax predicate (fast, node-type check only)
104+
├─ semantic extraction → Get*Info() → Result<Model>
105+
└─ RegisterSourceOutput → GenerateSource() → AddSource()
106+
```
107+
108+
**Incremental caching rules:**
109+
- All pipeline output models must implement value equality (`record`, `IEquatable<T>`, or `EquatableArray<T>`).
110+
- Never store `ISymbol`, `SyntaxNode`, `SemanticModel`, or `CancellationToken` in a model.
111+
- Use `EquatableArray<T>` (from `Core/Helpers`) instead of `ImmutableArray<T>` in models.
112+
113+
## Generators
114+
115+
| Generator class | Attribute | Input target |
116+
|-----------------|-----------|--------------|
117+
| `ReactiveGenerator` | `[Reactive]` | Field (all Roslyn) or partial property (ROSYLN_412+) |
118+
| `ReactiveCommandGenerator` | `[ReactiveCommand]` | Method |
119+
| `ObservableAsPropertyGenerator` | `[ObservableAsProperty]` | Field or observable method |
120+
| `IViewForGenerator` | `[IViewFor<T>]` | Class |
121+
| `RoutedControlHostGenerator` | `[RoutedControlHost]` | Class |
122+
| `ViewModelControlHostGenerator` | `[ViewModelControlHost]` | Class |
123+
| `BindableDerivedListGenerator` | `[BindableDerivedList]` | Field (`ReadOnlyObservableCollection<T>`) |
124+
| `ReactiveCollectionGenerator` | `[ReactiveCollection]` | Field (`ObservableCollection<T>`) |
125+
| `ReactiveObjectGenerator` | `[IReactiveObject]` | Class |
126+
127+
## Analyzers & Suppressors
128+
129+
All diagnostics use the `RXUISG` prefix. All suppressions use the `RXUISPR` prefix.
130+
131+
| Class | ID range | Purpose |
132+
|-------|----------|---------|
133+
| `PropertyToReactiveFieldAnalyzer` | RXUISG0016 | Suggests converting auto-properties to `[Reactive]` fields |
134+
| `ReactiveAttributeMisuseAnalyzer` | RXUISG0020 | Detects `[Reactive]` on non-partial or non-partial-type members |
135+
| `PropertyToReactiveFieldCodeFixProvider` | — | Converts auto-property → `[Reactive]` field |
136+
| `ReactiveAttributeMisuseCodeFixProvider` | — | Fixes misuse of `[Reactive]` attribute |
137+
138+
Suppressors silence noisy Roslyn/Roslynator diagnostics that are expected for generator-backed patterns (e.g. fields never read, methods that don't need to be static).
139+
140+
### Analyzer Separation (Roslyn Best Practice)
141+
142+
- Generators do **not** report diagnostics — they only call `context.ReportDiagnostic` for internal invariant violations via `DiagnosticInfo` models.
143+
- The `ReactiveUI.SourceGenerators.Analyzers.CodeFixes` project owns all `RXUISG*` diagnostic descriptors and code fixers.
144+
- `DiagnosticDescriptors.cs` and related files are compiled from the shared Roslyn source via the linked `<Compile>` items.
145+
146+
## Testing
147+
148+
### Framework
149+
150+
- **TUnit** — test runner and assertion library (replaces xUnit/NUnit).
151+
- **Verify.SourceGenerators** — snapshot-based verification of generated source output.
152+
- **Microsoft.Testing.Platform** — native test execution (configured via `testconfig.json`).
153+
154+
### Test project targets
155+
156+
The test project multi-targets `net8.0;net9.0;net10.0` (controlled by `$(TestTfms)` in `Directory.Build.props`). Tests run against all three frameworks in CI.
157+
158+
### Snapshot tests
159+
160+
Generator tests extend `TestBase<TGenerator>` and call `TestHelper.TestPass(sourceCode)`. Verify saves `.verified.txt` snapshots in the appropriate subdirectory (`REACTIVE/`, `REACTIVECMD/`, `OAPH/`, `IVIEWFOR/`, `DERIVEDLIST/`, `REACTIVECOLL/`, `REACTIVEOBJ/`).
161+
162+
#### Accepting snapshot changes
163+
164+
1. Enable `VerifierSettings.AutoVerify()` in `ModuleInitializer.cs`.
165+
2. Run `dotnet test --project src/ReactiveUI.SourceGenerator.Tests -c Release`.
166+
3. Disable `VerifierSettings.AutoVerify()`.
167+
4. Re-run tests to confirm all pass without AutoVerify.
168+
169+
### Test source language version
170+
171+
Test source strings are parsed with **CSharp13** (`LanguageVersion.CSharp13`). This is the version used by `TestHelper.RunGeneratorAndCheck`.
172+
173+
### Non-snapshot (unit) tests
174+
175+
Analyzer and helper tests use direct `CSharpCompilation` / `CompilationWithAnalyzers` to verify diagnostics without snapshots. See `PropertyToReactiveFieldAnalyzerTests.cs` for the pattern.
176+
177+
## Common Tasks
178+
179+
### Adding a New Generator
180+
181+
1. Create a value-equatable model record in `Core/Models/` or the generator's own `Models/` folder.
182+
2. Add attribute source text to `AttributeDefinitions.cs` using a `$$"""..."""` raw string literal.
183+
3. Create `<Name>Generator.cs` with `Initialize` wiring up `ForAttributeWithMetadataName`.
184+
4. Create `<Name>Generator.Execute.cs` with `Get*Info` (extraction) and `GenerateSource` (raw string template).
185+
5. Add snapshot tests in `ReactiveUI.SourceGenerator.Tests/UnitTests/`.
186+
6. Accept snapshots using the AutoVerify trick above.
187+
188+
### Adding a New Analyzer Diagnostic
189+
190+
1. Add a `DiagnosticDescriptor` to `DiagnosticDescriptors.cs`.
191+
2. Update `AnalyzerReleases.Unshipped.md`.
192+
3. Implement the analyzer in `ReactiveUI.SourceGenerators.Analyzers.CodeFixes/`.
193+
4. Add unit tests in `ReactiveUI.SourceGenerator.Tests/UnitTests/`.
194+
195+
### Running Tests
196+
197+
```pwsh
198+
dotnet test src/ReactiveUI.SourceGenerator.Tests --configuration Release
199+
```
200+
201+
### Building
202+
203+
```pwsh
204+
dotnet build src/ReactiveUI.SourceGenerators.sln
205+
```
206+
207+
## What to Avoid
208+
209+
- **`ISymbol` / `SyntaxNode` in pipeline output models** — breaks incremental caching; use value-equatable data records instead.
210+
- **`SyntaxFactory` for code generation** — use `$$"""..."""` raw string literals.
211+
- **`StringBuilder` for code generation** — use `$$"""..."""` raw string literals.
212+
- **Diagnostics reported inside generators** — use the separate analyzer project for all `RXUISG*` diagnostics.
213+
- **LINQ in hot Roslyn pipeline paths** — use `foreach` loops (Roslyn convention for incremental generators).
214+
- **Non-value-equatable models** in the incremental pipeline — will defeat caching and cause unnecessary regeneration.
215+
- **APIs unavailable in `netstandard2.0`** inside `ReactiveUI.SourceGenerators.Roslyn*` projects — the generator must run inside the compiler host which targets netstandard2.0.
216+
- **Runtime reflection** in generated code — breaks Native AOT compatibility.
217+
- **`#nullable enable` / nullable annotations in generated output** — these require C# 8+ features; generated code must be compatible with the minimum consumer C# version (12.0).
218+
- **File-scoped namespaces in generated output** — requires C# 10; use block-scoped namespaces.
219+
220+
## Important Notes
221+
222+
- **Required .NET SDKs:** .NET 8.0, 9.0, and 10.0 (all required for multi-targeting the test project).
223+
- **Generator + Analyzer targets:** `netstandard2.0` (Roslyn host requirement).
224+
- **Test project targets:** `net8.0;net9.0;net10.0`.
225+
- **No shallow clones:** The repository uses Nerdbank.GitVersioning; a full `git clone` is required for correct versioning.
226+
- **NuGet packaging:** The `ReactiveUI.SourceGenerators` project bundles all three versioned generator DLLs at different `analyzers/dotnet/roslyn*/cs` paths.
227+
- **Cross-platform tests:** On non-Windows platforms, WPF/WinForms types are injected as source stubs so generator tests compile cross-platform.
228+
- **`SyntaxFactory` helper:** https://roslynquoter.azurewebsites.net/ — useful for inspecting how Roslyn models a given syntax construct (reference only; do not use SyntaxFactory in code-gen paths).
229+
230+
**Philosophy:** Generate zero-reflection, AOT-compatible ReactiveUI boilerplate at compile-time. Separate diagnostic reporting from code generation. Keep the incremental pipeline pure and value-equatable so Roslyn can cache and skip unchanged work.

0 commit comments

Comments
 (0)