From 370c3a567d94530601ec6d9c3866912fde801524 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 9 Nov 2024 00:18:59 +0000 Subject: [PATCH 1/2] Fix for OAPH nullability for reference types Added Nested Project test to prove inheritance can operate --- .../Class1.cs | 18 +++++++++++++ ...UI.SourceGenerators.Execute.Nested1.csproj | 17 ++++++++++++ .../Class1.cs | 18 +++++++++++++ ...UI.SourceGenerators.Execute.Nested2.csproj | 14 ++++++++++ .../Class1.cs | 18 +++++++++++++ ...UI.SourceGenerators.Execute.Nested3.csproj | 16 ++++++++++++ .../TestViewModel.cs | 6 +++++ src/ReactiveUI.SourceGenerators.sln | 26 ++++++++++++++++++- ...opertyGenerator{FromObservable}.Execute.cs | 8 +++--- 9 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 src/ReactiveUI.SourceGenerators.Execute.Nested1/Class1.cs create mode 100644 src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj create mode 100644 src/ReactiveUI.SourceGenerators.Execute.Nested2/Class1.cs create mode 100644 src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj create mode 100644 src/ReactiveUI.SourceGenerators.Execute.Nested3/Class1.cs create mode 100644 src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested1/Class1.cs b/src/ReactiveUI.SourceGenerators.Execute.Nested1/Class1.cs new file mode 100644 index 0000000..503dae1 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested1/Class1.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2024 .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. + +using ReactiveUI; +using ReactiveUI.SourceGenerators; + +namespace SGReactiveUI.SourceGenerators.Execute.Nested1; + +/// +/// Class1. +/// +public partial class Class1 : ReactiveObject +{ + [Reactive] + private string? _property1; +} diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj b/src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj new file mode 100644 index 0000000..081b2ab --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested1/ReactiveUI.SourceGenerators.Execute.Nested1.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested2/Class1.cs b/src/ReactiveUI.SourceGenerators.Execute.Nested2/Class1.cs new file mode 100644 index 0000000..40f77bf --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested2/Class1.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2024 .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. + +using ReactiveUI; +using ReactiveUI.SourceGenerators; + +namespace SGReactiveUI.SourceGenerators.Execute.Nested2; + +/// +/// Class1. +/// +public partial class Class1 : ReactiveObject +{ + [Reactive] + private string? _property1; +} diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj b/src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj new file mode 100644 index 0000000..e4ab9f7 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested2/ReactiveUI.SourceGenerators.Execute.Nested2.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested3/Class1.cs b/src/ReactiveUI.SourceGenerators.Execute.Nested3/Class1.cs new file mode 100644 index 0000000..90524ce --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested3/Class1.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2024 .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. + +using ReactiveUI; +using ReactiveUI.SourceGenerators; + +namespace SGReactiveUI.SourceGenerators.Execute.Nested3; + +/// +/// Class1. +/// +public partial class Class1 : ReactiveObject +{ + [Reactive] + private string? _property1; +} diff --git a/src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj b/src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj new file mode 100644 index 0000000..2cbe8c2 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute.Nested3/ReactiveUI.SourceGenerators.Execute.Nested3.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + false + 12.0 + + + + + + + + diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index 37b31aa..065f661 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -234,6 +234,12 @@ public TestViewModel() /// public ViewModelActivator Activator { get; } = new(); + [ObservableAsProperty] + private IObservable ReferenceTypeObservable { get; } + + [ObservableAsProperty] + private IObservable NullableReferenceTypeObservable { get; } + /// /// Gets observables as property test. /// diff --git a/src/ReactiveUI.SourceGenerators.sln b/src/ReactiveUI.SourceGenerators.sln index 1a47a1d..762f6a7 100644 --- a/src/ReactiveUI.SourceGenerators.sln +++ b/src/ReactiveUI.SourceGenerators.sln @@ -1,5 +1,5 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 +# 17 VisualStudioVersion = 17.10.35027.167 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionConfig", "SolutionConfig", "{F29AF2F3-DEC8-58BC-043A-1447862C832D}" @@ -26,6 +26,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestLibs", "TestLibs", "{B8 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.SourceGenerators.Analyzers.CodeFixes", "ReactiveUI.SourceGenerators.Analyzers.CodeFixes\ReactiveUI.SourceGenerators.Analyzers.CodeFixes.csproj", "{BD4FADD9-C0E5-46E9-906E-01B04CC856B5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.SourceGenerators.Execute.Nested1", "ReactiveUI.SourceGenerators.Execute.Nested1\ReactiveUI.SourceGenerators.Execute.Nested1.csproj", "{A4971B7D-E35F-4891-BF32-BE911AE86900}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.SourceGenerators.Execute.Nested2", "ReactiveUI.SourceGenerators.Execute.Nested2\ReactiveUI.SourceGenerators.Execute.Nested2.csproj", "{CB36161E-8F9E-48B5-8CE0-AC130A73BD2A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.SourceGenerators.Execute.Nested3", "ReactiveUI.SourceGenerators.Execute.Nested3\ReactiveUI.SourceGenerators.Execute.Nested3.csproj", "{B42F683D-91D8-4378-9DFE-EC55DB0FE43A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NestedTest", "NestedTest", "{CAFBD27B-5078-4A0C-A4E9-19DCF2A7DF16}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -52,6 +60,18 @@ Global {BD4FADD9-C0E5-46E9-906E-01B04CC856B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {BD4FADD9-C0E5-46E9-906E-01B04CC856B5}.Release|Any CPU.ActiveCfg = Release|Any CPU {BD4FADD9-C0E5-46E9-906E-01B04CC856B5}.Release|Any CPU.Build.0 = Release|Any CPU + {A4971B7D-E35F-4891-BF32-BE911AE86900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4971B7D-E35F-4891-BF32-BE911AE86900}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4971B7D-E35F-4891-BF32-BE911AE86900}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4971B7D-E35F-4891-BF32-BE911AE86900}.Release|Any CPU.Build.0 = Release|Any CPU + {CB36161E-8F9E-48B5-8CE0-AC130A73BD2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB36161E-8F9E-48B5-8CE0-AC130A73BD2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB36161E-8F9E-48B5-8CE0-AC130A73BD2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB36161E-8F9E-48B5-8CE0-AC130A73BD2A}.Release|Any CPU.Build.0 = Release|Any CPU + {B42F683D-91D8-4378-9DFE-EC55DB0FE43A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B42F683D-91D8-4378-9DFE-EC55DB0FE43A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B42F683D-91D8-4378-9DFE-EC55DB0FE43A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B42F683D-91D8-4378-9DFE-EC55DB0FE43A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -59,6 +79,10 @@ Global GlobalSection(NestedProjects) = preSolution {76D5AC8C-4935-3E4B-BD12-71FAEC2B9A9D} = {B86ED9C1-AFFB-4854-AD80-F4B4050CAD0A} {849CACF4-B85F-47B5-84B3-7C94DE864E7E} = {B86ED9C1-AFFB-4854-AD80-F4B4050CAD0A} + {A4971B7D-E35F-4891-BF32-BE911AE86900} = {CAFBD27B-5078-4A0C-A4E9-19DCF2A7DF16} + {CB36161E-8F9E-48B5-8CE0-AC130A73BD2A} = {CAFBD27B-5078-4A0C-A4E9-19DCF2A7DF16} + {B42F683D-91D8-4378-9DFE-EC55DB0FE43A} = {CAFBD27B-5078-4A0C-A4E9-19DCF2A7DF16} + {CAFBD27B-5078-4A0C-A4E9-19DCF2A7DF16} = {B86ED9C1-AFFB-4854-AD80-F4B4050CAD0A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {173F891B-86A2-4226-B563-A7318CE0E2EC} diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs index 6e0817c..c23ddad 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs @@ -64,7 +64,7 @@ public sealed partial class ObservableAsPropertyGenerator var observableType = methodSymbol.ReturnType is not INamedTypeSymbol typeSymbol ? string.Empty - : typeSymbol.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + : typeSymbol.TypeArguments[0].GetFullyQualifiedNameWithNullabilityAnnotations(); // Get the hierarchy info for the target symbol, and try to gather the property info hierarchy = HierarchyInfo.From(methodSymbol.ContainingType); @@ -82,7 +82,7 @@ public sealed partial class ObservableAsPropertyGenerator targetInfo.TargetVisibility, targetInfo.TargetType, methodSymbol.Name, - methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + methodSymbol.ReturnType.GetFullyQualifiedNameWithNullabilityAnnotations(), methodSymbol.Parameters.FirstOrDefault()?.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), propertyName ?? (methodSymbol.Name + "Property"), observableType, @@ -109,7 +109,7 @@ public sealed partial class ObservableAsPropertyGenerator var observableType = propertySymbol.Type is not INamedTypeSymbol typeSymbol ? string.Empty - : typeSymbol.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + : typeSymbol.TypeArguments[0].GetFullyQualifiedNameWithNullabilityAnnotations(); // Get the hierarchy info for the target symbol, and try to gather the property info hierarchy = HierarchyInfo.From(propertySymbol.ContainingType); @@ -127,7 +127,7 @@ public sealed partial class ObservableAsPropertyGenerator targetInfo.TargetVisibility, targetInfo.TargetType, propertySymbol.Name, - propertySymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), propertySymbol.Parameters.FirstOrDefault()?.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), propertyName ?? (propertySymbol.Name + "Property"), observableType, From 3751dd416dcb6d12ccaffa7b6042db9da66676c4 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 9 Nov 2024 01:48:17 +0000 Subject: [PATCH 2/2] Add IsNullable, Fix tests --- ...bservableAsPropertyAttribute.g.verified.cs | 38 +++++++++++ ...ableAsPropertyFromObservable.g.verified.cs | 34 ++++++++++ ...bservableAsPropertyAttribute.g.verified.cs | 38 +++++++++++ ...ableAsPropertyFromObservable.g.verified.cs | 34 ++++++++++ .../OAPFromObservableGeneratorTests.cs | 68 +++++++++++++++++++ .../TestViewModel.cs | 5 ++ .../Core/Extensions/ITypeSymbolExtensions.cs | 9 +++ .../Models/ObservableMethodInfo.cs | 1 + ...opertyGenerator{FromObservable}.Execute.cs | 18 ++--- 9 files changed, 236 insertions(+), 9 deletions(-) create mode 100644 src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs create mode 100644 src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#TestVM.ObservableAsPropertyFromObservable.g.verified.cs create mode 100644 src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs create mode 100644 src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#TestVM.ObservableAsPropertyFromObservable.g.verified.cs diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs new file mode 100644 index 0000000..7e579df --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -0,0 +1,38 @@ +//HintName: ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.cs +// Copyright (c) 2024 .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. + +using System; + +// +#pragma warning disable +#nullable enable +namespace ReactiveUI.SourceGenerators; + +/// +/// ObservableAsPropertyAttribute. +/// +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +internal sealed class ObservableAsPropertyAttribute : Attribute +{ + /// + /// Gets the name of the property. + /// + /// + /// The name of the property. + /// + public string? PropertyName { get; init; } + + /// + /// Gets the Readonly state of the OAPH property. + /// + /// + /// The is read only of the OAPH property. + /// + public bool ReadOnly { get; init; } = true; +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#TestVM.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#TestVM.ObservableAsPropertyFromObservable.g.verified.cs new file mode 100644 index 0000000..7eef1ad --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#TestVM.ObservableAsPropertyFromObservable.g.verified.cs @@ -0,0 +1,34 @@ +//HintName: TestVM.ObservableAsPropertyFromObservable.g.cs +// +using ReactiveUI; + +#pragma warning disable +#nullable enable + +namespace TestNs +{ + /// + /// Partial class for the TestVM which contains ReactiveUI Reactive property initialization. + /// + public partial class TestVM + { + /// + private object? _test7Property; + + /// + private ReactiveUI.ObservableAsPropertyHelper? _test7PropertyHelper; + + /// + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.Text.Json.Serialization.JsonIncludeAttribute()] + public object? Test7Property { get => _test7Property = (_test7PropertyHelper == null ? _test7Property : _test7PropertyHelper.Value); } + + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + protected void InitializeOAPH() + { + _test7PropertyHelper = Test7!.ToProperty(this, nameof(Test7Property)); + } + } +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs new file mode 100644 index 0000000..7e579df --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -0,0 +1,38 @@ +//HintName: ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.cs +// Copyright (c) 2024 .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. + +using System; + +// +#pragma warning disable +#nullable enable +namespace ReactiveUI.SourceGenerators; + +/// +/// ObservableAsPropertyAttribute. +/// +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +internal sealed class ObservableAsPropertyAttribute : Attribute +{ + /// + /// Gets the name of the property. + /// + /// + /// The name of the property. + /// + public string? PropertyName { get; init; } + + /// + /// Gets the Readonly state of the OAPH property. + /// + /// + /// The is read only of the OAPH property. + /// + public bool ReadOnly { get; init; } = true; +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#TestVM.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#TestVM.ObservableAsPropertyFromObservable.g.verified.cs new file mode 100644 index 0000000..99a39e7 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#TestVM.ObservableAsPropertyFromObservable.g.verified.cs @@ -0,0 +1,34 @@ +//HintName: TestVM.ObservableAsPropertyFromObservable.g.cs +// +using ReactiveUI; + +#pragma warning disable +#nullable enable + +namespace TestNs +{ + /// + /// Partial class for the TestVM which contains ReactiveUI Reactive property initialization. + /// + public partial class TestVM + { + /// + private object _test6Property; + + /// + private ReactiveUI.ObservableAsPropertyHelper? _test6PropertyHelper; + + /// + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.Text.Json.Serialization.JsonIncludeAttribute()] + public object Test6Property { get => _test6Property = _test6PropertyHelper?.Value ?? _test6Property; } + + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + protected void InitializeOAPH() + { + _test6PropertyHelper = Test6!.ToProperty(this, nameof(Test6Property)); + } + } +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs index 5833d51..ca4e7d6 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs @@ -169,5 +169,73 @@ public partial class TestVM : ReactiveObject return VerifyGenerator(driver); } + /// + /// Tests that the source generator correctly generates observable properties. + /// + /// A task to monitor the async. + [Fact] + public Task FromObservablePropertiesWithAttributeRef() + { + // Arrange: Setup the source code that matches the generator input expectations. + const string sourceCode = """ + using System; + using System.Runtime.Serialization; + using System.Text.Json.Serialization; + using ReactiveUI; + using ReactiveUI.SourceGenerators; + using System.Reactive.Linq; + + namespace TestNs; + + public partial class TestVM : ReactiveObject + { + [ObservableAsProperty(PropertyName = "MyNamedProperty")] + [property: JsonInclude] + [DataMember] + public IObservable Test6 => Observable.Return(new object()); + } + """; + + // Act: Initialize the helper and run the generator. + var driver = TestHelper.TestPass(sourceCode); + + // Assert: Verify the generated code. + return VerifyGenerator(driver); + } + + /// + /// Tests that the source generator correctly generates observable properties. + /// + /// A task to monitor the async. + [Fact] + public Task FromObservablePropertiesWithAttributeNullableRef() + { + // Arrange: Setup the source code that matches the generator input expectations. + const string sourceCode = """ + using System; + using System.Runtime.Serialization; + using System.Text.Json.Serialization; + using ReactiveUI; + using ReactiveUI.SourceGenerators; + using System.Reactive.Linq; + + namespace TestNs; + + public partial class TestVM : ReactiveObject + { + [ObservableAsProperty(PropertyName = "MyNamedProperty")] + [property: JsonInclude] + [DataMember] + public IObservable Test7 => Observable.Return(new object()); + } + """; + + // Act: Initialize the helper and run the generator. + var driver = TestHelper.TestPass(sourceCode); + + // Assert: Verify the generated code. + return VerifyGenerator(driver); + } + private SettingsTask VerifyGenerator(GeneratorDriver driver) => Verify(driver).UseDirectory(TestHelper.VerifiedFilePath()).ScrubLinesContaining("[global::System.CodeDom.Compiler.GeneratedCode(\""); } diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index 065f661..3016808 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -79,6 +79,11 @@ public TestViewModel() _observableAsPropertyTest2Property = 11223344; Console.Out.WriteLine(ObservableAsPropertyTest2Property); Console.Out.WriteLine(_observableAsPropertyTest2Property); + + _referenceTypeObservableProperty = default!; + ReferenceTypeObservable = Observable.Return(new object()); + NullableReferenceTypeObservable = Observable.Return(new object()); + InitializeOAPH(); Console.Out.WriteLine(Test1Command); diff --git a/src/ReactiveUI.SourceGenerators/Core/Extensions/ITypeSymbolExtensions.cs b/src/ReactiveUI.SourceGenerators/Core/Extensions/ITypeSymbolExtensions.cs index 7fd8eef..0343dff 100644 --- a/src/ReactiveUI.SourceGenerators/Core/Extensions/ITypeSymbolExtensions.cs +++ b/src/ReactiveUI.SourceGenerators/Core/Extensions/ITypeSymbolExtensions.cs @@ -288,6 +288,15 @@ public static bool IsObservableBoolType(this ITypeSymbol? typeSymbol) return false; } + /// + /// Determines whether [is nullable type]. + /// + /// The type symbol. + /// + /// true if [is nullable type] [the specified type symbol]; otherwise, false. + /// + public static bool IsNullableType(this ITypeSymbol? typeSymbol) => typeSymbol?.NullableAnnotation == NullableAnnotation.Annotated; + public static ITypeSymbol GetTaskReturnType(this ITypeSymbol typeSymbol, Compilation compilation) => typeSymbol switch { INamedTypeSymbol { TypeArguments.Length: 1 } namedTypeSymbol => namedTypeSymbol.TypeArguments[0], diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableMethodInfo.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableMethodInfo.cs index b8f2380..a335d4a 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableMethodInfo.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableMethodInfo.cs @@ -20,6 +20,7 @@ internal record ObservableMethodInfo( string? ArgumentType, string PropertyName, string ObservableType, + bool IsNullableType, bool IsProperty, EquatableArray ForwardedPropertyAttributes) { diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs index c23ddad..7d759a4 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs @@ -66,6 +66,8 @@ public sealed partial class ObservableAsPropertyGenerator ? string.Empty : typeSymbol.TypeArguments[0].GetFullyQualifiedNameWithNullabilityAnnotations(); + var isNullableType = methodSymbol.ReturnType is INamedTypeSymbol nullcheck && nullcheck.TypeArguments[0].IsNullableType(); + // Get the hierarchy info for the target symbol, and try to gather the property info hierarchy = HierarchyInfo.From(methodSymbol.ContainingType); token.ThrowIfCancellationRequested(); @@ -86,6 +88,7 @@ public sealed partial class ObservableAsPropertyGenerator methodSymbol.Parameters.FirstOrDefault()?.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), propertyName ?? (methodSymbol.Name + "Property"), observableType, + isNullableType, false, propertyAttributes), diagnostics.ToImmutable()); @@ -111,6 +114,8 @@ public sealed partial class ObservableAsPropertyGenerator ? string.Empty : typeSymbol.TypeArguments[0].GetFullyQualifiedNameWithNullabilityAnnotations(); + var isNullableType = propertySymbol.Type is INamedTypeSymbol nullcheck && nullcheck.TypeArguments[0].IsNullableType(); + // Get the hierarchy info for the target symbol, and try to gather the property info hierarchy = HierarchyInfo.From(propertySymbol.ContainingType); token.ThrowIfCancellationRequested(); @@ -131,6 +136,7 @@ public sealed partial class ObservableAsPropertyGenerator propertySymbol.Parameters.FirstOrDefault()?.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), propertyName ?? (propertySymbol.Name + "Property"), observableType, + isNullableType, true, propertyAttributes), diagnostics.ToImmutable()); @@ -173,15 +179,9 @@ private static string GetPropertySyntax(ObservableMethodInfo propertyInfo) { var propertyAttributes = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(propertyInfo.ForwardedPropertyAttributes)); var getterFieldIdentifierName = propertyInfo.GetGeneratedFieldName(); - string getterArrowExpression; - if (propertyInfo.ObservableType.EndsWith("?")) - { - getterArrowExpression = $"{getterFieldIdentifierName} = ({getterFieldIdentifierName}Helper == null ? {getterFieldIdentifierName} : {getterFieldIdentifierName}Helper.Value)"; - } - else - { - getterArrowExpression = $"{getterFieldIdentifierName} = {getterFieldIdentifierName}Helper?.Value ?? {getterFieldIdentifierName}"; - } + var getterArrowExpression = propertyInfo.IsNullableType + ? $"{getterFieldIdentifierName} = ({getterFieldIdentifierName}Helper == null ? {getterFieldIdentifierName} : {getterFieldIdentifierName}Helper.Value)" + : $"{getterFieldIdentifierName} = {getterFieldIdentifierName}Helper?.Value ?? {getterFieldIdentifierName}"; return $$""" ///