// 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.Globalization; using System.Linq; using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; 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 diagnostics from the processing operation. /// The resulting instance for , if successful. public static PropertyInfo? TryGetInfo(IFieldSymbol fieldSymbol, out ImmutableArray diagnostics) { ImmutableArray.Builder builder = ImmutableArray.CreateBuilder(); // Validate the target type if (!IsTargetTypeValid(fieldSymbol, out bool shouldInvokeOnPropertyChanging)) { builder.Add( InvalidContainingTypeForObservablePropertyFieldError, fieldSymbol, fieldSymbol.ContainingType, fieldSymbol.Name); diagnostics = builder.ToImmutable(); return null; } // Get the property type and name string typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); string fieldName = fieldSymbol.Name; string propertyName = GetGeneratedPropertyName(fieldSymbol); // Check for name collisions if (fieldName == propertyName) { builder.Add( ObservablePropertyNameCollisionError, fieldSymbol, fieldSymbol.ContainingType, fieldSymbol.Name); diagnostics = builder.ToImmutable(); // If the generated property would collide, skip generating it entirely. This makes sure that // users only get the helpful diagnostic about the collsiion, and not the normal compiler error // about a definition for "Property" already existing on the target type, which might be confusing. return null; } ImmutableArray.Builder propertyChangedNames = ImmutableArray.CreateBuilder(); ImmutableArray.Builder propertyChangingNames = ImmutableArray.CreateBuilder(); ImmutableArray.Builder notifiedCommandNames = ImmutableArray.CreateBuilder(); ImmutableArray.Builder validationAttributes = ImmutableArray.CreateBuilder(); bool alsoBroadcastChange = false; // Track the property changing event for the property, if the type supports it if (shouldInvokeOnPropertyChanging) { propertyChangingNames.Add(propertyName); } // The current property is always notified propertyChangedNames.Add(propertyName); // Gather attributes info foreach (AttributeData attributeData in fieldSymbol.GetAttributes()) { // Gather dependent property and command names if (TryGatherDependentPropertyChangedNames(fieldSymbol, attributeData, propertyChangedNames, builder) || TryGatherDependentCommandNames(fieldSymbol, attributeData, notifiedCommandNames, builder) || TryGetIsBroadcastingChanges(fieldSymbol, attributeData, builder, out alsoBroadcastChange)) { continue; } // Track the current validation attribute, if applicable if (attributeData.AttributeClass?.InheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true) { validationAttributes.Add(AttributeInfo.From(attributeData)); } } // Log the diagnostics if needed if (validationAttributes.Count > 0 && !fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) { builder.Add( MissingObservableValidatorInheritanceError, fieldSymbol, fieldSymbol.ContainingType, fieldSymbol.Name, validationAttributes.Count); // Remove all validation attributes so that the generated code doesn't cause a build error about the // "ValidateProperty" method not existing (as the type doesn't inherit from ObservableValidator). The // compilation will still fail due to this diagnostics, but with just this easier to understand error. validationAttributes.Clear(); } diagnostics = builder.ToImmutable(); return new( typeNameWithNullabilityAnnotations, fieldName, propertyName, propertyChangingNames.ToImmutable(), propertyChangedNames.ToImmutable(), notifiedCommandNames.ToImmutable(), alsoBroadcastChange, validationAttributes.ToImmutable()); } /// /// Gets the diagnostics for a field with invalid attribute uses. /// /// The input instance to process. /// The resulting instance for . public static Diagnostic GetDiagnosticForFieldWithOrphanedDependentAttributes(IFieldSymbol fieldSymbol) { return FieldWithOrphanedDependentObservablePropertyAttributesError.CreateDiagnostic( fieldSymbol, fieldSymbol.ContainingType, fieldSymbol.Name); } /// /// Validates the containing type for a given field being annotated. /// /// The input instance to process. /// Whether or not property changing events should also be raised. /// Whether or not the containing type for is valid. private static bool IsTargetTypeValid( IFieldSymbol fieldSymbol, out bool shouldInvokeOnPropertyChanging) { // The [ObservableProperty] attribute can only be used in types that are known to expose the necessary OnPropertyChanged and OnPropertyChanging methods. // That means that the containing type for the field needs to match one of the following conditions: // - It inherits from ObservableObject (in which case it also implements INotifyPropertyChanging). // - It has the [ObservableObject] attribute (on itself or any of its base types). // - It has the [INotifyPropertyChanged] attribute (on itself or any of its base types). bool isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObject"); bool hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute"); bool hasINotifyPropertyChangedAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute"); shouldInvokeOnPropertyChanging = isObservableObject || hasObservableObjectAttribute; return isObservableObject || hasObservableObjectAttribute || hasINotifyPropertyChangedAttribute; } /// /// Tries to gather dependent properties from the given attribute. /// /// The input instance to process. /// The instance for . /// The target collection of dependent property names to populate. /// The current collection of gathered diagnostics. /// Whether or not was an attribute containing any dependent properties. private static bool TryGatherDependentPropertyChangedNames( IFieldSymbol fieldSymbol, AttributeData attributeData, ImmutableArray.Builder propertyChangedNames, ImmutableArray.Builder diagnostics) { // Validates a property name using existing properties bool IsPropertyNameValid(string propertyName) { return fieldSymbol.ContainingType.GetAllMembers(propertyName).OfType().Any(); } // Validate a property name including generated properties too bool IsPropertyNameValidWithGeneratedMembers(string propertyName) { foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers()) { if (member is IFieldSymbol otherFieldSymbol && !SymbolEqualityComparer.Default.Equals(fieldSymbol, otherFieldSymbol) && otherFieldSymbol.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") && propertyName == GetGeneratedPropertyName(otherFieldSymbol)) { return true; } } return false; } if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute") == true) { foreach (string? dependentPropertyName in attributeData.GetConstructorArguments()) { // Each target must be a (not null and not empty) string matching the name of a property from the containing type // of the annotated field being processed, or alternatively it must match the name of a property being generated. if (dependentPropertyName is not (null or "") && (IsPropertyNameValid(dependentPropertyName) || IsPropertyNameValidWithGeneratedMembers(dependentPropertyName))) { propertyChangedNames.Add(dependentPropertyName); } else { diagnostics.Add( AlsoNotifyChangeForInvalidTargetError, fieldSymbol, dependentPropertyName ?? "", fieldSymbol.ContainingType); } } return true; } return false; } /// /// Tries to gather dependent commands from the given attribute. /// /// The input instance to process. /// The instance for . /// The target collection of dependent command names to populate. /// The current collection of gathered diagnostics. /// Whether or not was an attribute containing any dependent commands. private static bool TryGatherDependentCommandNames( IFieldSymbol fieldSymbol, AttributeData attributeData, ImmutableArray.Builder notifiedCommandNames, ImmutableArray.Builder diagnostics) { // Validates a command name using existing properties bool IsCommandNameValid(string commandName, out bool shouldLookForGeneratedMembersToo) { // Each target must be a string matching the name of a property from the containing type of the annotated field, and the // property must be of type IRelayCommand, or any type that implements that interface (to avoid generating invalid code). if (fieldSymbol.ContainingType.GetAllMembers(commandName).OfType().FirstOrDefault() is IPropertySymbol propertySymbol) { // If there is a property member with the specified name, check that it's valid. If it isn't, the // target is definitely not valid, and the additional checks below can just be skipped. The property // is valid if it's of type IRelayCommand, or it has IRelayCommand in the set of all interfaces. if (propertySymbol.Type is INamedTypeSymbol typeSymbol && (typeSymbol.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.IRelayCommand") || typeSymbol.HasInterfaceWithFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.IRelayCommand"))) { shouldLookForGeneratedMembersToo = true; return true; } // If a property with this name exists but is not valid, the search should stop immediately, as // the target is already known not to be valid, so there is no reason to look for other members. shouldLookForGeneratedMembersToo = false; return false; } shouldLookForGeneratedMembersToo = true; return false; } // Validate a command name including generated command too bool IsCommandNameValidWithGeneratedMembers(string commandName) { foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers()) { if (member is IMethodSymbol methodSymbol && methodSymbol.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.ICommandAttribute") && commandName == ICommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol).PropertyName) { return true; } } return false; } if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute") == true) { foreach (string? commandName in attributeData.GetConstructorArguments()) { // Each command must be a (not null and not empty) string matching the name of an existing command from the containing // type (just like for properties), or it must match a generated command. The only caveat is the case where a property // with the requested name does exist, but it is not of the right type. In that case the search should stop immediately. if (commandName is not (null or "") && (IsCommandNameValid(commandName, out bool shouldLookForGeneratedMembersToo) || shouldLookForGeneratedMembersToo && IsCommandNameValidWithGeneratedMembers(commandName))) { notifiedCommandNames.Add(commandName); } else { diagnostics.Add( AlsoNotifyCanExecuteForInvalidTargetError, fieldSymbol, commandName ?? "", fieldSymbol.ContainingType); } } return true; } return false; } /// /// Checks whether a given generated property should also broadcast changes. /// /// The input instance to process. /// The instance for . /// The current collection of gathered diagnostics. /// Whether or not the resulting property should also broadcast changes. /// Whether or not the generated property for used [AlsoBroadcastChange]. private static bool TryGetIsBroadcastingChanges( IFieldSymbol fieldSymbol, AttributeData attributeData, ImmutableArray.Builder diagnostics, out bool alsoBroadcastChange) { if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoBroadcastChangeAttribute") == true) { // If the containing type is valid, track it if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) { alsoBroadcastChange = true; return true; } // Otherwise just emit the diagnostic and then ignore the attribute diagnostics.Add( InvalidContainingTypeForAlsoBroadcastChangeFieldError, fieldSymbol, fieldSymbol.ContainingType, fieldSymbol.Name); alsoBroadcastChange = false; return true; } alsoBroadcastChange = false; return false; } /// /// 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 GetPropertySyntax(PropertyInfo propertyInfo) { ImmutableArray.Builder setterStatements = ImmutableArray.CreateBuilder(); // Get the property type syntax TypeSyntax propertyType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); // 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) }; if (propertyInfo.AlsoBroadcastChange) { // If broadcasting changes are required, also store the old value. // This code generates a statement as follows: // // __oldValue = ; setterStatements.Add( LocalDeclarationStatement( VariableDeclaration(propertyType) .AddVariables( VariableDeclarator(Identifier("__oldValue")) .WithInitializer(EqualsValueClause(fieldExpression))))); } // Add the OnPropertyChanging() call first: // // OnChanging(value); setterStatements.Add( ExpressionStatement( InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing")) .AddArgumentListArguments(Argument(IdentifierName("value"))))); // 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)))))); } // 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)))))); } // Add the OnPropertyChanged() call: // // OnChanged(value); setterStatements.Add( ExpressionStatement( InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changed")) .AddArgumentListArguments(Argument(IdentifierName("value"))))); // 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"))))); } // Also broadcast the change, if requested if (propertyInfo.AlsoBroadcastChange) { // This code generates a statement as follows: // // Broadcast(__oldValue, value, ""); setterStatements.Add( ExpressionStatement( InvocationExpression(IdentifierName("Broadcast")) .AddArgumentListArguments( Argument(IdentifierName("__oldValue")), Argument(IdentifierName("value")), Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName)))))); } // 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.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())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), 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 the instances for the OnPropertyChanging and OnPropertyChanged methods for the input field. /// /// The input instance to process. /// The generated instances for the OnPropertyChanging and OnPropertyChanged methods. public static ImmutableArray GetOnPropertyChangeMethodsSyntax(PropertyInfo propertyInfo) { // Get the property type syntax TypeSyntax parameterType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); // Construct the generated method as follows: // // /// Executes the logic for when is changing. // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // partial void OnChanging( value); MemberDeclarationSyntax onPropertyChangingDeclaration = MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changing")) .AddModifiers(Token(SyntaxKind.PartialKeyword)) .AddParameterListParameters(Parameter(Identifier("value")).WithType(parameterType)) .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())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// Executes the logic for when is changing.")), SyntaxKind.OpenBracketToken, TriviaList()))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); // Construct the generated method as follows: // // /// Executes the logic for when ust changed. // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // partial void OnChanged( value); MemberDeclarationSyntax onPropertyChangedDeclaration = MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changed")) .AddModifiers(Token(SyntaxKind.PartialKeyword)) .AddParameterListParameters(Parameter(Identifier("value")).WithType(parameterType)) .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())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// Executes the logic for when just changed.")), SyntaxKind.OpenBracketToken, TriviaList()))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); return ImmutableArray.Create(onPropertyChangingDeclaration, onPropertyChangedDeclaration); } /// /// 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 // #nullable enable // 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)), Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), 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(); } /// /// 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], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}"; } } }