// 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 more information. using System.Collections.Immutable; using System.ComponentModel; using System.Linq; using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace CommunityToolkit.Mvvm.SourceGenerators; /// partial class ObservablePropertyGenerator { /// /// A container for all the logic for . /// internal static class Execute { /// /// Processes a given field. /// /// The input instance to process. /// The resulting instance for . public static PropertyInfo GetInfo(IFieldSymbol fieldSymbol) { // Check whether the containing type implements INotifyPropertyChanging and whether it inherits from ObservableValidator bool isObservableObject = fieldSymbol.ContainingType.InheritsFrom("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObject"); bool isObservableValidator = fieldSymbol.ContainingType.InheritsFrom("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"); bool isNotifyPropertyChanging = fieldSymbol.ContainingType.AllInterfaces.Any(static i => i.HasFullyQualifiedName("global::System.ComponentModel.INotifyPropertyChanging")); bool hasObservableObjectAttribute = fieldSymbol.ContainingType.GetAttributes().Any(static a => a.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute") == true); // Get the property type and name string typeName = fieldSymbol.Type.GetFullyQualifiedName(); bool isNullableReferenceType = fieldSymbol.Type is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated }; string fieldName = fieldSymbol.Name; string propertyName = GetGeneratedPropertyName(fieldSymbol); ImmutableArray.Builder propertyChangedNames = ImmutableArray.CreateBuilder(); ImmutableArray.Builder propertyChangingNames = ImmutableArray.CreateBuilder(); ImmutableArray.Builder notifiedCommandNames = ImmutableArray.CreateBuilder(); ImmutableArray.Builder validationAttributes = ImmutableArray.CreateBuilder(); // Track the property changing event for the property, if the type supports it if (isObservableObject || isNotifyPropertyChanging || hasObservableObjectAttribute) { propertyChangingNames.Add(propertyName); } // The current property is always notified propertyChangedNames.Add(propertyName); // Gather attributes info foreach (AttributeData attributeData in fieldSymbol.GetAttributes()) { // Add dependent property notifications, if needed if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute") == true) { foreach (string dependentPropertyName in attributeData.GetConstructorArguments()) { propertyChangedNames.Add(dependentPropertyName); } } else if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute") == true) { // Add dependent relay command notifications, if needed foreach (string commandName in attributeData.GetConstructorArguments()) { notifiedCommandNames.Add(commandName); } } else if (attributeData.AttributeClass?.InheritsFrom("global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true) { // Track the current validation attribute validationAttributes.Add(AttributeInfo.From(attributeData)); } } return new( typeName, isNullableReferenceType, fieldName, propertyName, propertyChangingNames.ToImmutable(), propertyChangedNames.ToImmutable(), notifiedCommandNames.ToImmutable(), validationAttributes.ToImmutable()); } /// /// Gets a instance with the cached args for property changing notifications. /// /// The sequence of property names to cache args for. /// A instance with the sequence of cached args, if any. public static CompilationUnitSyntax? GetKnownPropertyChangingArgsSyntax(ImmutableArray names) { return GetKnownPropertyChangingOrChangedArgsSyntax( "__KnownINotifyPropertyChangingArgs", "global::System.ComponentModel.PropertyChangingEventArgs", names); } /// /// Gets a instance with the cached args for property changed notifications. /// /// The sequence of property names to cache args for. /// A instance with the sequence of cached args, if any. public static CompilationUnitSyntax? GetKnownPropertyChangedArgsSyntax(ImmutableArray names) { return GetKnownPropertyChangingOrChangedArgsSyntax( "__KnownINotifyPropertyChangedArgs", "global::System.ComponentModel.PropertyChangedEventArgs", names); } /// /// Gets the instance for the input field. /// /// The input instance to process. /// The generated instance for . public static MemberDeclarationSyntax GetSyntax(PropertyInfo propertyInfo) { ImmutableArray.Builder setterStatements = ImmutableArray.CreateBuilder(); // Gather the statements to notify dependent properties foreach (string propertyName in propertyInfo.PropertyChangingNames) { // This code generates a statement as follows: // // OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.); setterStatements.Add( ExpressionStatement( InvocationExpression(IdentifierName("OnPropertyChanging")) .AddArgumentListArguments(Argument(MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs"), IdentifierName(propertyName)))))); } // In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments // with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter. ExpressionSyntax fieldExpression = propertyInfo.FieldName switch { "value" => MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName("value")), string name => IdentifierName(name) }; // Add the assignment statement: // // = value; setterStatements.Add( ExpressionStatement( AssignmentExpression( SyntaxKind.SimpleAssignmentExpression, fieldExpression, IdentifierName("value")))); // If there are validation attributes, add a call to ValidateProperty: // // ValidateProperty(value, ); if (propertyInfo.ValidationAttributes.Length > 0) { setterStatements.Add( ExpressionStatement( InvocationExpression(IdentifierName("ValidateProperty")) .AddArgumentListArguments( Argument(IdentifierName("value")), Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName)))))); } // Gather the statements to notify dependent properties foreach (string propertyName in propertyInfo.PropertyChangedNames) { // This code generates a statement as follows: // // OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.); setterStatements.Add( ExpressionStatement( InvocationExpression(IdentifierName("OnPropertyChanged")) .AddArgumentListArguments(Argument(MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs"), IdentifierName(propertyName)))))); } // Gather the statements to notify commands foreach (string commandName in propertyInfo.NotifiedCommandNames) { // This code generates a statement as follows: // // .NotifyCanExecuteChanged(); setterStatements.Add( ExpressionStatement( InvocationExpression(MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName(commandName), IdentifierName("NotifyCanExecuteChanged"))))); } // Get the property type syntax (adding the nullability annotation, if needed) TypeSyntax propertyType = propertyInfo.IsNullableReferenceType ? NullableType(IdentifierName(propertyInfo.TypeName)) : IdentifierName(propertyInfo.TypeName); // Generate the inner setter block as follows: // // if (!global::System.Collections.Generic.EqualityComparer<>.Default.Equals(, value)) // { // // } IfStatementSyntax setterIfStatement = IfStatement( PrefixUnaryExpression( SyntaxKind.LogicalNotExpression, InvocationExpression( MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, GenericName(Identifier("global::System.Collections.Generic.EqualityComparer")) .AddTypeArgumentListArguments(propertyType), IdentifierName("Default")), IdentifierName("Equals"))) .AddArgumentListArguments( Argument(fieldExpression), Argument(IdentifierName("value")))), Block(setterStatements)); // Prepare the validation attributes, if any ImmutableArray validationAttributes = propertyInfo.ValidationAttributes .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) .ToImmutableArray(); // Construct the generated property as follows: // // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.DebuggerNonUserCode] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // // public // { // get => ; // set // { // // } // } return PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName)) .AddAttributeLists( AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) .AddAttributeLists(validationAttributes.ToArray()) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithExpressionBody(ArrowExpressionClause(IdentifierName(propertyInfo.FieldName))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) .WithBody(Block(setterIfStatement))); } /// /// Gets a instance with the cached args of a specified type. /// /// The name of the generated type. /// The argument type name. /// The sequence of property names to cache args for. /// A instance with the sequence of cached args, if any. private static CompilationUnitSyntax? GetKnownPropertyChangingOrChangedArgsSyntax( string ContainingTypeName, string ArgsTypeName, ImmutableArray names) { if (names.IsEmpty) { return null; } // This code takes a class symbol and produces a compilation unit as follows: // // // // #pragma warning disable // namespace CommunityToolkit.Mvvm.ComponentModel.__Internals // { // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.DebuggerNonUserCode] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] // [global::System.Obsolete("This type is not intended to be used directly by user code")] // internal static class // { // // } // } return CompilationUnit().AddMembers( NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList( Comment("// "), Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers( ClassDeclaration(ContainingTypeName).AddModifiers( Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword)).AddAttributeLists( AttributeList(SingletonSeparatedList( Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")) .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))), AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( AttributeArgument(LiteralExpression( SyntaxKind.StringLiteralExpression, Literal("This type is not intended to be used directly by user code"))))))) .AddMembers(names.Select(name => CreateFieldDeclaration(ArgsTypeName, name)).ToArray()))) .NormalizeWhitespace(eol: "\n"); } /// /// Creates a field declaration for a cached property changing/changed name. /// /// The field type name (either or ). /// The name of the cached property name. /// A instance for the input cached property name. private static FieldDeclarationSyntax CreateFieldDeclaration(string typeName, string propertyName) { // Create a static field with a cached property changed/changing argument for a specified property. // This code produces a field declaration as follows: // // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] // [global::System.Obsolete("This field is not intended to be referenced directly by user code")] // public static readonly = new(""); return FieldDeclaration( VariableDeclaration(IdentifierName(typeName)) .AddVariables( VariableDeclarator(Identifier(propertyName)) .WithInitializer(EqualsValueClause( ImplicitObjectCreationExpression() .AddArgumentListArguments(Argument( LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName)))))))) .AddModifiers( Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.ReadOnlyKeyword)) .AddAttributeLists( AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( AttributeArgument(LiteralExpression( SyntaxKind.StringLiteralExpression, Literal("This field is not intended to be referenced directly by user code"))))))); } /// /// Get the generated property name for an input field. /// /// The input instance to process. /// The generated property name for . public static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol) { string propertyName = fieldSymbol.Name; if (propertyName.StartsWith("m_")) { propertyName = propertyName.Substring(2); } else if (propertyName.StartsWith("_")) { propertyName = propertyName.TrimStart('_'); } return $"{char.ToUpper(propertyName[0])}{propertyName.Substring(1)}"; } } }