diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..1093fae
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,230 @@
+# CLAUDE.md — ReactiveUI.SourceGenerators
+
+This document provides guidance for AI assistants and contributors working in this repository.
+
+## Overview
+
+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.
+
+**Minimum consumer requirements:** C# 12.0 · Visual Studio 17.8.0 · ReactiveUI 19.5.31+
+
+## Architecture Overview
+
+The repository ships **three versioned generator assemblies** built from a single shared source folder:
+
+| Project | Roslyn version | Preprocessor constant | Extra features |
+|---------|---------------|-----------------------|----------------|
+| `ReactiveUI.SourceGenerators.Roslyn480` | 4.8.x (baseline) | _(none)_ | Field-based `[Reactive]`, `[ObservableAsProperty]`, `[ReactiveCommand]`, etc. |
+| `ReactiveUI.SourceGenerators.Roslyn4120` | 4.12.0 | `ROSYLN_412` | + partial-property `[Reactive]` and `[ObservableAsProperty]` |
+| `ReactiveUI.SourceGenerators.Roslyn5000` | 5.0.0 | `ROSYLN_500` | + same partial-property support on Roslyn 5 |
+
+Each versioned project links all `.cs` files from `ReactiveUI.SourceGenerators.Roslyn/` via:
+
+```xml
+
+```
+
+`#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.12/cs`, and `analyzers/dotnet/roslyn5.0/cs` paths, so NuGet/MSBuild automatically selects the right build based on the host compiler.
+
+Diagnostics are **not** reported by generators. All `RXUISG*` diagnostics live in the separate `ReactiveUI.SourceGenerators.Analyzers.CodeFixes` project.
+
+## Project Structure
+
+```
+src/
+├── ReactiveUI.SourceGenerators.Roslyn/ # Shared source (linked into all versioned projects)
+│ ├── AttributeDefinitions.cs # Injected attribute source texts
+│ ├── Reactive/ # [Reactive] generator + Execute + models
+│ ├── ReactiveCommand/ # [ReactiveCommand] generator + Execute + models
+│ ├── ObservableAsProperty/ # [ObservableAsProperty] generator + Execute + models
+│ ├── IViewFor/ # [IViewFor] generator + Execute + models
+│ ├── RoutedControlHost/ # [RoutedControlHost] generator
+│ ├── ViewModelControlHost/ # [ViewModelControlHost] generator
+│ ├── BindableDerivedList/ # [BindableDerivedList] generator
+│ ├── ReactiveCollection/ # [ReactiveCollection] generator
+│ ├── ReactiveObject/ # [IReactiveObject] generator
+│ ├── Diagnostics/ # DiagnosticDescriptors, SuppressionDescriptors
+│ └── Core/
+│ ├── Extensions/ # ISymbol*, ITypeSymbol*, INamedTypeSymbol*, AttributeData extensions
+│ ├── Helpers/ # ImmutableArrayBuilder, EquatableArray, HashCode, etc.
+│ └── Models/ # Result, DiagnosticInfo, TargetInfo, etc.
+├── ReactiveUI.SourceGenerators.Roslyn480/ # Roslyn 4.8 build (no define)
+├── ReactiveUI.SourceGenerators.Roslyn4120/ # Roslyn 4.12 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.SourceGenerator.Tests/ # TUnit + Verify snapshot tests
+├── ReactiveUI.SourceGenerators.Execute*/ # Compile-time execution verification projects
+└── TestApps/ # Manual test applications (WPF, WinForms, MAUI, Avalonia)
+```
+
+## Code Generation Strategy
+
+All generated C# source is produced using **raw string literals** (`$$"""..."""`). Do **not** use `StringBuilder` or `SyntaxFactory` for code generation.
+
+```csharp
+// CORRECT — raw string literal with $$ interpolation
+internal static string GenerateProperty(string name, string type) => $$"""
+ public {{type}} {{name}}
+ {
+ get => _{{char.ToLower(name[0])}{{name.Substring(1)}}};
+ set => this.RaiseAndSetIfChanged(ref _{{char.ToLower(name[0])}{{name.Substring(1)}}}, value);
+ }
+ """;
+
+// WRONG — do not use StringBuilder
+var sb = new StringBuilder();
+sb.AppendLine($"public {type} {name}");
+// ...
+
+// WRONG — do not use SyntaxFactory
+SyntaxFactory.PropertyDeclaration(...)
+```
+
+Raw string literals preserve formatting intent, are trivially diffable in code review, and do not require the overhead of SyntaxFactory node construction.
+
+The injected attribute source texts (in `AttributeDefinitions.cs`) also use `$$"""..."""` raw string literals.
+
+## 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.
+2. **`Run*`** — builds the `IncrementalValuesProvider` using `ForAttributeWithMetadataName` + a syntax predicate + a semantic extraction function.
+3. **`Get*Info` (Execute file)** — stateless extraction function. Returns `Result` with embedded diagnostics. Must be pure; must not capture any `ISymbol` or `SyntaxNode` beyond this call.
+4. **`GenerateSource` (Execute file)** — pure function that converts model → raw string source text. No Roslyn symbols allowed here.
+
+```
+Initialize()
+ ├─ RegisterPostInitializationOutput → inject attribute definitions
+ └─ SyntaxProvider.ForAttributeWithMetadataName
+ ├─ syntax predicate (fast, node-type check only)
+ ├─ semantic extraction → Get*Info() → Result
+ └─ RegisterSourceOutput → GenerateSource() → AddSource()
+```
+
+**Incremental caching rules:**
+- All pipeline output models must implement value equality (`record`, `IEquatable`, or `EquatableArray`).
+- Never store `ISymbol`, `SyntaxNode`, `SemanticModel`, or `CancellationToken` in a model.
+- Use `EquatableArray` (from `Core/Helpers`) instead of `ImmutableArray` in models.
+
+## Generators
+
+| Generator class | Attribute | Input target |
+|-----------------|-----------|--------------|
+| `ReactiveGenerator` | `[Reactive]` | Field (all Roslyn) or partial property (ROSYLN_412+) |
+| `ReactiveCommandGenerator` | `[ReactiveCommand]` | Method |
+| `ObservableAsPropertyGenerator` | `[ObservableAsProperty]` | Field or observable method |
+| `IViewForGenerator` | `[IViewFor]` | Class |
+| `RoutedControlHostGenerator` | `[RoutedControlHost]` | Class |
+| `ViewModelControlHostGenerator` | `[ViewModelControlHost]` | Class |
+| `BindableDerivedListGenerator` | `[BindableDerivedList]` | Field (`ReadOnlyObservableCollection`) |
+| `ReactiveCollectionGenerator` | `[ReactiveCollection]` | Field (`ObservableCollection`) |
+| `ReactiveObjectGenerator` | `[IReactiveObject]` | Class |
+
+## Analyzers & Suppressors
+
+All diagnostics use the `RXUISG` prefix. All suppressions use the `RXUISPR` prefix.
+
+| Class | ID range | Purpose |
+|-------|----------|---------|
+| `PropertyToReactiveFieldAnalyzer` | RXUISG0016 | Suggests converting auto-properties to `[Reactive]` fields |
+| `ReactiveAttributeMisuseAnalyzer` | RXUISG0020 | Detects `[Reactive]` on non-partial or non-partial-type members |
+| `PropertyToReactiveFieldCodeFixProvider` | — | Converts auto-property → `[Reactive]` field |
+| `ReactiveAttributeMisuseCodeFixProvider` | — | Fixes misuse of `[Reactive]` attribute |
+
+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).
+
+### Analyzer Separation (Roslyn Best Practice)
+
+- Generators do **not** report diagnostics — they only call `context.ReportDiagnostic` for internal invariant violations via `DiagnosticInfo` models.
+- The `ReactiveUI.SourceGenerators.Analyzers.CodeFixes` project owns all `RXUISG*` diagnostic descriptors and code fixers.
+- `DiagnosticDescriptors.cs` and related files are compiled from the shared Roslyn source via the linked `` items.
+
+## Testing
+
+### Framework
+
+- **TUnit** — test runner and assertion library (replaces xUnit/NUnit).
+- **Verify.SourceGenerators** — snapshot-based verification of generated source output.
+- **Microsoft.Testing.Platform** — native test execution (configured via `testconfig.json`).
+
+### Test project targets
+
+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.
+
+### Snapshot tests
+
+Generator tests extend `TestBase` and call `TestHelper.TestPass(sourceCode)`. Verify saves `.verified.txt` snapshots in the appropriate subdirectory (`REACTIVE/`, `REACTIVECMD/`, `OAPH/`, `IVIEWFOR/`, `DERIVEDLIST/`, `REACTIVECOLL/`, `REACTIVEOBJ/`).
+
+#### Accepting snapshot changes
+
+1. Enable `VerifierSettings.AutoVerify()` in `ModuleInitializer.cs`.
+2. Run `dotnet test --project src/ReactiveUI.SourceGenerator.Tests -c Release`.
+3. Disable `VerifierSettings.AutoVerify()`.
+4. Re-run tests to confirm all pass without AutoVerify.
+
+### Test source language version
+
+Test source strings are parsed with **CSharp13** (`LanguageVersion.CSharp13`). This is the version used by `TestHelper.RunGeneratorAndCheck`.
+
+### Non-snapshot (unit) tests
+
+Analyzer and helper tests use direct `CSharpCompilation` / `CompilationWithAnalyzers` to verify diagnostics without snapshots. See `PropertyToReactiveFieldAnalyzerTests.cs` for the pattern.
+
+## Common Tasks
+
+### 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` using a `$$"""..."""` raw string literal.
+3. Create `Generator.cs` with `Initialize` wiring up `ForAttributeWithMetadataName`.
+4. Create `Generator.Execute.cs` with `Get*Info` (extraction) and `GenerateSource` (raw string template).
+5. Add snapshot tests in `ReactiveUI.SourceGenerator.Tests/UnitTests/`.
+6. Accept snapshots using the AutoVerify trick above.
+
+### Adding a New Analyzer Diagnostic
+
+1. Add a `DiagnosticDescriptor` to `DiagnosticDescriptors.cs`.
+2. Update `AnalyzerReleases.Unshipped.md`.
+3. Implement the analyzer in `ReactiveUI.SourceGenerators.Analyzers.CodeFixes/`.
+4. Add unit tests in `ReactiveUI.SourceGenerator.Tests/UnitTests/`.
+
+### Running Tests
+
+```pwsh
+dotnet test src/ReactiveUI.SourceGenerator.Tests --configuration Release
+```
+
+### Building
+
+```pwsh
+dotnet build src/ReactiveUI.SourceGenerators.sln
+```
+
+## What to Avoid
+
+- **`ISymbol` / `SyntaxNode` in pipeline output models** — breaks incremental caching; use value-equatable data records instead.
+- **`SyntaxFactory` for code generation** — use `$$"""..."""` raw string literals.
+- **`StringBuilder` for code generation** — use `$$"""..."""` raw string literals.
+- **Diagnostics reported inside generators** — use the separate analyzer project for all `RXUISG*` diagnostics.
+- **LINQ in hot Roslyn pipeline paths** — use `foreach` loops (Roslyn convention for incremental generators).
+- **Non-value-equatable models** in the incremental pipeline — will defeat caching and cause unnecessary regeneration.
+- **APIs unavailable in `netstandard2.0`** inside `ReactiveUI.SourceGenerators.Roslyn*` projects — the generator must run inside the compiler host which targets netstandard2.0.
+- **Runtime reflection** in generated code — breaks Native AOT compatibility.
+- **`#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).
+- **File-scoped namespaces in generated output** — requires C# 10; use block-scoped namespaces.
+
+## Important Notes
+
+- **Required .NET SDKs:** .NET 8.0, 9.0, and 10.0 (all required for multi-targeting the test project).
+- **Generator + Analyzer targets:** `netstandard2.0` (Roslyn host requirement).
+- **Test project targets:** `net8.0;net9.0;net10.0`.
+- **No shallow clones:** The repository uses Nerdbank.GitVersioning; a full `git clone` is required for correct versioning.
+- **NuGet packaging:** The `ReactiveUI.SourceGenerators` project bundles all three versioned generator DLLs at different `analyzers/dotnet/roslyn*/cs` paths.
+- **Cross-platform tests:** On non-Windows platforms, WPF/WinForms types are injected as source stubs so generator tests compile cross-platform.
+- **`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).
+
+**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.
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 2d47b1f..84b8046 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -56,6 +56,21 @@
net9.0-android;net9.0-ios;net9.0-maccatalyst;net9.0-windows10.0.19041.0;net10.0-android;net10.0-ios;net10.0-maccatalyst;net10.0-windows10.0.19041.0
+
+ true
+ true
+ true
+ Exe
+
+
+
+
+
+ $(AssemblyName).testconfig.json
+ PreserveNewest
+
+
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index f8ff885..c7dc358 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -7,8 +7,6 @@
-
-
@@ -26,25 +24,13 @@
-
-
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
diff --git a/src/ReactiveUI.SourceGenerator.Tests/AssemblyInfo.Parallel.cs b/src/ReactiveUI.SourceGenerator.Tests/AssemblyInfo.Parallel.cs
deleted file mode 100644
index 7fea955..0000000
--- a/src/ReactiveUI.SourceGenerator.Tests/AssemblyInfo.Parallel.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved.
-// Licensed to the ReactiveUI and contributors under one or more agreements.
-// The ReactiveUI and contributors licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-[assembly: Parallelizable(ParallelScope.Fixtures)]
-[assembly: LevelOfParallelism(4)]
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/BindableDerivedListGeneratorTests.FromReactiveProperties#TestNs.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/BindableDerivedListGeneratorTests.FromReactiveProperties#TestNs.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..a59391b
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/BindableDerivedListGeneratorTests.FromReactiveProperties#TestNs.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: TestNs.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection Test1 => _test1;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.ComplexGeneric#TestNs.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.ComplexGeneric#TestNs.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..ca5f477
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.ComplexGeneric#TestNs.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: TestNs.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Items => _items;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.DateTimeType#TestNs.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.DateTimeType#TestNs.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..2ed8ef7
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.DateTimeType#TestNs.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,24 @@
+//HintName: TestNs.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Dates => _dates;
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Timestamps => _timestamps;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.DiffNamespaces#Namespace1.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.DiffNamespaces#Namespace1.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..15396e5
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.DiffNamespaces#Namespace1.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: Namespace1.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace Namespace1
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Numbers => _numbers;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.DiffNamespaces#Namespace2.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.DiffNamespaces#Namespace2.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..2e67d3f
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.DiffNamespaces#Namespace2.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: Namespace2.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace Namespace2
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Names => _names;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.EnumType#TestNs.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.EnumType#TestNs.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..33eeeff
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.EnumType#TestNs.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: TestNs.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Statuses => _statuses;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.GenericClass#TestNs.GenericVM`1.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.GenericClass#TestNs.GenericVM`1.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..201d33d
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.GenericClass#TestNs.GenericVM`1.BindableDerivedList.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: TestNs.GenericVM`1.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class GenericVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Items => _items;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.GuidType#TestNs.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.GuidType#TestNs.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..5d417e5
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.GuidType#TestNs.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: TestNs.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Ids => _ids;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.InterfaceType#TestNs.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.InterfaceType#TestNs.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..1d65ddc
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.InterfaceType#TestNs.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,24 @@
+//HintName: TestNs.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Items => _items;
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Disposables => _disposables;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.MultipleLists#TestNs.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.MultipleLists#TestNs.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..e20ef2e
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.MultipleLists#TestNs.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,27 @@
+//HintName: TestNs.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Names => _names;
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Numbers => _numbers;
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Values => _values;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NestedClass#TestNs.OuterVM+InnerVM+DeepInnerVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NestedClass#TestNs.OuterVM+InnerVM+DeepInnerVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..4f05c1b
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NestedClass#TestNs.OuterVM+InnerVM+DeepInnerVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,28 @@
+//HintName: TestNs.OuterVM+InnerVM+DeepInnerVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ public partial class OuterVM
+{
+public partial class InnerVM
+{
+
+ public partial class DeepInnerVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? DeepList => _deepList;
+ }
+}
+}
+
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NestedClass#TestNs.OuterVM+InnerVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NestedClass#TestNs.OuterVM+InnerVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..fd893ab
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NestedClass#TestNs.OuterVM+InnerVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,25 @@
+//HintName: TestNs.OuterVM+InnerVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ public partial class OuterVM
+{
+
+ public partial class InnerVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? InnerList => _innerList;
+ }
+}
+
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NestedClass#TestNs.OuterVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NestedClass#TestNs.OuterVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..b44a109
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NestedClass#TestNs.OuterVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: TestNs.OuterVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class OuterVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? OuterList => _outerList;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NullableElements#TestNs.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NullableElements#TestNs.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..9ddf0bf
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.NullableElements#TestNs.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: TestNs.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? NullableStrings => _nullableStrings;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.RecordClass#TestNs.TestVMRecord.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.RecordClass#TestNs.TestVMRecord.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..4146e6b
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.RecordClass#TestNs.TestVMRecord.BindableDerivedList.g.verified.cs
@@ -0,0 +1,24 @@
+//HintName: TestNs.TestVMRecord.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial record TestVMRecord
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Numbers => _numbers;
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Names => _names;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.StructType#TestNs.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.StructType#TestNs.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..b4825ac
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.StructType#TestNs.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: TestNs.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection? Points => _points;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.TupleType#TestNs.TestVM.BindableDerivedList.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.TupleType#TestNs.TestVM.BindableDerivedList.g.verified.cs
new file mode 100644
index 0000000..54ab92b
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/DERIVEDLIST/DerivedListExtTests.TupleType#TestNs.TestVM.BindableDerivedList.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: TestNs.TestVM.BindableDerivedList.g.cs
+//
+using System.Collections.ObjectModel;
+using DynamicData;
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::System.Collections.ObjectModel.ReadOnlyObservableCollection<(int Id, string Name)>? Tuples => _tuples;
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.Basic#TestNs.TestViewWpf.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.Basic#TestNs.TestViewWpf.IViewFor.g.verified.cs
new file mode 100644
index 0000000..7fcbdc9
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.Basic#TestNs.TestViewWpf.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.TestViewWpf.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the TestViewWpf which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class TestViewWpf : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.TestViewModel), typeof(TestViewWpf), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.TestViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.TestViewModel ViewModel { get => (TestNs.TestViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.TestViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.AllRegOpts#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.AllRegOpts#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
index ee04db7..08cdb68 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.AllRegOpts#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.AllRegOpts#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
@@ -16,6 +16,8 @@ internal static class ReactiveUISourceGeneratorsExtensions
public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat.IMutableDependencyResolver resolver)
{
if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver));
+ resolver.RegisterConstant>(new global::TestNs.FullView());
+ resolver.Register();
}
}
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.AllRegOpts#TestNs.FullView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.AllRegOpts#TestNs.FullView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..a035107
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.AllRegOpts#TestNs.FullView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.FullView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the FullView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class FullView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.FullViewModel), typeof(FullView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.FullViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.FullViewModel ViewModel { get => (TestNs.FullViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.FullViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Constant#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Constant#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
index ee04db7..c6758a7 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Constant#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Constant#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
@@ -16,6 +16,7 @@ internal static class ReactiveUISourceGeneratorsExtensions
public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat.IMutableDependencyResolver resolver)
{
if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver));
+ resolver.RegisterConstant>(new global::TestNs.TestView());
}
}
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Constant#TestNs.TestView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Constant#TestNs.TestView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..cf05cbc
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Constant#TestNs.TestView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.TestView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the TestView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class TestView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.TestViewModel), typeof(TestView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.TestViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.TestViewModel ViewModel { get => (TestNs.TestViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.TestViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.DiffNs#Views.ProductView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.DiffNs#Views.ProductView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..05bb18e
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.DiffNs#Views.ProductView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: Views.ProductView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace Views
+{
+ ///
+ /// Partial class for the ProductView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class ProductView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(ViewModels.ProductViewModel), typeof(ProductView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public ViewModels.ProductViewModel BindingRoot => ViewModel;
+
+ ///
+ public ViewModels.ProductViewModel ViewModel { get => (ViewModels.ProductViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (ViewModels.ProductViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.ExtNs#App.Views.ExternalView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.ExtNs#App.Views.ExternalView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..412cfcd
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.ExtNs#App.Views.ExternalView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: App.Views.ExternalView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace App.Views
+{
+ ///
+ /// Partial class for the ExternalView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class ExternalView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(External.ViewModels.ExternalViewModel), typeof(ExternalView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public External.ViewModels.ExternalViewModel BindingRoot => ViewModel;
+
+ ///
+ public External.ViewModels.ExternalViewModel ViewModel { get => (External.ViewModels.ExternalViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (External.ViewModels.ExternalViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithNestedViewClass#TestNs.OuterContainer+MiddleContainer+NestedView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithNestedViewClass#TestNs.OuterContainer+MiddleContainer+NestedView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..1da30ec
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithNestedViewClass#TestNs.OuterContainer+MiddleContainer+NestedView.IViewFor.g.verified.cs
@@ -0,0 +1,42 @@
+//HintName: TestNs.OuterContainer+MiddleContainer+NestedView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+public partial class OuterContainer
+{
+public partial class MiddleContainer
+{
+ ///
+ /// Partial class for the NestedView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class NestedView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.TestViewModel), typeof(NestedView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.TestViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.TestViewModel ViewModel { get => (TestNs.TestViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.TestViewModel)value; }
+ }
+
+}
+}
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithReactiveCommandsViewModel#TestNs.CommandsView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithReactiveCommandsViewModel#TestNs.CommandsView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..c041712
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithReactiveCommandsViewModel#TestNs.CommandsView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.CommandsView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the CommandsView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class CommandsView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.CommandsViewModel), typeof(CommandsView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.CommandsViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.CommandsViewModel ViewModel { get => (TestNs.CommandsViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.CommandsViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithReactivePropertiesViewModel#TestNs.ReactivePropertiesView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithReactivePropertiesViewModel#TestNs.ReactivePropertiesView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..f24a001
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithReactivePropertiesViewModel#TestNs.ReactivePropertiesView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.ReactivePropertiesView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the ReactivePropertiesView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class ReactivePropertiesView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.ReactivePropertiesViewModel), typeof(ReactivePropertiesView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.ReactivePropertiesViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.ReactivePropertiesViewModel ViewModel { get => (TestNs.ReactivePropertiesViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.ReactivePropertiesViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithStringViewModelType#TestNs.TestView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithStringViewModelType#TestNs.TestView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..cf05cbc
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithStringViewModelType#TestNs.TestView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.TestView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the TestView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class TestView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.TestViewModel), typeof(TestView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.TestViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.TestViewModel ViewModel { get => (TestNs.TestViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.TestViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithViewModelRegistration#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithViewModelRegistration#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
index ee04db7..9b9b7c8 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithViewModelRegistration#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithViewModelRegistration#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
@@ -16,6 +16,8 @@ internal static class ReactiveUISourceGeneratorsExtensions
public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat.IMutableDependencyResolver resolver)
{
if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver));
+ resolver.RegisterLazySingleton>(() => new global::TestNs.TestView());
+ resolver.RegisterLazySingleton(() => new global::TestNs.TestViewModel());
}
}
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithViewModelRegistration#TestNs.TestView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithViewModelRegistration#TestNs.TestView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..cf05cbc
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromIViewForWithViewModelRegistration#TestNs.TestView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.TestView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the TestView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class TestView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.TestViewModel), typeof(TestView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.TestViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.TestViewModel ViewModel { get => (TestNs.TestViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.TestViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
index ee04db7..e812246 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
@@ -16,6 +16,7 @@ internal static class ReactiveUISourceGeneratorsExtensions
public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat.IMutableDependencyResolver resolver)
{
if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver));
+ resolver.RegisterLazySingleton>(() => new global::TestNs.View3());
}
}
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#TestNs.View1.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#TestNs.View1.IViewFor.g.verified.cs
new file mode 100644
index 0000000..ee2f20c
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#TestNs.View1.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.View1.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the View1 which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class View1 : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.ViewModel1), typeof(View1), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.ViewModel1 BindingRoot => ViewModel;
+
+ ///
+ public TestNs.ViewModel1 ViewModel { get => (TestNs.ViewModel1)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.ViewModel1)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#TestNs.View2.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#TestNs.View2.IViewFor.g.verified.cs
new file mode 100644
index 0000000..4f704a7
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#TestNs.View2.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.View2.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the View2 which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class View2 : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.ViewModel2), typeof(View2), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.ViewModel2 BindingRoot => ViewModel;
+
+ ///
+ public TestNs.ViewModel2 ViewModel { get => (TestNs.ViewModel2)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.ViewModel2)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#TestNs.View3.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#TestNs.View3.IViewFor.g.verified.cs
new file mode 100644
index 0000000..2b2f1db
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.FromMultipleIViewForInSameNamespace#TestNs.View3.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.View3.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the View3 which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class View3 : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.ViewModel3), typeof(View3), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.ViewModel3 BindingRoot => ViewModel;
+
+ ///
+ public TestNs.ViewModel3 ViewModel { get => (TestNs.ViewModel3)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.ViewModel3)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Generic#TestNs.StringItemView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Generic#TestNs.StringItemView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..cbca553
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Generic#TestNs.StringItemView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.StringItemView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the StringItemView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class StringItemView : IViewFor>
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.GenericViewModel), typeof(StringItemView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.GenericViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.GenericViewModel ViewModel { get => (TestNs.GenericViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.GenericViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Interface#TestNs.InterfaceView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Interface#TestNs.InterfaceView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..5a45ea8
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Interface#TestNs.InterfaceView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.InterfaceView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the InterfaceView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class InterfaceView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.ITestViewModel), typeof(InterfaceView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.ITestViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.ITestViewModel ViewModel { get => (TestNs.ITestViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.ITestViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.LazySingle#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.LazySingle#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
index ee04db7..09a9a9f 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.LazySingle#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.LazySingle#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
@@ -16,6 +16,7 @@ internal static class ReactiveUISourceGeneratorsExtensions
public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat.IMutableDependencyResolver resolver)
{
if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver));
+ resolver.RegisterLazySingleton>(() => new global::TestNs.TestView());
}
}
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.LazySingle#TestNs.TestView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.LazySingle#TestNs.TestView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..cf05cbc
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.LazySingle#TestNs.TestView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.TestView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the TestView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class TestView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.TestViewModel), typeof(TestView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.TestViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.TestViewModel ViewModel { get => (TestNs.TestViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.TestViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Nested#TestNs.ChildView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Nested#TestNs.ChildView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..6a22827
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Nested#TestNs.ChildView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.ChildView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the ChildView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class ChildView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.ParentViewModel.ChildViewModel), typeof(ChildView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.ParentViewModel.ChildViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.ParentViewModel.ChildViewModel ViewModel { get => (TestNs.ParentViewModel.ChildViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.ParentViewModel.ChildViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.PerReq#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.PerReq#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
index ee04db7..d5913d0 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.PerReq#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.PerReq#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs
@@ -16,6 +16,7 @@ internal static class ReactiveUISourceGeneratorsExtensions
public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat.IMutableDependencyResolver resolver)
{
if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver));
+ resolver.Register, global::TestNs.TestView>();
}
}
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.PerReq#TestNs.TestView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.PerReq#TestNs.TestView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..cf05cbc
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.PerReq#TestNs.TestView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.TestView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the TestView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class TestView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.TestViewModel), typeof(TestView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.TestViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.TestViewModel ViewModel { get => (TestNs.TestViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.TestViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Record#TestNs.RecordView.IViewFor.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Record#TestNs.RecordView.IViewFor.g.verified.cs
new file mode 100644
index 0000000..d442e6f
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/ViewForExtTests.Record#TestNs.RecordView.IViewFor.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: TestNs.RecordView.IViewFor.g.cs
+//
+using ReactiveUI;
+using System.Windows;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+ ///
+ /// Partial class for the RecordView which contains ReactiveUI IViewFor initialization.
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial class RecordView : IViewFor
+ {
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TestNs.RecordViewModel), typeof(RecordView), new PropertyMetadata(null));
+
+ ///
+ /// Gets the binding root view model.
+ ///
+ public TestNs.RecordViewModel BindingRoot => ViewModel;
+
+ ///
+ public TestNs.RecordViewModel ViewModel { get => (TestNs.RecordViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); }
+
+ ///
+ object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TestNs.RecordViewModel)value; }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Access#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Access#TestNs.TestVM.ReactiveCommands.g.verified.cs
index 7b3f721..77c1462 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Access#TestNs.TestVM.ReactiveCommands.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.Access#TestNs.TestVM.ReactiveCommands.g.verified.cs
@@ -1,4 +1,4 @@
-//HintName: TestNs.TestVM.ReactiveCommands.g.cs
+//HintName: TestNs.TestVM.ReactiveCommands.g.cs
//
#pragma warning disable
@@ -13,7 +13,7 @@ public partial class TestVM
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
- public global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.Create(Test1); }
+ internal global::ReactiveUI.ReactiveCommand Test1Command { get => _test1Command ??= global::ReactiveUI.ReactiveCommand.Create(Test1); }
}
}
#nullable restore
diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromMultipleReactiveCommands#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromMultipleReactiveCommands#TestNs.TestVM.ReactiveCommands.g.verified.cs
index 46f7d19..0c7f7db 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromMultipleReactiveCommands#TestNs.TestVM.ReactiveCommands.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromMultipleReactiveCommands#TestNs.TestVM.ReactiveCommands.g.verified.cs
@@ -1,4 +1,4 @@
-//HintName: TestNs.TestVM.ReactiveCommands.g.cs
+//HintName: TestNs.TestVM.ReactiveCommands.g.cs
//
#pragma warning disable
@@ -28,8 +28,8 @@ public partial class TestVM
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
- public global::ReactiveUI.ReactiveCommand CalculateCommand { get => _calculateCommand ??= global::ReactiveUI.ReactiveCommand.Create(Calculate); }
+ internal global::ReactiveUI.ReactiveCommand CalculateCommand { get => _calculateCommand ??= global::ReactiveUI.ReactiveCommand.Create(Calculate); }
}
}
#nullable restore
-#pragma warning restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInGenericClass#TestNs.GenericVM`1.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInGenericClass#TestNs.GenericVM`1.ReactiveCommands.g.verified.cs
index 1eaab91..33a9bda 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInGenericClass#TestNs.GenericVM`1.ReactiveCommands.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandInGenericClass#TestNs.GenericVM`1.ReactiveCommands.g.verified.cs
@@ -14,11 +14,11 @@ public partial class GenericVM
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public global::ReactiveUI.ReactiveCommand ProcessItemCommand { get => _processItemCommand ??= global::ReactiveUI.ReactiveCommand.Create(ProcessItem); }
- private global::ReactiveUI.ReactiveCommand? _processItemCommand;
+ private global::ReactiveUI.ReactiveCommand? _processItemLaterCommand;
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
- public global::ReactiveUI.ReactiveCommand ProcessItemCommand { get => _processItemCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(ProcessItemAsync); }
+ public global::ReactiveUI.ReactiveCommand ProcessItemLaterCommand { get => _processItemLaterCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(ProcessItemLater); }
}
}
#nullable restore
diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteMethod#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteMethod#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs
new file mode 100644
index 0000000..42a5e54
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteMethod#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs
@@ -0,0 +1,44 @@
+//HintName: ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.cs
+// Copyright (c) 2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+//
+#pragma warning disable
+#nullable enable
+namespace ReactiveUI.SourceGenerators;
+
+///
+/// ReactiveCommand Attribute.
+///
+///
+[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
+internal sealed class ReactiveCommandAttribute : global::System.Attribute
+{
+ ///
+ /// Gets the can execute method or property.
+ ///
+ ///
+ /// The name of the CanExecute Observable of bool.
+ ///
+ public string? CanExecute { get; init; }
+
+ ///
+ /// Gets the output scheduler.
+ ///
+ ///
+ /// The output scheduler.
+ ///
+ public string? OutputScheduler { get; init; }
+
+ ///
+ /// Gets the AccessModifier of the ReactiveCommand property.
+ ///
+ ///
+ /// The AccessModifier of the property.
+ ///
+ public PropertyAccessModifier AccessModifier { get; init; }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteMethod#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteMethod#TestNs.TestVM.ReactiveCommands.g.verified.cs
new file mode 100644
index 0000000..be917d9
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithCanExecuteMethod#TestNs.TestVM.ReactiveCommands.g.verified.cs
@@ -0,0 +1,20 @@
+//HintName: TestNs.TestVM.ReactiveCommands.g.cs
+//
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ private global::ReactiveUI.ReactiveCommand? _runCommand;
+
+
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::ReactiveUI.ReactiveCommand RunCommand { get => _runCommand ??= global::ReactiveUI.ReactiveCommand.Create(Run, CanRun()); }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithPrivateAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithPrivateAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs
index cbaf4d4..1cefe47 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithPrivateAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithPrivateAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs
@@ -1,4 +1,4 @@
-//HintName: TestNs.TestVM.ReactiveCommands.g.cs
+//HintName: TestNs.TestVM.ReactiveCommands.g.cs
//
#pragma warning disable
@@ -13,8 +13,8 @@ public partial class TestVM
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
- public global::ReactiveUI.ReactiveCommand PrivateCommandCommand { get => _privateCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(PrivateCommand); }
+ private global::ReactiveUI.ReactiveCommand PrivateCommandCommand { get => _privateCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(PrivateCommand); }
}
}
#nullable restore
-#pragma warning restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithProtectedAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithProtectedAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs
index 79413af..14c5426 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithProtectedAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandWithProtectedAccess#TestNs.TestVM.ReactiveCommands.g.verified.cs
@@ -1,4 +1,4 @@
-//HintName: TestNs.TestVM.ReactiveCommands.g.cs
+//HintName: TestNs.TestVM.ReactiveCommands.g.cs
//
#pragma warning disable
@@ -13,8 +13,8 @@ public partial class TestVM
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
- public global::ReactiveUI.ReactiveCommand ProtectedCommandCommand { get => _protectedCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(ProtectedCommand); }
+ protected global::ReactiveUI.ReactiveCommand ProtectedCommandCommand { get => _protectedCommandCommand ??= global::ReactiveUI.ReactiveCommand.Create(ProtectedCommand); }
}
}
#nullable restore
-#pragma warning restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandsAcrossPartialDeclarations#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandsAcrossPartialDeclarations#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs
new file mode 100644
index 0000000..42a5e54
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandsAcrossPartialDeclarations#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs
@@ -0,0 +1,44 @@
+//HintName: ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.cs
+// Copyright (c) 2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+//
+#pragma warning disable
+#nullable enable
+namespace ReactiveUI.SourceGenerators;
+
+///
+/// ReactiveCommand Attribute.
+///
+///
+[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
+internal sealed class ReactiveCommandAttribute : global::System.Attribute
+{
+ ///
+ /// Gets the can execute method or property.
+ ///
+ ///
+ /// The name of the CanExecute Observable of bool.
+ ///
+ public string? CanExecute { get; init; }
+
+ ///
+ /// Gets the output scheduler.
+ ///
+ ///
+ /// The output scheduler.
+ ///
+ public string? OutputScheduler { get; init; }
+
+ ///
+ /// Gets the AccessModifier of the ReactiveCommand property.
+ ///
+ ///
+ /// The AccessModifier of the property.
+ ///
+ public PropertyAccessModifier AccessModifier { get; init; }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandsAcrossPartialDeclarations#TestNs.TestVM.ReactiveCommands.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandsAcrossPartialDeclarations#TestNs.TestVM.ReactiveCommands.g.verified.cs
new file mode 100644
index 0000000..aa900e9
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/RxCmdExtTests.FromReactiveCommandsAcrossPartialDeclarations#TestNs.TestVM.ReactiveCommands.g.verified.cs
@@ -0,0 +1,25 @@
+//HintName: TestNs.TestVM.ReactiveCommands.g.cs
+//
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ private global::ReactiveUI.ReactiveCommand? _createCommand;
+
+
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::ReactiveUI.ReactiveCommand CreateCommand { get => _createCommand ??= global::ReactiveUI.ReactiveCommand.Create(Create); }
+ private global::ReactiveUI.ReactiveCommand? _loadCommand;
+
+
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public global::ReactiveUI.ReactiveCommand LoadCommand { get => _loadCommand ??= global::ReactiveUI.ReactiveCommand.CreateFromTask(LoadAsync); }
+ }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj b/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj
index c729923..b297f9e 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj
+++ b/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj
@@ -5,26 +5,19 @@
enable
enable
13.0
+ Exe
false
true
$(NoWarn);CA1812;CA1001
+ true
-
-
-
-
-
-
-
-
+
+
-
-
-
@@ -35,14 +28,21 @@
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
diff --git a/src/ReactiveUI.SourceGenerator.Tests/TestBase.cs b/src/ReactiveUI.SourceGenerator.Tests/TestBase.cs
index 8b74cdd..dd7fc2c 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/TestBase.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/TestBase.cs
@@ -18,22 +18,6 @@ public abstract class TestBase : IDisposable
///
protected TestHelper TestHelper { get; } = new();
- ///
- /// Initializes the test helper asynchronously.
- ///
- /// A task representing the asynchronous operation.
- [OneTimeSetUp]
- public Task InitializeAsync() => TestHelper.InitializeAsync();
-
- ///
- /// Disposes the test helper.
- ///
- [OneTimeTearDown]
- public void DisposeAsync()
- {
- TestHelper.Dispose();
- }
-
///
public void Dispose()
{
diff --git a/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs b/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs
index 2ee1e5e..abd8a9c 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs
@@ -4,9 +4,7 @@
// See the LICENSE file in the project root for full license information.
using System.Reflection;
-using ReactiveMarbles.NuGet.Helpers;
-
-using ReactiveMarbles.SourceGenerator.TestNuGetHelper.Compilation;
+using System.Runtime.InteropServices;
using ReactiveUI.SourceGenerators.WinForms;
namespace ReactiveUI.SourceGenerator.Tests;
@@ -20,38 +18,14 @@ namespace ReactiveUI.SourceGenerator.Tests;
public sealed partial class TestHelper : IDisposable
where T : IIncrementalGenerator, new()
{
+ // Cache support references per generator type T. The support assembly compiles attribute
+ // definitions that are NOT injected by T via RegisterPostInitializationOutput — an expensive
+ // Roslyn compilation + Emit step that produces an identical result for every test in the same
+ // generator class. Compute it once and reuse it for all subsequent tests.
+ private static readonly Lazy> supportReferences =
+ new(CreateSupportReferences, LazyThreadSafetyMode.ExecutionAndPublication);
///
- /// Represents the NuGet library dependency for the Splat library.
- ///
- private static readonly LibraryRange SplatLibrary =
- new("Splat", VersionRange.AllStable, LibraryDependencyTarget.Package);
-
- ///
- /// Represents the NuGet library dependency for the ReactiveUI library.
- ///
- private static readonly LibraryRange ReactiveuiLibrary =
- new("ReactiveUI", VersionRange.AllStable, LibraryDependencyTarget.Package);
-
- private static readonly string mscorlibPath = Path.Combine(
- System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(),
- "mscorlib.dll");
-
- private static readonly Assembly[] References =
- [
- typeof(object).Assembly,
- typeof(Enumerable).Assembly,
- typeof(T).Assembly,
- typeof(TestHelper).Assembly,
- typeof(IViewFor).Assembly,
- ];
-
- ///
- /// Holds the compiler instance used for event-related code generation.
- ///
- private EventBuilderCompiler? _eventCompiler;
-
- ///
- /// Verifieds the file path.
+ /// Gets the verified file path for generator type .
///
///
/// A string.
@@ -75,49 +49,26 @@ public string VerifiedFilePath()
}
///
- /// Asynchronously initializes the source generator helper by downloading required packages.
+ /// Asynchronously initializes the source generator helper.
///
- /// A task representing the asynchronous initialization operation.
- public async Task InitializeAsync()
- {
-#if NET10_0_OR_GREATER
- NuGetFramework[] targetFrameworks = [new NuGetFramework(".NETCoreApp", new Version(10, 0, 0, 0))];
- #elif NET9_0_OR_GREATER
- NuGetFramework[] targetFrameworks = [new NuGetFramework(".NETCoreApp", new Version(9, 0, 0, 0))];
-#else
- NuGetFramework[] targetFrameworks = [new NuGetFramework(".NETCoreApp", new Version(8, 0, 0, 0))];
-#endif
-
- // Download necessary NuGet package files.
- var inputGroup = await NuGetPackageHelper.DownloadPackageFilesAndFolder(
- [SplatLibrary, ReactiveuiLibrary],
- targetFrameworks,
- packageOutputDirectory: null).ConfigureAwait(false);
-
- // Initialize the event compiler with downloaded packages and target framework.
- var framework = targetFrameworks[0];
- _eventCompiler = new EventBuilderCompiler(inputGroup, inputGroup, framework);
- }
+ /// A task representing the completed initialization operation.
+ public Task InitializeAsync() => Task.CompletedTask;
///
/// Tests a generator expecting it to fail by throwing an .
///
/// The source code to test.
- public void TestFail(
+ /// A task representing the asynchronous assertion operation.
+ public Task TestFail(
string source)
{
- if (_eventCompiler is null)
- {
- throw new InvalidOperationException("Must have valid compiler instance.");
- }
-
- var utility = new SourceGeneratorUtility(x => TestContext.Out.WriteLine(x));
-
#pragma warning disable IDE0053 // Use expression body for lambda expression
#pragma warning disable RCS1021 // Convert lambda expression body to expression body
Assert.Throws(() => { RunGeneratorAndCheck(source); });
#pragma warning restore RCS1021 // Convert lambda expression body to expression body
#pragma warning restore IDE0053 // Use expression body for lambda expression
+
+ return Task.CompletedTask;
}
///
@@ -125,25 +76,18 @@ public void TestFail(
///
/// The source code to test.
/// if set to true [with pre diagnosics].
- ///
- /// The driver.
- ///
+ /// A task representing the asynchronous verification operation.
/// Must have valid compiler instance.
/// callerType.
- public SettingsTask TestPass(
+ public Task TestPass(
string source,
bool withPreDiagnosics = false)
- {
- if (_eventCompiler is null)
- {
- throw new InvalidOperationException("Must have valid compiler instance.");
- }
-
- return RunGeneratorAndCheck(source, withPreDiagnosics);
- }
+ => RunGeneratorAndCheck(source, withPreDiagnosics);
///
- public void Dispose() => _eventCompiler?.Dispose();
+ public void Dispose()
+ {
+ }
///
/// Runs the specified source generator and validates the generated code.
@@ -154,43 +98,103 @@ public SettingsTask TestPass(
///
/// The generator driver used to run the generator.
///
- /// Thrown if the compiler instance is not valid or if the compilation fails.
+ /// Thrown if the compilation fails.
public SettingsTask RunGeneratorAndCheck(
string code,
bool withPreDiagnosics = false,
bool rerunCompilation = true)
{
- if (_eventCompiler is null)
+ // Collect required assembly references: runtime assemblies plus a support assembly
+ // that provides attribute/enum definitions for generators OTHER than the active generator T.
+ // Generator T injects its own definitions via RegisterPostInitializationOutput, so those
+ // are excluded from the support assembly to avoid CS0433 duplicate-type errors.
+ var assemblies = new HashSet(
+ TestCompilationReferences.CreateDefault().Concat(supportReferences.Value));
+
+ var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13);
+ var syntaxTrees = new List
{
- throw new InvalidOperationException("Must have a valid compiler instance.");
+ // Mirror the test project's GlobalUsings.g.cs so test sources can use unqualified
+ // attribute names (e.g. [BindableDerivedList]) without an explicit 'using' directive.
+ CSharpSyntaxTree.ParseText(
+ "global using ReactiveUI.SourceGenerators;",
+ parseOptions,
+ path: "GlobalUsings.g.cs"),
+ CSharpSyntaxTree.ParseText(code, parseOptions),
+ };
+
+ // When the active generator is NOT ReactiveGenerator, the shared enum types
+ // (AccessModifier, PropertyAccessModifier, InheritanceModifier, SplatRegistrationType)
+ // are not injected by any generator but may be referenced by test source code or by the
+ // generator's own output. Add them directly as source trees so they are visible in both
+ // the input and output compilations at the correct (non-internal) accessibility level.
+ if (typeof(T) != typeof(ReactiveGenerator))
+ {
+ syntaxTrees.Add(CSharpSyntaxTree.ParseText(
+ GetAttributeDefinitionsMethodResult("GetAccessModifierEnum"),
+ parseOptions,
+ path: "AccessModifierEnum.g.cs"));
}
- IEnumerable basicReferences;
-#if NET10_0_OR_GREATER
- basicReferences = Basic.Reference.Assemblies.Net100.References.All;
-#elif NET9_0_OR_GREATER
- basicReferences = Basic.Reference.Assemblies.Net90.References.All;
-#else
- basicReferences = Basic.Reference.Assemblies.Net80.References.All;
-#endif
+ // When the active generator is IViewForGenerator, the [Reactive] and [ReactiveCommand]
+ // attributes are not injected (those belong to ReactiveGenerator and ReactiveCommandGenerator).
+ // They are also excluded from the support DLL (above), so add them directly as inline source
+ // trees — this makes them visible in the test source compilation without CS0122.
+ if (typeof(T) == typeof(IViewForGenerator))
+ {
+ syntaxTrees.Add(CSharpSyntaxTree.ParseText(
+ GetAttributeDefinitionsPropertyResult("ReactiveAttribute"),
+ parseOptions,
+ path: "ReactiveAttribute.g.cs"));
+ syntaxTrees.Add(CSharpSyntaxTree.ParseText(
+ GetAttributeDefinitionsPropertyResult("ReactiveCommandAttribute"),
+ parseOptions,
+ path: "ReactiveCommandAttribute.g.cs"));
+ }
- basicReferences.Concat([MetadataReference.CreateFromFile(mscorlibPath)]);
- basicReferences.Concat(GetTransitiveReferences(References));
+ // When the active generator is ReactiveObjectGenerator, [Reactive] and [ObservableAsProperty]
+ // attributes are not injected by this generator (they belong to ReactiveGenerator and
+ // ObservableAsPropertyGenerator). They are excluded from the support DLL to avoid CS0433,
+ // so add them directly as inline source trees for accessibility.
+ if (typeof(T) == typeof(ReactiveObjectGenerator))
+ {
+ syntaxTrees.Add(CSharpSyntaxTree.ParseText(
+ GetAttributeDefinitionsPropertyResult("ReactiveAttribute"),
+ parseOptions,
+ path: "ReactiveAttribute.g.cs"));
+ syntaxTrees.Add(CSharpSyntaxTree.ParseText(
+ GetAttributeDefinitionsPropertyResult("ObservableAsPropertyAttribute"),
+ parseOptions,
+ path: "ObservableAsPropertyAttribute.g.cs"));
+ }
- // Collect required assembly references.
- var assemblies = new HashSet(
- basicReferences
- .Concat(basicReferences)
- .Concat(_eventCompiler.Modules.Select(x => MetadataReference.CreateFromFile(x.PEFile!.FileName)))
- .Concat(_eventCompiler.ReferencedModules.Select(x => MetadataReference.CreateFromFile(x.PEFile!.FileName)))
- .Concat(_eventCompiler.NeededModules.Select(x => MetadataReference.CreateFromFile(x.PEFile!.FileName))));
+ // BindableDerivedListGenerator and ReactiveCollectionGenerator inject their own attribute
+ // via RegisterPostInitializationOutput. Tests that also use [Reactive] (WithReactive tests)
+ // need ReactiveAttribute as an inline source tree because it is excluded from the support DLL.
+ if (typeof(T) == typeof(BindableDerivedListGenerator) || typeof(T) == typeof(ReactiveCollectionGenerator))
+ {
+ syntaxTrees.Add(CSharpSyntaxTree.ParseText(
+ GetAttributeDefinitionsPropertyResult("ReactiveAttribute"),
+ parseOptions,
+ path: "ReactiveAttribute.g.cs"));
+ }
- var syntaxTree = CSharpSyntaxTree.ParseText(code, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13));
+ // On non-Windows platforms the Microsoft.WindowsDesktop.App shared framework is unavailable,
+ // so test sources that inherit from System.Windows.Window (WPF) or use Windows Forms types
+ // cannot resolve those types from assembly references. Inject lightweight source stubs so
+ // the in-memory compilation succeeds cross-platform.
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ syntaxTrees.Add(CSharpSyntaxTree.ParseText(
+ TestCompilationReferences.WindowsDesktopStubs,
+ parseOptions,
+ path: "WindowsDesktopStubs.g.cs"));
+ }
// Create a compilation with the provided source code.
var compilation = CSharpCompilation.Create(
"TestProject",
- [syntaxTree],
+ syntaxTrees,
assemblies,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, deterministic: true));
@@ -200,11 +204,20 @@ public SettingsTask RunGeneratorAndCheck(
var prediagnostics = compilation.GetDiagnostics()
.Where(d => d.Severity > DiagnosticSeverity.Warning)
.ToList();
- Assert.That(prediagnostics, Is.Empty);
+
+ if (prediagnostics.Count > 0)
+ {
+ foreach (var diagnostic in prediagnostics)
+ {
+ WriteTestOutput($"Diagnostic: {diagnostic.Id} - {diagnostic.GetMessage()}");
+ }
+
+ throw new InvalidOperationException("Pre-generator compilation failed due to the above diagnostics.");
+ }
}
var generator = new T();
- var driver = CSharpGeneratorDriver.Create(generator).WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options);
+ var driver = CSharpGeneratorDriver.Create(generator).WithUpdatedParseOptions(parseOptions);
if (rerunCompilation)
{
@@ -220,12 +233,29 @@ public SettingsTask RunGeneratorAndCheck(
{
foreach (var diagnostic in offendingDiagnostics)
{
- TestContext.Out.WriteLine($"Diagnostic: {diagnostic.Id} - {diagnostic.GetMessage()}");
+ WriteTestOutput($"Diagnostic: {diagnostic.Id} - {diagnostic.GetMessage()}");
}
throw new InvalidOperationException("Compilation failed due to the above diagnostics.");
}
+ var outputDiagnosticsToReport = outputCompilation.GetDiagnostics()
+ .Where(d => d.Severity >= DiagnosticSeverity.Error)
+ .Where(d => !IsKnownExpectedOutputDiagnostic(d))
+ .ToList();
+
+ if (outputDiagnosticsToReport.Count > 0)
+ {
+ var diagnosticMessage = string.Join(Environment.NewLine, outputDiagnosticsToReport.Select(static d => $"{d.Id} - {d.GetMessage()}"));
+
+ foreach (var diagnostic in outputDiagnosticsToReport)
+ {
+ WriteTestOutput($"Output diagnostic: {diagnostic.Id} - {diagnostic.GetMessage()}");
+ }
+
+ throw new InvalidOperationException($"Output compilation failed due to the above diagnostics.{Environment.NewLine}{diagnosticMessage}");
+ }
+
// Validate generated code contains expected features
ValidateGeneratedCode(code, rerunDriver);
@@ -236,6 +266,126 @@ public SettingsTask RunGeneratorAndCheck(
return VerifyGenerator(driver.RunGenerators(compilation));
}
+ ///
+ /// Returns all attribute/enum source strings that are NOT already injected by generator T
+ /// via RegisterPostInitializationOutput. Including sources that the active generator also
+ /// emits would create CS0433 (duplicate type) in the output compilation.
+ ///
+ private static IEnumerable GetGeneratedSupportSources()
+ {
+ // Always include the shared enum block (AccessModifier, PropertyAccessModifier,
+ // InheritanceModifier, SplatRegistrationType). These are internal types so they
+ // live inside the support-assembly DLL and never cause CS0433 conflicts, even when
+ // ReactiveGenerator also injects them into the test compilation as source.
+ // Omitting this block breaks ReactiveCommandAttribute (needs PropertyAccessModifier)
+ // and IViewForAttribute (needs SplatRegistrationType).
+ yield return GetAttributeDefinitionsMethodResult("GetAccessModifierEnum");
+
+ // Yield each attribute definition only if generator T does NOT inject it.
+ // Note: for IViewForGenerator, ReactiveAttribute and ReactiveCommandAttribute are
+ // added as inline SyntaxTrees below (not in the support DLL) so they are accessible
+ // in the test source compilation without CS0122 internal-visibility errors.
+ if (typeof(T) != typeof(ReactiveCommandGenerator) && typeof(T) != typeof(IViewForGenerator))
+ {
+ yield return GetAttributeDefinitionsPropertyResult("ReactiveCommandAttribute");
+ }
+
+ if (typeof(T) != typeof(ReactiveGenerator) && typeof(T) != typeof(IViewForGenerator) && typeof(T) != typeof(ReactiveObjectGenerator)
+ && typeof(T) != typeof(BindableDerivedListGenerator) && typeof(T) != typeof(ReactiveCollectionGenerator))
+ {
+ yield return GetAttributeDefinitionsPropertyResult("ReactiveAttribute");
+ }
+
+ if (typeof(T) != typeof(IViewForGenerator))
+ {
+ yield return GetAttributeDefinitionsPropertyResult("IViewForAttribute");
+ }
+
+ if (typeof(T) != typeof(ObservableAsPropertyGenerator) && typeof(T) != typeof(ReactiveObjectGenerator))
+ {
+ yield return GetAttributeDefinitionsPropertyResult("ObservableAsPropertyAttribute");
+ }
+
+ if (typeof(T) != typeof(BindableDerivedListGenerator))
+ {
+ yield return GetAttributeDefinitionsPropertyResult("BindableDerivedListAttribute");
+ }
+
+ if (typeof(T) != typeof(ReactiveCollectionGenerator))
+ {
+ yield return GetAttributeDefinitionsPropertyResult("ReactiveCollectionAttribute");
+ }
+
+ if (typeof(T) != typeof(ReactiveObjectGenerator))
+ {
+ yield return GetAttributeDefinitionsPropertyResult("ReactiveObjectAttribute");
+ }
+
+ if (typeof(T) != typeof(RoutedControlHostGenerator))
+ {
+ yield return GetAttributeDefinitionsMethodResult("GetRoutedControlHostAttribute");
+ }
+
+ if (typeof(T) != typeof(ViewModelControlHostGenerator))
+ {
+ yield return GetAttributeDefinitionsPropertyResult("ViewModelControlHostAttribute");
+ }
+ }
+
+ private static ImmutableArray CreateSupportReferences()
+ {
+ var supportSources = GetGeneratedSupportSources().ToArray();
+ if (supportSources.Length == 0)
+ {
+ return [];
+ }
+
+ var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13);
+ var supportCompilation = CSharpCompilation.Create(
+ $"{typeof(T).Name}.Support",
+ supportSources.Select((source, index) => CSharpSyntaxTree.ParseText(source, parseOptions, path: $"Support{index}.g.cs")),
+ TestCompilationReferences.CreateDefault(),
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, deterministic: true));
+
+ using var stream = new MemoryStream();
+ var emitResult = supportCompilation.Emit(stream);
+
+ if (!emitResult.Success)
+ {
+ var diagnostics = string.Join(Environment.NewLine, emitResult.Diagnostics.Select(static d => d.ToString()));
+ throw new InvalidOperationException($"Support assembly compilation failed for {typeof(T).Name}.{Environment.NewLine}{diagnostics}");
+ }
+
+ return [MetadataReference.CreateFromImage(stream.ToArray())];
+ }
+
+ private static string GetAttributeDefinitionsMethodResult(string methodName)
+ {
+ var attributeDefinitionsType = typeof(ReactiveGenerator).Assembly.GetType("ReactiveUI.SourceGenerators.Helpers.AttributeDefinitions", throwOnError: false, ignoreCase: false)
+ ?? throw new InvalidOperationException("Could not locate AttributeDefinitions type.");
+
+ var method = attributeDefinitionsType.GetMethod(methodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException($"Could not locate AttributeDefinitions.{methodName}.");
+
+ return (string?)method.Invoke(null, null)
+ ?? throw new InvalidOperationException($"AttributeDefinitions.{methodName} returned null.");
+ }
+
+ private static string GetAttributeDefinitionsPropertyResult(string propertyName)
+ {
+ var attributeDefinitionsType = typeof(ReactiveGenerator).Assembly.GetType("ReactiveUI.SourceGenerators.Helpers.AttributeDefinitions", throwOnError: false, ignoreCase: false)
+ ?? throw new InvalidOperationException("Could not locate AttributeDefinitions type.");
+
+ var property = attributeDefinitionsType.GetProperty(propertyName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException($"Could not locate AttributeDefinitions.{propertyName}.");
+
+ return (string?)property.GetValue(null)
+ ?? throw new InvalidOperationException($"AttributeDefinitions.{propertyName} returned null.");
+ }
+
+ private static bool IsKnownExpectedOutputDiagnostic(Diagnostic d) =>
+ d.Id is "CS0579" or "CS8864" or "CS0115" or "CS8867" or "CS8866";
+
[GeneratedRegex(@"\[Reactive\((?:.*?nameof\((\w+)\))+", RegexOptions.Singleline)]
private static partial Regex ReactiveRegex();
@@ -253,29 +403,43 @@ private static void ValidateGeneratedCode(string sourceCode, GeneratorDriver dri
var generatedTrees = runResult.Results.SelectMany(r => r.GeneratedSources).ToList();
var allGeneratedCode = string.Join("\n", generatedTrees.Select(t => t.SourceText.ToString()));
+ if (typeof(T) == typeof(ReactiveCommandGenerator))
+ {
+ var hasReactiveCommandOutput = generatedTrees.Any(static s => s.HintName.EndsWith(".ReactiveCommands.g.cs", StringComparison.Ordinal));
+
+ if (!hasReactiveCommandOutput)
+ {
+ WriteTestOutput("=== VALIDATION FAILURE ===");
+ WriteTestOutput("ReactiveCommand generator produced no command source output.");
+ WriteTestOutput("=== GENERATED HINTS ===");
+
+ foreach (var generatedTree in generatedTrees)
+ {
+ WriteTestOutput(generatedTree.HintName);
+ }
+
+ WriteTestOutput("=== END ===");
+
+ throw new InvalidOperationException("ReactiveCommand generator produced no command source output.");
+ }
+ }
+
// Check for AlsoNotify feature in Reactive attributes
// Pattern matches: [Reactive(nameof(PropertyName))] or [Reactive(nameof(Prop1), nameof(Prop2))]
var alsoNotifyPattern = ReactiveRegex();
var nameofPattern = NameOfRegex();
var matches = alsoNotifyPattern.Matches(sourceCode);
- TestContext.Out.WriteLine("=== VALIDATION DEBUG ===");
- TestContext.Out.WriteLine("Found {0} Reactive attributes with nameof", matches.Count);
-
if (matches.Count > 0)
{
foreach (Match match in matches)
{
- TestContext.Out.WriteLine("Checking attribute: {0}", match.Value);
-
// Extract all nameof() references within this attribute
var nameofMatches = nameofPattern.Matches(match.Value);
- TestContext.Out.WriteLine("Found {0} nameof references in this attribute", nameofMatches.Count);
foreach (Match nameofMatch in nameofMatches)
{
var propertyToNotify = nameofMatch.Groups[1].Value;
- TestContext.Out.WriteLine("Checking for notification of property: {0}", propertyToNotify);
// Verify that the generated code contains calls to raise property changed for the additional property
// Check for various forms of property change notification
@@ -285,72 +449,31 @@ private static void ValidateGeneratedCode(string sourceCode, GeneratorDriver dri
allGeneratedCode.Contains($"RaisePropertyChanged(nameof({propertyToNotify}))") ||
allGeneratedCode.Contains($"RaisePropertyChanged(\"{propertyToNotify}\")");
- TestContext.Out.WriteLine("Has notification: {0}", hasNotification);
-
if (!hasNotification)
{
var errorMessage = $"Generated code does not include AlsoNotify for property '{propertyToNotify}'. " +
$"Expected to find property change notification for '{propertyToNotify}' in the generated code.\n" +
$"Source attribute: {match.Value}";
- TestContext.Out.WriteLine("=== VALIDATION FAILURE ===");
- TestContext.Out.WriteLine(errorMessage);
- TestContext.Out.WriteLine("=== SOURCE CODE SNIPPET ===");
- TestContext.Out.WriteLine(match.Value);
- TestContext.Out.WriteLine("=== GENERATED CODE ===");
- TestContext.Out.WriteLine(allGeneratedCode);
- TestContext.Out.WriteLine("=== END ===");
+ WriteTestOutput("=== VALIDATION FAILURE ===");
+ WriteTestOutput(errorMessage);
+ WriteTestOutput("=== SOURCE CODE SNIPPET ===");
+ WriteTestOutput(match.Value);
+ WriteTestOutput("=== GENERATED CODE ===");
+ WriteTestOutput(allGeneratedCode);
+ WriteTestOutput("=== END ===");
throw new InvalidOperationException(errorMessage);
}
}
}
}
-
- TestContext.Out.WriteLine("=== END VALIDATION DEBUG ===");
}
- ///
- /// Recursively walks assembly references from the seed assemblies to collect
- /// all transitive dependencies as metadata references.
- ///
- /// The root assemblies to start from.
- /// Metadata references for all reachable assemblies.
- private static IEnumerable GetTransitiveReferences(params Assembly[] seedAssemblies)
- {
- var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
- var queue = new Queue(seedAssemblies);
-
- while (queue.Count > 0)
- {
- var assembly = queue.Dequeue();
- if (assembly.IsDynamic || string.IsNullOrEmpty(assembly.Location))
- {
- continue;
- }
-
- if (!seen.Add(assembly.Location))
- {
- continue;
- }
-
- yield return MetadataReference.CreateFromFile(assembly.Location);
+ private static void WriteTestOutput(string message) => TestContext.Current?.OutputWriter.WriteLine(message);
- foreach (var referencedName in assembly.GetReferencedAssemblies())
- {
- try
- {
- queue.Enqueue(System.Reflection.Assembly.Load(referencedName));
- }
- catch
- {
- // System assemblies already covered by Basic.Reference.Assemblies
- }
- }
- }
+ private SettingsTask VerifyGenerator(GeneratorDriver driver)
+ => Verify(driver)
+ .UseDirectory(VerifiedFilePath())
+ .ScrubLinesContaining("[global::System.CodeDom.Compiler.GeneratedCode(\"");
}
-
- private SettingsTask VerifyGenerator(GeneratorDriver driver) => Verify(driver)
- .UseDirectory(VerifiedFilePath())
- .ScrubLinesContaining("[global::System.CodeDom.Compiler.GeneratedCode(\"");
-}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttrMisuseExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttrMisuseExtTests.cs
index c8c853e..a4880e1 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttrMisuseExtTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttrMisuseExtTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Extended unit tests for .
///
-[TestFixture]
public sealed class AttrMisuseExtTests
{
///
@@ -32,7 +31,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -56,7 +55,7 @@ public class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -80,7 +79,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -107,7 +106,7 @@ public class InnerVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -134,7 +133,7 @@ public partial class InnerVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -158,7 +157,7 @@ public record TestVMRecord : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -182,7 +181,7 @@ public partial record TestVMRecord : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -212,7 +211,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Count(d => d.Id == "RXUISG0020"), Is.EqualTo(3));
+ AssertDiagnosticCount(diagnostics, "RXUISG0020", 3);
}
///
@@ -236,7 +235,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -266,7 +265,7 @@ public class Level3 : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -296,7 +295,7 @@ public partial class Level3 : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -319,7 +318,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -341,7 +340,7 @@ public class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -365,7 +364,7 @@ public partial class GenericVM : ReactiveObject where T : class
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -389,7 +388,7 @@ public partial class GenericVM : ReactiveObject where T : class
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -413,7 +412,7 @@ public record struct TestVMStruct
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -439,7 +438,7 @@ public partial class TestVM : ReactiveObject
// [Reactive] on non-partial property should produce RXUISG0020
// because the attribute requires the property to be partial
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
}
private static Diagnostic[] GetDiagnostics(string source)
@@ -460,4 +459,29 @@ private static Diagnostic[] GetDiagnostics(string source)
var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer));
return compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().GetAwaiter().GetResult().ToArray();
}
+
+ private static void AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId)
+ {
+ if (!diagnostics.Any(d => d.Id == diagnosticId))
+ {
+ throw new InvalidOperationException($"Expected diagnostic '{diagnosticId}' was not reported.");
+ }
+ }
+
+ private static void AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId)
+ {
+ if (diagnostics.Any(d => d.Id == diagnosticId))
+ {
+ throw new InvalidOperationException($"Diagnostic '{diagnosticId}' was reported unexpectedly.");
+ }
+ }
+
+ private static void AssertDiagnosticCount(IEnumerable diagnostics, string diagnosticId, int expectedCount)
+ {
+ var actualCount = diagnostics.Count(d => d.Id == diagnosticId);
+ if (actualCount != expectedCount)
+ {
+ throw new InvalidOperationException($"Expected {expectedCount} '{diagnosticId}' diagnostics but found {actualCount}.");
+ }
+ }
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttributeDataExtensionTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttributeDataExtensionTests.cs
new file mode 100644
index 0000000..5879d24
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/AttributeDataExtensionTests.cs
@@ -0,0 +1,401 @@
+// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved.
+// Licensed to the ReactiveUI and contributors under one or more agreements.
+// The ReactiveUI and contributors licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using ReactiveUI.SourceGenerators.Extensions;
+
+namespace ReactiveUI.SourceGenerator.Tests;
+
+///
+/// Unit tests for covering
+/// TryGetNamedArgument, GetNamedArgument, GetConstructorArguments,
+/// and GetGenericType.
+///
+public sealed class AttributeDataExtensionTests
+{
+ ///
+ /// TryGetNamedArgument returns true and the correct value when the argument exists.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenNamedArgumentPresentThenTryGetReturnsTrue()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MyAttr : Attribute
+ {
+ public int Count { get; set; }
+ }
+ [MyAttr(Count = 42)]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "MyAttr");
+
+ var found = attribute.TryGetNamedArgument("Count", out int? value);
+
+ await Assert.That(found).IsTrue();
+ await Assert.That(value).IsEqualTo(42);
+ }
+
+ ///
+ /// TryGetNamedArgument returns false when the argument is not present.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenNamedArgumentAbsentThenTryGetReturnsFalse()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MyAttr : Attribute
+ {
+ public int Count { get; set; }
+ }
+ [MyAttr]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "MyAttr");
+
+ var found = attribute.TryGetNamedArgument("Count", out int? value);
+
+ await Assert.That(found).IsFalse();
+ await Assert.That(value).IsNull();
+ }
+
+ ///
+ /// TryGetNamedArgument returns false and default when the argument name does not match.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenWrongArgumentNameThenTryGetReturnsFalse()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MyAttr : Attribute
+ {
+ public int Count { get; set; }
+ }
+ [MyAttr(Count = 5)]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "MyAttr");
+
+ var found = attribute.TryGetNamedArgument("Other", out int? value);
+
+ await Assert.That(found).IsFalse();
+ await Assert.That(value).IsNull();
+ }
+
+ ///
+ /// TryGetNamedArgument returns false when called on a null AttributeData.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenAttributeDataIsNullThenTryGetReturnsFalse()
+ {
+ AttributeData? nullAttr = null;
+ var found = nullAttr!.TryGetNamedArgument("X", out int? value);
+
+ await Assert.That(found).IsFalse();
+ await Assert.That(value).IsNull();
+ }
+
+ ///
+ /// TryGetNamedArgument retrieves a string named argument.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenStringNamedArgumentPresentThenTryGetReturnsValue()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MyAttr : Attribute
+ {
+ public string? Name { get; set; }
+ }
+ [MyAttr(Name = "hello")]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "MyAttr");
+
+ var found = attribute.TryGetNamedArgument("Name", out string? value);
+
+ await Assert.That(found).IsTrue();
+ await Assert.That(value).IsEqualTo("hello");
+ }
+
+ ///
+ /// TryGetNamedArgument retrieves a bool named argument.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenBoolNamedArgumentPresentThenTryGetReturnsValue()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MyAttr : Attribute
+ {
+ public bool IsEnabled { get; set; }
+ }
+ [MyAttr(IsEnabled = true)]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "MyAttr");
+
+ var found = attribute.TryGetNamedArgument("IsEnabled", out bool? value);
+
+ await Assert.That(found).IsTrue();
+ await Assert.That(value).IsTrue();
+ }
+
+ ///
+ /// GetNamedArgument returns the value when the argument is present.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenNamedArgumentPresentThenGetNamedArgumentReturnsValue()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MyAttr : Attribute
+ {
+ public int Count { get; set; }
+ }
+ [MyAttr(Count = 7)]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "MyAttr");
+
+ var value = attribute.GetNamedArgument("Count");
+
+ await Assert.That(value).IsEqualTo(7);
+ }
+
+ ///
+ /// GetNamedArgument returns default when the argument is absent.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenNamedArgumentAbsentThenGetNamedArgumentReturnsDefault()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MyAttr : Attribute
+ {
+ public int Count { get; set; }
+ }
+ [MyAttr]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "MyAttr");
+
+ var value = attribute.GetNamedArgument("Count");
+
+ await Assert.That(value).IsEqualTo(0);
+ }
+
+ ///
+ /// GetNamedArgument returns default when called on a null AttributeData.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenAttributeDataIsNullThenGetNamedArgumentReturnsDefault()
+ {
+ AttributeData? nullAttr = null;
+ var value = nullAttr!.GetNamedArgument("X");
+
+ await Assert.That(value).IsEqualTo(0);
+ }
+
+ ///
+ /// GetConstructorArguments yields all string constructor arguments.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenStringConstructorArgsPresentThenGetConstructorArgumentsYieldsAll()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MyAttr : Attribute
+ {
+ public MyAttr(string a, string b) { }
+ }
+ [MyAttr("hello", "world")]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "MyAttr");
+
+ var args = attribute.GetConstructorArguments().ToList();
+
+ await Assert.That(args.Count).IsEqualTo(2);
+ await Assert.That(args[0]).IsEqualTo("hello");
+ await Assert.That(args[1]).IsEqualTo("world");
+ }
+
+ ///
+ /// GetConstructorArguments yields nothing when there are no constructor arguments of the requested type.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenNoMatchingConstructorArgsThenGetConstructorArgumentsIsEmpty()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MyAttr : Attribute { }
+ [MyAttr]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "MyAttr");
+
+ var args = attribute.GetConstructorArguments().ToList();
+
+ await Assert.That(args.Count).IsEqualTo(0);
+ }
+
+ ///
+ /// GetGenericType returns the type argument name for a generic attribute.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenGenericAttributeThenGetGenericTypeReturnsTypeName()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MyAttr : Attribute { }
+ [MyAttr]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "MyAttr");
+
+ var type = attribute.GetGenericType();
+
+ await Assert.That(type).IsEqualTo("int");
+ }
+
+ ///
+ /// GetGenericType returns null for a non-generic attribute.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenNonGenericAttributeThenGetGenericTypeReturnsNull()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class MyAttr : Attribute { }
+ [MyAttr]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "MyAttr");
+
+ var type = attribute.GetGenericType();
+
+ await Assert.That(type).IsNull();
+ }
+
+ ///
+ /// GetGenericType returns the type keyword for a generic argument using a built-in type.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenGenericAttributeWithClassTypeThenGetGenericTypeReturnsClassName()
+ {
+ const string source = """
+ using System;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class WrapAttr : Attribute { }
+ [WrapAttr]
+ public class C { }
+ """;
+ var attribute = GetAttribute(source, "T.C", "WrapAttr");
+
+ var type = attribute.GetGenericType();
+
+ await Assert.That(type).IsEqualTo("string");
+ }
+
+ ///
+ /// GatherForwardedAttributesFromClass collects non-trigger attributes from the class declaration.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenClassHasAttributesThenForwardedAttributesCollected()
+ {
+ const string source = """
+ using System;
+ using System.ComponentModel;
+ namespace T;
+ [AttributeUsage(AttributeTargets.Class)]
+ public class TriggerAttr : Attribute { }
+ [TriggerAttr]
+ [Description("test")]
+ public class C { }
+ """;
+
+ var compilation = CreateCompilation(source);
+ var classDecl = compilation.SyntaxTrees
+ .SelectMany(t => t.GetRoot().DescendantNodes())
+ .OfType()
+ .First(c => c.Identifier.Text == "C");
+
+ var semanticModel = compilation.GetSemanticModel(classDecl.SyntaxTree);
+ var typeSymbol = (INamedTypeSymbol)compilation.GetTypeByMetadataName("T.C")!;
+ var triggerAttr = typeSymbol.GetAttributes()
+ .First(a => a.AttributeClass?.Name == "TriggerAttr");
+
+ triggerAttr.GatherForwardedAttributesFromClass(
+ semanticModel,
+ classDecl,
+ default,
+ out var forwarded);
+
+ await Assert.That(forwarded.Length).IsGreaterThan(0);
+ await Assert.That(forwarded.Any(a => a.TypeName.Contains("TriggerAttr"))).IsFalse();
+ }
+
+ private static AttributeData GetAttribute(string source, string typeName, string attributeSimpleName)
+ {
+ var compilation = CreateCompilation(source);
+ var typeSymbol = compilation.GetTypeByMetadataName(typeName)
+ ?? throw new InvalidOperationException($"Type '{typeName}' not found in compilation.");
+
+ return typeSymbol.GetAttributes()
+ .First(a => a.AttributeClass?.Name == attributeSimpleName);
+ }
+
+ private static CSharpCompilation CreateCompilation(string source)
+ {
+ var syntaxTree = CSharpSyntaxTree.ParseText(
+ source,
+ CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13));
+
+ return CSharpCompilation.Create(
+ assemblyName: "AttrDataExtTests",
+ syntaxTrees: [syntaxTree],
+ references: TestCompilationReferences.CreateDefault(),
+ options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/BindableDerivedListGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/BindableDerivedListGeneratorTests.cs
index 633794c..50b1651 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/BindableDerivedListGeneratorTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/BindableDerivedListGeneratorTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// BindableDerivedListGeneratorTests.
///
-[TestFixture]
public class BindableDerivedListGeneratorTests : TestBase
{
///
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/DerivedListExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/DerivedListExtTests.cs
index 384e8d0..1f5939d 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/DerivedListExtTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/DerivedListExtTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Extended unit tests for the BindableDerivedList generator covering edge cases.
///
-[TestFixture]
public class DerivedListExtTests : TestBase
{
///
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/EquatableArrayTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/EquatableArrayTests.cs
new file mode 100644
index 0000000..01869a2
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/EquatableArrayTests.cs
@@ -0,0 +1,264 @@
+// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved.
+// Licensed to the ReactiveUI and contributors under one or more agreements.
+// The ReactiveUI and contributors licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using ReactiveUI.SourceGenerators.Helpers;
+
+namespace ReactiveUI.SourceGenerator.Tests;
+
+///
+/// Unit tests for .
+///
+public sealed class EquatableArrayTests
+{
+ ///
+ /// Two arrays with identical elements are equal.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenSameElementsThenEqual()
+ {
+ var a = ImmutableArray.Create(1, 2, 3).AsEquatableArray();
+ var b = ImmutableArray.Create(1, 2, 3).AsEquatableArray();
+
+ await Assert.That(a == b).IsTrue();
+ await Assert.That(a.Equals(b)).IsTrue();
+ await Assert.That(a != b).IsFalse();
+ }
+
+ ///
+ /// Two arrays with different elements are not equal.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenDifferentElementsThenNotEqual()
+ {
+ var a = ImmutableArray.Create(1, 2, 3).AsEquatableArray();
+ var b = ImmutableArray.Create(1, 2, 4).AsEquatableArray();
+
+ await Assert.That(a == b).IsFalse();
+ await Assert.That(a != b).IsTrue();
+ }
+
+ ///
+ /// Arrays with the same elements in different order are not equal.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenDifferentOrderThenNotEqual()
+ {
+ var a = ImmutableArray.Create(1, 2, 3).AsEquatableArray();
+ var b = ImmutableArray.Create(3, 2, 1).AsEquatableArray();
+
+ await Assert.That(a == b).IsFalse();
+ }
+
+ ///
+ /// An empty array equals another empty array.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenBothEmptyThenEqual()
+ {
+ var a = ImmutableArray.Empty.AsEquatableArray();
+ var b = ImmutableArray.Empty.AsEquatableArray();
+
+ await Assert.That(a == b).IsTrue();
+ await Assert.That(a.IsEmpty).IsTrue();
+ }
+
+ ///
+ /// An empty array is not equal to a non-empty array.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenOneEmptyThenNotEqual()
+ {
+ var a = ImmutableArray.Empty.AsEquatableArray();
+ var b = ImmutableArray.Create(1).AsEquatableArray();
+
+ await Assert.That(a == b).IsFalse();
+ }
+
+ ///
+ /// The indexer returns the element at the given position.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenIndexedThenReturnsCorrectElement()
+ {
+ var arr = ImmutableArray.Create(10, 20, 30).AsEquatableArray();
+
+ await Assert.That(arr[0]).IsEqualTo(10);
+ await Assert.That(arr[1]).IsEqualTo(20);
+ await Assert.That(arr[2]).IsEqualTo(30);
+ }
+
+ ///
+ /// Enumeration yields all elements in order.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenEnumeratedThenYieldsAllElements()
+ {
+ var expected = new[] { 1, 2, 3 };
+ var arr = ImmutableArray.Create(expected).AsEquatableArray();
+
+ var actual = arr.ToList();
+
+ await Assert.That(actual.Count).IsEqualTo(3);
+ await Assert.That(actual[0]).IsEqualTo(1);
+ await Assert.That(actual[1]).IsEqualTo(2);
+ await Assert.That(actual[2]).IsEqualTo(3);
+ }
+
+ ///
+ /// Implicit conversion from ImmutableArray preserves elements.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenImplicitlyConvertedFromImmutableArrayThenPreservesElements()
+ {
+ var immutable = ImmutableArray.Create(5, 6, 7);
+ EquatableArray equatable = immutable;
+
+ await Assert.That(equatable[0]).IsEqualTo(5);
+ await Assert.That(equatable[1]).IsEqualTo(6);
+ await Assert.That(equatable[2]).IsEqualTo(7);
+ }
+
+ ///
+ /// Implicit conversion to ImmutableArray preserves elements.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenImplicitlyConvertedToImmutableArrayThenPreservesElements()
+ {
+ var equatable = ImmutableArray.Create(8, 9).AsEquatableArray();
+ ImmutableArray immutable = equatable;
+
+ await Assert.That(immutable.Length).IsEqualTo(2);
+ await Assert.That(immutable[0]).IsEqualTo(8);
+ await Assert.That(immutable[1]).IsEqualTo(9);
+ }
+
+ ///
+ /// ToArray returns a mutable copy with the same elements.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenToArrayCalledThenReturnsMutableCopy()
+ {
+ var arr = ImmutableArray.Create(1, 2, 3).AsEquatableArray();
+ var copy = arr.ToArray();
+
+ await Assert.That(copy.Length).IsEqualTo(3);
+ await Assert.That(copy[0]).IsEqualTo(1);
+ await Assert.That(copy[2]).IsEqualTo(3);
+ }
+
+ ///
+ /// AsSpan returns a span over the elements.
+ ///
+ [Test]
+ public void WhenAsSpanCalledThenSpanCoversElements()
+ {
+ var arr = ImmutableArray.Create(1, 2, 3).AsEquatableArray();
+ var span = arr.AsSpan();
+ var length = span.Length;
+ var mid = span[1];
+
+ if (length != 3)
+ {
+ throw new InvalidOperationException($"Expected span length 3, got {length}.");
+ }
+
+ if (mid != 2)
+ {
+ throw new InvalidOperationException($"Expected span[1] == 2, got {mid}.");
+ }
+ }
+
+ ///
+ /// GetHashCode returns the same value for equal arrays.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenEqualArraysThenSameHashCode()
+ {
+ var a = ImmutableArray.Create(1, 2, 3).AsEquatableArray();
+ var b = ImmutableArray.Create(1, 2, 3).AsEquatableArray();
+
+ await Assert.That(a.GetHashCode()).IsEqualTo(b.GetHashCode());
+ }
+
+ ///
+ /// Equals(object) returns true when passed an equal EquatableArray.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenEqualsObjectCalledWithEqualArrayThenReturnsTrue()
+ {
+ var a = ImmutableArray.Create(1, 2).AsEquatableArray();
+ object b = ImmutableArray.Create(1, 2).AsEquatableArray();
+
+ await Assert.That(a.Equals(b)).IsTrue();
+ }
+
+ ///
+ /// Equals(object) returns false when passed null.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenEqualsObjectCalledWithNullThenReturnsFalse()
+ {
+ var a = ImmutableArray.Create(1).AsEquatableArray();
+
+ await Assert.That(a.Equals(null)).IsFalse();
+ }
+
+ ///
+ /// AsImmutableArray round-trips back to ImmutableArray.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenAsImmutableArrayCalledThenRoundTrips()
+ {
+ var source = ImmutableArray.Create("x", "y", "z");
+ var equatable = source.AsEquatableArray();
+ var roundTripped = equatable.AsImmutableArray();
+
+ await Assert.That(roundTripped.SequenceEqual(source)).IsTrue();
+ }
+
+ ///
+ /// FromImmutableArray static factory produces an equal instance to the extension method.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenCreatedViaFactoryThenEqualsExtensionMethod()
+ {
+ var immutable = ImmutableArray.Create(1, 2, 3);
+ var viaExtension = immutable.AsEquatableArray();
+ var viaFactory = EquatableArray.FromImmutableArray(immutable);
+
+ await Assert.That(viaExtension == viaFactory).IsTrue();
+ }
+
+ ///
+ /// IEnumerable<T> explicit interface yields elements correctly.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenEnumeratedAsIEnumerableThenYieldsCorrectly()
+ {
+ IEnumerable arr = ImmutableArray.Create(7, 8, 9).AsEquatableArray();
+ var list = arr.ToList();
+
+ await Assert.That(list.Count).IsEqualTo(3);
+ await Assert.That(list[0]).IsEqualTo(7);
+ await Assert.That(list[1]).IsEqualTo(8);
+ await Assert.That(list[2]).IsEqualTo(9);
+ }
+}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/FieldSyntaxExtensionTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/FieldSyntaxExtensionTests.cs
new file mode 100644
index 0000000..bc31ed9
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/FieldSyntaxExtensionTests.cs
@@ -0,0 +1,216 @@
+// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved.
+// Licensed to the ReactiveUI and contributors under one or more agreements.
+// The ReactiveUI and contributors licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using ReactiveUI.SourceGenerators.Extensions;
+
+namespace ReactiveUI.SourceGenerator.Tests;
+
+///
+/// Unit tests for covering
+/// GetGeneratedPropertyName and GetGeneratedFieldName.
+/// These are verified by running the generator over source strings and inspecting
+/// the field/property symbols extracted from the resulting compilation.
+///
+public sealed class FieldSyntaxExtensionTests
+{
+ ///
+ /// A field named with leading underscore prefix has the underscore stripped
+ /// and the first letter upper-cased.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenFieldHasLeadingUnderscoreThenPropertyNameCapitalises()
+ {
+ const string source = """
+ namespace T;
+ public class C
+ {
+ private int _myField;
+ }
+ """;
+ var fieldSymbol = GetFieldSymbol(source, "_myField");
+
+ var name = fieldSymbol.GetGeneratedPropertyName();
+
+ await Assert.That(name).IsEqualTo("MyField");
+ }
+
+ ///
+ /// A field named with "m_" prefix has the prefix stripped and the first remaining
+ /// letter upper-cased.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenFieldHasMUnderscorePrefixThenPropertyNameCapitalises()
+ {
+ const string source = """
+ namespace T;
+ public class C
+ {
+ private int m_value;
+ }
+ """;
+ var fieldSymbol = GetFieldSymbol(source, "m_value");
+
+ var name = fieldSymbol.GetGeneratedPropertyName();
+
+ await Assert.That(name).IsEqualTo("Value");
+ }
+
+ ///
+ /// A field named with lowerCamel (no prefix) has its first letter upper-cased.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenFieldHasLowerCamelThenPropertyNameCapitalises()
+ {
+ const string source = """
+ namespace T;
+ public class C
+ {
+ private int count;
+ }
+ """;
+ var fieldSymbol = GetFieldSymbol(source, "count");
+
+ var name = fieldSymbol.GetGeneratedPropertyName();
+
+ await Assert.That(name).IsEqualTo("Count");
+ }
+
+ ///
+ /// Multiple leading underscores are all stripped before capitalisation.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenFieldHasMultipleLeadingUnderscoresThenAllStripped()
+ {
+ const string source = """
+ namespace T;
+ public class C
+ {
+ private int __item;
+ }
+ """;
+ var fieldSymbol = GetFieldSymbol(source, "__item");
+
+ var name = fieldSymbol.GetGeneratedPropertyName();
+
+ await Assert.That(name).IsEqualTo("Item");
+ }
+
+ ///
+ /// A single-character field name (after stripping) still capitalises correctly.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenFieldIsOneCharacterThenCapitalisedCorrectly()
+ {
+ const string source = """
+ namespace T;
+ public class C
+ {
+ private int _x;
+ }
+ """;
+ var fieldSymbol = GetFieldSymbol(source, "_x");
+
+ var name = fieldSymbol.GetGeneratedPropertyName();
+
+ await Assert.That(name).IsEqualTo("X");
+ }
+
+ ///
+ /// A property named "MyProperty" produces a backing field named "_myProperty".
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenPropertyNamedMyPropertyThenFieldIsUnderscoreLower()
+ {
+ const string source = """
+ namespace T;
+ public class C
+ {
+ public int MyProperty { get; set; }
+ }
+ """;
+ var propertySymbol = GetPropertySymbol(source, "MyProperty");
+
+ var name = propertySymbol.GetGeneratedFieldName();
+
+ await Assert.That(name).IsEqualTo("_myProperty");
+ }
+
+ ///
+ /// A single-character property name produces a correct field name.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenPropertyIsSingleCharacterThenFieldNameIsCorrect()
+ {
+ const string source = """
+ namespace T;
+ public class C
+ {
+ public int X { get; set; }
+ }
+ """;
+ var propertySymbol = GetPropertySymbol(source, "X");
+
+ var name = propertySymbol.GetGeneratedFieldName();
+
+ await Assert.That(name).IsEqualTo("_x");
+ }
+
+ ///
+ /// A property already starting with a lowercase letter still prefixes underscore.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenPropertyStartsWithLowercaseThenFieldNameHasUnderscore()
+ {
+ const string source = """
+ namespace T;
+ public class C
+ {
+ public int value { get; set; }
+ }
+ """;
+ var propertySymbol = GetPropertySymbol(source, "value");
+
+ var name = propertySymbol.GetGeneratedFieldName();
+
+ await Assert.That(name).IsEqualTo("_value");
+ }
+
+ private static IFieldSymbol GetFieldSymbol(string source, string fieldName)
+ {
+ var compilation = CreateCompilation(source);
+ return compilation.GetSymbolsWithName(fieldName, SymbolFilter.Member)
+ .OfType()
+ .Single();
+ }
+
+ private static IPropertySymbol GetPropertySymbol(string source, string propertyName)
+ {
+ var compilation = CreateCompilation(source);
+ return compilation.GetSymbolsWithName(propertyName, SymbolFilter.Member)
+ .OfType()
+ .Single();
+ }
+
+ private static CSharpCompilation CreateCompilation(string source)
+ {
+ var syntaxTree = CSharpSyntaxTree.ParseText(
+ source,
+ CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13));
+
+ return CSharpCompilation.Create(
+ assemblyName: "FieldSyntaxTests",
+ syntaxTrees: [syntaxTree],
+ references: TestCompilationReferences.CreateDefault(),
+ options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/IViewForGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/IViewForGeneratorTests.cs
index fa15979..63080bb 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/IViewForGeneratorTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/IViewForGeneratorTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// IViewForGeneratorTests.
///
-[TestFixture]
public class IViewForGeneratorTests : TestBase
{
///
@@ -21,6 +20,7 @@ public Task Basic()
// Arrange: Setup the source code that matches the generator input expectations.
const string sourceCode = """
using System.Collections.ObjectModel;
+ using System.Windows;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ImmutableArrayBuilderTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ImmutableArrayBuilderTests.cs
new file mode 100644
index 0000000..6a5a0b1
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ImmutableArrayBuilderTests.cs
@@ -0,0 +1,269 @@
+// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved.
+// Licensed to the ReactiveUI and contributors under one or more agreements.
+// The ReactiveUI and contributors licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using ReactiveUI.SourceGenerators.Helpers;
+
+namespace ReactiveUI.SourceGenerator.Tests;
+
+///
+/// Unit tests for .
+///
+public sealed class ImmutableArrayBuilderTests
+{
+ ///
+ /// A freshly rented builder starts with Count == 0.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenRentedThenCountIsZero()
+ {
+ int count;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ count = builder.Count;
+ }
+
+ await Assert.That(count).IsEqualTo(0);
+ }
+
+ ///
+ /// Adding a single item increments Count to 1.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenItemAddedThenCountIncrements()
+ {
+ int count;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ builder.Add(42);
+ count = builder.Count;
+ }
+
+ await Assert.That(count).IsEqualTo(1);
+ }
+
+ ///
+ /// Multiple adds are reflected in Count.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenMultipleItemsAddedThenCountMatchesAdded()
+ {
+ int count;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ builder.Add(1);
+ builder.Add(2);
+ builder.Add(3);
+ count = builder.Count;
+ }
+
+ await Assert.That(count).IsEqualTo(3);
+ }
+
+ ///
+ /// ToImmutable returns an array containing all added items in order.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenToImmutableCalledThenContainsAddedItems()
+ {
+ ImmutableArray result;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ builder.Add(10);
+ builder.Add(20);
+ builder.Add(30);
+ result = builder.ToImmutable();
+ }
+
+ await Assert.That(result.Length).IsEqualTo(3);
+ await Assert.That(result[0]).IsEqualTo(10);
+ await Assert.That(result[1]).IsEqualTo(20);
+ await Assert.That(result[2]).IsEqualTo(30);
+ }
+
+ ///
+ /// ToArray returns a mutable array with the same elements.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenToArrayCalledThenReturnsMutableArray()
+ {
+ string[] result;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ builder.Add("a");
+ builder.Add("b");
+ result = builder.ToArray();
+ }
+
+ await Assert.That(result.Length).IsEqualTo(2);
+ await Assert.That(result[0]).IsEqualTo("a");
+ await Assert.That(result[1]).IsEqualTo("b");
+ }
+
+ ///
+ /// AddRange appends all items from the span.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenAddRangeCalledThenAllItemsAppended()
+ {
+ int count;
+ ImmutableArray result;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ ReadOnlySpan items = [1, 2, 3, 4, 5];
+ builder.AddRange(items);
+ count = builder.Count;
+ result = builder.ToImmutable();
+ }
+
+ await Assert.That(count).IsEqualTo(5);
+ await Assert.That(result[4]).IsEqualTo(5);
+ }
+
+ ///
+ /// WrittenSpan reflects the items added so far.
+ ///
+ [Test]
+ public void WhenWrittenSpanAccessedThenReflectsCurrentItems()
+ {
+ int length;
+ int first;
+ int second;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ builder.Add(7);
+ builder.Add(8);
+ var span = builder.WrittenSpan;
+ length = span.Length;
+ first = span[0];
+ second = span[1];
+ }
+
+ if (length != 2)
+ {
+ throw new InvalidOperationException($"Expected WrittenSpan.Length 2, got {length}.");
+ }
+
+ if (first != 7 || second != 8)
+ {
+ throw new InvalidOperationException($"Expected 7, 8 but got {first}, {second}.");
+ }
+ }
+
+ ///
+ /// AsEnumerable returns an IEnumerable containing all added items.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenAsEnumerableCalledThenYieldsAllItems()
+ {
+ List list;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ builder.Add(100);
+ builder.Add(200);
+ list = builder.AsEnumerable().ToList();
+ }
+
+ await Assert.That(list.Count).IsEqualTo(2);
+ await Assert.That(list[0]).IsEqualTo(100);
+ await Assert.That(list[1]).IsEqualTo(200);
+ }
+
+ ///
+ /// Builder can hold more than the initial capacity (pool growth).
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenManyItemsAddedThenBuilderGrowsCorrectly()
+ {
+ int count;
+ ImmutableArray result;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ for (var i = 0; i < 100; i++)
+ {
+ builder.Add(i);
+ }
+
+ count = builder.Count;
+ result = builder.ToImmutable();
+ }
+
+ await Assert.That(count).IsEqualTo(100);
+ await Assert.That(result[99]).IsEqualTo(99);
+ }
+
+ ///
+ /// ToImmutable on an empty builder returns an empty ImmutableArray.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenEmptyThenToImmutableReturnsEmpty()
+ {
+ ImmutableArray result;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ result = builder.ToImmutable();
+ }
+
+ await Assert.That(result.IsEmpty).IsTrue();
+ }
+
+ ///
+ /// ToString returns the WrittenSpan string representation without throwing.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenToStringCalledThenDoesNotThrow()
+ {
+ string result;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ builder.Add('H');
+ builder.Add('i');
+ result = builder.ToString();
+ }
+
+ await Assert.That(result).IsNotNull();
+ }
+
+ ///
+ /// Dispose can be called multiple times without throwing.
+ ///
+ [Test]
+ public void WhenDisposedTwiceThenDoesNotThrow()
+ {
+ var builder = ImmutableArrayBuilder.Rent();
+ builder.Add(1);
+ builder.Dispose();
+ builder.Dispose();
+ }
+
+ ///
+ /// AddRange followed by Add correctly appends items in order.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenAddRangeThenAddThenOrderPreserved()
+ {
+ ImmutableArray result;
+ using (var builder = ImmutableArrayBuilder.Rent())
+ {
+ ReadOnlySpan range = [1, 2, 3];
+ builder.AddRange(range);
+ builder.Add(4);
+ result = builder.ToImmutable();
+ }
+
+ await Assert.That(result.Length).IsEqualTo(4);
+ await Assert.That(result[3]).IsEqualTo(4);
+ }
+}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs
index 53f5ea6..8797462 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Unit tests for the ObservableAsProperty generator.
///
-[TestFixture]
public class OAPFromObservableGeneratorTests : TestBase
{
///
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPGeneratorTests.cs
index a7d993e..d5addf8 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPGeneratorTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPGeneratorTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Unit tests for the ObservableAsProperty generator.
///
-[TestFixture]
public class OAPGeneratorTests : TestBase
{
///
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OapExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OapExtTests.cs
index 34f7ff3..b143937 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OapExtTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OapExtTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Extended unit tests for the ObservableAsProperty generator covering edge cases.
///
-[TestFixture]
public class OapExtTests : TestBase
{
///
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropAnalyzerExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropAnalyzerExtTests.cs
index 614ee57..95ac367 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropAnalyzerExtTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropAnalyzerExtTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Extended unit tests for .
///
-[TestFixture]
public sealed class PropAnalyzerExtTests
{
///
@@ -30,7 +29,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -52,7 +51,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -74,7 +73,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -96,7 +95,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -119,7 +118,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -146,7 +145,7 @@ public string Name
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -169,7 +168,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -191,7 +190,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -215,7 +214,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -239,7 +238,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Count(d => d.Id == "RXUISG0016"), Is.EqualTo(3));
+ AssertDiagnosticCount(diagnostics, "RXUISG0016", 3);
}
///
@@ -259,7 +258,7 @@ public class TestVM
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -284,7 +283,7 @@ public void RaisePropertyChanged(PropertyChangedEventArgs args) { }
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -309,7 +308,7 @@ public partial class InnerVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -331,7 +330,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -353,7 +352,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -375,7 +374,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -397,7 +396,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -420,7 +419,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
// Init-only properties have a setter (init), so the analyzer reports them
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -442,7 +441,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0016");
}
private static Diagnostic[] GetDiagnostics(string source)
@@ -460,4 +459,29 @@ private static Diagnostic[] GetDiagnostics(string source)
var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer));
return compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().GetAwaiter().GetResult().ToArray();
}
+
+ private static void AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId)
+ {
+ if (!diagnostics.Any(d => d.Id == diagnosticId))
+ {
+ throw new InvalidOperationException($"Expected diagnostic '{diagnosticId}' was not reported.");
+ }
+ }
+
+ private static void AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId)
+ {
+ if (diagnostics.Any(d => d.Id == diagnosticId))
+ {
+ throw new InvalidOperationException($"Diagnostic '{diagnosticId}' was reported unexpectedly.");
+ }
+ }
+
+ private static void AssertDiagnosticCount(IEnumerable diagnostics, string diagnosticId, int expectedCount)
+ {
+ var actualCount = diagnostics.Count(d => d.Id == diagnosticId);
+ if (actualCount != expectedCount)
+ {
+ throw new InvalidOperationException($"Expected {expectedCount} '{diagnosticId}' diagnostics but found {actualCount}.");
+ }
+ }
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldAnalyzerTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldAnalyzerTests.cs
index 75b4cab..2bc6aca 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldAnalyzerTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldAnalyzerTests.cs
@@ -8,9 +8,26 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Unit tests for .
///
-[TestFixture]
public sealed class PropertyToReactiveFieldAnalyzerTests
{
+ ///
+ /// Validates the analyzer rejects a null analysis context.
+ ///
+ [Test]
+ public void InitializeWithNullContextThrows()
+ {
+ var analyzer = new PropertyToReactiveFieldAnalyzer();
+
+ try
+ {
+ analyzer.Initialize(null!);
+ throw new InvalidOperationException("Expected ArgumentNullException was not thrown.");
+ }
+ catch (ArgumentNullException ex) when (ex.ParamName == "context")
+ {
+ }
+ }
+
///
/// Validates a public auto-property triggers the suggestion to convert it into a reactive field.
///
@@ -30,7 +47,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0016");
}
///
@@ -54,7 +71,50 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0016"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
+ }
+
+ ///
+ /// Validates the syntax-based Reactive attribute fallback handles qualified names.
+ ///
+ [Test]
+ public void WhenQualifiedReactiveAttributePresentThenDoesNotReportDiagnostic()
+ {
+ const string source = """
+ using ReactiveUI;
+
+ namespace TestNs;
+
+ public partial class TestVM : ReactiveObject
+ {
+ [ReactiveUI.SourceGenerators.Reactive]
+ public bool IsVisible { get; set; }
+ }
+ """;
+
+ var diagnostics = GetDiagnostics(source);
+
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0016");
+ }
+
+ ///
+ /// Validates the analyzer recognizes a fully qualified ReactiveObject base type.
+ ///
+ [Test]
+ public void WhenQualifiedReactiveBaseTypeThenReportsDiagnostic()
+ {
+ const string source = """
+ namespace TestNs;
+
+ public partial class TestVM : ReactiveUI.ReactiveObject
+ {
+ public bool IsVisible { get; set; }
+ }
+ """;
+
+ var diagnostics = GetDiagnostics(source);
+
+ AssertContainsDiagnostic(diagnostics, "RXUISG0016");
}
private static Diagnostic[] GetDiagnostics(string source)
@@ -72,4 +132,20 @@ private static Diagnostic[] GetDiagnostics(string source)
var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer));
return compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().GetAwaiter().GetResult().ToArray();
}
+
+ private static void AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId)
+ {
+ if (!diagnostics.Any(d => d.Id == diagnosticId))
+ {
+ throw new InvalidOperationException($"Expected diagnostic '{diagnosticId}' was not reported.");
+ }
+ }
+
+ private static void AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId)
+ {
+ if (diagnostics.Any(d => d.Id == diagnosticId))
+ {
+ throw new InvalidOperationException($"Diagnostic '{diagnosticId}' was reported unexpectedly.");
+ }
+ }
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldCodeFixProviderTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldCodeFixProviderTests.cs
index ffe3a43..90e2612 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldCodeFixProviderTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/PropertyToReactiveFieldCodeFixProviderTests.cs
@@ -10,9 +10,34 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Unit tests for .
///
-[TestFixture]
public sealed class PropertyToReactiveFieldCodeFixProviderTests
{
+ ///
+ /// Validates the code fix provider advertises the expected diagnostic ID.
+ ///
+ [Test]
+ public void FixableDiagnosticIdsIncludesReactiveFieldRule()
+ {
+ var provider = new PropertyToReactiveFieldCodeFixProvider();
+ if (!provider.FixableDiagnosticIds.Contains("RXUISG0016"))
+ {
+ throw new InvalidOperationException("Expected RXUISG0016 to be fixable.");
+ }
+ }
+
+ ///
+ /// Validates the code fix provider exposes a fix-all implementation.
+ ///
+ [Test]
+ public void GetFixAllProviderReturnsBatchFixer()
+ {
+ var provider = new PropertyToReactiveFieldCodeFixProvider();
+ if (provider.GetFixAllProvider() is null)
+ {
+ throw new InvalidOperationException("Expected a fix-all provider.");
+ }
+ }
+
///
/// Validates a public auto-property is converted to a private field annotated with [Reactive].
///
@@ -32,9 +57,9 @@ public partial class TestVM : ReactiveObject
var fixedSource = ApplyFix(source);
- Assert.That(fixedSource, Does.Contain("[ReactiveUI.SourceGenerators.Reactive]"));
- Assert.That(fixedSource, Does.Contain("private bool _isVisible"));
- Assert.That(fixedSource, Does.Not.Contain("public bool IsVisible"));
+ AssertContains(fixedSource, "[ReactiveUI.SourceGenerators.Reactive]");
+ AssertContains(fixedSource, "private bool _isVisible");
+ AssertDoesNotContain(fixedSource, "public bool IsVisible");
}
private static string ApplyFix(string source)
@@ -83,4 +108,20 @@ private static string ApplyFix(string source)
var updatedDoc = document.Project.Solution.Workspace.CurrentSolution.GetDocument(document.Id);
return updatedDoc!.GetTextAsync(CancellationToken.None).GetAwaiter().GetResult().ToString();
}
+
+ private static void AssertContains(string actual, string expected)
+ {
+ if (!actual.Contains(expected, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException($"Expected output to contain '{expected}'.");
+ }
+ }
+
+ private static void AssertDoesNotContain(string actual, string unexpected)
+ {
+ if (actual.Contains(unexpected, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException($"Expected output not to contain '{unexpected}'.");
+ }
+ }
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseAnalyzerTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseAnalyzerTests.cs
index 0594255..fafaa78 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseAnalyzerTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseAnalyzerTests.cs
@@ -8,9 +8,26 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Unit tests for .
///
-[TestFixture]
public sealed class ReactiveAttributeMisuseAnalyzerTests
{
+ ///
+ /// Validates the analyzer rejects a null analysis context.
+ ///
+ [Test]
+ public void InitializeWithNullContextThrows()
+ {
+ var analyzer = new ReactiveAttributeMisuseAnalyzer();
+
+ try
+ {
+ analyzer.Initialize(null!);
+ throw new InvalidOperationException("Expected ArgumentNullException was not thrown.");
+ }
+ catch (ArgumentNullException ex) when (ex.ParamName == "context")
+ {
+ }
+ }
+
///
/// Verifies a non-partial property annotated with [Reactive] produces a warning.
///
@@ -32,7 +49,7 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -56,7 +73,7 @@ public class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.True);
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
}
///
@@ -80,7 +97,55 @@ public partial class TestVM : ReactiveObject
var diagnostics = GetDiagnostics(source);
- Assert.That(diagnostics.Any(d => d.Id == "RXUISG0020"), Is.False);
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020");
+ }
+
+ ///
+ /// Verifies the analyzer recognizes the explicit ReactiveAttribute name.
+ ///
+ [Test]
+ public void WhenReactiveAttributeSuffixUsedThenWarns()
+ {
+ const string source = """
+ using ReactiveUI;
+ using ReactiveUI.SourceGenerators;
+
+ namespace TestNs;
+
+ public partial class TestVM : ReactiveObject
+ {
+ [ReactiveAttribute]
+ public bool IsVisible { get; set; }
+ }
+ """;
+
+ var diagnostics = GetDiagnostics(source);
+
+ AssertContainsDiagnostic(diagnostics, "RXUISG0020");
+ }
+
+ ///
+ /// Verifies unrelated attributes do not trigger the diagnostic.
+ ///
+ [Test]
+ public void WhenOnlyNonReactiveAttributesExistThenDoesNotWarn()
+ {
+ const string source = """
+ using System;
+ using ReactiveUI;
+
+ namespace TestNs;
+
+ public partial class TestVM : ReactiveObject
+ {
+ [Obsolete]
+ public bool IsVisible { get; set; }
+ }
+ """;
+
+ var diagnostics = GetDiagnostics(source);
+
+ AssertDoesNotContainDiagnostic(diagnostics, "RXUISG0020");
}
private static Diagnostic[] GetDiagnostics(string source)
@@ -101,4 +166,20 @@ private static Diagnostic[] GetDiagnostics(string source)
var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer));
return compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().GetAwaiter().GetResult().ToArray();
}
+
+ private static void AssertContainsDiagnostic(IEnumerable diagnostics, string diagnosticId)
+ {
+ if (!diagnostics.Any(d => d.Id == diagnosticId))
+ {
+ throw new InvalidOperationException($"Expected diagnostic '{diagnosticId}' was not reported.");
+ }
+ }
+
+ private static void AssertDoesNotContainDiagnostic(IEnumerable diagnostics, string diagnosticId)
+ {
+ if (diagnostics.Any(d => d.Id == diagnosticId))
+ {
+ throw new InvalidOperationException($"Diagnostic '{diagnosticId}' was reported unexpectedly.");
+ }
+ }
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseCodeFixProviderTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseCodeFixProviderTests.cs
index d11286f..c483749 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseCodeFixProviderTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveAttributeMisuseCodeFixProviderTests.cs
@@ -10,9 +10,34 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Unit tests for .
///
-[TestFixture]
public sealed class ReactiveAttributeMisuseCodeFixProviderTests
{
+ ///
+ /// Validates the code fix provider advertises the expected diagnostic ID.
+ ///
+ [Test]
+ public void FixableDiagnosticIdsIncludesReactivePartialRule()
+ {
+ var provider = new ReactiveAttributeMisuseCodeFixProvider();
+ if (!provider.FixableDiagnosticIds.Contains("RXUISG0020"))
+ {
+ throw new InvalidOperationException("Expected RXUISG0020 to be fixable.");
+ }
+ }
+
+ ///
+ /// Validates the code fix provider exposes a fix-all implementation.
+ ///
+ [Test]
+ public void GetFixAllProviderReturnsBatchFixer()
+ {
+ var provider = new ReactiveAttributeMisuseCodeFixProvider();
+ if (provider.GetFixAllProvider() is null)
+ {
+ throw new InvalidOperationException("Expected a fix-all provider.");
+ }
+ }
+
///
/// Verifies `required` stays before `partial` when applying the code fix.
///
@@ -34,8 +59,49 @@ public partial class TestVM : ReactiveObject
var fixedSource = ApplyFix(source);
- Assert.That(fixedSource, Does.Contain("public required partial string? PartialRequiredPropertyTest"));
- Assert.That(fixedSource, Does.Not.Contain("public partial required string? PartialRequiredPropertyTest"));
+ AssertContains(fixedSource, "public required partial string? PartialRequiredPropertyTest");
+ AssertDoesNotContain(fixedSource, "public partial required string? PartialRequiredPropertyTest");
+ }
+
+ ///
+ /// Verifies no code fix is registered when the diagnostic location is outside a property declaration.
+ ///
+ [Test]
+ public void WhenDiagnosticDoesNotTargetAPropertyThenNoCodeFixIsRegistered()
+ {
+ const string source = """
+ using ReactiveUI;
+
+ namespace TestNs;
+
+ public class TestVM : ReactiveObject
+ {
+ }
+ """;
+
+ using var workspace = new AdhocWorkspace();
+ var project = workspace.CurrentSolution
+ .AddProject("p", "p", LanguageNames.CSharp)
+ .WithParseOptions(CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13))
+ .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
+ .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
+ .AddMetadataReference(MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location));
+
+ var document = project.AddDocument("t.cs", source);
+ var root = document.GetSyntaxRootAsync(CancellationToken.None).GetAwaiter().GetResult()!;
+ var classDeclaration = root.DescendantNodes().OfType().Single();
+ var diagnosticDescriptor = new ReactiveAttributeMisuseAnalyzer().SupportedDiagnostics.Single(d => d.Id == "RXUISG0020");
+ var diagnostic = Diagnostic.Create(diagnosticDescriptor, classDeclaration.Identifier.GetLocation());
+ var actions = new List();
+ var context = new CodeFixContext(document, diagnostic, (a, _) => actions.Add(a), CancellationToken.None);
+
+ var provider = new ReactiveAttributeMisuseCodeFixProvider();
+ provider.RegisterCodeFixesAsync(context).GetAwaiter().GetResult();
+
+ if (actions.Count != 0)
+ {
+ throw new InvalidOperationException("Expected no code fixes to be registered.");
+ }
}
private static string ApplyFix(string source)
@@ -85,4 +151,20 @@ private static string ApplyFix(string source)
var updatedDoc = document.Project.Solution.Workspace.CurrentSolution.GetDocument(document.Id);
return updatedDoc!.GetTextAsync(CancellationToken.None).GetAwaiter().GetResult().ToString();
}
+
+ private static void AssertContains(string actual, string expected)
+ {
+ if (!actual.Contains(expected, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException($"Expected output to contain '{expected}'.");
+ }
+ }
+
+ private static void AssertDoesNotContain(string actual, string unexpected)
+ {
+ if (actual.Contains(unexpected, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException($"Expected output not to contain '{unexpected}'.");
+ }
+ }
}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs
index 732c98b..6791a8b 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Unit tests for the ReactiveCommand generator.
///
-[TestFixture]
public class ReactiveCMDGeneratorTests : TestBase
{
///
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCollectionGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCollectionGeneratorTests.cs
index c05f5a4..90018a5 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCollectionGeneratorTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCollectionGeneratorTests.cs
@@ -9,7 +9,6 @@ namespace ReactiveUI.SourceGenerators.Tests;
///
/// ReactiveCollectionGeneratorTests.
///
-[TestFixture]
public class ReactiveCollectionGeneratorTests : TestBase
{
///
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs
index 9efb05d..17bf575 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveGeneratorTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Unit tests for the Reactive generator.
///
-[TestFixture]
public class ReactiveGeneratorTests : TestBase
{
///
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveObjectGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveObjectGeneratorTests.cs
index 8ee4aa0..f70f901 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveObjectGeneratorTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveObjectGeneratorTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Unit tests for the Reactive generator.
///
-[TestFixture]
public class ReactiveObjectGeneratorTests : TestBase
{
///
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCmdExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCmdExtTests.cs
index 2020ce0..ef9182b 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCmdExtTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCmdExtTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Extended unit tests for the ReactiveCommand generator covering edge cases.
///
-[TestFixture]
public class RxCmdExtTests : TestBase
{
///
@@ -39,6 +38,33 @@ private void DoWork() { }
return TestHelper.TestPass(sourceCode);
}
+ ///
+ /// Tests ReactiveCommand with CanExecute observable method.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public Task FromReactiveCommandWithCanExecuteMethod()
+ {
+ const string sourceCode = """
+ using System;
+ using System.Reactive.Linq;
+ using ReactiveUI;
+ using ReactiveUI.SourceGenerators;
+
+ namespace TestNs;
+
+ public partial class TestVM : ReactiveObject
+ {
+ [ReactiveCommand(CanExecute = nameof(CanRun))]
+ private int Run() => 42;
+
+ private IObservable CanRun() => Observable.Return(true);
+ }
+ """;
+
+ return TestHelper.TestPass(sourceCode);
+ }
+
///
/// Tests ReactiveCommand with CancellationToken parameter.
///
@@ -68,6 +94,41 @@ private async Task LongRunningOperation(CancellationToken ct)
return TestHelper.TestPass(sourceCode);
}
+ ///
+ /// Tests ReactiveCommands distributed across partial declarations.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public Task FromReactiveCommandsAcrossPartialDeclarations()
+ {
+ const string sourceCode = """
+ using System;
+ using System.Threading.Tasks;
+ using ReactiveUI;
+ using ReactiveUI.SourceGenerators;
+
+ namespace TestNs;
+
+ public partial class TestVM : ReactiveObject
+ {
+ [ReactiveCommand]
+ private void Create() { }
+ }
+
+ public partial class TestVM
+ {
+ [ReactiveCommand]
+ private async Task LoadAsync()
+ {
+ await Task.Delay(10);
+ return 5;
+ }
+ }
+ """;
+
+ return TestHelper.TestPass(sourceCode);
+ }
+
///
/// Tests ReactiveCommand with CancellationToken and parameter.
///
@@ -337,7 +398,7 @@ public partial class GenericVM : ReactiveObject where T : class
}
[ReactiveCommand]
- private async Task ProcessItemAsync(T? item)
+ private async Task ProcessItemLater(T? item)
{
await Task.Delay(10);
return item;
@@ -530,7 +591,7 @@ public Task FromReactiveCommandInRecordClass()
namespace TestNs;
- public partial record TestVMRecord : ReactiveObject
+ public partial record TestVMRecord
{
[ReactiveCommand]
private void DoSomething() { }
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCollExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCollExtTests.cs
index fa69313..d914937 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCollExtTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxCollExtTests.cs
@@ -10,7 +10,6 @@ namespace ReactiveUI.SourceGenerators.Tests;
///
/// Extended unit tests for the ReactiveCollection generator covering edge cases.
///
-[TestFixture]
public class RxCollExtTests : TestBase
{
///
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxGenExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxGenExtTests.cs
index 10ea600..32c0e5a 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxGenExtTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxGenExtTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Extended unit tests for the Reactive generator covering edge cases.
///
-[TestFixture]
public class RxGenExtTests : TestBase
{
///
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxObjExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxObjExtTests.cs
index 33d5a69..ff1c66a 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxObjExtTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/RxObjExtTests.cs
@@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests;
///
/// Extended unit tests for the ReactiveObject generator covering edge cases.
///
-[TestFixture]
public class RxObjExtTests : TestBase
{
///
@@ -487,8 +486,6 @@ public partial class TestVM
[Reactive]
private string? _lastName;
-
- public string FullName => $"{FirstName} {LastName}";
}
""";
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/SymbolExtensionTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/SymbolExtensionTests.cs
new file mode 100644
index 0000000..87f56c9
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/SymbolExtensionTests.cs
@@ -0,0 +1,524 @@
+// Copyright (c) 2026 ReactiveUI and contributors. All rights reserved.
+// Licensed to the ReactiveUI and contributors under one or more agreements.
+// The ReactiveUI and contributors licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using ReactiveUI.SourceGenerators.Extensions;
+
+namespace ReactiveUI.SourceGenerator.Tests;
+
+///
+/// Unit tests for , ,
+/// , and .
+/// All tests compile small in-memory programs to obtain real Roslyn symbol instances.
+///
+public sealed class SymbolExtensionTests
+{
+ ///
+ /// GetFullyQualifiedName returns the global:: prefixed name.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenGetFullyQualifiedNameCalledThenReturnsGlobalPrefixedName()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ namespace Foo.Bar;
+ public class MyClass { }
+ """,
+ "MyClass");
+
+ var name = symbol.GetFullyQualifiedName();
+
+ await Assert.That(name).IsEqualTo("global::Foo.Bar.MyClass");
+ }
+
+ ///
+ /// GetFullyQualifiedNameWithNullabilityAnnotations includes the nullable annotation marker.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenGetFullyQualifiedNameWithNullabilityCalledThenIncludesAnnotation()
+ {
+ var symbol = GetFieldSymbol(
+ """
+ #nullable enable
+ namespace T;
+ public class C
+ {
+ public string? _name;
+ }
+ """,
+ "_name");
+
+ var name = symbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations();
+
+ await Assert.That(name).IsEqualTo("string?");
+ }
+
+ ///
+ /// HasAttributeWithFullyQualifiedMetadataName returns true when the attribute is present.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenAttributePresentThenHasAttributeWithNameReturnsTrue()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ using System;
+ namespace T;
+ [Obsolete]
+ public class C { }
+ """,
+ "C");
+
+ var result = symbol.HasAttributeWithFullyQualifiedMetadataName("System.ObsoleteAttribute");
+
+ await Assert.That(result).IsTrue();
+ }
+
+ ///
+ /// HasAttributeWithFullyQualifiedMetadataName returns false when the attribute is absent.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenAttributeAbsentThenHasAttributeWithNameReturnsFalse()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ namespace T;
+ public class C { }
+ """,
+ "C");
+
+ var result = symbol.HasAttributeWithFullyQualifiedMetadataName("System.ObsoleteAttribute");
+
+ await Assert.That(result).IsFalse();
+ }
+
+ ///
+ /// TryGetAttributeWithFullyQualifiedMetadataName returns true and outputs AttributeData when present.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenAttributePresentThenTryGetAttributeSucceeds()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ using System;
+ namespace T;
+ [Obsolete("old")]
+ public class C { }
+ """,
+ "C");
+
+ var found = symbol.TryGetAttributeWithFullyQualifiedMetadataName(
+ "System.ObsoleteAttribute",
+ out var attributeData);
+
+ await Assert.That(found).IsTrue();
+ await Assert.That(attributeData).IsNotNull();
+ }
+
+ ///
+ /// TryGetAttributeWithFullyQualifiedMetadataName returns false when attribute is absent.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenAttributeAbsentThenTryGetAttributeFails()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ namespace T;
+ public class C { }
+ """,
+ "C");
+
+ var found = symbol.TryGetAttributeWithFullyQualifiedMetadataName(
+ "System.ObsoleteAttribute",
+ out var attributeData);
+
+ await Assert.That(found).IsFalse();
+ await Assert.That(attributeData).IsNull();
+ }
+
+ ///
+ /// GetEffectiveAccessibility returns Public for a public class.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenPublicClassThenEffectiveAccessibilityIsPublic()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ namespace T;
+ public class C { }
+ """,
+ "C");
+
+ var accessibility = symbol.GetEffectiveAccessibility();
+
+ await Assert.That(accessibility).IsEqualTo(Accessibility.Public);
+ }
+
+ ///
+ /// GetEffectiveAccessibility returns Internal for an internal class.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenInternalClassThenEffectiveAccessibilityIsInternal()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ namespace T;
+ internal class C { }
+ """,
+ "C");
+
+ var accessibility = symbol.GetEffectiveAccessibility();
+
+ await Assert.That(accessibility).IsEqualTo(Accessibility.Internal);
+ }
+
+ ///
+ /// GetAccessibilityString returns "public" for a public symbol.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenPublicThenGetAccessibilityStringReturnsPublic()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ namespace T;
+ public class C { }
+ """,
+ "C");
+
+ await Assert.That(symbol.GetAccessibilityString()).IsEqualTo("public");
+ }
+
+ ///
+ /// GetAccessibilityString returns "internal" for an internal symbol.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenInternalThenGetAccessibilityStringReturnsInternal()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ namespace T;
+ internal class C { }
+ """,
+ "C");
+
+ await Assert.That(symbol.GetAccessibilityString()).IsEqualTo("internal");
+ }
+
+ ///
+ /// HasOrInheritsFromFullyQualifiedMetadataName returns true for the type itself.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenTypeIsSelfThenHasOrInheritsReturnsTrue()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ namespace T;
+ public class C { }
+ """,
+ "C");
+
+ var result = symbol.HasOrInheritsFromFullyQualifiedMetadataName("T.C");
+
+ await Assert.That(result).IsTrue();
+ }
+
+ ///
+ /// HasOrInheritsFromFullyQualifiedMetadataName returns true for a direct base class.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenTypeDerivedFromBaseThenHasOrInheritsReturnsTrue()
+ {
+ var compilation = CreateCompilation("""
+ namespace T;
+ public class Base { }
+ public class Derived : Base { }
+ """);
+
+ var derived = compilation.GetTypeByMetadataName("T.Derived")!;
+ var result = derived.HasOrInheritsFromFullyQualifiedMetadataName("T.Base");
+
+ await Assert.That(result).IsTrue();
+ }
+
+ ///
+ /// HasOrInheritsFromFullyQualifiedMetadataName returns false for an unrelated type.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenTypeUnrelatedThenHasOrInheritsReturnsFalse()
+ {
+ var compilation = CreateCompilation("""
+ namespace T;
+ public class A { }
+ public class B { }
+ """);
+
+ var a = compilation.GetTypeByMetadataName("T.A")!;
+ var result = a.HasOrInheritsFromFullyQualifiedMetadataName("T.B");
+
+ await Assert.That(result).IsFalse();
+ }
+
+ ///
+ /// InheritsFromFullyQualifiedMetadataName returns false for the type itself (not inherited).
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenTypeSelfThenInheritsReturnsFalse()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ namespace T;
+ public class C { }
+ """,
+ "C");
+
+ var result = symbol.InheritsFromFullyQualifiedMetadataName("T.C");
+
+ await Assert.That(result).IsFalse();
+ }
+
+ ///
+ /// ImplementsFullyQualifiedMetadataName returns true when the interface is implemented.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenInterfaceImplementedThenImplementsReturnsTrue()
+ {
+ var compilation = CreateCompilation("""
+ namespace T;
+ public interface IFoo { }
+ public class C : IFoo { }
+ """);
+
+ var c = compilation.GetTypeByMetadataName("T.C")!;
+ var result = c.ImplementsFullyQualifiedMetadataName("T.IFoo");
+
+ await Assert.That(result).IsTrue();
+ }
+
+ ///
+ /// GetFullyQualifiedMetadataName returns dotted name without global:: prefix.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenGetFullyQualifiedMetadataNameCalledThenReturnsDottedName()
+ {
+ var symbol = GetTypeSymbol(
+ """
+ namespace Foo.Bar;
+ public class Baz { }
+ """,
+ "Baz");
+
+ var name = symbol.GetFullyQualifiedMetadataName();
+
+ await Assert.That(name).IsEqualTo("Foo.Bar.Baz");
+ }
+
+ ///
+ /// GetAllMembers returns members from both the type and its base types.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenGetAllMembersCalledThenIncludesInheritedMembers()
+ {
+ var compilation = CreateCompilation("""
+ namespace T;
+ public class Base
+ {
+ public int BaseField;
+ }
+ public class Derived : Base
+ {
+ public int DerivedField;
+ }
+ """);
+
+ var derived = (INamedTypeSymbol)compilation.GetTypeByMetadataName("T.Derived")!;
+ var members = derived.GetAllMembers().Select(m => m.Name).ToList();
+
+ await Assert.That(members.Contains("DerivedField")).IsTrue();
+ await Assert.That(members.Contains("BaseField")).IsTrue();
+ }
+
+ ///
+ /// GetAllMembers(name) returns members with the matching name from base types.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenGetAllMembersWithNameCalledThenFiltersCorrectly()
+ {
+ var compilation = CreateCompilation("""
+ namespace T;
+ public class Base
+ {
+ public int Shared;
+ }
+ public class Derived : Base
+ {
+ public int Unique;
+ }
+ """);
+
+ var derived = (INamedTypeSymbol)compilation.GetTypeByMetadataName("T.Derived")!;
+ var members = derived.GetAllMembers("Shared").ToList();
+
+ await Assert.That(members.Count).IsEqualTo(1);
+ await Assert.That(members[0].Name).IsEqualTo("Shared");
+ }
+
+ ///
+ /// GetTypeString returns "class" for a regular class.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenRegularClassThenGetTypeStringReturnsClass()
+ {
+ var symbol = (INamedTypeSymbol)GetTypeSymbol(
+ """
+ namespace T;
+ public class C { }
+ """,
+ "C");
+
+ await Assert.That(symbol.GetTypeString()).IsEqualTo("class");
+ }
+
+ ///
+ /// GetTypeString returns "record" for a record class.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenRecordClassThenGetTypeStringReturnsRecord()
+ {
+ var symbol = (INamedTypeSymbol)GetTypeSymbol(
+ """
+ namespace T;
+ public record C { }
+ """,
+ "C");
+
+ await Assert.That(symbol.GetTypeString()).IsEqualTo("record");
+ }
+
+ ///
+ /// GetTypeString returns "struct" for a regular struct.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenStructThenGetTypeStringReturnsStruct()
+ {
+ var symbol = (INamedTypeSymbol)GetTypeSymbol(
+ """
+ namespace T;
+ public struct S { }
+ """,
+ "S");
+
+ await Assert.That(symbol.GetTypeString()).IsEqualTo("struct");
+ }
+
+ ///
+ /// GetTypeString returns "record struct" for a record struct.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenRecordStructThenGetTypeStringReturnsRecordStruct()
+ {
+ var symbol = (INamedTypeSymbol)GetTypeSymbol(
+ """
+ namespace T;
+ public record struct RS { }
+ """,
+ "RS");
+
+ await Assert.That(symbol.GetTypeString()).IsEqualTo("record struct");
+ }
+
+ ///
+ /// GetTypeString returns "interface" for an interface.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenInterfaceThenGetTypeStringReturnsInterface()
+ {
+ var symbol = (INamedTypeSymbol)GetTypeSymbol(
+ """
+ namespace T;
+ public interface IFoo { }
+ """,
+ "IFoo");
+
+ await Assert.That(symbol.GetTypeString()).IsEqualTo("interface");
+ }
+
+ ///
+ /// HasAccessibleTypeWithMetadataName returns true for System.String (always accessible).
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenWellKnownTypeThenHasAccessibleTypeReturnsTrue()
+ {
+ var compilation = CreateCompilation("namespace T; public class C {}");
+
+ var result = compilation.HasAccessibleTypeWithMetadataName("System.String");
+
+ await Assert.That(result).IsTrue();
+ }
+
+ ///
+ /// HasAccessibleTypeWithMetadataName returns false for a type that doesn't exist.
+ ///
+ /// A task to monitor the async.
+ [Test]
+ public async Task WhenUnknownTypeThenHasAccessibleTypeReturnsFalse()
+ {
+ var compilation = CreateCompilation("namespace T; public class C {}");
+
+ var result = compilation.HasAccessibleTypeWithMetadataName("DoesNot.Exist.Type");
+
+ await Assert.That(result).IsFalse();
+ }
+
+ private static ITypeSymbol GetTypeSymbol(string source, string typeName)
+ {
+ var compilation = CreateCompilation(source);
+ return compilation.GetSymbolsWithName(typeName, SymbolFilter.Type)
+ .OfType()
+ .Single();
+ }
+
+ private static IFieldSymbol GetFieldSymbol(string source, string fieldName)
+ {
+ var compilation = CreateCompilation(source);
+ return compilation.GetSymbolsWithName(fieldName, SymbolFilter.Member)
+ .OfType()
+ .Single();
+ }
+
+ private static CSharpCompilation CreateCompilation(string source)
+ {
+ var syntaxTree = CSharpSyntaxTree.ParseText(
+ source,
+ CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13));
+
+ return CSharpCompilation.Create(
+ assemblyName: "SymbolExtTests",
+ syntaxTrees: [syntaxTree],
+ references: TestCompilationReferences.CreateDefault(),
+ options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+}
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/TestCompilationReferences.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/TestCompilationReferences.cs
index 67a34c2..fa7a5fa 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/TestCompilationReferences.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/TestCompilationReferences.cs
@@ -4,72 +4,287 @@
// See the LICENSE file in the project root for full license information.
using System.Reflection;
+using System.Runtime.InteropServices;
namespace ReactiveUI.SourceGenerator.Tests;
internal static class TestCompilationReferences
{
- internal static ImmutableArray CreateDefault()
+ ///
+ /// Minimal source stubs for WPF and WinForms types that are only available
+ /// via the Microsoft.WindowsDesktop.App shared framework on Windows.
+ /// Used in non-Windows test compilations to allow test sources that reference
+ /// System.Windows.Window or System.Windows.Forms.UserControl
+ /// to compile cross-platform without requiring platform-specific assemblies.
+ ///
+ internal const string WindowsDesktopStubs = """
+ namespace System.Windows
+ {
+ public class DependencyProperty
+ {
+ public static DependencyProperty Register(string name, global::System.Type propertyType, global::System.Type ownerType, PropertyMetadata typeMetadata) => null!;
+ }
+ public class PropertyMetadata
+ {
+ public PropertyMetadata(object? defaultValue) { }
+ }
+ public class DependencyObject
+ {
+ public object GetValue(DependencyProperty dp) => null!;
+ public void SetValue(DependencyProperty dp, object value) { }
+ }
+ public class UIElement : DependencyObject { }
+ public class FrameworkElement : UIElement { }
+ public class Window : FrameworkElement { }
+ }
+ namespace System.Windows.Controls
+ {
+ public class UserControl : System.Windows.FrameworkElement { }
+ public class Page : System.Windows.FrameworkElement { }
+ }
+ namespace System.Windows.Forms
+ {
+ public class Control { }
+ public class Form : Control { }
+ public class UserControl : Control { }
+ }
+ """;
+
+ // Cache the default references so that the expensive assembly-scanning/file-I/O is only
+ // performed once per process, not on every test invocation.
+ private static readonly Lazy> defaultReferences =
+ new(CreateDefaultCore, LazyThreadSafetyMode.ExecutionAndPublication);
+
+ ///
+ /// Returns metadata references for all assemblies required by the in-memory test compilations.
+ /// Uses only runtime assemblies already loaded into the current process — no NuGet downloads,
+ /// no Basic.Reference.Assemblies mixing — to avoid CS1704/CS0433/CS0518 duplicate-type errors.
+ /// The result is cached after the first call to avoid repeated assembly scanning and file I/O.
+ ///
+ internal static ImmutableArray CreateDefault() => defaultReferences.Value;
+
+ private static ImmutableArray CreateDefaultCore()
{
- // Use assemblies already referenced by the test project to create a compilation that
- // can resolve ReactiveUI types used in the in-memory source strings.
- var root = typeof(ReactiveUI.SourceGenerators.CodeFixers.PropertyToReactiveFieldAnalyzer).Assembly;
+ var visited = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var result = ImmutableArray.CreateBuilder();
- var assemblies = new HashSet
+ // Seed with the key assemblies whose transitive closure covers BCL + ReactiveUI + Splat +
+ // System.Reactive and everything else the test source strings depend on.
+ var seeds = new[]
{
- typeof(object).Assembly,
- typeof(Enumerable).Assembly,
- root,
+ typeof(object).Assembly, // System.Private.CoreLib
+ typeof(Enumerable).Assembly, // System.Linq
+ typeof(System.ComponentModel.INotifyPropertyChanged).Assembly, // System.ObjectModel
+ typeof(ReactiveUI.ReactiveObject).Assembly, // ReactiveUI
+ typeof(ReactiveUI.SourceGenerators.ReactiveGenerator).Assembly, // ReactiveUI.SourceGenerators
+ typeof(ReactiveUI.SourceGenerators.CodeFixers.PropertyToReactiveFieldAnalyzer).Assembly, // analyzer assembly
+ typeof(Splat.Locator).Assembly, // Splat
};
- // Load the dependency closure for the analyzer assembly to ensure ReactiveUI is present.
- TryAdd(root.GetName(), assemblies);
+ foreach (var seed in seeds)
+ {
+ AddTransitive(seed, visited, result);
+ }
+
+ // Also sweep all assemblies already loaded — catches System.Reactive, DynamicData, etc.
+ foreach (var loaded in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ if (!loaded.IsDynamic && !string.IsNullOrWhiteSpace(loaded.Location)
+ && visited.Add(loaded.Location))
+ {
+ result.Add(MetadataReference.CreateFromFile(loaded.Location));
+ }
+ }
+
+ // Add WPF and WinForms assemblies on Windows so test source strings that inherit from
+ // Window or use Windows Forms controls compile correctly.
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ AddWindowsDesktopAssemblies(visited, result);
+ }
+
+ return result.ToImmutable();
+ }
- // Also add currently loaded assemblies (helps when running under different test hosts).
- foreach (var assemblyName in AppDomain.CurrentDomain.GetAssemblies()
- .Select(static a => a.GetName())
- .DistinctBy(static a => a.FullName))
+ ///