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)) + /// + /// Adds WPF (PresentationFramework + dependencies) and Windows Forms assemblies to the + /// reference set, resolving them from the Microsoft.WindowsDesktop.App shared framework + /// directory that corresponds to the current runtime version. + /// + private static void AddWindowsDesktopAssemblies( + HashSet visited, + ImmutableArray.Builder result) + { + var versionDir = FindWindowsDesktopAppVersionDir(); + if (versionDir is null) { - TryAdd(assemblyName, assemblies); + return; } - return assemblies - .Where(static a => !string.IsNullOrWhiteSpace(a.Location)) - .Select(static a => (MetadataReference)MetadataReference.CreateFromFile(a.Location)) - .ToImmutableArray(); + // WPF assemblies required for tests that use Window as a base class. + var wpfAssemblies = new[] + { + "PresentationFramework.dll", + "PresentationCore.dll", + "WindowsBase.dll", + "System.Xaml.dll", + }; + + // WinForms assemblies required for tests that use Windows Forms controls. + var winFormsAssemblies = new[] + { + "System.Windows.Forms.dll", + "System.Windows.Forms.Primitives.dll", + }; + + foreach (var name in wpfAssemblies.Concat(winFormsAssemblies)) + { + var path = Path.Combine(versionDir, name); + if (File.Exists(path) && visited.Add(path)) + { + result.Add(MetadataReference.CreateFromFile(path)); + } + } } - private static void TryAdd(AssemblyName assemblyName, HashSet set) + /// + /// Locates the best matching Microsoft.WindowsDesktop.App version directory. + /// Uses multiple discovery strategies: runtime-relative path, DOTNET_ROOT env var, + /// and well-known installation paths. + /// + private static string? FindWindowsDesktopAppVersionDir() { - try + var runtimeVersion = Environment.Version; + var majorMinor = $"{runtimeVersion.Major}.{runtimeVersion.Minor}"; + + // Collect unique candidate parent directories to try, in priority order. + var candidateRoots = new List(); + + // Strategy 1a: RuntimeEnvironment.GetRuntimeDirectory() — the most reliable way to + // locate the actual .NET shared framework even when running under a VS test host + // or PowerShell where typeof(object).Assembly.Location may point elsewhere. + // Returns e.g. C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.14\ + var runtimeDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); + candidateRoots.Add(Path.GetDirectoryName(runtimeDir?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); + + // Strategy 1b: Walk up from typeof(object).Assembly.Location (works under dotnet CLI). + var coreLibDir = Path.GetDirectoryName(typeof(object).Assembly.Location); + candidateRoots.Add(coreLibDir); + + // Try each root two levels up to reach shared\Microsoft.WindowsDesktop.App + foreach (var root in candidateRoots.Where(r => !string.IsNullOrEmpty(r))) + { + var candidate = Path.GetFullPath(Path.Combine(root!, "..", "Microsoft.WindowsDesktop.App")); + var dir = PickBestVersionDir(candidate, majorMinor); + if (dir is not null) + { + return dir; + } + + // One extra level for layouts where root is already the version directory. + candidate = Path.GetFullPath(Path.Combine(root!, "..", "..", "Microsoft.WindowsDesktop.App")); + dir = PickBestVersionDir(candidate, majorMinor); + if (dir is not null) + { + return dir; + } + } + + // Strategy 2: DOTNET_ROOT environment variable. + var dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT") + ?? Environment.GetEnvironmentVariable("DOTNET_ROOT(x64)"); + if (!string.IsNullOrEmpty(dotnetRoot)) { - var loaded = Assembly.Load(assemblyName); + var candidate = Path.Combine(dotnetRoot, "shared", "Microsoft.WindowsDesktop.App"); + var dir = PickBestVersionDir(candidate, majorMinor); + if (dir is not null) + { + return dir; + } + } - if (!string.IsNullOrWhiteSpace(loaded.Location)) + // Strategy 3: Standard installation paths on Windows. + foreach (var programFiles in new[] + { + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), + }) + { + if (string.IsNullOrEmpty(programFiles)) { - set.Add(loaded); + continue; } - foreach (var referenced in loaded.GetReferencedAssemblies()) + var candidate = Path.Combine(programFiles, "dotnet", "shared", "Microsoft.WindowsDesktop.App"); + var dir = PickBestVersionDir(candidate, majorMinor); + if (dir is not null) { - try - { - var referencedLoaded = Assembly.Load(referenced); - - if (!string.IsNullOrWhiteSpace(referencedLoaded.Location)) - { - set.Add(referencedLoaded); - } - } - catch - { - // Best-effort only. - } + return dir; } } - catch + + return null; + } + + /// + /// Returns the best version directory under that matches + /// (e.g., "9.0"), falling back to the newest available. + /// + private static string? PickBestVersionDir(string sharedRoot, string majorMinor) + { + if (!Directory.Exists(sharedRoot)) + { + return null; + } + + var dirs = Directory.GetDirectories(sharedRoot); + if (dirs.Length == 0) + { + return null; + } + + // Prefer exact major.minor match, ordered descending (newest patch first). + var best = dirs + .Where(d => Path.GetFileName(d).StartsWith(majorMinor + ".", StringComparison.Ordinal) + || Path.GetFileName(d).Equals(majorMinor, StringComparison.Ordinal)) + .OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault() + ?? dirs.OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase).FirstOrDefault(); + + // Validate it actually contains PresentationFramework.dll + return best is not null && File.Exists(Path.Combine(best, "PresentationFramework.dll")) + ? best + : null; + } + + private static void AddTransitive( + Assembly assembly, + HashSet visited, + ImmutableArray.Builder result) + { + if (assembly.IsDynamic || string.IsNullOrWhiteSpace(assembly.Location)) + { + return; + } + + if (!visited.Add(assembly.Location)) + { + return; + } + + result.Add(MetadataReference.CreateFromFile(assembly.Location)); + + foreach (var referencedName in assembly.GetReferencedAssemblies()) { - // Best-effort only. + try + { + var referenced = System.Reflection.Assembly.Load(referencedName); + AddTransitive(referenced, visited, result); + } + catch + { + // Best-effort — system assemblies not found in some environments are skipped. + } } } } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ViewForExtTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ViewForExtTests.cs index 1a3904c..3e28732 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ViewForExtTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ViewForExtTests.cs @@ -8,7 +8,6 @@ namespace ReactiveUI.SourceGenerator.Tests; /// /// Extended unit tests for the IViewFor generator covering edge cases. /// -[TestFixture] public class ViewForExtTests : TestBase { /// @@ -20,6 +19,7 @@ public Task LazySingle() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -49,6 +49,7 @@ public Task Constant() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -78,6 +79,7 @@ public Task PerReq() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -107,6 +109,7 @@ public Task FromIViewForWithViewModelRegistration() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -138,6 +141,7 @@ public Task Nested() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -172,6 +176,7 @@ public Task FromIViewForWithStringViewModelType() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -201,25 +206,28 @@ public Task DiffNs() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; - namespace ViewModels; - - public partial class ProductViewModel : ReactiveObject + namespace ViewModels { - public string? ProductName { get; set; } - public decimal Price { get; set; } + public partial class ProductViewModel : ReactiveObject + { + public string? ProductName { get; set; } + public decimal Price { get; set; } + } } - namespace Views; - - using ViewModels; - - [IViewFor] - public partial class ProductView : Window + namespace Views { - public ProductView() => ViewModel = new ProductViewModel(); + using ViewModels; + + [IViewFor] + public partial class ProductView : Window + { + public ProductView() => ViewModel = new ProductViewModel(); + } } """; @@ -235,6 +243,7 @@ public Task FromMultipleIViewForInSameNamespace() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -286,12 +295,13 @@ public Task Generic() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; namespace TestNs; - public partial class GenericViewModel : ReactiveObject where T : class + public partial class GenericViewModel { public T? Item { get; set; } } @@ -315,12 +325,13 @@ public Task Record() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; namespace TestNs; - public partial record RecordViewModel : ReactiveObject + public partial record RecordViewModel { public string? Name { get; set; } public int Age { get; set; } @@ -345,6 +356,7 @@ public Task FromIViewForWithNestedViewClass() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -380,12 +392,13 @@ public Task FromIViewForWithReactivePropertiesViewModel() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; namespace TestNs; - public partial class ReactivePropertiesViewModel : ReactiveObject + public partial class ReactivePropertiesViewModel { [Reactive] private string? _firstName; @@ -418,6 +431,7 @@ public Task FromIViewForWithReactiveCommandsViewModel() using System; using System.Threading.Tasks; using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -454,12 +468,13 @@ public Task AllRegOpts() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; namespace TestNs; - public partial class FullViewModel : ReactiveObject + public partial class FullViewModel { public string? Title { get; set; } } @@ -485,6 +500,7 @@ public Task Interface() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -519,6 +535,7 @@ public Task ExtNs() { const string sourceCode = """ using System.Collections.ObjectModel; + using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; diff --git a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs index b2c726c..95c56a1 100644 --- a/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs +++ b/src/ReactiveUI.SourceGenerators.Analyzers.CodeFixes/Core/Helpers/EquatableArray{T}.cs @@ -98,7 +98,7 @@ public ref readonly T this[int index] /// /// The object. /// A bool. - public override bool Equals([NotNullWhen(true)] object? obj) => obj is EquatableArray array && Equals(this, array); + public override bool Equals([NotNullWhen(true)] object? obj) => obj is EquatableArray array && Equals(array); /// /// Gets the hash code. diff --git a/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj b/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj index 32309f1..c5a6349 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute.Maui/ReactiveUI.SourceGenerators.Execute.Maui.csproj @@ -7,6 +7,7 @@ enable false latest + true 12.2 15.0 diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj b/src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj index 398d0c0..aaa1737 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj @@ -6,6 +6,7 @@ enable false 13.0 + true diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj b/src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj index b72210e..df9cde6 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj @@ -6,6 +6,7 @@ enable false 13.0 + true diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj b/src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj index d1e5031..a41196e 100644 --- a/src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj @@ -6,6 +6,7 @@ enable false 13.0 + true diff --git a/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj b/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj index 8848a56..4182983 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj @@ -10,6 +10,7 @@ false preview $(NoWarn);CA1812 + true A MVVM framework that integrates with the Reactive Extensions for .NET to create elegant, testable User Interfaces that run on any mobile or desktop platform. This is the Source Generators package for ReactiveUI diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs index 466f4f2..bffae8e 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs @@ -31,11 +31,22 @@ internal static class AttributeDataExtensions /// Whether or not contains an argument named with a valid value. public static bool TryGetNamedArgument(this AttributeData attributeData, string name, out T? value) { - foreach (var properties in attributeData.NamedArguments) + if (attributeData is null) { - if (properties.Key == name) + value = default; + return false; + } + + // NamedArguments returns a default ImmutableArray when attribute data is incomplete/malformed. + // Guard with IsDefaultOrEmpty rather than catching NullReferenceException to avoid masking real bugs. + if (!attributeData.NamedArguments.IsDefaultOrEmpty) + { + foreach (var properties in attributeData.NamedArguments) { - return TryConvertNamedArgument(properties.Value, out value); + if (properties.Key == name) + { + return TryConvertNamedArgument(properties.Value, out value); + } } } @@ -53,11 +64,21 @@ public static bool TryGetNamedArgument(this AttributeData attributeData, stri /// The named argument value. public static T? GetNamedArgument(this AttributeData attributeData, string name) { - foreach (var properties in attributeData.NamedArguments) + if (attributeData is null) { - if (properties.Key == name) + return default; + } + + // NamedArguments returns a default ImmutableArray when attribute data is incomplete/malformed. + // Guard with IsDefaultOrEmpty rather than catching NullReferenceException to avoid masking real bugs. + if (!attributeData.NamedArguments.IsDefaultOrEmpty) + { + foreach (var properties in attributeData.NamedArguments) { - return TryConvertNamedArgument(properties.Value, out T? value) ? value : default; + if (properties.Key == name) + { + return TryConvertNamedArgument(properties.Value, out T? value) ? value : default; + } } } @@ -168,8 +189,18 @@ static void GatherForwardedAttributes( public static string? GetGenericType(this AttributeData attributeData) { var success = attributeData?.AttributeClass?.ToDisplayString(); - var start = success?.IndexOf('<') + 1 ?? 0; - return success?.Substring(start, success.Length - start - 1); + if (string.IsNullOrWhiteSpace(success)) + { + return null; + } + + var attributeClassName = success ?? string.Empty; + var start = attributeClassName.IndexOf('<'); + var end = attributeClassName.LastIndexOf('>'); + + return start >= 0 && end > start + ? attributeClassName.Substring(start + 1, end - start - 1) + : null; } private static bool TryConvertNamedArgument(in TypedConstant typedConstant, out T? value) @@ -190,6 +221,26 @@ private static bool TryConvertNamedArgument(in TypedConstant typedConstant, o var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + if (targetType.IsEnum) + { + try + { + if (rawValue is string enumName) + { + value = (T)Enum.Parse(targetType, enumName, ignoreCase: false); + return true; + } + + value = (T)Enum.ToObject(targetType, rawValue); + return true; + } + catch (ArgumentException) + { + value = default; + return false; + } + } + try { value = (T)Convert.ChangeType(rawValue, targetType, CultureInfo.InvariantCulture); @@ -214,6 +265,11 @@ private static bool TryConvertNamedArgument(in TypedConstant typedConstant, o private static object? TryGetRawValue(in TypedConstant typedConstant) { + if (typedConstant.Kind == TypedConstantKind.Error) + { + return null; + } + if (typedConstant.Type?.TypeKind == TypeKind.Enum) { if (typedConstant.Value is IFieldSymbol fieldSymbol) @@ -228,9 +284,16 @@ private static bool TryConvertNamedArgument(in TypedConstant typedConstant, o if (typedConstant.Type is INamedTypeSymbol enumType) { - var enumMemberName = typedConstant.ToCSharpString().Split('.').LastOrDefault(); - return enumType.GetMembers(enumMemberName ?? string.Empty).OfType().FirstOrDefault()?.ConstantValue; + var csharpValue = typedConstant.ToCSharpString(); + + if (!string.IsNullOrWhiteSpace(csharpValue)) + { + var enumMemberName = csharpValue.Split('.').LastOrDefault(); + return enumType.GetMembers(enumMemberName ?? string.Empty).OfType().FirstOrDefault()?.ConstantValue; + } } + + return null; } return typedConstant.Value; diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/EquatableArray{T}.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/EquatableArray{T}.cs index b2c726c..95c56a1 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/EquatableArray{T}.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Helpers/EquatableArray{T}.cs @@ -98,7 +98,7 @@ public ref readonly T this[int index] /// /// The object. /// A bool. - public override bool Equals([NotNullWhen(true)] object? obj) => obj is EquatableArray array && Equals(this, array); + public override bool Equals([NotNullWhen(true)] object? obj) => obj is EquatableArray array && Equals(array); /// /// Gets the hash code. diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs index 8ef736a..280ec47 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs @@ -120,11 +120,16 @@ public partial class IViewForGenerator viewModelRegistrationType); } - private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, IViewForInfo iviewForInfo) + private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, IViewForInfo iviewForInfo, Models.TargetInfo? parentInfo = null) { // Prepare any forwarded property attributes var forwardedAttributesString = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage); + // Build parent class wrapping (for nested types) + var (parentDeclarations, parentClosing) = parentInfo is null + ? (string.Empty, string.Empty) + : Models.TargetInfo.GenerateParentClassDeclarations([parentInfo]); + switch (iviewForInfo.BaseType) { case IViewForBaseType.None: @@ -158,7 +163,7 @@ private static string GenerateSource(string containingTypeName, string containin namespace {{containingNamespace}} { - /// +{{parentDeclarations}} /// /// Partial class for the {{containingTypeName}} which contains ReactiveUI IViewFor initialization. /// {{forwardedAttributesString}} @@ -181,7 +186,7 @@ namespace {{containingNamespace}} /// object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = ({{iviewForInfo.ViewModelTypeName}})value; } } -} +{{parentClosing}}} #nullable restore #pragma warning restore """; @@ -196,7 +201,7 @@ namespace {{containingNamespace}} namespace {{containingNamespace}} { - /// +{{parentDeclarations}} /// /// Partial class for the {{containingTypeName}} which contains ReactiveUI IViewFor initialization. /// {{forwardedAttributesString}} @@ -213,7 +218,7 @@ namespace {{containingNamespace}} /// object? IViewFor.ViewModel {get => ViewModel; set => ViewModel = ({{iviewForInfo.ViewModelTypeName}}? )value; } } -} +{{parentClosing}}} #nullable restore #pragma warning restore """; @@ -230,7 +235,7 @@ namespace {{containingNamespace}} namespace {{containingNamespace}} { - /// +{{parentDeclarations}} /// /// Partial class for the {{containingTypeName}} which contains ReactiveUI IViewFor initialization. /// {{forwardedAttributesString}} @@ -273,7 +278,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } } } -} +{{parentClosing}}} #nullable restore #pragma warning restore """; @@ -289,7 +294,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang namespace {{containingNamespace}} { - {{forwardedAttributesString}} +{{parentDeclarations}} {{forwardedAttributesString}} {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : IViewFor<{{iviewForInfo.ViewModelTypeName}}> { public static readonly BindableProperty ViewModelProperty = BindableProperty.Create(nameof(ViewModel), typeof({{iviewForInfo.ViewModelTypeName}}), typeof(IViewFor<{{iviewForInfo.ViewModelTypeName}}>), default({{iviewForInfo.ViewModelTypeName}}), BindingMode.OneWay, propertyChanged: OnViewModelChanged); @@ -314,7 +319,7 @@ protected override void OnBindingContextChanged() private static void OnViewModelChanged(BindableObject bindableObject, object oldValue, object newValue) => bindableObject.BindingContext = newValue; } -} +{{parentClosing}}} #nullable restore #pragma warning restore """; diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs index e281572..ea96745 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs @@ -67,7 +67,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) continue; } - var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.FirstOrDefault()); + var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.FirstOrDefault(), grouping.FirstOrDefault()?.TargetInfo?.ParentInfo); // Only add source if it's not empty (i.e., a supported UI framework base type was detected) if (!string.IsNullOrWhiteSpace(source)) diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs index 811f7a4..55a9cdf 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs @@ -40,16 +40,14 @@ public partial class ReactiveCommandGenerator private static CommandInfo? GetMethodInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) { var symbol = context.TargetSymbol; - if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveCommandAttributeType, out var attributeData)) - { - return null; - } if (symbol is not IMethodSymbol methodSymbol) { return default; } + var attributeData = context.Attributes[0]; + token.ThrowIfCancellationRequested(); using var builder = ImmutableArrayBuilder.Rent(); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn4120/ReactiveUI.SourceGenerators.Roslyn4120.csproj b/src/ReactiveUI.SourceGenerators.Roslyn4120/ReactiveUI.SourceGenerators.Roslyn4120.csproj index 660fb21..4b46181 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn4120/ReactiveUI.SourceGenerators.Roslyn4120.csproj +++ b/src/ReactiveUI.SourceGenerators.Roslyn4120/ReactiveUI.SourceGenerators.Roslyn4120.csproj @@ -27,6 +27,7 @@ + diff --git a/src/ReactiveUI.SourceGenerators.slnx b/src/ReactiveUI.SourceGenerators.slnx index 1293b37..135387d 100644 --- a/src/ReactiveUI.SourceGenerators.slnx +++ b/src/ReactiveUI.SourceGenerators.slnx @@ -7,7 +7,9 @@ + + diff --git a/src/global.json b/src/global.json new file mode 100644 index 0000000..3140116 --- /dev/null +++ b/src/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/src/testconfig.json b/src/testconfig.json new file mode 100644 index 0000000..bd14f0f --- /dev/null +++ b/src/testconfig.json @@ -0,0 +1,26 @@ +{ + "platform": { + "execution": { + "parallel": true + } + }, + "extensions": [ + { + "extensionId": "Microsoft.Testing.Extensions.CodeCoverage", + "settings": { + "format": "cobertura", + "skipAutoProperties": true, + "modulePaths": { + "include": [ + "ReactiveUI\\.SourceGenerators(\\..+)?" + ], + "exclude": [ + ".*Tests.*", + ".*TestRunner.*", + ".*TestModels.*" + ] + } + } + } + ] +}