mirror of
https://github.com/CommunityToolkit/dotnet.git
synced 2026-10-22 07:06:32 +08:00
394 lines
22 KiB
C#
394 lines
22 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.Collections.Immutable;
|
|
using System.ComponentModel;
|
|
using System.Linq;
|
|
using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
|
|
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
|
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 ObservablePropertyGenerator
|
|
{
|
|
/// <summary>
|
|
/// A container for all the logic for <see cref="ObservablePropertyGenerator"/>.
|
|
/// </summary>
|
|
internal static class Execute
|
|
{
|
|
/// <summary>
|
|
/// Processes a given field.
|
|
/// </summary>
|
|
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
|
/// <returns>The resulting <see cref="PropertyInfo"/> instance for <paramref name="fieldSymbol"/>.</returns>
|
|
public static PropertyInfo GetInfo(IFieldSymbol fieldSymbol)
|
|
{
|
|
// Check whether the containing type implements INotifyPropertyChanging and whether it inherits from ObservableValidator
|
|
bool isObservableObject = fieldSymbol.ContainingType.InheritsFrom("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObject");
|
|
bool isObservableValidator = fieldSymbol.ContainingType.InheritsFrom("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator");
|
|
bool isNotifyPropertyChanging = fieldSymbol.ContainingType.AllInterfaces.Any(static i => i.HasFullyQualifiedName("global::System.ComponentModel.INotifyPropertyChanging"));
|
|
bool hasObservableObjectAttribute = fieldSymbol.ContainingType.GetAttributes().Any(static a => a.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute") == true);
|
|
|
|
// Get the property type and name
|
|
string typeName = fieldSymbol.Type.GetFullyQualifiedName();
|
|
bool isNullableReferenceType = fieldSymbol.Type is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated };
|
|
string fieldName = fieldSymbol.Name;
|
|
string propertyName = GetGeneratedPropertyName(fieldSymbol);
|
|
|
|
ImmutableArray<string>.Builder propertyChangedNames = ImmutableArray.CreateBuilder<string>();
|
|
ImmutableArray<string>.Builder propertyChangingNames = ImmutableArray.CreateBuilder<string>();
|
|
ImmutableArray<string>.Builder notifiedCommandNames = ImmutableArray.CreateBuilder<string>();
|
|
ImmutableArray<AttributeInfo>.Builder validationAttributes = ImmutableArray.CreateBuilder<AttributeInfo>();
|
|
|
|
// Track the property changing event for the property, if the type supports it
|
|
if (isObservableObject || isNotifyPropertyChanging || hasObservableObjectAttribute)
|
|
{
|
|
propertyChangingNames.Add(propertyName);
|
|
}
|
|
|
|
// The current property is always notified
|
|
propertyChangedNames.Add(propertyName);
|
|
|
|
// Gather attributes info
|
|
foreach (AttributeData attributeData in fieldSymbol.GetAttributes())
|
|
{
|
|
// Add dependent property notifications, if needed
|
|
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute") == true)
|
|
{
|
|
foreach (string dependentPropertyName in attributeData.GetConstructorArguments<string>())
|
|
{
|
|
propertyChangedNames.Add(dependentPropertyName);
|
|
}
|
|
}
|
|
else if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute") == true)
|
|
{
|
|
// Add dependent relay command notifications, if needed
|
|
foreach (string commandName in attributeData.GetConstructorArguments<string>())
|
|
{
|
|
notifiedCommandNames.Add(commandName);
|
|
}
|
|
}
|
|
else if (attributeData.AttributeClass?.InheritsFrom("global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true)
|
|
{
|
|
// Track the current validation attribute
|
|
validationAttributes.Add(AttributeInfo.From(attributeData));
|
|
}
|
|
}
|
|
|
|
return new(
|
|
typeName,
|
|
isNullableReferenceType,
|
|
fieldName,
|
|
propertyName,
|
|
propertyChangingNames.ToImmutable(),
|
|
propertyChangedNames.ToImmutable(),
|
|
notifiedCommandNames.ToImmutable(),
|
|
validationAttributes.ToImmutable());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
|
|
/// </summary>
|
|
/// <param name="names">The sequence of property names to cache args for.</param>
|
|
/// <returns>A <see cref="CompilationUnitSyntax"/> instance with the sequence of cached args, if any.</returns>
|
|
public static CompilationUnitSyntax? GetKnownPropertyChangingArgsSyntax(ImmutableArray<string> names)
|
|
{
|
|
return GetKnownPropertyChangingOrChangedArgsSyntax(
|
|
"__KnownINotifyPropertyChangingArgs",
|
|
"global::System.ComponentModel.PropertyChangingEventArgs",
|
|
names);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changed notifications.
|
|
/// </summary>
|
|
/// <param name="names">The sequence of property names to cache args for.</param>
|
|
/// <returns>A <see cref="CompilationUnitSyntax"/> instance with the sequence of cached args, if any.</returns>
|
|
public static CompilationUnitSyntax? GetKnownPropertyChangedArgsSyntax(ImmutableArray<string> names)
|
|
{
|
|
return GetKnownPropertyChangingOrChangedArgsSyntax(
|
|
"__KnownINotifyPropertyChangedArgs",
|
|
"global::System.ComponentModel.PropertyChangedEventArgs",
|
|
names);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="MemberDeclarationSyntax"/> instance for the input field.
|
|
/// </summary>
|
|
/// <param name="propertyInfo">The input <see cref="PropertyInfo"/> instance to process.</param>
|
|
/// <returns>The generated <see cref="MemberDeclarationSyntax"/> instance for <paramref name="propertyInfo"/>.</returns>
|
|
public static MemberDeclarationSyntax GetSyntax(PropertyInfo propertyInfo)
|
|
{
|
|
ImmutableArray<StatementSyntax>.Builder setterStatements = ImmutableArray.CreateBuilder<StatementSyntax>();
|
|
|
|
// 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.<PROPERTY_NAME>);
|
|
setterStatements.Add(
|
|
ExpressionStatement(
|
|
InvocationExpression(IdentifierName("OnPropertyChanging"))
|
|
.AddArgumentListArguments(Argument(MemberAccessExpression(
|
|
SyntaxKind.SimpleMemberAccessExpression,
|
|
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs"),
|
|
IdentifierName(propertyName))))));
|
|
}
|
|
|
|
// 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)
|
|
};
|
|
|
|
// Add the assignment statement:
|
|
//
|
|
// <FIELD_EXPRESSION> = value;
|
|
setterStatements.Add(
|
|
ExpressionStatement(
|
|
AssignmentExpression(
|
|
SyntaxKind.SimpleAssignmentExpression,
|
|
fieldExpression,
|
|
IdentifierName("value"))));
|
|
|
|
// If there are validation attributes, add a call to ValidateProperty:
|
|
//
|
|
// ValidateProperty(value, <PROPERTY_NAME>);
|
|
if (propertyInfo.ValidationAttributes.Length > 0)
|
|
{
|
|
setterStatements.Add(
|
|
ExpressionStatement(
|
|
InvocationExpression(IdentifierName("ValidateProperty"))
|
|
.AddArgumentListArguments(
|
|
Argument(IdentifierName("value")),
|
|
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName))))));
|
|
}
|
|
|
|
// 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.<PROPERTY_NAME>);
|
|
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:
|
|
//
|
|
// <COMMAND_NAME>.NotifyCanExecuteChanged();
|
|
setterStatements.Add(
|
|
ExpressionStatement(
|
|
InvocationExpression(MemberAccessExpression(
|
|
SyntaxKind.SimpleMemberAccessExpression,
|
|
IdentifierName(commandName),
|
|
IdentifierName("NotifyCanExecuteChanged")))));
|
|
}
|
|
|
|
// Get the property type syntax (adding the nullability annotation, if needed)
|
|
TypeSyntax propertyType = propertyInfo.IsNullableReferenceType
|
|
? NullableType(IdentifierName(propertyInfo.TypeName))
|
|
: IdentifierName(propertyInfo.TypeName);
|
|
|
|
// Generate the inner setter block as follows:
|
|
//
|
|
// if (!global::System.Collections.Generic.EqualityComparer<<PROPERTY_TYPE>>.Default.Equals(<FIELD_EXPRESSION>, value))
|
|
// {
|
|
// <STATEMENTS>
|
|
// }
|
|
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<AttributeListSyntax> 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.DebuggerNonUserCode]
|
|
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
|
// <VALIDATION_ATTRIBUTES>
|
|
// public <FIELD_TYPE><NULLABLE_ANNOTATION?> <PROPERTY_NAME>
|
|
// {
|
|
// get => <FIELD_NAME>;
|
|
// set
|
|
// {
|
|
// <BODY>
|
|
// }
|
|
// }
|
|
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())))))),
|
|
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
|
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)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args of a specified type.
|
|
/// </summary>
|
|
/// <param name="ContainingTypeName">The name of the generated type.</param>
|
|
/// <param name="ArgsTypeName">The argument type name.</param>
|
|
/// <param name="names">The sequence of property names to cache args for.</param>
|
|
/// <returns>A <see cref="CompilationUnitSyntax"/> instance with the sequence of cached args, if any.</returns>
|
|
private static CompilationUnitSyntax? GetKnownPropertyChangingOrChangedArgsSyntax(
|
|
string ContainingTypeName,
|
|
string ArgsTypeName,
|
|
ImmutableArray<string> names)
|
|
{
|
|
if (names.IsEmpty)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// This code takes a class symbol and produces a compilation unit as follows:
|
|
//
|
|
// // <auto-generated/>
|
|
// #pragma warning disable
|
|
// 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 <CONTAINING_TYPE_NAME>
|
|
// {
|
|
// <FIELDS>
|
|
// }
|
|
// }
|
|
return
|
|
CompilationUnit().AddMembers(
|
|
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
|
|
Comment("// <auto-generated/>"),
|
|
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), 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(eol: "\n");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a field declaration for a cached property changing/changed name.
|
|
/// </summary>
|
|
/// <param name="typeName">The field type name (either <see cref="PropertyChangedEventArgs"/> or <see cref="PropertyChangingEventArgs"/>).</param>
|
|
/// <param name="propertyName">The name of the cached property name.</param>
|
|
/// <returns>A <see cref="FieldDeclarationSyntax"/> instance for the input cached property name.</returns>
|
|
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 <ARG_TYPE> <PROPERTY_NAME> = new("<PROPERTY_NAME>");
|
|
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")))))));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the generated property name for an input field.
|
|
/// </summary>
|
|
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
|
/// <returns>The generated property name for <paramref name="fieldSymbol"/>.</returns>
|
|
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])}{propertyName.Substring(1)}";
|
|
}
|
|
}
|
|
}
|