mirror of
https://github.com/chylex/.NET-Community-Toolkit.git
synced 2024-11-24 07:42:45 +01:00
301 lines
18 KiB
C#
301 lines
18 KiB
C#
// 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;
|
|
|
|
/// <inheritdoc/>
|
|
partial class ObservableValidatorValidateAllPropertiesGenerator
|
|
{
|
|
/// <summary>
|
|
/// A container for all the logic for <see cref="ObservableValidatorValidateAllPropertiesGenerator"/>.
|
|
/// </summary>
|
|
private static class Execute
|
|
{
|
|
/// <summary>
|
|
/// Checks whether a given type inherits from <c>ObservableValidator</c>.
|
|
/// </summary>
|
|
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to check.</param>
|
|
/// <returns>Whether <paramref name="typeSymbol"/> inherits from <c>ObservableValidator</c>.</returns>
|
|
public static bool IsObservableValidator(INamedTypeSymbol typeSymbol)
|
|
{
|
|
return typeSymbol.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="ValidationInfo"/> instance from an input symbol.
|
|
/// </summary>
|
|
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to inspect.</param>
|
|
/// <returns>The resulting <see cref="ValidationInfo"/> instance for <paramref name="typeSymbol"/>.</returns>
|
|
public static ValidationInfo GetInfo(INamedTypeSymbol typeSymbol)
|
|
{
|
|
ImmutableArray<string>.Builder propertyNames = ImmutableArray.CreateBuilder<string>();
|
|
|
|
foreach (ISymbol memberSymbol in typeSymbol.GetMembers())
|
|
{
|
|
if (memberSymbol is { IsStatic: true } or not (IPropertySymbol { IsIndexer: false } or IFieldSymbol))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ImmutableArray<AttributeData> 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());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="RecipientInfo"/> instance from the given info.
|
|
/// </summary>
|
|
/// <param name="typeSymbol">The type symbol for the target type being inspected.</param>
|
|
/// <param name="interfaceSymbols">The input array of interface type symbols being handled.</param>
|
|
/// <returns>A <see cref="RecipientInfo"/> instance for the current type being inspected.</returns>
|
|
public static RecipientInfo GetInfo(INamedTypeSymbol typeSymbol, ImmutableArray<INamedTypeSymbol> interfaceSymbols)
|
|
{
|
|
ImmutableArray<string>.Builder names = ImmutableArray.CreateBuilder<string>(interfaceSymbols.Length);
|
|
|
|
foreach (INamedTypeSymbol interfaceSymbol in interfaceSymbols)
|
|
{
|
|
names.Add(interfaceSymbol.TypeArguments[0].GetFullyQualifiedName());
|
|
}
|
|
|
|
return new(
|
|
typeSymbol.GetFullMetadataNameForFileName(),
|
|
typeSymbol.GetFullyQualifiedName(),
|
|
names.MoveToImmutable());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the head <see cref="CompilationUnitSyntax"/> instance.
|
|
/// </summary>
|
|
/// <param name="isDynamicallyAccessedMembersAttributeAvailable">Indicates whether <c>[DynamicallyAccessedMembers]</c> should be generated.</param>
|
|
/// <returns>The head <see cref="CompilationUnitSyntax"/> instance with the type attributes.</returns>
|
|
public static CompilationUnitSyntax GetSyntax(bool isDynamicallyAccessedMembersAttributeAvailable)
|
|
{
|
|
int numberOfAttributes = 5 + (isDynamicallyAccessedMembersAttributeAvailable ? 1 : 0);
|
|
ImmutableArray<AttributeListSyntax>.Builder attributes = ImmutableArray.CreateBuilder<AttributeListSyntax>(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:
|
|
//
|
|
// // <auto-generated/>
|
|
// #pragma warning disable
|
|
// namespace CommunityToolkit.Mvvm.ComponentModel.__Internals
|
|
// {
|
|
// <ATTRIBUTES>
|
|
// internal static partial class __ObservableValidatorExtensions
|
|
// {
|
|
// }
|
|
// }
|
|
return
|
|
CompilationUnit().AddMembers(
|
|
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
|
|
Comment("// <auto-generated/>"),
|
|
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
|
|
ClassDeclaration("__ObservableValidatorExtensions").AddModifiers(
|
|
Token(SyntaxKind.InternalKeyword),
|
|
Token(SyntaxKind.StaticKeyword),
|
|
Token(SyntaxKind.PartialKeyword))
|
|
.AddAttributeLists(attributes.MoveToImmutable().ToArray())))
|
|
.NormalizeWhitespace();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="CompilationUnitSyntax"/> instance for the input recipient.
|
|
/// </summary>
|
|
/// <param name="validationInfo">The input <see cref="ValidationInfo"/> instance to process.</param>
|
|
/// <returns>The generated <see cref="CompilationUnitSyntax"/> instance for <paramref name="validationInfo"/>.</returns>
|
|
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<object> 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:
|
|
//
|
|
// // <auto-generated/>
|
|
// #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<object> CreateAllPropertiesValidator(<INSTANCE_TYPE> _)
|
|
// {
|
|
// static void ValidateAllProperties(object obj)
|
|
// {
|
|
// var instance = (<INSTANCE_TYPE>)obj;
|
|
// <BODY>
|
|
// }
|
|
//
|
|
// return ValidateAllProperties;
|
|
// }
|
|
// }
|
|
// }
|
|
return
|
|
CompilationUnit().AddMembers(
|
|
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
|
|
Comment("// <auto-generated/>"),
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a sequence of statements to validate declared properties.
|
|
/// </summary>
|
|
/// <param name="validationInfo">The input <see cref="ValidationInfo"/> instance to process.</param>
|
|
/// <returns>The sequence of <see cref="StatementSyntax"/> instances to validate declared properties.</returns>
|
|
private static ImmutableArray<StatementSyntax> EnumerateValidationStatements(ValidationInfo validationInfo)
|
|
{
|
|
ImmutableArray<StatementSyntax>.Builder statements = ImmutableArray.CreateBuilder<StatementSyntax>(validationInfo.PropertyNames.Length);
|
|
|
|
// This loop produces a sequence of statements as follows:
|
|
//
|
|
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_0>, nameof(instance.<PROPERTY_0>));
|
|
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_0>, nameof(instance.<PROPERTY_0>));
|
|
// ...
|
|
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_1>, nameof(instance.<PROPERTY_1>));
|
|
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();
|
|
}
|
|
}
|
|
}
|