Added initial support for generated validation property attributes

This commit is contained in:
Sergio Pedri
2021-03-30 18:50:34 +02:00
parent e3ca7429d3
commit 596e13efea
2 changed files with 100 additions and 10 deletions
@@ -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<AttributeSyntax> 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]
// <VALIDATION_ATTRIBUTES> // Optional
// public <FIELD_TYPE> <PROPERTY_NAME>
// {
// get => <FIELD_NAME>;
@@ -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(
@@ -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;
}
/// <summary>
/// Creates an <see cref="AttributeSyntax"/> node that is equivalent to the input <see cref="AttributeData"/> instance.
/// </summary>
/// <param name="attributeData">The input <see cref="AttributeData"/> instance to process.</param>
/// <returns>An <see cref="AttributeSyntax"/> replicating the data in <paramref name="attributeData"/>.</returns>
[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<ExpressionSyntax>(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();
}
}
}
}
}