Files
dotnet/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
T
2021-12-29 18:30:53 +01:00

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)}";
}
}
}