diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index d515d409..ab304818 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -165,22 +165,32 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators IdentifierName("value"))), ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanged")))); - INamedTypeSymbol attributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyForAttribute).FullName)!; + INamedTypeSymbol alsoNotifyForAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyForAttribute).FullName)!; + INamedTypeSymbol? validationAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute"); - // Add dependent property notifications, if needed - if (fieldSymbol.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol)) is AttributeData attributeData && - attributeData.ConstructorArguments.Length == 1) + List validationAttributes = new(); + + foreach (AttributeData attributeData in fieldSymbol.GetAttributes()) { - foreach (TypedConstant attributeArgument in attributeData.ConstructorArguments[0].Values) + // Add dependent property notifications, if needed + if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyForAttributeSymbol)) { - if (attributeArgument.Value is string dependentPropertyName) + foreach (TypedConstant attributeArgument in attributeData.ConstructorArguments[0].Values) { - // OnPropertyChanged("OtherPropertyName"); - setter = setter.AddStatements(ExpressionStatement( - InvocationExpression(IdentifierName("OnPropertyChanged")) - .AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(dependentPropertyName)))))); + if (attributeArgument.Value is string dependentPropertyName) + { + // OnPropertyChanged("OtherPropertyName"); + setter = setter.AddStatements(ExpressionStatement( + InvocationExpression(IdentifierName("OnPropertyChanged")) + .AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(dependentPropertyName)))))); + } } } + else if (validationAttributeSymbol is not null && + attributeData.AttributeClass?.InheritsFrom(validationAttributeSymbol) == true) + { + validationAttributes.Add(attributeData.AsAttributeSyntax()); + } } // Construct the generated property as follows: @@ -189,6 +199,7 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.DebuggerNonUserCode] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + // // Optional // public // { // get => ; @@ -212,6 +223,7 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) + .AddAttributeLists(validationAttributes.Select(static a => AttributeList(SingletonSeparatedList(a))).ToArray()) .WithLeadingTrivia(leadingTrivia) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs index 0d5c5834..93f9ac9f 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs @@ -2,9 +2,14 @@ // 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.Generic; using System.Diagnostics.Contracts; +using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions { @@ -36,5 +41,78 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions return false; } + + /// + /// Creates an node that is equivalent to the input instance. + /// + /// The input instance to process. + /// An replicating the data in . + [Pure] + public static AttributeSyntax AsAttributeSyntax(this AttributeData attributeData) + { + IdentifierNameSyntax attributeType = IdentifierName(attributeData.AttributeClass!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + AttributeArgumentSyntax[] arguments = + attributeData.ConstructorArguments + .Select(static arg => AttributeArgument(ToExpression(arg))).Concat( + attributeData.NamedArguments + .Select(static arg => + AttributeArgument(ToExpression(arg.Value)) + .WithNameEquals(NameEquals(IdentifierName(arg.Key))))).ToArray(); + + return Attribute(attributeType, AttributeArgumentList(SeparatedList(SeparatedList(arguments)))); + + static ExpressionSyntax ToExpression(TypedConstant arg) + { + if (arg.IsNull) + { + return LiteralExpression(SyntaxKind.NullLiteralExpression); + } + + if (arg.Kind == TypedConstantKind.Array) + { + string elementType = ((IArrayTypeSymbol)arg.Type!).ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + return + ArrayCreationExpression( + ArrayType(IdentifierName(elementType)) + .AddRankSpecifiers(ArrayRankSpecifier(SingletonSeparatedList(OmittedArraySizeExpression())))) + .WithInitializer(InitializerExpression(SyntaxKind.ArrayInitializerExpression) + .AddExpressions(arg.Values.Select(ToExpression).ToArray())); + } + + switch ((arg.Kind, arg.Value)) + { + case (TypedConstantKind.Primitive, string text): + return LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(text)); + case (TypedConstantKind.Primitive, bool flag) when flag: + return LiteralExpression(SyntaxKind.TrueLiteralExpression); + case (TypedConstantKind.Primitive, bool): + return LiteralExpression(SyntaxKind.FalseLiteralExpression); + case (TypedConstantKind.Primitive, object value): + return LiteralExpression(SyntaxKind.NumericLiteralExpression, value switch + { + byte b => Literal(b), + char c => Literal(c), + double d => Literal(d), + float f => Literal(f), + int i => Literal(i), + long l => Literal(l), + sbyte sb => Literal(sb), + short sh => Literal(sh), + uint ui => Literal(ui), + ulong ul => Literal(ul), + ushort ush => Literal(ush), + _ => throw new ArgumentException() + }); + case (TypedConstantKind.Type, ITypeSymbol type): + return TypeOfExpression(IdentifierName(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); + case (TypedConstantKind.Enum, object value): + return CastExpression( + IdentifierName(arg.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + LiteralExpression(SyntaxKind.NumericLiteralExpression, ParseToken(value.ToString()))); + default: throw new ArgumentException(); + } + } + } } }