// 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; using System.Collections.Immutable; using System.Linq; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using CommunityToolkit.Mvvm.SourceGenerators.Input.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace CommunityToolkit.Mvvm.SourceGenerators; /// partial class ObservableValidatorValidateAllPropertiesGenerator { /// /// A container for all the logic for . /// private static class Execute { /// /// Checks whether a given type inherits from ObservableValidator. /// /// The input instance to check. /// Whether inherits from ObservableValidator. public static bool IsObservableValidator(INamedTypeSymbol typeSymbol) { return typeSymbol.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"); } /// /// Gets the instance from an input symbol. /// /// The input instance to inspect. /// The resulting instance for . public static ValidationInfo GetInfo(INamedTypeSymbol typeSymbol) { ImmutableArray.Builder propertyNames = ImmutableArray.CreateBuilder(); foreach (ISymbol memberSymbol in typeSymbol.GetMembers()) { if (memberSymbol is { IsStatic: true } or not (IPropertySymbol { IsIndexer: false } or IFieldSymbol)) { continue; } ImmutableArray attributes = memberSymbol.GetAttributes(); // Also include fields that are annotated with [ObservableProperty]. This is necessary because // all generators run in an undefined order and looking at the same original compilation, so the // current one wouldn't be able to see generated properties from other generators directly. if (memberSymbol is IFieldSymbol && !attributes.Any(static a => a.AttributeClass?.HasFullyQualifiedName( "global::CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") == true)) { continue; } // Skip the current member if there are no validation attributes applied to it if (!attributes.Any(a => a.AttributeClass?.InheritsFromFullyQualifiedName( "global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true)) { continue; } // Get the target property name either directly or matching the generated one string propertyName = memberSymbol switch { IPropertySymbol propertySymbol => propertySymbol.Name, IFieldSymbol fieldSymbol => ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol), _ => throw new InvalidOperationException("Invalid symbol type") }; propertyNames.Add(propertyName); } return new( typeSymbol.GetFullMetadataNameForFileName(), typeSymbol.GetFullyQualifiedName(), propertyNames.ToImmutable()); } /// /// Gets the instance from the given info. /// /// The type symbol for the target type being inspected. /// The input array of interface type symbols being handled. /// A instance for the current type being inspected. public static RecipientInfo GetInfo(INamedTypeSymbol typeSymbol, ImmutableArray interfaceSymbols) { ImmutableArray.Builder names = ImmutableArray.CreateBuilder(interfaceSymbols.Length); foreach (INamedTypeSymbol interfaceSymbol in interfaceSymbols) { names.Add(interfaceSymbol.TypeArguments[0].GetFullyQualifiedName()); } return new( typeSymbol.GetFullMetadataNameForFileName(), typeSymbol.GetFullyQualifiedName(), names.MoveToImmutable()); } /// /// Gets the head instance. /// /// Indicates whether [DynamicallyAccessedMembers] should be generated. /// The head instance with the type attributes. public static CompilationUnitSyntax GetSyntax(bool isDynamicallyAccessedMembersAttributeAvailable) { int numberOfAttributes = 5 + (isDynamicallyAccessedMembersAttributeAvailable ? 1 : 0); ImmutableArray.Builder attributes = ImmutableArray.CreateBuilder(numberOfAttributes); // Prepare the base attributes with are always present: // // [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")] attributes.Add( AttributeList(SingletonSeparatedList( Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")).AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableValidatorValidateAllPropertiesGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableValidatorValidateAllPropertiesGenerator).Assembly.GetName().Version.ToString()))))))); attributes.Add(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode"))))); attributes.Add(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))); attributes.Add( AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never")))))); attributes.Add( 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"))))))); if (isDynamicallyAccessedMembersAttributeAvailable) { // Conditionally add the attribute to inform trimming, if the type is available: // // [global::System.CodeDom.Compiler.DynamicallyAccessedMembersAttribute(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods)] attributes.Add( AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute")).AddArgumentListArguments( AttributeArgument(ParseExpression("global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods")))))); } // This code produces a compilation unit as follows: // // // // #pragma warning disable // namespace CommunityToolkit.Mvvm.ComponentModel.__Internals // { // // internal static partial class __ObservableValidatorExtensions // { // } // } return CompilationUnit().AddMembers( NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList( Comment("// "), Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers( ClassDeclaration("__ObservableValidatorExtensions").AddModifiers( Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.PartialKeyword)) .AddAttributeLists(attributes.MoveToImmutable().ToArray()))) .NormalizeWhitespace(); } /// /// Gets the instance for the input recipient. /// /// The input instance to process. /// The generated instance for . public static CompilationUnitSyntax GetSyntax(ValidationInfo validationInfo) { // Create a static factory method creating a delegate that can be used to validate all properties in a given class. // This pattern is used so that the library doesn't have to use MakeGenericType(...) at runtime, nor use unsafe casts // over the created delegate to be able to cache it as an Action instance. This pattern enables the same // functionality and with almost identical performance (not noticeable in this context anyway), but while preserving // full runtime type safety (as a safe cast is used to validate the input argument), and with less reflection needed. // Note that we're deliberately creating a new delegate instance here and not using code that could see the C# compiler // create a static class to cache a reusable delegate, because each generated method will only be called at most once, // as the returned delegate will be cached by the MVVM Toolkit itself. So this ensures the the produced code is minimal, // and that there will be no unnecessary static fields and objects being created and possibly never collected. // This code will produce a syntax tree as follows: // // // // #pragma warning disable // namespace CommunityToolkit.Mvvm.ComponentModel.__Internals // { // partial class __ObservableValidatorExtensions // { // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] // [global::System.Obsolete("This method is not intended to be called directly by user code")] // public static global::System.Action CreateAllPropertiesValidator( _) // { // static void ValidateAllProperties(object obj) // { // var instance = ()obj; // // } // // return ValidateAllProperties; // } // } // } return CompilationUnit().AddMembers( NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList( Comment("// "), Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers( ClassDeclaration("__ObservableValidatorExtensions").AddModifiers(Token(SyntaxKind.PartialKeyword)).AddMembers( MethodDeclaration( GenericName("global::System.Action").AddTypeArgumentListArguments(PredefinedType(Token(SyntaxKind.ObjectKeyword))), Identifier("CreateAllPropertiesValidator")).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 method is not intended to be called directly by user code"))))))).AddModifiers( Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)).AddParameterListParameters( Parameter(Identifier("_")).WithType(IdentifierName(validationInfo.TypeName))) .WithBody(Block( LocalFunctionStatement( PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier("ValidateAllProperties")) .AddModifiers(Token(SyntaxKind.StaticKeyword)) .AddParameterListParameters( Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword)))) .WithBody(Block( LocalDeclarationStatement( VariableDeclaration(IdentifierName("var")) // Cannot use Token(SyntaxKind.VarKeyword) here (throws an ArgumentException) .AddVariables( VariableDeclarator(Identifier("instance")) .WithInitializer(EqualsValueClause( CastExpression( IdentifierName(validationInfo.TypeName), IdentifierName("obj"))))))) .AddStatements(EnumerateValidationStatements(validationInfo).ToArray())), ReturnStatement(IdentifierName("ValidateAllProperties"))))))) .NormalizeWhitespace(); } /// /// Gets a sequence of statements to validate declared properties. /// /// The input instance to process. /// The sequence of instances to validate declared properties. private static ImmutableArray EnumerateValidationStatements(ValidationInfo validationInfo) { ImmutableArray.Builder statements = ImmutableArray.CreateBuilder(validationInfo.PropertyNames.Length); // This loop produces a sequence of statements as follows: // // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.)); // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.)); // ... // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.)); foreach (string propertyName in validationInfo.PropertyNames) { statements.Add( ExpressionStatement( InvocationExpression( MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName("__ObservableValidatorHelper"), IdentifierName("ValidateProperty"))) .AddArgumentListArguments( Argument(IdentifierName("instance")), Argument( MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName("instance"), IdentifierName(propertyName))), Argument( InvocationExpression(IdentifierName("nameof")) .AddArgumentListArguments(Argument( MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName("instance"), IdentifierName(propertyName)))))))); } return statements.MoveToImmutable(); } } }