This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- NEVER abandon work halfway through - if something gets difficult, push through it
- NEVER use
git stashto hide incomplete work - fix the problem directly - NEVER give up because a task is complex - break it down and keep going
- If a tool call is rejected, adapt your approach immediately and continue
This project uses Microsoft Testing Platform (MTP) with the TUnit testing framework. Test commands differ significantly from traditional VSTest.
See: https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test?tabs=dotnet-test-with-mtp
# Check .NET installation (.NET 8.0, 9.0, and 10.0 required)
dotnet --info
# Restore NuGet packages
cd src
dotnet restore ReactiveUI.Binding.SourceGenerators.slnxNote: This project uses the modern .slnx (XML-based solution file) format instead of the legacy .sln format.
CRITICAL: The working folder must be ./src folder. These commands won't function properly without the correct working folder.
# Build the solution
dotnet build ReactiveUI.Binding.SourceGenerators.slnx -c Release
# Build with warnings as errors (includes StyleCop violations)
dotnet build ReactiveUI.Binding.SourceGenerators.slnx -c Release -warnaserror
# Clean the solution
dotnet clean ReactiveUI.Binding.SourceGenerators.slnxCRITICAL: This repository uses MTP configured in testconfig.json. All TUnit-specific arguments must be passed after --:
The working folder must be ./src folder.
IMPORTANT:
- Do NOT use
--no-buildflag when running tests. Always build before testing to ensure all code changes are compiled. - Use
--output Detailedto see Console.WriteLine output from tests (place BEFORE any--separator).
# Run all tests in the solution
dotnet test --solution ReactiveUI.Binding.SourceGenerators.slnx -c Release
# Run all tests in a specific project
dotnet test --project tests/ReactiveUI.Binding.Analyzer.Tests/ReactiveUI.Binding.Analyzer.Tests.csproj -c Release
dotnet test --project tests/ReactiveUI.Binding.SourceGenerators.Tests/ReactiveUI.Binding.SourceGenerators.Tests.csproj -c Release
dotnet test --project tests/ReactiveUI.Binding.Tests/ReactiveUI.Binding.Tests.csproj -c Release
# Run a single test method using treenode-filter
dotnet test --project tests/ReactiveUI.Binding.SourceGenerators.Tests/ReactiveUI.Binding.SourceGenerators.Tests.csproj -- --treenode-filter "/*/*/*/MyTestMethod"
# Run all tests in a specific class
dotnet test --project tests/ReactiveUI.Binding.SourceGenerators.Tests/ReactiveUI.Binding.SourceGenerators.Tests.csproj -- --treenode-filter "/*/*/WhenChangedGeneratorTests/*"
# Run tests with code coverage
dotnet test --solution ReactiveUI.Binding.SourceGenerators.slnx -- --coverage --coverage-output-format coberturaThe --treenode-filter follows the pattern: /{AssemblyName}/{Namespace}/{ClassName}/{TestMethodName}
- Single test:
--treenode-filter "/*/*/*/MyTestMethod" - All tests in class:
--treenode-filter "/*/*/MyClassName/*" - Use single asterisks (
*) to match segments.
src/ReactiveUI.Binding.SourceGenerators.slnx- Modern XML-based solution filesrc/testconfig.json- Configures test execution and code coveragesrc/Directory.Build.props- Common build properties, package metadatasrc/Directory.Packages.props- Central package managementsrc/Directory.Build.targets- Build targets
- Generator tests use Verify.SourceGenerators for snapshot testing
- Snapshots stored as
*.verified.csfiles alongside test classes - To accept new/changed snapshots:
- Enable
VerifierSettings.AutoVerify()inAssemblySetup.cs - Run tests to accept all snapshots
- Disable
VerifierSettings.AutoVerify()after accepting - Re-run tests to confirm they pass without AutoVerify
- Enable
Generator tests use a two-tier language version strategy to verify generated output compiles under C# 7.3 (the minimum supported version for consumer projects):
- Default: C# 7.3 —
TestHelper.CreateCompilation()andRunGenerator()default toLanguageVersion.CSharp7_3. This ensures generated output contains no C# 8+ syntax (no nullable reference type annotations, nostaticlambdas, no#nullable enable). - CallerArgumentExpression tests: explicit C# 10 — Tests that verify
CallerArgumentExpression-based dispatch (the primary dispatch mechanism for C# 10+ projects) must passLanguageVersion.CSharp10explicitly. These are the majority of snapshot tests. - CallerFilePath fallback tests: explicit C# 7.3 — Tests that verify
CallerFilePath + CallerLineNumberdispatch (the fallback for pre-C# 10 projects) passLanguageVersion.CSharp7_3explicitly to document intent, even though it matches the default. - Edge case tests (
RunGeneratorwithout snapshot verification) — These use the C# 7.3 default. They verify the generator doesn't crash on invalid lambdas and produces no dispatch code. They don't callCompilationSucceeds()since their test source may contain C# 8+ features that are only diagnostically invalid under C# 7.3. - Runtime execution tests — These use
LanguageVersion.CSharp10because their inline source uses C# 8+ features and they callCompilationSucceeds().
When adding new generator tests:
- If the test verifies CallerArgumentExpression dispatch → pass
LanguageVersion.CSharp10 - If the test verifies CallerFilePath fallback dispatch → pass
LanguageVersion.CSharp7_3 - If the test verifies the generator skips invalid input → use the default (no parameter)
- If the test compiles and loads the generated assembly → pass
LanguageVersion.CSharp10
Code coverage uses Microsoft.Testing.Extensions.CodeCoverage configured in src/testconfig.json. Coverage is collected for production assemblies only (test projects and TestModels are excluded).
# Run tests with code coverage (from src/ folder)
dotnet test --solution ReactiveUI.Binding.SourceGenerators.slnx -c Release -- --coverage --coverage-output-format cobertura
# Generate HTML report using ReportGenerator (install if needed: dotnet tool install -g dotnet-reportgenerator-globaltool)
# Find all cobertura files and generate report to /tmp/<folder>
reportgenerator \
-reports:"tests/**/TestResults/**/*.cobertura.xml" \
-targetdir:/tmp/code_coverage \
-reporttypes:"Html;TextSummary"
# View the text summary
cat /tmp/code_coverage/Summary.txt
# Open HTML report in browser
xdg-open /tmp/code_coverage/index.html # Linux
open /tmp/code_coverage/index.html # macOSKey configuration (src/testconfig.json):
modulePaths.include:ReactiveUI\\.Binding\\..*— covers all production assembliesmodulePaths.exclude:.*Tests.*,.*TestRunner.*,.*TestModels.*— excludes test/runner/model assembliesskipAutoProperties: true— auto-properties excluded from coverage metrics
Tips:
- Always clean
bin/andobj/folders before coverage runs to avoid stale results - The
ReactiveUI.Binding.GeneratedCode.TestModelsassembly has[assembly: ExcludeFromCodeCoverage]so it won't appear in reports even though its module path matches the include pattern DiagnosticWarnings.cscoverage appears as 0% inReactiveUI.Binding.SourceGenerators— this is a linked-file artifact; the code is actually tested via theReactiveUI.Binding.Analyzerassembly- Put coverage reports in
/tmp/to avoid accidentally committing them
ReactiveUI.Binding.SourceGenerators is an incremental source generator that replaces ReactiveUI's runtime expression tree analysis with compile-time code generation for property observation and binding. It eliminates runtime reflection, is fully AOT/trimming safe, and supports all ReactiveUI platform notification mechanisms.
src/
├── ReactiveUI.Binding/ # Runtime library (net8.0;net9.0;net10.0;net462-net481)
│ ├── Interfaces/ # ICreatesObservableForProperty, IObservedChange, etc.
│ └── View/ # ViewLocator, DefaultViewLocator, IViewFor<T>, attributes
│
├── ReactiveUI.Binding.SourceGenerators/ # Source generator (netstandard2.0)
│ ├── BindingGenerator.cs # [Generator] IIncrementalGenerator entry point
│ ├── Constants.cs # API stub text, metadata names (linked to Analyzer)
│ ├── DiagnosticWarnings.cs # Diagnostic descriptors (linked to Analyzer)
│ ├── RoslynHelpers.cs # Syntax predicates for CreateSyntaxProvider
│ ├── MetadataExtractor.cs # Semantic model → POCO extraction
│ ├── Models/ # Value-equatable pipeline POCOs
│ │ ├── EquatableArray.cs
│ │ ├── ClassBindingInfo.cs # Type-level: notification mechanism flags
│ │ ├── InvocationInfo.cs # Per-call-site: WhenChanged/WhenChanging
│ │ ├── BindingInvocationInfo.cs # Per-call-site: BindOneWay/BindTwoWay
│ │ └── ViewRegistrationInfo.cs # Per-IViewFor<T>: view dispatch mapping
│ ├── Generators/ # Per-kind fallback generators (Pipeline A)
│ │ ├── ReactiveObjectBindingGenerator.cs # IReactiveObject (affinity 24)
│ │ ├── INPCBindingGenerator.cs # INotifyPropertyChanged (affinity 21)
│ │ ├── WpfBindingGenerator.cs # WPF DependencyObject (affinity 20)
│ │ ├── WinUIBindingGenerator.cs # WinUI DependencyObject (affinity 22)
│ │ ├── KVOBindingGenerator.cs # Apple KVO/NSObject (affinity 25)
│ │ ├── WinFormsBindingGenerator.cs # WinForms Component (affinity 23)
│ │ ├── AndroidBindingGenerator.cs # Android View (affinity 19)
│ │ ├── RegistrationGenerator.cs # Consolidates all → [ModuleInitializer]
│ │ └── ViewLocatorDispatchGenerator.cs # IViewFor<T> → AOT view dispatch (Pipeline C)
│ ├── Invocations/ # Per-invocation generators (Pipeline B)
│ │ ├── WhenChangedInvocationGenerator.cs # After-change observation
│ │ ├── WhenChangingInvocationGenerator.cs # Before-change observation
│ │ ├── BindOneWayInvocationGenerator.cs # One-way binding
│ │ ├── BindTwoWayInvocationGenerator.cs # Two-way binding
│ │ └── WhenAnyValueInvocationGenerator.cs # WhenAnyValue compat shim
│ ├── Helpers/ # Extraction and validation helpers
│ │ ├── ViewRegistrationExtractor.cs # IViewFor<T> → ViewRegistrationInfo extraction
│ │ └── ... # ExtractorValidation, SymbolHelpers, etc.
│ └── CodeGeneration/
│ └── CodeGenerator.cs # StringBuilder-based code generation
│
├── ReactiveUI.Binding.Analyzer/ # Roslyn analyzer (netstandard2.0)
│ └── Analyzers/
│ ├── BindingInvocationAnalyzer.cs # RXUIBIND001, 003, 004, 005
│ └── TypeAnalyzer.cs # RXUIBIND002
│
└── tests/
├── ReactiveUI.Binding.SourceGenerators.Tests/ # Generator snapshot tests
├── ReactiveUI.Binding.Analyzer.Tests/ # Analyzer diagnostic tests
└── ReactiveUI.Binding.Tests/ # Runtime library tests
Pipeline A (Type Detection): Scans classes with base lists → builds ClassBindingInfo POCOs with boolean flags for each notification mechanism (IReactiveObject, INPC, WPF DP, WinUI DP, KVO, WinForms, Android). Per-kind generators filter from this shared pipeline. Consolidates into a single [ModuleInitializer] registration.
Pipeline B (Invocation Detection): Scans method invocations (WhenChanged, WhenChanging, BindOneWay, BindTwoWay, WhenAnyValue) → extracts lambda property paths → generates optimized per-call-site observation/binding code. Uses CallerFilePath + CallerLineNumber dispatch: API stubs capture caller info, generated dispatch table routes to compile-time generated methods.
Pipeline C (View Dispatch): Scans classes implementing IViewFor<T> → extracts ViewRegistrationInfo POCOs (VM FQN, View FQN, constructor availability, [ViewContract] contract, [SingleInstanceView] flag) → generates ViewDispatch.g.cs with a type-switch dispatch function. Supports contract-based multi-view resolution (contract checks emitted before default), singleton caching via Interlocked.CompareExchange, and 3-tier resolution (service locator → direct construction → null). Views can be excluded with [ExcludeFromViewRegistration].
// User writes:
var obs = vm.WhenChanged(x => x.Name);
// Generator emits API stub (PostInitializationOutput) with CallerInfo dispatch:
public static IObservable<TReturn> WhenChanged<TObj, TReturn>(
this TObj obj, Expression<Func<TObj, TReturn>> property,
[CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = 0) where TObj : class
{
if (__GeneratedBindingDispatcher.TryGetWhenChanged(callerFilePath, callerLineNumber, obj, out var result))
return (IObservable<TReturn>)result!;
throw new InvalidOperationException("..."); // Runtime fallback TBD
}
// Generator emits per-invocation optimized method:
private static IObservable<string> __WhenChanged_0(MyViewModel obj)
{
return Observable.Create<string>(observer => { ... PropertyChanged subscription ... })
.StartWith(obj.Name);
}| API | Interface | Event | Timing |
|---|---|---|---|
WhenChanged |
INotifyPropertyChanged |
PropertyChanged |
After value changes |
WhenChanging |
INotifyPropertyChanging |
PropertyChanging |
Before value changes |
Not all platforms support before-change notifications (WPF DP, WinUI DP, WinForms, Android do not). The analyzer reports RXUIBIND004 when WhenChanging targets an unsupported platform type.
| ID | Severity | Description |
|---|---|---|
| RXUIBIND001 | Info | Expression must be inline lambda for compile-time optimization |
| RXUIBIND002 | Warning | Type has no observable properties |
| RXUIBIND003 | Warning | Expression contains private/protected member |
| RXUIBIND004 | Warning | Type does not support before-change notifications |
| RXUIBIND005 | Info | Source type implements INotifyDataErrorInfo; validation binding requires runtime engine |
CRITICAL: All code must comply with ReactiveUI contribution guidelines: https://www.reactiveui.net/contribute/index.html
- EditorConfig rules (
.editorconfig) - StyleCop Analyzers - builds fail on violations
- Roslynator Analyzers - additional code quality rules
- All public APIs require XML documentation comments
- RS2008: Analyzer release tracking enabled (
AnalyzerReleases.Shipped.md/AnalyzerReleases.Unshipped.md)
- Braces: Allman style
- Indentation: 4 spaces, no tabs
- Fields:
_camelCasefor private/internal - Visibility: Always explicit, visibility first modifier
- Namespaces: File-scoped preferred
- Modern C#: Nullable reference types, pattern matching, records, init setters
- netstandard2.0 targets: Use
IsExternalInit.cspolyfill for records; avoid APIs not available in netstandard2.0 (e.g., useif (x is null) throw new System.ArgumentNullException(...)instead ofArgumentNullException.ThrowIfNull())
All pipeline models are sealed record types with value equality. NEVER include ISymbol, SyntaxNode, or Location in pipeline outputs. Use EquatableArray<T> for array equality. Extract strings from symbols using ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).
- Uses StringBuilder, NOT SyntaxFactory
- Generated code emitted as C# source via
context.AddSource() #pragma warning disableat top of generated files- All generated types use
[Microsoft.CodeAnalysis.Embedded]attribute
There are two distinct C# language contexts in this project:
Generator source code (the .cs files in ReactiveUI.Binding.SourceGenerators/):
- Compiled with the latest C# language version (currently C# 12)
- Can freely use raw string literals (
$$"""), file-scoped namespaces, pattern matching (is not), records, switch expressions, etc. - Must target netstandard2.0 (Roslyn requirement), but the SDK/language version is latest
Generated output (the strings emitted by the generator into user projects):
- Must be C# 7.3 compatible — user projects may target older frameworks
- Must follow the ReactiveUI coding standard (https://www.reactiveui.net/contribute/index.html) as closely as possible within C# 7.3 constraints
- Key rules for generated output:
- Allman-style braces — each brace on a new line
- 4-space indentation — no tabs
- Properly formatted multi-line code — no single-line walls of text for non-trivial expressions
- Explicit visibility modifiers — visibility first (e.g.
private static, notstatic private) - Method bodies indented consistently at 12 spaces (namespace=0, class=4, member=8, body=12)
- C# 7.3 restrictions for generated output — do NOT use:
is not,and,orpattern combinators (C# 9)??=null-coalescing assignment (C# 8)- Switch expressions (C# 8)
requiredmembers (C# 11)- Raw string literals (C# 11)
- File-scoped namespaces (C# 10)
initsetters (C# 9)
- Generated output must NOT use
#nullable enableor nullable reference type annotations (T?where T is a reference type) — these are C# 8+ features staticlambdas are C# 9 — do not use in generated output
- Generator does NOT report diagnostics
- Separate analyzer project reports all RXUIBIND diagnostics
DiagnosticWarnings.csandConstants.csare linked from generator to analyzer via<Compile Include="..." Link="..." />
The analyzer project links shared files from the generator project:
<Compile Include="..\ReactiveUI.Binding.SourceGenerators\DiagnosticWarnings.cs" Link="DiagnosticWarnings.cs" />
<Compile Include="..\ReactiveUI.Binding.SourceGenerators\Constants.cs" Link="Constants.cs" />MetadataExtractor.cs uses ConditionalWeakTable<Compilation, WellKnownSymbolsBox> to cache resolved well-known type symbols per compilation, avoiding repeated GetTypeByMetadataName calls.
- Create value-equatable POCO in
Models/ - Add syntax predicate to
RoslynHelpers.cs - Add extraction logic to
MetadataExtractor.cs - Create invocation generator in
Invocations/withRegister()method - Wire into
BindingGenerator.csInitialize() - Add code generation to
CodeGeneration/CodeGenerator.cs - Add snapshot test in generator test project
- Accept snapshots using
VerifierSettings.AutoVerify()trick
- Add descriptor to
DiagnosticWarnings.cs(shared file) - Update
AnalyzerReleases.Unshipped.mdin both projects - Create/update analyzer in
ReactiveUI.Binding.Analyzer/Analyzers/ - Add tests in
ReactiveUI.Binding.Analyzer.Tests/ - Use
AnalyzerTestHelper.GetDiagnosticsAsync<T>()for testing
- Enable
VerifierSettings.AutoVerify()inAssemblySetup.cs - Run tests:
dotnet test --project tests/ReactiveUI.Binding.SourceGenerators.Tests/... -c Release - Disable
VerifierSettings.AutoVerify()inAssemblySetup.cs - Re-run tests to confirm they pass without AutoVerify
- ISymbol/SyntaxNode in pipeline outputs - breaks incremental caching
- Runtime reflection in generated code - breaks AOT compatibility
- SyntaxFactory for code generation - use StringBuilder instead
- Diagnostics in generator - use separate analyzer project
- LINQ in hot paths - use manual loops (Roslyn convention)
- Non-value-equatable models in pipeline - breaks caching
- APIs unavailable in netstandard2.0 in generator/analyzer projects
- Required .NET SDKs: .NET 8.0, 9.0, and 10.0
- Generator + Analyzer targets: netstandard2.0 (Roslyn requirement)
- Runtime library targets: net8.0;net9.0;net10.0;net462;net472;net481
- No shallow clones: Repository requires full clone for Nerdbank.GitVersioning
- PackBuildOutputs target: Generator .csproj packages both generator and analyzer DLLs into
analyzers/dotnet/cs
Philosophy: Generate zero-reflection, AOT-compatible property observation and binding code at compile-time. Support all ReactiveUI platform notification mechanisms. Fall back to runtime expression analysis only when compile-time analysis is not possible.