diff --git a/README.md b/README.md index df69ef7..5c286bc 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,10 @@ This documentation covers using ReactiveUI Source Generators to simplify and enh ReactiveUI Source Generators automatically generate ReactiveUI objects to streamline your code. These Source Generators are designed to work with ReactiveUI V19.5.31+ and support the following features: - `[Reactive]` With field and access modifiers, partial property support (C# 13 Visual Studio Version 17.12.0) -- `[ObservableAsProperty]` +- `[ObservableAsProperty]` With field, method, Observable property and partial property support (C# 13 Visual Studio Version 17.12.0) +- `[ObservableAsProperty(ReadOnly = false)]` Removes readonly keyword from the generated helper field - `[ObservableAsProperty(PropertyName = "ReadOnlyPropertyName")]` +- `[ObservableAsProperty(InitialValue = "Default Value")]` Only valid for partial properties using (C# 13 Visual Studio Version 17.12.0) - `[ReactiveCommand]` - `[ReactiveCommand(CanExecute = nameof(IObservableBoolName))]` with CanExecute - `[ReactiveCommand(OutputScheduler = "RxApp.MainThreadScheduler")]` using a ReactiveUI Scheduler @@ -296,6 +298,26 @@ public partial class MyReactiveClass : ReactiveObject } ``` +### Usage ObservableAsPropertyHelper with partial Property and a default value +```csharp +using ReactiveUI.SourceGenerators; + +public partial class MyReactiveClass : ReactiveObject +{ + public MyReactiveClass() + { + // The value of MyProperty will be "Default Value" until the Observable is initialized + _myPrpertyHelper = MyPropertyObservable() + .ToProperty(this, nameof(MyProperty)); + } + + [ObservableAsProperty(InitialValue = "Default Value")] + public string MyProperty { get; } + + public IObservable MyPropertyObservable() => Observable.Return("Test Value"); +} +``` + ## Usage ReactiveCommand `[ReactiveCommand]` ### Usage ReactiveCommand without parameter diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs new file mode 100644 index 0000000..1ceb7dd --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -0,0 +1,54 @@ +//HintName: ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.cs +// Copyright (c) 2025 .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; + + /// + /// Gets the AccessModifier of the OAPH property. + /// + /// + /// The AccessModifier of the OAPH property, protected if true. + /// + public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs new file mode 100644 index 0000000..fc5c288 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromField#TestNs.TestVM.ObservableAsProperties.g.verified.cs @@ -0,0 +1,23 @@ +//HintName: TestNs.TestVM.ObservableAsProperties.g.cs +// +#pragma warning disable +#nullable enable +namespace TestNs +{ + /// + /// Partial class for the TestVM which contains ReactiveUI Observable As Property initialization. + /// + public partial class TestVM + { + /// + private readonly ReactiveUI.ObservableAsPropertyHelper _testPropertyHelper; + + /// + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.Runtime.Serialization.DataMemberAttribute()] + [global::System.Text.Json.Serialization.JsonIncludeAttribute()] + public double? TestProperty { get => _testProperty = (_testPropertyHelper == null ? _testProperty : _testPropertyHelper.Value); } + } +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs index d243e9b..1ceb7dd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethodsWithName#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethodsWithName#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs index d243e9b..1ceb7dd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethodsWithName#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethodsWithName#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableProp#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableProp#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs index d243e9b..1ceb7dd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableProp#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableProp#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropNestedClasses#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropNestedClasses#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs index d243e9b..1ceb7dd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropNestedClasses#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropNestedClasses#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttribute#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttribute#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs index d243e9b..1ceb7dd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttribute#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttribute#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttribute#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttribute#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs index ac0b780..faa3ac7 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttribute#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttribute#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs @@ -20,6 +20,7 @@ public partial class TestVM /// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.Runtime.Serialization.DataMemberAttribute()] [global::System.Text.Json.Serialization.JsonIncludeAttribute()] public int Test5Property { get => _test5Property = _test5PropertyHelper?.Value ?? _test5Property; } 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 index d243e9b..1ceb7dd 100644 --- 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 @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs index a140e04..4019a14 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeNullableRef#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs @@ -20,6 +20,7 @@ public partial class TestVM /// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.Runtime.Serialization.DataMemberAttribute()] [global::System.Text.Json.Serialization.JsonIncludeAttribute()] public object? Test7Property { get => _test7Property = (_test7PropertyHelper == null ? _test7Property : _test7PropertyHelper.Value); } 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 index d243e9b..1ceb7dd 100644 --- 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 @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs index c480a82..794f535 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithAttributeRef#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs @@ -20,6 +20,7 @@ public partial class TestVM /// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.Runtime.Serialization.DataMemberAttribute()] [global::System.Text.Json.Serialization.JsonIncludeAttribute()] public object Test6Property { get => _test6Property = _test6PropertyHelper?.Value ?? _test6Property; } diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithName#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithName#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs index d243e9b..1ceb7dd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithName#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservablePropertiesWithName#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialProperty#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialProperty#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs new file mode 100644 index 0000000..1ceb7dd --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialProperty#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -0,0 +1,54 @@ +//HintName: ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.cs +// Copyright (c) 2025 .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; + + /// + /// Gets the AccessModifier of the OAPH property. + /// + /// + /// The AccessModifier of the OAPH property, protected if true. + /// + public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialProperty#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialProperty#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs new file mode 100644 index 0000000..71a06ba --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialProperty#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs @@ -0,0 +1,34 @@ +//HintName: TestNs.TestVM.ObservableAsPropertyFromObservable.g.cs +// +using ReactiveUI; + +#pragma warning disable +#nullable enable + +namespace TestNs +{ + /// + /// Partial class for the TestVM which contains ReactiveUI Observable As Property initialization. + /// + public partial class TestVM + { + /// + private double? _testProperty; + + /// + private ReactiveUI.ObservableAsPropertyHelper? _testPropertyHelper; + + /// + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.Runtime.Serialization.DataMemberAttribute()] + public partial double? TestProperty { get => _testProperty = _testPropertyHelper?.Value ?? _testProperty; } + + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + protected void InitializeOAPH() + { + + } + } +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs index d243e9b..1ceb7dd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromFieldNestedClass#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromFieldNestedClass#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs index d243e9b..1ceb7dd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromFieldNestedClass#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.FromFieldNestedClass#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NamedFromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NamedFromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs index d243e9b..1ceb7dd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NamedFromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NamedFromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs index d243e9b..1ceb7dd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromField#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromFieldProtected#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromFieldProtected#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs index d243e9b..1ceb7dd 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromFieldProtected#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPGeneratorTests.NonReadOnlyFromFieldProtected#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs @@ -41,6 +41,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #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 a1c328d..0dee610 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs @@ -259,4 +259,86 @@ public partial class TestVM : ReactiveObject // Act: Initialize the helper and run the generator. Assert: Verify the generated code. return TestHelper.TestPass(sourceCode); } + + /// + /// Tests that the source generator correctly generates observable properties. + /// + /// + /// A task to monitor the async. + /// + [Fact] + public Task FromField() + { + // 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; + using System.Reactive.Subjects; + + namespace TestNs; + + public partial class TestVM : ReactiveObject + { + private readonly Subject _testSubject = new(); + + [property: JsonInclude] + [DataMember] + [ObservableAsProperty] + private double? _testProperty = 1.1d; + + public TestVM() + { + _testPropertyHelper = _testSubject.ToProperty(this, nameof(TestProperty)); + } + } + """; + + // Act: Initialize the helper and run the generator. Assert: Verify the generated code. + return TestHelper.TestPass(sourceCode); + } + + /// + /// Tests that the source generator correctly generates observable properties. + /// + /// + /// A task to monitor the async. + /// + [Fact] + public Task FromPartialProperty() + { + // 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; + using System.Reactive.Subjects; + + namespace TestNs; + + public partial class TestVM : ReactiveObject + { + private readonly Subject _testSubject = new(); + + public TestVM() + { + _testPropertyHelper = _testSubject.ToProperty(this, nameof(TestProperty)); + } + + [JsonInclude] + [DataMember] + [ObservableAsProperty(InitialValue = "1.1d")] + public partial double? TestProperty { get; } + } + """; + + // Act: Initialize the helper and run the generator. Assert: Verify the generated code. + return TestHelper.TestPass(sourceCode); + } } diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index 2ed0103..6b0ad5c 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -28,7 +28,8 @@ public partial class TestViewModel : ReactiveObject, IActivatableViewModel, IDis private readonly IObservable _observable = Observable.Return(true); private readonly Subject _testSubject = new(); private readonly Subject _testNonNullSubject = new(); - private IScheduler _scheduler = RxApp.MainThreadScheduler; + private readonly Subject _fromPartialTestSubject = new(); + private readonly IScheduler _scheduler = RxApp.MainThreadScheduler; [property: JsonInclude] [DataMember] @@ -181,6 +182,10 @@ public TestViewModel() Test10Command?.Execute(200).Subscribe(r => Console.Out.WriteLine(r)); TestPrivateCanExecuteCommand?.Execute().Subscribe(); + Console.Out.WriteLine($"Observable unset, value should be 10, value is : {ObservableAsPropertyFromProperty}"); + _observableAsPropertyFromPropertyHelper = _fromPartialTestSubject.ToProperty(this, x => x.ObservableAsPropertyFromProperty); + _fromPartialTestSubject.OnNext(11); + Console.Out.WriteLine($"Observable updated, value should be 11, value is : {ObservableAsPropertyFromProperty}"); Console.ReadLine(); } @@ -267,6 +272,15 @@ public TestViewModel() /// public ViewModelActivator Activator { get; } = new(); + /// + /// Gets the observable as property from property. + /// + /// + /// The observable as property from property. + /// + [ObservableAsProperty(InitialValue = "10")] + public partial int ObservableAsPropertyFromProperty { get; } + [ObservableAsProperty] private IObservable ReferenceTypeObservable { get; } @@ -311,6 +325,7 @@ protected virtual void Dispose(bool disposing) { _testSubject.Dispose(); _testNonNullSubject.Dispose(); + _fromPartialTestSubject.Dispose(); } _disposedValue = true; diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs index bbea4ac..b678a65 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs @@ -226,7 +226,7 @@ internal sealed class ReactiveAttribute : Attribute #endif public const string ObservableAsPropertyAttributeType = "ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute"; - +#if ROSYLN_412 public static string ObservableAsPropertyAttribute => $$""" // Copyright (c) {{DateTime.Now.Year}} .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. @@ -271,11 +271,68 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The AccessModifier of the OAPH property, protected if true. /// public bool UseProtected { get; init; } = false; + + /// + /// Gets the Initial value of the OAPH property. + /// + /// + /// The initial value of the OAPH property. + /// + public string? InitialValue { get; init; } } #nullable restore #pragma warning restore """; +#else + public static string ObservableAsPropertyAttribute => $$""" +// Copyright (c) {{DateTime.Now.Year}} .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. +/// +/// +[global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", "1.1.0.0")] +[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; + /// + /// Gets the AccessModifier of the OAPH property. + /// + /// + /// The AccessModifier of the OAPH property, protected if true. + /// + public bool UseProtected { get; init; } = false; +} +#nullable restore +#pragma warning restore +"""; +#endif public const string IViewForAttributeType = "ReactiveUI.SourceGenerators.IViewForAttribute"; public static string IViewForAttribute => $$""" diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs index 668184c..97ebcc4 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/AttributeDataExtensions.cs @@ -140,147 +140,6 @@ static void GatherForwardedAttributes( classAttributesInfo = classAttributesInfoBuilder.ToImmutable(); } - /// - /// Gathers all forwarded attributes for the generated field and property. - /// - /// The input instance to process. - /// The instance for the current run. - /// The method declaration. - /// The cancellation token for the current operation. - /// The resulting property attributes to forward. - public static void GatherForwardedAttributesFromMethod( - this IMethodSymbol methodSymbol, - SemanticModel semanticModel, - MethodDeclarationSyntax methodDeclaration, - CancellationToken token, - out ImmutableArray propertyAttributeInfos) - { - using var propertyAttributesInfo = ImmutableArrayBuilder.Rent(); - - static void GatherForwardedAttributesFromMethod( - IMethodSymbol methodSymbol, - SemanticModel semanticModel, - MethodDeclarationSyntax methodDeclaration, - CancellationToken token, - ImmutableArrayBuilder propertyAttributesInfos) - { - // Get the single syntax reference for the input method symbol (there should be only one) - if (methodSymbol.DeclaringSyntaxReferences is not [SyntaxReference syntaxReference]) - { - return; - } - - // Gather explicit forwarded attributes info - foreach (var attributeList in methodDeclaration.AttributeLists) - { - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) - { - continue; - } - - foreach (var attribute in attributeList.Attributes) - { - if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) - { - continue; - } - - var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out var attributeInfo)) - { - continue; - } - - // Add the new attribute info to the right builder - if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.PropertyKeyword)) - { - propertyAttributesInfos.Add(attributeInfo); - } - } - } - } - - // If the method is a partial definition, also gather attributes from the implementation part - if (methodSymbol is { IsPartialDefinition: true } or { PartialDefinitionPart: not null }) - { - var partialDefinition = methodSymbol.PartialDefinitionPart ?? methodSymbol; - var partialImplementation = methodSymbol.PartialImplementationPart ?? methodSymbol; - - // We always give priority to the partial definition, to ensure a predictable and testable ordering - GatherForwardedAttributesFromMethod(partialDefinition, semanticModel, methodDeclaration, token, propertyAttributesInfo); - GatherForwardedAttributesFromMethod(partialImplementation, semanticModel, methodDeclaration, token, propertyAttributesInfo); - } - else - { - // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications - GatherForwardedAttributesFromMethod(methodSymbol, semanticModel, methodDeclaration, token, propertyAttributesInfo); - } - - propertyAttributeInfos = propertyAttributesInfo.ToImmutable(); - } - - public static void GatherForwardedAttributesFromProperty( - this IPropertySymbol propertySymbol, - SemanticModel semanticModel, - PropertyDeclarationSyntax propertyDeclaration, - CancellationToken token, - out ImmutableArray propertyAttributesInfos) - { - using var propertyAttributesInfo = ImmutableArrayBuilder.Rent(); - - static void GatherForwardedAttributesFromProperty( - IPropertySymbol propertySymbol, - SemanticModel semanticModel, - PropertyDeclarationSyntax propertyDeclaration, - CancellationToken token, - ImmutableArrayBuilder propertyAttributesInfos) - { - // Get the single syntax reference for the input method symbol (there should be only one) - if (propertySymbol.DeclaringSyntaxReferences is not [SyntaxReference syntaxReference]) - { - return; - } - - // Gather explicit forwarded attributes info - foreach (var attributeList in propertyDeclaration.AttributeLists) - { - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) - { - continue; - } - - foreach (var attribute in attributeList.Attributes) - { - if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) - { - continue; - } - - var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out var attributeInfo)) - { - continue; - } - - // Add the new attribute info to the right builder - if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.PropertyKeyword)) - { - propertyAttributesInfos.Add(attributeInfo); - } - } - } - } - - // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications - GatherForwardedAttributesFromProperty(propertySymbol, semanticModel, propertyDeclaration, token, propertyAttributesInfo); - - propertyAttributesInfos = propertyAttributesInfo.ToImmutable(); - } - /// /// Gets the type of the generic. /// diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ContextExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ContextExtensions.cs new file mode 100644 index 0000000..e8858dd --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/ContextExtensions.cs @@ -0,0 +1,107 @@ +// 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.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; +using static ReactiveUI.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace ReactiveUI.SourceGenerators.Extensions; + +internal static class ContextExtensions +{ + internal static void GetForwardedAttributes( + this in GeneratorAttributeSyntaxContext context, + ImmutableArrayBuilder builder, + ISymbol symbol, + in SyntaxList attributeListSyntaxes, + CancellationToken token, + out ImmutableArray forwardedAttributes) + { + using var forwardedAttributeBuilder = ImmutableArrayBuilder.Rent(); + + // Gather attributes info + foreach (var attribute in symbol.GetAttributes()) + { + token.ThrowIfCancellationRequested(); + + // Track the current attribute for forwarding if it is a validation attribute + if (attribute.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true) + { + forwardedAttributeBuilder.Add(AttributeInfo.Create(attribute)); + } + + // Track the current attribute for forwarding if it is a Json Serialization attribute + if (attribute.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.Text.Json.Serialization.JsonAttribute") == true) + { + forwardedAttributeBuilder.Add(AttributeInfo.Create(attribute)); + } + + // Also track the current attribute for forwarding if it is of any of the following types: + if (attribute.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || + attribute.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || + attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || + attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || + attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true || + attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.DataMemberAttribute") == true || + attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.IgnoreDataMemberAttribute") == true) + { + forwardedAttributeBuilder.Add(AttributeInfo.Create(attribute)); + } + } + + token.ThrowIfCancellationRequested(); + + // Gather explicit forwarded attributes info + foreach (var attributeList in attributeListSyntaxes) + { + // Only look for attribute lists explicitly targeting the (generated) property. Roslyn will normally emit a + // CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic suppressor + // that recognizes uses of this target specifically to support [ObservableAsProperty]. + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + foreach (var attribute in attributeList.Attributes) + { + if (!context.SemanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) + { + builder.Add( + InvalidPropertyTargetedAttributeOnObservableAsPropertyField, + attribute, + symbol, + attribute.Name); + continue; + } + + var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); + + // Try to extract the forwarded attribute + if (!AttributeInfo.TryCreate(attributeTypeSymbol, context.SemanticModel, attributeArguments, token, out var attributeInfo)) + { + builder.Add( + InvalidPropertyTargetedAttributeExpressionOnObservableAsPropertyField, + attribute, + symbol, + attribute.Name); + continue; + } + + forwardedAttributeBuilder.Add(attributeInfo); + } + } + + var attributes = forwardedAttributeBuilder.ToImmutable(); + forwardedAttributes = attributes.Select(static a => a.ToString()).ToImmutableArray(); + } +} diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs index 65eb662..ffb47ac 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs @@ -114,4 +114,32 @@ internal static void GetNullabilityInfo( propertySymbol.Type.NullableAnnotation != NullableAnnotation.Annotated && semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); } + + /// + /// Validates the containing type for a given field being annotated. + /// + /// The input instance to process. + /// Whether or not the containing type for is valid. + internal static bool IsTargetTypeValid(this IFieldSymbol fieldSymbol) + { + var isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); + var isIObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); + var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); + + return isIObservableObject || isObservableObject || hasObservableObjectAttribute; + } + + /// + /// Validates the containing type for a given field being annotated. + /// + /// The input instance to process. + /// Whether or not the containing type for is valid. + internal static bool IsTargetTypeValid(this IPropertySymbol propertySymbol) + { + var isObservableObject = propertySymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); + var isIObservableObject = propertySymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); + var hasObservableObjectAttribute = propertySymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); + + return isIObservableObject || isObservableObject || hasObservableObjectAttribute; + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableMethodInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableMethodInfo.cs index 48b8869..2afb46c 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableMethodInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/Models/ObservableMethodInfo.cs @@ -19,8 +19,13 @@ internal record ObservableMethodInfo( bool IsNullableType, bool IsProperty, EquatableArray ForwardedPropertyAttributes, - string AccessModifier) + string AccessModifier, + string? InitialValue) { + public bool IsFromPartialProperty => ObservableType.Contains("##FromPartialProperty##"); + + public string PartialPropertyType => ObservableType.Replace("##FromPartialProperty##", string.Empty); + public string GetGeneratedFieldName() { var propertyName = PropertyName; diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs index 57b24fa..701ae59 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.Execute.cs @@ -3,9 +3,8 @@ // 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.Collections.Generic; +using System; using System.Collections.Immutable; -using System.Globalization; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; @@ -43,7 +42,7 @@ public sealed partial class ObservableAsPropertyGenerator } // Validate the target type - if (!IsTargetTypeValid(fieldSymbol)) + if (!fieldSymbol.IsTargetTypeValid()) { builder.Add( InvalidObservableAsPropertyError, @@ -76,7 +75,7 @@ public sealed partial class ObservableAsPropertyGenerator // Get the property type and name var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); var fieldName = fieldSymbol.Name; - var propertyName = GetGeneratedPropertyName(fieldSymbol); + var propertyName = fieldSymbol.GetGeneratedPropertyName(); // Check for name collisions if (fieldName == propertyName) @@ -94,107 +93,22 @@ public sealed partial class ObservableAsPropertyGenerator token.ThrowIfCancellationRequested(); - using var forwardedAttributes = ImmutableArrayBuilder.Rent(); - - // Gather attributes info - foreach (var attribute in fieldSymbol.GetAttributes()) - { - token.ThrowIfCancellationRequested(); - - // Track the current attribute for forwarding if it is a validation attribute - if (attribute.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true) - { - forwardedAttributes.Add(AttributeInfo.Create(attribute)); - } - - // Track the current attribute for forwarding if it is a Json Serialization attribute - if (attribute.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.Text.Json.Serialization.JsonAttribute") == true) - { - forwardedAttributes.Add(AttributeInfo.Create(attribute)); - } - - // Also track the current attribute for forwarding if it is of any of the following types: - if (attribute.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || - attribute.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.DataMemberAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.IgnoreDataMemberAttribute") == true) - { - forwardedAttributes.Add(AttributeInfo.Create(attribute)); - } - } - - token.ThrowIfCancellationRequested(); - - // Gather explicit forwarded attributes info - foreach (var attributeList in fieldDeclaration.AttributeLists) - { - // Only look for attribute lists explicitly targeting the (generated) property. Roslyn will normally emit a - // CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic suppressor - // that recognizes uses of this target specifically to support [ObservableAsProperty]. - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) - { - continue; - } - - token.ThrowIfCancellationRequested(); - - foreach (var attribute in attributeList.Attributes) - { - // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the AttributeData as usual. - // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: - // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not - // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. - // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. - // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. - // - We then go over each attribute argument expression and get the operation for it. This will still be available even - // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all - // constant values to build the AttributeInfo model. After all, attributes only support constant values, typeof(T) - // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. - // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. - // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the - // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the - // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. - if (!context.SemanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) - { - builder.Add( - InvalidPropertyTargetedAttributeOnObservableAsPropertyField, - attribute, - fieldSymbol, - attribute.Name); - continue; - } - - var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, context.SemanticModel, attributeArguments, token, out var attributeInfo)) - { - builder.Add( - InvalidPropertyTargetedAttributeExpressionOnObservableAsPropertyField, - attribute, - fieldSymbol, - attribute.Name); - continue; - } - - forwardedAttributes.Add(attributeInfo); - } - } + context.GetForwardedAttributes( + builder, + fieldSymbol, + fieldDeclaration.AttributeLists, + token, + out var forwardedPropertyAttributes); token.ThrowIfCancellationRequested(); // Get the nullability info for the property fieldSymbol.GetNullabilityInfo( - context.SemanticModel, - out var isReferenceTypeOrUnconstraindTypeParameter, - out var includeMemberNotNullOnSetAccessor); + context.SemanticModel, + out var isReferenceTypeOrUnconstraindTypeParameter, + out var includeMemberNotNullOnSetAccessor); token.ThrowIfCancellationRequested(); - var attributes = forwardedAttributes.ToImmutable(); - var forwardedPropertyAttributes = attributes.Select(static a => a.ToString()).ToImmutableArray(); // Get the containing type info var targetInfo = TargetInfo.From(fieldSymbol.ContainingType); @@ -291,39 +205,4 @@ private static string GetPropertySyntax(ObservableFieldInfo propertyInfo) public {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} {{getter}} """; } - - /// - /// Get the generated property name for an input field. - /// - /// The input instance to process. - /// The generated property name for . - private static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol) - { - var propertyName = fieldSymbol.Name; - - if (propertyName.StartsWith("m_")) - { - propertyName = propertyName.Substring(2); - } - else if (propertyName.StartsWith("_")) - { - propertyName = propertyName.TrimStart('_'); - } - - return $"{char.ToUpper(propertyName[0], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}"; - } - - /// - /// Validates the containing type for a given field being annotated. - /// - /// The input instance to process. - /// Whether or not the containing type for is valid. - private static bool IsTargetTypeValid(IFieldSymbol fieldSymbol) - { - var isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); - var isIObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); - - return isIObservableObject || isObservableObject || hasObservableObjectAttribute; - } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs index deee37f..5e08985 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs @@ -5,9 +5,10 @@ using System.Collections.Immutable; using System.Linq; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using ReactiveUI.SourceGenerators.Extensions; + using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators; diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs index df8d55e..11080b5 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs @@ -33,6 +33,9 @@ public sealed partial class ObservableAsPropertyGenerator // Get the can PropertyName member, if any attributeData.TryGetNamedArgument("PropertyName", out string? propertyName); + // Get the can InitialValue member, if any + attributeData.TryGetNamedArgument("InitialValue", out string? initialValue); + token.ThrowIfCancellationRequested(); attributeData.TryGetNamedArgument("UseProtected", out bool useProtected); @@ -54,15 +57,18 @@ public sealed partial class ObservableAsPropertyGenerator } var isObservable = methodSymbol.ReturnType.IsObservableReturnType(); + if (!isObservable) + { + return default; + } token.ThrowIfCancellationRequested(); - - methodSymbol.GatherForwardedAttributesFromMethod( - context.SemanticModel, - methodSyntax, + context.GetForwardedAttributes( + diagnostics, + methodSymbol, + methodSyntax.AttributeLists, token, - out var attributes); - var propertyAttributes = attributes.Select(x => x.ToString()).ToImmutableArray(); + out var propertyAttributes); token.ThrowIfCancellationRequested(); @@ -88,33 +94,103 @@ public sealed partial class ObservableAsPropertyGenerator isNullableType, false, propertyAttributes, - useProtectedModifier), + useProtectedModifier, + initialValue), diagnostics.ToImmutable()); } if (context.TargetNode is PropertyDeclarationSyntax propertySyntax) { - var propertySymbol = (IPropertySymbol)symbol!; - var isObservable = propertySymbol.Type.IsObservableReturnType(); + if (symbol is not IPropertySymbol propertySymbol) + { + return default; + } - token.ThrowIfCancellationRequested(); + var observableType = string.Empty; + var isNullableType = false; - propertySymbol.GatherForwardedAttributesFromProperty( - context.SemanticModel, - propertySyntax, + token.ThrowIfCancellationRequested(); + context.GetForwardedAttributes( + diagnostics, + propertySymbol, + propertySyntax.AttributeLists, token, - out var attributes); - var propertyAttributes = attributes.Select(x => x.ToString()).ToImmutableArray(); + out var propertyAttributes); token.ThrowIfCancellationRequested(); - var observableType = propertySymbol.Type is not INamedTypeSymbol typeSymbol - ? string.Empty - : typeSymbol.TypeArguments[0].GetFullyQualifiedNameWithNullabilityAnnotations(); + if (propertySymbol.Type.IsObservableReturnType()) + { + observableType = propertySymbol.Type is not INamedTypeSymbol typeSymbol + ? string.Empty + : typeSymbol.TypeArguments[0].GetFullyQualifiedNameWithNullabilityAnnotations(); - var isNullableType = propertySymbol.Type is INamedTypeSymbol nullcheck && nullcheck.TypeArguments[0].IsNullableType(); + token.ThrowIfCancellationRequested(); - token.ThrowIfCancellationRequested(); + isNullableType = propertySymbol.Type is INamedTypeSymbol nullcheck && nullcheck.TypeArguments[0].IsNullableType(); + } +#if ROSYLN_412 + else + { + if (!propertySymbol.IsPartialDefinition || propertySymbol.IsStatic) + { + return default; + } + + // Validate the target type + if (!propertySymbol.IsTargetTypeValid()) + { + diagnostics.Add( + InvalidObservableAsPropertyError, + propertySymbol, + propertySymbol.ContainingType, + propertySymbol.Name); + return new(default, diagnostics.ToImmutable()); + } + + token.ThrowIfCancellationRequested(); + + var inheritance = propertySymbol.IsVirtual ? " virtual" : propertySymbol.IsOverride ? " override" : string.Empty; + + // Get the can ReadOnly member, if any + attributeData.TryGetNamedArgument("ReadOnly", out bool? isReadonly); + + token.ThrowIfCancellationRequested(); + + // Get the property type and name + var typeNameWithNullabilityAnnotations = propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); + + // Get the field name + var fieldName = propertySymbol.GetGeneratedFieldName(); + propertyName = propertySymbol.Name; + + // Check for names for collisions + if (fieldName == propertyName) + { + diagnostics.Add( + ReactivePropertyNameCollisionError, + propertySymbol, + propertySymbol.ContainingType, + propertySymbol.Name); + return new(default, diagnostics.ToImmutable()); + } + + var propertyDeclaration = (PropertyDeclarationSyntax)context.TargetNode!; + + token.ThrowIfCancellationRequested(); + + context.GetForwardedAttributes( + diagnostics, + propertySymbol, + propertyDeclaration.AttributeLists, + token, + out var forwardedPropertyAttributes); + + token.ThrowIfCancellationRequested(); + + observableType = "##FromPartialProperty##" + typeNameWithNullabilityAnnotations; + } +#endif // Get the containing type info var targetInfo = TargetInfo.From(propertySymbol.ContainingType); @@ -130,7 +206,8 @@ public sealed partial class ObservableAsPropertyGenerator isNullableType, true, propertyAttributes, - useProtectedModifier), + useProtectedModifier, + initialValue), diagnostics.ToImmutable()); } @@ -198,16 +275,25 @@ private static string GetPropertySyntax(ObservableMethodInfo propertyInfo) ? $"{getterFieldIdentifierName} = ({getterFieldIdentifierName}Helper == null ? {getterFieldIdentifierName} : {getterFieldIdentifierName}Helper.Value)" : $"{getterFieldIdentifierName} = {getterFieldIdentifierName}Helper?.Value ?? {getterFieldIdentifierName}"; + var isPartialProperty = string.Empty; + var propertyType = propertyInfo.ObservableType; + var initialValue = string.IsNullOrWhiteSpace(propertyInfo.InitialValue) ? string.Empty : " = " + propertyInfo.InitialValue; + if (propertyInfo.IsFromPartialProperty) + { + isPartialProperty = "partial "; + propertyType = propertyInfo.PartialPropertyType; + } + return $$""" /// - private {{propertyInfo.ObservableType}} {{getterFieldIdentifierName}}; + private {{propertyType}} {{getterFieldIdentifierName}}{{initialValue}}; /// - {{propertyInfo.AccessModifier}} ReactiveUI.ObservableAsPropertyHelper<{{propertyInfo.ObservableType}}>? {{getterFieldIdentifierName}}Helper; + {{propertyInfo.AccessModifier}} ReactiveUI.ObservableAsPropertyHelper<{{propertyType}}>? {{getterFieldIdentifierName}}Helper; /// {{propertyAttributes}} - public {{propertyInfo.ObservableType}} {{propertyInfo.PropertyName}} { get => {{getterArrowExpression}}; } + public {{isPartialProperty}}{{propertyType}} {{propertyInfo.PropertyName}} { get => {{getterArrowExpression}}; } """; } @@ -217,6 +303,11 @@ private static string GetPropertyInitiliser(ObservableMethodInfo[] propertyInfos foreach (var propertyInfo in propertyInfos) { + if (propertyInfo.IsFromPartialProperty) + { + continue; + } + var fieldIdentifierName = propertyInfo.GetGeneratedFieldName(); if (propertyInfo.IsProperty) { diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs index 72b5067..f1f3a5f 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs @@ -9,7 +9,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators; diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs index d64e46a..f75c55f 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; @@ -48,7 +47,7 @@ public sealed partial class ReactiveGenerator return default; } - if (!IsTargetTypeValid(propertySymbol)) + if (!propertySymbol.IsTargetTypeValid()) { builder.Add( InvalidReactiveError, @@ -132,7 +131,7 @@ public sealed partial class ReactiveGenerator return default; } - if (!IsTargetTypeValid(fieldSymbol)) + if (!fieldSymbol.IsTargetTypeValid()) { builder.Add( InvalidReactiveError, @@ -200,98 +199,16 @@ public sealed partial class ReactiveGenerator out var isReferenceTypeOrUnconstraindTypeParameter, out var includeMemberNotNullOnSetAccessor); - using var forwardedAttributes = ImmutableArrayBuilder.Rent(); - - // Gather attributes info - foreach (var attribute in fieldSymbol.GetAttributes()) - { - token.ThrowIfCancellationRequested(); - - // Track the current attribute for forwarding if it is a validation attribute - if (attribute.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true) - { - forwardedAttributes.Add(AttributeInfo.Create(attribute)); - } - - // Track the current attribute for forwarding if it is a Json Serialization attribute - if (attributeData.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.Text.Json.Serialization.JsonAttribute") == true) - { - forwardedAttributes.Add(AttributeInfo.Create(attribute)); - } - - // Also track the current attribute for forwarding if it is of any of the following types: - if (attribute.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || - attribute.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.DataMemberAttribute") == true || - attribute.AttributeClass?.HasFullyQualifiedMetadataName("System.Runtime.Serialization.IgnoreDataMemberAttribute") == true) - { - forwardedAttributes.Add(AttributeInfo.Create(attribute)); - } - } - token.ThrowIfCancellationRequested(); var fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; - // Gather explicit forwarded attributes info - foreach (var attributeList in fieldDeclaration.AttributeLists) - { - // Only look for attribute lists explicitly targeting the (generated) property. Roslyn will normally emit a - // CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic suppressor - // that recognizes uses of this target specifically to support [Reactive]. - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) - { - continue; - } - - token.ThrowIfCancellationRequested(); - - foreach (var attribute in attributeList.Attributes) - { - // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the AttributeData as usual. - // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: - // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not - // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. - // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. - // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. - // - We then go over each attribute argument expression and get the operation for it. This will still be available even - // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all - // constant values to build the AttributeInfo model. After all, attributes only support constant values, typeof(T) - // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. - // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. - // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the - // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the - // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. - if (!context.SemanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) - { - builder.Add( - InvalidPropertyTargetedAttributeOnReactiveField, - attribute, - fieldSymbol, - attribute.Name); - continue; - } - - var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, context.SemanticModel, attributeArguments, token, out var attributeInfo)) - { - builder.Add( - InvalidPropertyTargetedAttributeExpressionOnReactiveField, - attribute, - fieldSymbol, - attribute.Name); - continue; - } - - forwardedAttributes.Add(attributeInfo); - } - } + context.GetForwardedAttributes( + builder, + fieldSymbol, + fieldDeclaration.AttributeLists, + token, + out var forwardedAttributesString); - var forwardedAttributesString = forwardedAttributes.ToImmutable().Select(x => x.ToString()).ToImmutableArray(); token.ThrowIfCancellationRequested(); // Get the containing type info @@ -420,27 +337,4 @@ private static string GetPropertySyntax(PropertyInfo propertyInfo) {{propertyInfo.TargetInfo.TargetVisibility}}{{propertyInfo.Inheritance}} {{partialModifier}}{{propertyInfo.UseRequired}}{{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} { get => {{propertyInfo.FieldName}}; {{propertyInfo.AccessModifier}} => this.RaiseAndSetIfChanged(ref {{propertyInfo.FieldName}}, value); } """; } - - /// - /// Validates the containing type for a given field being annotated. - /// - /// The input instance to process. - /// Whether or not the containing type for is valid. - private static bool IsTargetTypeValid(IFieldSymbol fieldSymbol) - { - var isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); - var isIObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); - - return isIObservableObject || isObservableObject || hasObservableObjectAttribute; - } - - private static bool IsTargetTypeValid(IPropertySymbol propertySymbol) - { - var isObservableObject = propertySymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); - var isIObservableObject = propertySymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = propertySymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); - - return isIObservableObject || isObservableObject || hasObservableObjectAttribute; - } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs index 21ec950..c93df65 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs @@ -53,6 +53,7 @@ public partial class ReactiveCommandGenerator } token.ThrowIfCancellationRequested(); + using var builder = ImmutableArrayBuilder.Rent(); var isTask = methodSymbol.ReturnType.IsTaskReturnType(); var isObservable = methodSymbol.ReturnType.IsObservableReturnType(); @@ -91,8 +92,14 @@ public partial class ReactiveCommandGenerator token.ThrowIfCancellationRequested(); var methodSyntax = (MethodDeclarationSyntax)context.TargetNode; - methodSymbol.GatherForwardedAttributesFromMethod(context.SemanticModel, methodSyntax, token, out var attributes); - var forwardedPropertyAttributes = attributes.Select(static a => a.ToString()).ToImmutableArray(); + + context.GetForwardedAttributes( + builder, + methodSymbol, + methodSyntax.AttributeLists, + token, + out var forwardedPropertyAttributes); + token.ThrowIfCancellationRequested(); // Get the containing type info diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveUI.SourceGenerators.Roslyn.projitems b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveUI.SourceGenerators.Roslyn.projitems index b4fc3dc..538ce7c 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveUI.SourceGenerators.Roslyn.projitems +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveUI.SourceGenerators.Roslyn.projitems @@ -12,6 +12,7 @@ + diff --git a/version.json b/version.json index d79cdab..9242147 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "2.0", + "version": "2.1", "publicReleaseRefSpec": [ "^refs/heads/master$", // we release out of master "^refs/heads/main$",