Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,24 @@ Each versioned project links all `.cs` files from `ReactiveUI.SourceGenerators.R

`#if ROSYLN_412 || ROSYLN_500` guards inside the shared source enable partial-property pipelines only on the newer Roslyn builds.

The `ReactiveUI.SourceGenerators` NuGet project packages all three DLLs under separate `analyzers/dotnet/roslyn4.8/cs`, `analyzers/dotnet/roslyn4.14/cs`, and `analyzers/dotnet/roslyn5.0/cs` paths, so NuGet/MSBuild automatically selects the right build based on the host compiler.
Each versioned project builds `ReactiveUI.SourceGenerators.Roslyn.dll`. The `ReactiveUI.SourceGenerators` project is
the package consumers install: it builds the attributes into `ReactiveUI.SourceGenerators.dll` under `lib/`, and bundles
the three generator DLLs, each with the code fixes, under `analyzers/dotnet/roslyn4.8/cs`, `analyzers/dotnet/roslyn4.14/cs`,
and `analyzers/dotnet/roslyn5.0/cs`, so NuGet/MSBuild automatically selects the right build based on the host compiler.

### Generated code never declares a shared type

The attributes and the enums they take (`AccessModifier`, `PropertyAccessModifier`, `InheritanceModifier`) are public
types in `ReactiveUI.SourceGenerators`, which targets `$(LibraryTfms)` (net8.0-net11.0, net462-net481). A type
declared into each consumer would repeat in every assembly, and an assembly granted `InternalsVisibleTo` would see two
copies (CS0436).

- **Do not use `RegisterPostInitializationOutput`**, or emit any type whose fully qualified name another assembly
could also declare.
- A new attribute goes in `ReactiveUI.SourceGenerators` and is `[Conditional(KeepAttributes.Symbol)]`: the generators
read it from source, so a consumer keeps no reference to the attributes assembly.
- Generators, analyzers and code fixes stay `netstandard2.0`, the Roslyn host's framework. Only the attributes library
targets `$(LibraryTfms)`.

Generators report only the `RXUISG*` diagnostics about input they cannot generate from (see
[Analyzer Separation](#analyzer-separation-roslyn-best-practice)). Diagnostics about how code should be written, and
Expand All @@ -37,7 +54,7 @@ their code fixes, live in the separate `ReactiveUI.SourceGenerators.Analyzers.Co
```
src/
├── ReactiveUI.SourceGenerators.Roslyn/ # Shared source (linked into all versioned projects)
│ ├── AttributeDefinitions.cs # Injected attribute source texts
│ ├── AttributeDefinitions.cs # Metadata names of the attributes the generators read
│ ├── Reactive/ # [Reactive] generator + Execute + models
│ ├── ReactiveCommand/ # [ReactiveCommand] generator + Execute + models
│ ├── RoutedControlHost/ # [RoutedControlHost] generator
Expand All @@ -52,11 +69,11 @@ src/
│ ├── Extensions/ # ISymbol*, ITypeSymbol*, INamedTypeSymbol*, AttributeData extensions
│ ├── Helpers/ # ImmutableArrayBuilder<T>, EquatableArray<T>, HashCode, etc.
│ └── Models/ # Result<T>, DiagnosticInfo, TargetInfo, etc.
├── ReactiveUI.SourceGenerators.Roslyn480/ # Roslyn 4.8 build (no define)
├── ReactiveUI.SourceGenerators.Roslyn480/ # Roslyn 4.8 build (no define) of ReactiveUI.SourceGenerators.Roslyn.dll
├── ReactiveUI.SourceGenerators.Roslyn4140/ # Roslyn 4.14 build (ROSYLN_412)
├── ReactiveUI.SourceGenerators.Roslyn5000/ # Roslyn 5.0 build (ROSYLN_500)
├── ReactiveUI.SourceGenerators.Analyzers.CodeFixes/ # Analyzers + code fixers
├── ReactiveUI.SourceGenerators/ # NuGet packaging project (bundles all three DLLs)
├── ReactiveUI.SourceGenerators/ # The package: public attributes, with the Roslyn builds bundled
├── ReactiveUI.SourceGenerator.Tests/ # TUnit tests with generator snapshots (GeneratorSnapshot)
├── benchmarks/ # BenchmarkDotNet generation benchmarks with EventPipe tracing
├── ReactiveUI.SourceGenerators.Execute*/ # Compile-time execution verification projects
Expand Down Expand Up @@ -98,24 +115,20 @@ _ = writer.Lines("""
nothing), `OpenContainingTypes`, `OpenPartialType`, `CloseBlocks`, `InheritDoc`, `ExcludeFromCodeCoverage`. Build a
generator's `GeneratedCode` attribute once, in a static field, with `SourceWriterExtensions.GeneratedCodeAttribute`.

The injected attribute source texts (in `AttributeDefinitions.cs`) are fixed `$$"""..."""` raw strings, each built once
per process.

A change to an emitter must keep the output token-for-token the same unless it deliberately changes generated code;
compare changed snapshots with whitespace removed to prove it.

## Roslyn Incremental Pipeline Pattern

Each generator follows this structure:

1. **`Initialize`** — registers post-initialization output (inject attribute source), then calls one or more `Run*` methods.
1. **`Initialize`** — calls one or more `Run*` methods. It registers no post-initialization output.
2. **`Run*`** — builds the `IncrementalValuesProvider` using `ForAttributeWithMetadataName` + a syntax predicate + a semantic extraction function.
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.
4. **`GenerateSource` (Execute file)** — pure function that writes a model through `SourceWriter`. No Roslyn symbols allowed here.

```
Initialize()
├─ RegisterPostInitializationOutput → inject attribute definitions
└─ SyntaxProvider.ForAttributeWithMetadataName
├─ syntax predicate (fast, node-type check only)
├─ semantic extraction → Get*Info() → Result<Model>
Expand Down Expand Up @@ -220,7 +233,8 @@ Analyzer and helper tests use direct `CSharpCompilation` / `CompilationWithAnaly
### Adding a New Generator

1. Create a value-equatable model record in `Core/Models/` or the generator's own `Models/` folder.
2. Add attribute source text to `AttributeDefinitions.cs` as a `$$"""..."""` raw string property initialised once.
2. Add the attribute as a public `[Conditional(KeepAttributes.Symbol)]` type in `ReactiveUI.SourceGenerators`, and its
metadata name to `AttributeDefinitions.cs`.
3. Create `<Name>Generator.cs` with `Initialize` wiring up `ForAttributeWithMetadataName`.
4. Create `<Name>Generator.Execute.cs` with `Get*Info` (extraction) and `GenerateSource` (writes through `SourceWriter`).
5. Add snapshot tests in `ReactiveUI.SourceGenerator.Tests/UnitTests/`.
Expand Down
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ dotnet add package ReactiveUI.SourceGenerators

Ensure the package is loaded with `PrivateAssets="all"` to avoid issues with generated code in consuming projects.

The attributes (`[Reactive]`, `[ReactiveCommand]` and the rest) are public types in the package's
`ReactiveUI.SourceGenerators` assembly; the generators no longer declare them in your project, so assemblies
that share internals through `InternalsVisibleTo` no longer see two copies. Your project compiles against that assembly
but keeps no reference to it. If an older install added an `<IncludeAssets>` line without `compile`, remove it:

```xml
<PackageReference Include="ReactiveUI.SourceGenerators" Version="..." PrivateAssets="all" />
```

ReactiveUI V24.x.x consumers can reference either `ReactiveUI` for the primitives-based API without
System.Reactive, or `ReactiveUI.Reactive` for the System.Reactive-based API. The generators detect
the referenced API surface automatically. ReactiveUI releases from 23.2.28 remain supported.
Expand All @@ -40,7 +49,8 @@ ReactiveUI Source Generators automatically generate ReactiveUI objects to stream
- `[Reactive(UseRequired = true)]` With field and access modifiers. This will generate a required property, (Not Required for partial properties, use required keyword for property declaration).
- `[Reactive(nameof(RaiseProperty1), nameof(RaiseProperty2))]` With field and property changed notification for additional properties.
- `[ReactiveCommand]`
- `[ReactiveCommand(RunInBackground = true)]` runs a synchronous command on ReactiveUI's background scheduler
- `[ReactiveCommand(RunInBackground = true)]` runs a synchronous command on ReactiveUI's background scheduler, and starts a task-returning command with `Task.Run`
- `[ReactiveCommand(BackgroundScheduler = nameof(_scheduler))]` runs a synchronous command on the given scheduler
- `[ReactiveCommand(CanExecute = nameof(IObservableBoolName))]` with CanExecute
- `[ReactiveCommand(OutputScheduler = "RxSchedulers.MainThreadScheduler")]` using a ReactiveUI Scheduler
- `[ReactiveCommand(OutputScheduler = nameof(_isheduler))]` using a Scheduler defined in the class
Expand Down Expand Up @@ -325,7 +335,9 @@ public partial class MyReactiveClass

### Usage ReactiveCommand on the background scheduler

Use `RunInBackground` for synchronous command methods that should be created with `ReactiveCommand.CreateRunInBackground`. Task- and observable-returning methods continue to use their asynchronous ReactiveCommand factories.
Use `RunInBackground` to run a command's method off the calling thread. A synchronous method's command is created with `ReactiveCommand.CreateRunInBackground`. A task-returning method is started with `Task.Run`, so the code before its first `await` no longer runs on the calling thread; the command parameter and `CancellationToken` are passed through. Observable-returning methods are unaffected.

`BackgroundScheduler` chooses the scheduler a synchronous method runs on, from a scheduler member of the class or a built-in ReactiveUI scheduler, and implies `RunInBackground`. A task-returning method always starts on the thread pool.

```csharp
using ReactiveUI.SourceGenerators;
Expand All @@ -334,6 +346,12 @@ public partial class MyReactiveClass
{
[ReactiveCommand(RunInBackground = true)]
private void ExecuteExpensiveWork() { }

[ReactiveCommand(RunInBackground = true)]
private async Task<int> LoadAsync(int id, CancellationToken token) => await _repository.LoadAsync(id, token);

[ReactiveCommand(BackgroundScheduler = nameof(_workScheduler))]
private void Crunch() { }
}
```

Expand Down
2 changes: 2 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
<PropertyGroup>
<!-- Roslyn projects target netstandard2.0 -->
<RoslynTfm>netstandard2.0</RoslynTfm>
<!-- The ReactiveUI.SourceGenerators attributes library consumers compile against: modern .NET and .NET Framework -->
<LibraryTfms>net8.0;net9.0;net10.0;net11.0;net462;net47;net471;net472;net48;net481</LibraryTfms>
<!-- Test projects multi-target net8.0, net9.0, and net10.0 -->
<TestTfms>net8.0;net9.0;net10.0</TestTfms>
</PropertyGroup>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//HintName: TestNs.TestVM.BindableDerivedList.g.cs
// <auto-generated/>
using System.Collections.ObjectModel;
using DynamicData;
using ReactiveUI;

#pragma warning disable
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//HintName: TestNs.TestVM.BindableDerivedList.g.cs
// <auto-generated/>
using System.Collections.ObjectModel;
using DynamicData;
using ReactiveUI;

#pragma warning disable
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//HintName: TestNs.TestVM.BindableDerivedList.g.cs
// <auto-generated/>
using System.Collections.ObjectModel;
using DynamicData;
using ReactiveUI;

#pragma warning disable
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//HintName: TestNs.TestVM.BindableDerivedList.g.cs
// <auto-generated/>
using System.Collections.ObjectModel;
using DynamicData;
using ReactiveUI;

#pragma warning disable
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//HintName: Namespace1.TestVM.BindableDerivedList.g.cs
// <auto-generated/>
using System.Collections.ObjectModel;
using DynamicData;
using ReactiveUI;

#pragma warning disable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//HintName: Namespace2.TestVM.BindableDerivedList.g.cs
// <auto-generated/>
using System.Collections.ObjectModel;
using DynamicData;
using ReactiveUI;

#pragma warning disable
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//HintName: TestNs.TestVM.BindableDerivedList.g.cs
// <auto-generated/>
using System.Collections.ObjectModel;
using DynamicData;
using ReactiveUI;

#pragma warning disable
Expand Down
Loading
Loading