Files
dotnet/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs
2021-12-09 21:02:08 +01:00

570 lines
33 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.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
namespace CommunityToolkit.Mvvm.SourceGenerators;
/// <summary>
/// A source generator for the <c>ObservablePropertyAttribute</c> type.
/// </summary>
[Generator]
public sealed partial class ObservablePropertyGenerator : ISourceGenerator
{
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver());
}
/// <inheritdoc/>
public void Execute(GeneratorExecutionContext context)
{
// Get the syntax receiver with the candidate nodes
if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver ||
syntaxReceiver.GatheredInfo.Count == 0)
{
return;
}
// Validate the language version. Note that we're emitting this diagnostic in each generator (excluding the one
// only emitting the nullability annotation attributes if missing) so that the diagnostic is emitted only when
// users are using one of these generators, and not by default as soon as they add a reference to the MVVM Toolkit.
// This ensures that users not using any of the source generators won't be broken when upgrading to this new version.
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 })
{
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null));
}
// Sets of discovered property names
HashSet<string> propertyChangedNames = new();
HashSet<string> propertyChangingNames = new();
// Process the annotated fields
foreach (IGrouping<INamedTypeSymbol, SyntaxReceiver.Item>? items in syntaxReceiver.GatheredInfo.GroupBy<SyntaxReceiver.Item, INamedTypeSymbol>(static item => item.FieldSymbol.ContainingType, SymbolEqualityComparer.Default))
{
if (items.Key.DeclaringSyntaxReferences.Length > 0 &&
items.Key.DeclaringSyntaxReferences.First().GetSyntax() is ClassDeclarationSyntax classDeclaration)
{
try
{
OnExecuteForProperties(context, classDeclaration, items.Key, items, propertyChangedNames, propertyChangingNames);
}
catch
{
context.ReportDiagnostic(ObservablePropertyGeneratorError, items.Key, items.Key);
}
}
}
// Process the fields for the cached args
OnExecuteForPropertyArgs(context, propertyChangedNames, propertyChangingNames);
}
/// <summary>
/// Processes a given target type for declared observable properties.
/// </summary>
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> node to process.</param>
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for <paramref name="classDeclaration"/>.</param>
/// <param name="items">The sequence of fields to process.</param>
/// <param name="propertyChangedNames">The collection of discovered property changed names.</param>
/// <param name="propertyChangingNames">The collection of discovered property changing names.</param>
private static void OnExecuteForProperties(
GeneratorExecutionContext context,
ClassDeclarationSyntax classDeclaration,
INamedTypeSymbol classDeclarationSymbol,
IEnumerable<SyntaxReceiver.Item> items,
ICollection<string> propertyChangedNames,
ICollection<string> propertyChangingNames)
{
INamedTypeSymbol iNotifyPropertyChangingSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanging")!;
INamedTypeSymbol observableObjectSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject")!;
INamedTypeSymbol observableObjectAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute")!;
INamedTypeSymbol observableValidatorSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")!;
// Check whether the current type implements INotifyPropertyChanging and whether it inherits from ObservableValidator
bool isObservableObject = classDeclarationSymbol.InheritsFrom(observableObjectSymbol);
bool isObservableValidator = classDeclarationSymbol.InheritsFrom(observableValidatorSymbol);
bool isNotifyPropertyChanging =
isObservableObject ||
classDeclarationSymbol.AllInterfaces.Contains(iNotifyPropertyChangingSymbol, SymbolEqualityComparer.Default) ||
classDeclarationSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, observableObjectAttributeSymbol));
// Create the class declaration for the user type. This will produce a tree as follows:
//
// <MODIFIERS> <CLASS_NAME>
// {
// <MEMBERS>
// }
ClassDeclarationSyntax? classDeclarationSyntax =
ClassDeclaration(classDeclarationSymbol.Name)
.WithModifiers(classDeclaration.Modifiers)
.AddMembers(items.Select(item =>
CreatePropertyDeclaration(
context,
item.LeadingTrivia,
item.FieldSymbol,
isNotifyPropertyChanging,
isObservableValidator,
propertyChangedNames,
propertyChangingNames)).ToArray());
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
// Add all parent types in ascending order, if any
foreach (TypeDeclarationSyntax? parentType in classDeclaration.Ancestors().OfType<TypeDeclarationSyntax>())
{
typeDeclarationSyntax = parentType
.WithMembers(SingletonList<MemberDeclarationSyntax>(typeDeclarationSyntax))
.WithConstraintClauses(List<TypeParameterConstraintClauseSyntax>())
.WithBaseList(null)
.WithAttributeLists(List<AttributeListSyntax>())
.WithoutTrivia();
}
// Create the compilation unit with the namespace and target member.
// From this, we can finally generate the source code to output.
string? namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces));
// Create the final compilation unit to generate (with leading trivia)
string? source =
CompilationUnit().AddMembers(
NamespaceDeclaration(IdentifierName(namespaceName)).WithLeadingTrivia(TriviaList(
Comment("// <auto-generated/>"),
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))))
.AddMembers(typeDeclarationSyntax))
.NormalizeWhitespace()
.ToFullString();
// Add the partial type
context.AddSource($"{classDeclarationSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8));
}
/// <summary>
/// Creates a <see cref="PropertyDeclarationSyntax"/> instance for a specified field.
/// </summary>
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
/// <param name="leadingTrivia">The leading trivia for the field to process.</param>
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
/// <param name="isNotifyPropertyChanging">Indicates whether or not <see cref="INotifyPropertyChanging"/> is also implemented.</param>
/// <param name="isObservableValidator">Indicates whether or not the containing type inherits from <c>ObservableValidator</c>.</param>
/// <param name="propertyChangedNames">The collection of discovered property changed names.</param>
/// <param name="propertyChangingNames">The collection of discovered property changing names.</param>
/// <returns>A generated <see cref="PropertyDeclarationSyntax"/> instance for the input field.</returns>
[Pure]
private static PropertyDeclarationSyntax CreatePropertyDeclaration(
GeneratorExecutionContext context,
SyntaxTriviaList leadingTrivia,
IFieldSymbol fieldSymbol,
bool isNotifyPropertyChanging,
bool isObservableValidator,
ICollection<string> propertyChangedNames,
ICollection<string> propertyChangingNames)
{
// Get the field type and the target property name
string typeName = fieldSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
string propertyName = GetGeneratedPropertyName(fieldSymbol);
INamedTypeSymbol alsoNotifyChangeForAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute")!;
INamedTypeSymbol alsoNotifyCanExecuteForAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute")!;
INamedTypeSymbol? validationAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute");
List<StatementSyntax> dependentNotificationStatements = new();
List<AttributeSyntax> validationAttributes = new();
foreach (AttributeData attributeData in fieldSymbol.GetAttributes())
{
// Add dependent property notifications, if needed
if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyChangeForAttributeSymbol))
{
foreach (string dependentPropertyName in attributeData.GetConstructorArguments<string>())
{
propertyChangedNames.Add(dependentPropertyName);
// OnPropertyChanged("<PROPERTY_NAME>");
dependentNotificationStatements.Add(ExpressionStatement(
InvocationExpression(IdentifierName("OnPropertyChanged"))
.AddArgumentListArguments(Argument(MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
IdentifierName($"{dependentPropertyName}{nameof(PropertyChangedEventArgs)}"))))));
}
}
else if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyCanExecuteForAttributeSymbol))
{
// Add dependent relay command notifications, if needed
foreach (string commandName in attributeData.GetConstructorArguments<string>())
{
// <PROPERTY_NAME>.NotifyCanExecuteChanged();
dependentNotificationStatements.Add(ExpressionStatement(
InvocationExpression(MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(commandName),
IdentifierName("NotifyCanExecuteChanged")))));
}
}
else if (validationAttributeSymbol is not null &&
attributeData.AttributeClass?.InheritsFrom(validationAttributeSymbol) == true)
{
// Track the current validation attribute
validationAttributes.Add(attributeData.AsAttributeSyntax());
}
}
// 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 = fieldSymbol.Name switch
{
"value" => MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName("value")),
string name => IdentifierName(name)
};
BlockSyntax setterBlock;
if (validationAttributes.Count > 0)
{
// Emit a diagnostic if the current type doesn't inherit from ObservableValidator
if (!isObservableValidator)
{
context.ReportDiagnostic(
MissingObservableValidatorInheritanceError,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name,
validationAttributes.Count);
setterBlock = Block();
}
else
{
propertyChangedNames.Add(propertyName);
propertyChangingNames.Add(propertyName);
// Generate the inner setter block as follows:
//
// if (!global::System.Collections.Generic.EqualityComparer<<FIELD_TYPE>>.Default.Equals(this.<FIELD_NAME>, value))
// {
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangingEventArgs); // Optional
// this.<FIELD_NAME> = value;
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangedEventArgs);
// ValidateProperty(value, <PROPERTY_NAME>);
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_1>PropertyChangedEventArgs); // Optional
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_2>PropertyChangedEventArgs);
// ...
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_N>PropertyChangedEventArgs);
// <COMMAND_1>.NotifyCanExecuteChanged(); // Optional
// <COMMAND_2>.NotifyCanExecuteChanged();
// ...
// <COMMAND_N>.NotifyCanExecuteChanged();
// }
//
// The reason why the code is explicitly generated instead of just calling ObservableValidator.SetProperty() is so that we can
// take advantage of the cached property changed arguments for the current property as well, not just for the dependent ones.
setterBlock = Block(
IfStatement(
PrefixUnaryExpression(
SyntaxKind.LogicalNotExpression,
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
GenericName(Identifier("global::System.Collections.Generic.EqualityComparer"))
.AddTypeArgumentListArguments(IdentifierName(typeName)),
IdentifierName("Default")),
IdentifierName("Equals")))
.AddArgumentListArguments(
Argument(fieldExpression),
Argument(IdentifierName("value")))),
Block(
ExpressionStatement(
InvocationExpression(IdentifierName("OnPropertyChanging"))
.AddArgumentListArguments(Argument(MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
IdentifierName($"{propertyName}{nameof(PropertyChangingEventArgs)}"))))),
ExpressionStatement(
AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
fieldExpression,
IdentifierName("value"))),
ExpressionStatement(
InvocationExpression(IdentifierName("OnPropertyChanged"))
.AddArgumentListArguments(Argument(MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
IdentifierName($"{propertyName}{nameof(PropertyChangedEventArgs)}"))))),
ExpressionStatement(
InvocationExpression(IdentifierName("ValidateProperty"))
.AddArgumentListArguments(
Argument(IdentifierName("value")),
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName))))))
.AddStatements(dependentNotificationStatements.ToArray())));
}
}
else
{
BlockSyntax updateAndNotificationBlock = Block();
// Add OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangingEventArgs) if necessary
if (isNotifyPropertyChanging)
{
propertyChangingNames.Add(propertyName);
updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(ExpressionStatement(
InvocationExpression(IdentifierName("OnPropertyChanging"))
.AddArgumentListArguments(Argument(MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
IdentifierName($"{propertyName}{nameof(PropertyChangingEventArgs)}"))))));
}
propertyChangedNames.Add(propertyName);
// Add the following statements:
//
// <FIELD_NAME> = value;
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangedEventArgs);
updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(
ExpressionStatement(
AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
fieldExpression,
IdentifierName("value"))),
ExpressionStatement(
InvocationExpression(IdentifierName("OnPropertyChanged"))
.AddArgumentListArguments(Argument(MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
IdentifierName($"{propertyName}{nameof(PropertyChangedEventArgs)}"))))));
// Add the dependent property notifications at the end
updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(dependentNotificationStatements.ToArray());
// Generate the inner setter block as follows:
//
// if (!global::System.Collections.Generic.EqualityComparer<<FIELD_TYPE>>.Default.Equals(<FIELD_NAME>, value))
// {
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangingEventArgs); // Optional
// <FIELD_NAME> = value;
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangedEventArgs);
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_1>PropertyChangedEventArgs); // Optional
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_2>PropertyChangedEventArgs);
// ...
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_N>PropertyChangedEventArgs);
// <COMMAND_1>.NotifyCanExecuteChanged(); // Optional
// <COMMAND_2>.NotifyCanExecuteChanged();
// ...
// <COMMAND_N>.NotifyCanExecuteChanged();
// }
setterBlock = Block(
IfStatement(
PrefixUnaryExpression(
SyntaxKind.LogicalNotExpression,
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
GenericName(Identifier("global::System.Collections.Generic.EqualityComparer"))
.AddTypeArgumentListArguments(IdentifierName(typeName)),
IdentifierName("Default")),
IdentifierName("Equals")))
.AddArgumentListArguments(
Argument(fieldExpression),
Argument(IdentifierName("value")))),
updateAndNotificationBlock));
}
// Get the right type for the declared property (including nullability annotations)
TypeSyntax propertyType = IdentifierName(typeName);
if (fieldSymbol.Type is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated })
{
propertyType = NullableType(propertyType);
}
// Construct the generated property as follows:
//
// <FIELD_TRIVIA>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// [global::System.Diagnostics.DebuggerNonUserCode]
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
// <VALIDATION_ATTRIBUTE1> // Optional
// <VALIDATION_ATTRIBUTE2>
// ...
// <VALIDATION_ATTRIBUTEN>
// public <FIELD_TYPE><NULLABLE_ANNOTATION?> <PROPERTY_NAME>
// {
// get => <FIELD_NAME>;
// set
// {
// <BODY>
// }
// }
return
PropertyDeclaration(propertyType, Identifier(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.Select(static a => AttributeList(SingletonSeparatedList(a))).ToArray())
.WithLeadingTrivia(leadingTrivia.Where(static trivia => !trivia.IsKind(SyntaxKind.RegionDirectiveTrivia) && !trivia.IsKind(SyntaxKind.EndRegionDirectiveTrivia)))
.AddModifiers(Token(SyntaxKind.PublicKeyword))
.AddAccessorListAccessors(
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithExpressionBody(ArrowExpressionClause(IdentifierName(fieldSymbol.Name)))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
.WithBody(setterBlock));
}
/// <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>
[Pure]
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)}";
}
/// <summary>
/// Processes the cached property changed/changing args.
/// </summary>
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
/// <param name="propertyChangedNames">The collection of discovered property changed names.</param>
/// <param name="propertyChangingNames">The collection of discovered property changing names.</param>
public void OnExecuteForPropertyArgs(GeneratorExecutionContext context, IReadOnlyCollection<string> propertyChangedNames, IReadOnlyCollection<string> propertyChangingNames)
{
if (propertyChangedNames.Count == 0 &&
propertyChangingNames.Count == 0)
{
return;
}
INamedTypeSymbol propertyChangedEventArgsSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangedEventArgs")!;
INamedTypeSymbol propertyChangingEventArgsSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangingEventArgs")!;
// Create a static method to validate all properties in a given class.
// 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 __KnownINotifyPropertyChangedOrChangingArgs
// {
// <FIELDS>
// }
// }
string? source =
CompilationUnit().AddMembers(
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
Comment("// <auto-generated/>"),
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
ClassDeclaration("__KnownINotifyPropertyChangedOrChangingArgs").AddModifiers(
Token(SyntaxKind.InternalKeyword),
Token(SyntaxKind.StaticKeyword)).AddAttributeLists(
AttributeList(SingletonSeparatedList(
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode"))
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))),
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")))),
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(propertyChangedNames.Select(name => CreateFieldDeclaration(propertyChangedEventArgsSymbol, name)).ToArray())
.AddMembers(propertyChangingNames.Select(name => CreateFieldDeclaration(propertyChangingEventArgsSymbol, name)).ToArray())))
.NormalizeWhitespace()
.ToFullString();
// Add the partial type
context.AddSource("__KnownINotifyPropertyChangedOrChangingArgs.cs", SourceText.From(source, Encoding.UTF8));
}
/// <summary>
/// Creates a field declaration for a cached property change name.
/// </summary>
/// <param name="type">The type of cached property change argument (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>
[Pure]
private static FieldDeclarationSyntax CreateFieldDeclaration(INamedTypeSymbol type, 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><ARG_TYPE> = new("<PROPERTY_NAME>");
return
FieldDeclaration(
VariableDeclaration(IdentifierName(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))
.AddVariables(
VariableDeclarator(Identifier($"{propertyName}{type.Name}"))
.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")))))));
}
}