diff --git a/README.md b/README.md index 2027d2b..d45b0ed 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,17 @@ public partial class MyReactiveClass : ReactiveObject } ``` +### Usage Reactive property with set Access Modifier +```csharp +using ReactiveUI.SourceGenerators; + +public partial class MyReactiveClass : ReactiveObject +{ + [Reactive(SetModifier = AccessModifier.Protected)] + private string _myProperty; +} +``` + ## Usage ObservableAsPropertyHelper `[ObservableAsProperty]` ### Usage ObservableAsPropertyHelper with Field diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index e5cacd3..245e4c2 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using System.Runtime.Serialization; using System.Text.Json.Serialization; +using System.Windows.Media.TextFormatting; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -26,7 +27,7 @@ public partial class TestViewModel : ReactiveObject private double _test2Property = 1.1d; [JsonInclude] - [Reactive] + [Reactive(SetModifier = AccessModifier.Protected)] [DataMember] private int _test1Property = 10; diff --git a/src/ReactiveUI.SourceGenerators.slnx b/src/ReactiveUI.SourceGenerators.slnx index 4adf8d0..752cecd 100644 --- a/src/ReactiveUI.SourceGenerators.slnx +++ b/src/ReactiveUI.SourceGenerators.slnx @@ -23,6 +23,10 @@ + + + + diff --git a/src/ReactiveUI.SourceGenerators/Core/Helpers/AttributeDefinitions.cs b/src/ReactiveUI.SourceGenerators/Core/Helpers/AttributeDefinitions.cs index 8ae6dc2..3e4692d 100644 --- a/src/ReactiveUI.SourceGenerators/Core/Helpers/AttributeDefinitions.cs +++ b/src/ReactiveUI.SourceGenerators/Core/Helpers/AttributeDefinitions.cs @@ -12,6 +12,34 @@ internal static class AttributeDefinitions public const string GeneratedCode = "global::System.CodeDom.Compiler.GeneratedCode"; public const string ExcludeFromCodeCoverage = "global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"; public const string Obsolete = "global::System.Obsolete"; + + public const string AccessModifierType = "ReactiveUI.SourceGenerators.AccessModifier"; + public const string AccessModifierEnum = + """ + // 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. + + // + #pragma warning disable + #nullable enable + namespace ReactiveUI.SourceGenerators; + + /// + /// AccessModifier. + /// + internal enum AccessModifier + { + Public, + Protected, + Internal, + Private, + InternalProtected, + PrivateProtected, + } + """; + public const string ReactiveObjectAttribute = """ // Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. @@ -90,7 +118,16 @@ namespace ReactiveUI.SourceGenerators; /// [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ReactiveGenerator", "1.1.0.0")] [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] -internal sealed class ReactiveAttribute : Attribute; +internal sealed class ReactiveAttribute : Attribute +{ + /// + /// Gets the AccessModifier of the set property. + /// + /// + /// The AccessModifier of the set property. + /// + public AccessModifier SetModifier { get; init; } +} #nullable restore #pragma warning restore """; diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs index af1d4b1..05617fa 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs @@ -272,7 +272,8 @@ internal static bool GetFieldInfoFromClass( initializer, isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor, - forwardedAttributes.ToImmutable()); + forwardedAttributes.ToImmutable(), + "public"); diagnostics = builder.ToImmutable(); diff --git a/src/ReactiveUI.SourceGenerators/Reactive/Models/PropertyInfo.cs b/src/ReactiveUI.SourceGenerators/Reactive/Models/PropertyInfo.cs index c39ba59..500e933 100644 --- a/src/ReactiveUI.SourceGenerators/Reactive/Models/PropertyInfo.cs +++ b/src/ReactiveUI.SourceGenerators/Reactive/Models/PropertyInfo.cs @@ -3,6 +3,7 @@ // 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 Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using ReactiveUI.SourceGenerators.Helpers; @@ -18,4 +19,5 @@ internal sealed record PropertyInfo( EqualsValueClauseSyntax? Initializer, bool IsReferenceTypeOrUnconstraindTypeParameter, bool IncludeMemberNotNullOnSetAccessor, - EquatableArray ForwardedAttributes); + EquatableArray ForwardedAttributes, + string AccessModifier); diff --git a/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs index 35cd2c8..8d024d8 100644 --- a/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.Execute.cs @@ -80,10 +80,22 @@ internal static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyI setterFieldExpression, IdentifierName("value")))); + SyntaxToken[] syntaxKinds = propertyInfo.AccessModifier switch + { + "public" => [], + "protected" => [Token(SyntaxKind.ProtectedKeyword)], + "internal" => [Token(SyntaxKind.InternalKeyword)], + "private" => [Token(SyntaxKind.PrivateKeyword)], + "internal protected" => [Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.ProtectedKeyword)], + "private protected" => [Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.ProtectedKeyword)], + _ => [], + }; + // Create the setter for the generated property: - // + // Literal(propertyInfo.AccessModifier) // set => this.RaiseAndSetIfChanged(ref , value); var setAccessor = AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .AddModifiers(syntaxKinds) .WithExpressionBody(ArrowExpressionClause(ParseExpression($"this.RaiseAndSetIfChanged(ref {getterFieldIdentifierName}, {IdentifierName("value")});"))); // Add the [MemberNotNull] attribute if needed: @@ -168,6 +180,29 @@ internal static bool GetFieldInfoFromClass( var propertyName = GetGeneratedPropertyName(fieldSymbol); var initializer = fieldSyntax.Declaration.Variables.FirstOrDefault()?.Initializer; + if (!fieldSymbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveAttributeType, out var attributeData1)) + { + propertyInfo = null; + diagnostics = builder.ToImmutable(); + + return false; + } + + token.ThrowIfCancellationRequested(); + + // Get AccessModifier enum value from the attribute + attributeData1.TryGetNamedArgument("SetModifier", out int? accessModifierArgument); + var accessModifier = accessModifierArgument switch + { + 0 => "public", + 1 => "protected", + 2 => "internal", + 3 => "private", + 4 => "internal protected", + 5 => "private protected", + _ => "public", + }; + // Check for name collisions if (fieldName == propertyName) { @@ -298,7 +333,8 @@ internal static bool GetFieldInfoFromClass( initializer, isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor, - forwardedAttributes.ToImmutable()); + forwardedAttributes.ToImmutable(), + accessModifier); diagnostics = builder.ToImmutable(); diff --git a/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.cs b/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.cs index 382a62d..252fad9 100644 --- a/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.cs +++ b/src/ReactiveUI.SourceGenerators/Reactive/ReactiveGenerator.cs @@ -27,7 +27,13 @@ public sealed partial class ReactiveGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(ctx => - ctx.AddSource($"{AttributeDefinitions.ReactiveAttributeType}.g.cs", SourceText.From(AttributeDefinitions.ReactiveAttribute, Encoding.UTF8))); + { + // Add the AccessModifier enum to the compilation + ctx.AddSource($"{AttributeDefinitions.AccessModifierType}.g.cs", SourceText.From(AttributeDefinitions.AccessModifierEnum, Encoding.UTF8)); + + // Add the ReactiveAttribute to the compilation + ctx.AddSource($"{AttributeDefinitions.ReactiveAttributeType}.g.cs", SourceText.From(AttributeDefinitions.ReactiveAttribute, Encoding.UTF8)); + }); // Gather info for all annotated command methods (starting from method declarations with at least one attribute) IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> propertyInfoWithErrors =