|
| 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