diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index c4cbb324e..9dc77b7d1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -48,7 +48,7 @@ - + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs index 1009cd4c1..74b456063 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs @@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using CommunityToolkit.Mvvm.SourceGenerators.Helpers; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -17,10 +18,12 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// /// A model representing an attribute declaration. /// +/// Indicates the target of the attribute. /// The type name of the attribute. /// The values for all constructor arguments for the attribute. /// The values for all named arguments for the attribute. internal sealed record AttributeInfo( + SyntaxKind AttributeTarget, string TypeName, EquatableArray ConstructorArgumentInfo, EquatableArray<(string Name, TypedConstantInfo Value)> NamedArgumentInfo) @@ -50,6 +53,7 @@ public static AttributeInfo Create(AttributeData attributeData) } return new( + SyntaxKind.PropertyKeyword, typeName, constructorArguments.ToImmutable(), namedArguments.ToImmutable()); @@ -61,6 +65,7 @@ public static AttributeInfo Create(AttributeData attributeData) /// The symbol for the attribute type. /// The instance for the current run. /// The sequence of instances to process. + /// The kind of target for the attribute. /// The cancellation token for the current operation. /// The resulting instance, if available /// Whether a resulting instance could be created. @@ -68,6 +73,7 @@ public static bool TryCreate( INamedTypeSymbol typeSymbol, SemanticModel semanticModel, IEnumerable arguments, + SyntaxKind syntaxKind, CancellationToken token, [NotNullWhen(true)] out AttributeInfo? info) { @@ -105,6 +111,7 @@ public static bool TryCreate( } info = new AttributeInfo( + syntaxKind, typeName, constructorArguments.ToImmutable(), namedArguments.ToImmutable()); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 52b7f44fe..b127bc7a3 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -216,10 +216,10 @@ public static bool TryGetInfo( // Gather explicit forwarded attributes info foreach (AttributeListSyntax attributeList in fieldSyntax.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 [ObservableProperty]. - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) + // Only look for attribute lists explicitly targeting the (generated) property or one of its accessors. 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 [ObservableProperty]. + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.GetKeyword or SyntaxKind.SetKeyword) targetIdentifier) { continue; } @@ -256,7 +256,7 @@ public static bool TryGetInfo( IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out AttributeInfo? attributeInfo)) + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo)) { builder.Add( InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField, @@ -1025,11 +1025,22 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf Argument(IdentifierName("value")))), Block(setterStatements.AsEnumerable())); - // Prepare the forwarded attributes, if any - ImmutableArray forwardedAttributes = + // Prepare the forwarded attributes, if any, for all targets + AttributeListSyntax[] forwardedPropertyAttributes = propertyInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.PropertyKeyword) .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - .ToImmutableArray(); + .ToArray(); + AttributeListSyntax[] forwardedGetAccessorAttributes = + propertyInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.GetKeyword) + .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) + .ToArray(); + AttributeListSyntax[] forwardedSetAccessorAttributes = + propertyInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.SetKeyword) + .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) + .ToArray(); // Prepare the setter for the generated property: // @@ -1065,6 +1076,9 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("The type of the current instance cannot be statically discovered."))))))); } + // Also add any forwarded attributes + setAccessor = setAccessor.AddAttributeLists(forwardedSetAccessorAttributes); + // Construct the generated property as follows: // // /// @@ -1073,6 +1087,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // // public // { + // // get => ; // // } @@ -1086,12 +1101,13 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) - .AddAttributeLists(forwardedAttributes.ToArray()) + .AddAttributeLists(forwardedPropertyAttributes) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithExpressionBody(ArrowExpressionClause(getterFieldExpression)) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .AddAttributeLists(forwardedGetAccessorAttributes), setAccessor); } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs index 4a6c803a9..2f82c8b3a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs @@ -17,7 +17,15 @@ internal static class SuppressionDescriptors public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new( id: "MVVMTKSPR0001", suppressedDiagnosticId: "CS0657", - justification: "Fields using [ObservableProperty] can use [property:] attribute lists to forward attributes to the generated properties"); + justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties"); + + /// + /// Gets a for a field using [ObservableProperty] with an attribute list targeting a get or set accessor. + /// + public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyFieldAccessors = new( + id: "MVVMTKSPR0001", + suppressedDiagnosticId: "CS0658", + justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties"); /// /// Gets a for a method using [RelayCommand] with an attribute list targeting a field or property. diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs similarity index 85% rename from src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs rename to src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs index f27f3969a..0b9b4246d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs @@ -14,7 +14,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; /// /// -/// A diagnostic suppressor to suppress CS0657 warnings for fields with [ObservableProperty] using a [property:] attribute list. +/// A diagnostic suppressor to suppress CS0657 warnings for fields with [ObservableProperty] using a [property:] attribute list (or [set:] or [get:]). /// /// /// That is, this diagnostic suppressor will suppress the following diagnostic: @@ -29,10 +29,10 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; /// /// [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor +public sealed class ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor : DiagnosticSuppressor { /// - public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(PropertyAttributeListForObservablePropertyField); + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(PropertyAttributeListForObservablePropertyField, PropertyAttributeListForObservablePropertyFieldAccessors); /// public override void ReportSuppressions(SuppressionAnalysisContext context) @@ -43,7 +43,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) // Check that the target is effectively [property:] over a field declaration with at least one variable, which is the only case we are interested in if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: FieldDeclarationSyntax { Declaration.Variables.Count: > 0 } fieldDeclaration } attributeTarget && - attributeTarget.Identifier.IsKind(SyntaxKind.PropertyKeyword)) + (attributeTarget.Identifier.IsKind(SyntaxKind.PropertyKeyword) || attributeTarget.Identifier.IsKind(SyntaxKind.GetKeyword) || attributeTarget.Identifier.IsKind(SyntaxKind.SetKeyword))) { SemanticModel semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs index 16ca48c0a..70372a03f 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs @@ -23,8 +23,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models; /// Whether or not concurrent executions have been enabled. /// Whether or not exceptions should flow to the task scheduler. /// Whether or not to also generate a cancel command. -/// The sequence of forwarded attributes for the generated field. -/// The sequence of forwarded attributes for the generated property. +/// The sequence of forwarded attributes for the generated members. internal sealed record CommandInfo( string MethodName, string FieldName, @@ -39,5 +38,4 @@ internal sealed record CommandInfo( bool AllowConcurrentExecutions, bool FlowExceptionsToTaskScheduler, bool IncludeCancelCommand, - EquatableArray ForwardedFieldAttributes, - EquatableArray ForwardedPropertyAttributes); + EquatableArray ForwardedAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs index e5b753160..d40086135 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs @@ -141,8 +141,7 @@ public static bool TryGetInfo( semanticModel, token, in builder, - out ImmutableArray fieldAttributes, - out ImmutableArray propertyAttributes); + out ImmutableArray forwardedAttributes); token.ThrowIfCancellationRequested(); @@ -160,8 +159,7 @@ public static bool TryGetInfo( allowConcurrentExecutions, flowExceptionsToTaskScheduler, generateCancelCommand, - fieldAttributes, - propertyAttributes); + forwardedAttributes); diagnostics = builder.ToImmutable(); @@ -196,16 +194,18 @@ public static ImmutableArray GetSyntax(CommandInfo comm : $"{commandInfo.DelegateType}<{string.Join(", ", commandInfo.DelegateTypeArguments)}>"; // Prepare the forwarded field attributes, if any - ImmutableArray forwardedFieldAttributes = - commandInfo.ForwardedFieldAttributes + AttributeListSyntax[] forwardedFieldAttributes = + commandInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.FieldKeyword) .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - .ToImmutableArray(); + .ToArray(); // Also prepare any forwarded property attributes - ImmutableArray forwardedPropertyAttributes = - commandInfo.ForwardedPropertyAttributes + AttributeListSyntax[] forwardedPropertyAttributes = + commandInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.PropertyKeyword) .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - .ToImmutableArray(); + .ToArray(); // Construct the generated field as follows: // @@ -225,7 +225,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList()))) - .AddAttributeLists(forwardedFieldAttributes.ToArray()); + .AddAttributeLists(forwardedFieldAttributes); // Prepares the argument to pass the underlying method to invoke using ImmutableArrayBuilder commandCreationArguments = ImmutableArrayBuilder.Rent(); @@ -332,7 +332,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) - .AddAttributeLists(forwardedPropertyAttributes.ToArray()) + .AddAttributeLists(forwardedPropertyAttributes) .WithExpressionBody( ArrowExpressionClause( AssignmentExpression( @@ -972,26 +972,22 @@ private static bool TryGetCanExecuteMemberFromGeneratedProperty( /// The instance for the current run. /// The cancellation token for the current operation. /// The current collection of gathered diagnostics. - /// The resulting field attributes to forward. - /// The resulting property attributes to forward. + /// The resulting attributes to forward. private static void GatherForwardedAttributes( IMethodSymbol methodSymbol, SemanticModel semanticModel, CancellationToken token, in ImmutableArrayBuilder diagnostics, - out ImmutableArray fieldAttributes, - out ImmutableArray propertyAttributes) + out ImmutableArray forwardedAttributes) { - using ImmutableArrayBuilder fieldAttributesInfo = ImmutableArrayBuilder.Rent(); - using ImmutableArrayBuilder propertyAttributesInfo = ImmutableArrayBuilder.Rent(); + using ImmutableArrayBuilder forwardedAttributesInfo = ImmutableArrayBuilder.Rent(); static void GatherForwardedAttributes( IMethodSymbol methodSymbol, SemanticModel semanticModel, CancellationToken token, in ImmutableArrayBuilder diagnostics, - in ImmutableArrayBuilder fieldAttributesInfo, - in ImmutableArrayBuilder propertyAttributesInfo) + in ImmutableArrayBuilder forwardedAttributesInfo) { // Get the single syntax reference for the input method symbol (there should be only one) if (methodSymbol.DeclaringSyntaxReferences is not [SyntaxReference syntaxReference]) @@ -1009,7 +1005,7 @@ static void GatherForwardedAttributes( foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists) { // Same as in the [ObservableProperty] generator, except we're also looking for fields here - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword)) + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword) targetIdentifier) { continue; } @@ -1033,7 +1029,7 @@ static void GatherForwardedAttributes( IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out AttributeInfo? attributeInfo)) + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo)) { diagnostics.Add( InvalidFieldOrPropertyTargetedAttributeExpressionOnRelayCommandMethod, @@ -1044,15 +1040,8 @@ static void GatherForwardedAttributes( continue; } - // Add the new attribute info to the right builder - if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.FieldKeyword)) - { - fieldAttributesInfo.Add(attributeInfo); - } - else - { - propertyAttributesInfo.Add(attributeInfo); - } + // Add the new attribute info to the builder + forwardedAttributesInfo.Add(attributeInfo); } } } @@ -1064,17 +1053,16 @@ static void GatherForwardedAttributes( IMethodSymbol partialImplementation = methodSymbol.PartialImplementationPart ?? methodSymbol; // We always give priority to the partial definition, to ensure a predictable and testable ordering - GatherForwardedAttributes(partialDefinition, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo); - GatherForwardedAttributes(partialImplementation, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo); + GatherForwardedAttributes(partialDefinition, semanticModel, token, in diagnostics, in forwardedAttributesInfo); + GatherForwardedAttributes(partialImplementation, semanticModel, token, in diagnostics, in forwardedAttributesInfo); } else { // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications - GatherForwardedAttributes(methodSymbol, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo); + GatherForwardedAttributes(methodSymbol, semanticModel, token, in diagnostics, in forwardedAttributesInfo); } - fieldAttributes = fieldAttributesInfo.ToImmutable(); - propertyAttributes = propertyAttributesInfo.ToImmutable(); + forwardedAttributes = forwardedAttributesInfo.ToImmutable(); } } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index a1e954d85..6997cfaa1 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -3296,6 +3296,104 @@ public T Value VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); } + [TestMethod] + public void ObservablePropertyWithForwardedAttributes_OnPropertyAccessors() + { + string source = """ + using System.ComponentModel; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + [property: Test("Property1")] + [property: Test("Property2")] + [property: Test("Property3")] + [get: Test("Get1")] + [get: Test("Get2")] + [set: Test("Set1")] + [set: Test("Set2")] + private object? a; + } + + public class TestAttribute : Attribute + { + public TestAttribute(string value) + { + } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::MyApp.TestAttribute("Property1")] + [global::MyApp.TestAttribute("Property2")] + [global::MyApp.TestAttribute("Property3")] + public object? A + { + [global::MyApp.TestAttribute("Get1")] + [global::MyApp.TestAttribute("Get2")] + get => a; + [global::MyApp.TestAttribute("Set1")] + [global::MyApp.TestAttribute("Set2")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(a, value)) + { + OnAChanging(value); + OnAChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.A); + a = value; + OnAChanged(value); + OnAChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.A); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanging(object? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanging(object? oldValue, object? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanged(object? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanged(object? oldValue, object? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + /// /// Generates the requested sources /// diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs index 495a9cd59..6b3e25796 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs @@ -1797,4 +1797,30 @@ private sealed partial class ModelWithDependentPropertyAndNoPropertyChanging public string? FullName => ""; } + +#if NET6_0_OR_GREATER + // See https://github.com/CommunityToolkit/dotnet/issues/939 + public partial class ModelWithSecondaryPropertySetFromGeneratedSetter_DoesNotWarn : ObservableObject + { + [ObservableProperty] + [set: MemberNotNull(nameof(B))] + private string a; + + // This type validates forwarding attributes on generated accessors. In particular, there should + // be no nullability warning on this constructor (CS8618), thanks to 'MemberNotNullAttribute("B")' + // being forwarded to the generated setter in the generated property (see linked issue). + public ModelWithSecondaryPropertySetFromGeneratedSetter_DoesNotWarn() + { + A = ""; + } + + public string B { get; private set; } + + [MemberNotNull(nameof(B))] + partial void OnAChanged(string? oldValue, string newValue) + { + B = ""; + } + } +#endif }