.NET-Community-Toolkit/CommunityToolkit.Mvvm.Sourc.../ComponentModel/ObservablePropertyGenerator...

1021 lines
56 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.Globalization;
using System.Linq;
using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
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>
/// <param name="diagnostics">The resulting diagnostics from the processing operation.</param>
/// <returns>The resulting <see cref="PropertyInfo"/> instance for <paramref name="fieldSymbol"/>, if successful.</returns>
public static PropertyInfo? TryGetInfo(IFieldSymbol fieldSymbol, out ImmutableArray<Diagnostic> diagnostics)
{
ImmutableArray<Diagnostic>.Builder builder = ImmutableArray.CreateBuilder<Diagnostic>();
// Validate the target type
if (!IsTargetTypeValid(fieldSymbol, out bool shouldInvokeOnPropertyChanging))
{
builder.Add(
InvalidContainingTypeForObservablePropertyFieldError,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);
diagnostics = builder.ToImmutable();
return null;
}
// Get the property type and name
string typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations();
string fieldName = fieldSymbol.Name;
string propertyName = GetGeneratedPropertyName(fieldSymbol);
// Check for name collisions
if (fieldName == propertyName)
{
builder.Add(
ObservablePropertyNameCollisionError,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);
diagnostics = builder.ToImmutable();
// If the generated property would collide, skip generating it entirely. This makes sure that
// users only get the helpful diagnostic about the collision, and not the normal compiler error
// about a definition for "Property" already existing on the target type, which might be confusing.
return null;
}
// Check for special cases that are explicitly not allowed
if (IsGeneratedPropertyInvalid(propertyName, fieldSymbol.Type))
{
builder.Add(
InvalidObservablePropertyError,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);
diagnostics = builder.ToImmutable();
return null;
}
ImmutableArray<string>.Builder propertyChangedNames = ImmutableArray.CreateBuilder<string>();
ImmutableArray<string>.Builder propertyChangingNames = ImmutableArray.CreateBuilder<string>();
ImmutableArray<string>.Builder notifiedCommandNames = ImmutableArray.CreateBuilder<string>();
ImmutableArray<AttributeInfo>.Builder forwardedAttributes = ImmutableArray.CreateBuilder<AttributeInfo>();
bool notifyRecipients = false;
bool notifyDataErrorInfo = false;
bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients = false;
bool hasOrInheritsClassLevelNotifyDataErrorInfo = false;
bool hasAnyValidationAttributes = false;
// Track the property changing event for the property, if the type supports it
if (shouldInvokeOnPropertyChanging)
{
propertyChangingNames.Add(propertyName);
}
// The current property is always notified
propertyChangedNames.Add(propertyName);
// Get the class-level [NotifyPropertyChangedRecipients] setting, if any
if (TryGetIsNotifyingRecipients(fieldSymbol, out bool isBroadcastTargetValid))
{
notifyRecipients = isBroadcastTargetValid;
hasOrInheritsClassLevelNotifyPropertyChangedRecipients = true;
}
// Get the class-level [NotifyDataErrorInfo] setting, if any
if (TryGetNotifyDataErrorInfo(fieldSymbol, out bool isValidationTargetValid))
{
notifyDataErrorInfo = isValidationTargetValid;
hasOrInheritsClassLevelNotifyDataErrorInfo = true;
}
// Gather attributes info
foreach (AttributeData attributeData in fieldSymbol.GetAttributes())
{
// Gather dependent property and command names
if (TryGatherDependentPropertyChangedNames(fieldSymbol, attributeData, propertyChangedNames, builder) ||
TryGatherDependentCommandNames(fieldSymbol, attributeData, notifiedCommandNames, builder))
{
continue;
}
// Check whether the property should also notify recipients
if (TryGetIsNotifyingRecipients(fieldSymbol, attributeData, builder, hasOrInheritsClassLevelNotifyPropertyChangedRecipients, out isBroadcastTargetValid))
{
notifyRecipients = isBroadcastTargetValid;
continue;
}
// Check whether the property should also be validated
if (TryGetNotifyDataErrorInfo(fieldSymbol, attributeData, builder, hasOrInheritsClassLevelNotifyDataErrorInfo, out isValidationTargetValid))
{
notifyDataErrorInfo = isValidationTargetValid;
continue;
}
// Track the current attribute for forwarding if it is a validation attribute
if (attributeData.AttributeClass?.InheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true)
{
hasAnyValidationAttributes = true;
forwardedAttributes.Add(AttributeInfo.From(attributeData));
}
// Also track the current attribute for forwarding if it is of any of the following types:
// - Display attributes (System.ComponentModel.DataAnnotations.DisplayAttribute)
// - UI hint attributes(System.ComponentModel.DataAnnotations.UIHintAttribute)
// - Scaffold column attributes (System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute)
// - Editable attributes (System.ComponentModel.DataAnnotations.EditableAttribute)
// - Key attributes (System.ComponentModel.DataAnnotations.KeyAttribute)
if (attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.UIHintAttribute") == true ||
attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true ||
attributeData.AttributeClass?.HasFullyQualifiedName("global::System.ComponentModel.DataAnnotations.DisplayAttribute") == true ||
attributeData.AttributeClass?.HasFullyQualifiedName("global::System.ComponentModel.DataAnnotations.EditableAttribute") == true ||
attributeData.AttributeClass?.HasFullyQualifiedName("global::System.ComponentModel.DataAnnotations.KeyAttribute") == true)
{
forwardedAttributes.Add(AttributeInfo.From(attributeData));
}
}
// Log the diagnostic for missing ObservableValidator, if needed
if (hasAnyValidationAttributes &&
!fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
{
builder.Add(
MissingObservableValidatorInheritanceForValidationAttributeError,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name,
forwardedAttributes.Count);
}
// Log the diagnostic for missing validation attributes, if any
if (notifyDataErrorInfo && !hasAnyValidationAttributes)
{
builder.Add(
MissingValidationAttributesForNotifyDataErrorInfoError,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);
}
diagnostics = builder.ToImmutable();
return new(
typeNameWithNullabilityAnnotations,
fieldName,
propertyName,
propertyChangingNames.ToImmutable(),
propertyChangedNames.ToImmutable(),
notifiedCommandNames.ToImmutable(),
notifyRecipients,
notifyDataErrorInfo,
forwardedAttributes.ToImmutable());
}
/// <summary>
/// Gets the diagnostics for a field with invalid attribute uses.
/// </summary>
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
/// <returns>The resulting <see cref="Diagnostic"/> instance for <paramref name="fieldSymbol"/>.</returns>
public static Diagnostic GetDiagnosticForFieldWithOrphanedDependentAttributes(IFieldSymbol fieldSymbol)
{
return FieldWithOrphanedDependentObservablePropertyAttributesError.CreateDiagnostic(
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);
}
/// <summary>
/// Validates the containing type for a given field being annotated.
/// </summary>
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
/// <param name="shouldInvokeOnPropertyChanging">Whether or not property changing events should also be raised.</param>
/// <returns>Whether or not the containing type for <paramref name="fieldSymbol"/> is valid.</returns>
private static bool IsTargetTypeValid(
IFieldSymbol fieldSymbol,
out bool shouldInvokeOnPropertyChanging)
{
// The [ObservableProperty] attribute can only be used in types that are known to expose the necessary OnPropertyChanged and OnPropertyChanging methods.
// That means that the containing type for the field needs to match one of the following conditions:
// - It inherits from ObservableObject (in which case it also implements INotifyPropertyChanging).
// - It has the [ObservableObject] attribute (on itself or any of its base types).
// - It has the [INotifyPropertyChanged] attribute (on itself or any of its base types).
bool isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObject");
bool hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute");
bool hasINotifyPropertyChangedAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute");
shouldInvokeOnPropertyChanging = isObservableObject || hasObservableObjectAttribute;
return isObservableObject || hasObservableObjectAttribute || hasINotifyPropertyChangedAttribute;
}
/// <summary>
/// Checks whether the generated property would be a special case that is marked as invalid.
/// </summary>
/// <param name="propertyName">The property name.</param>
/// <param name="propertyType">The property type.</param>
/// <returns>Whether the generated property is invalid.</returns>
private static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol propertyType)
{
// If the generated property name is called "Property" and the type is either object or it is PropertyChangedEventArgs or
// PropertyChangingEventArgs (or a type derived from either of those two types), consider it invalid. This is needed because
// if such a property was generated, the partial On<PROPERTY_NAME>Changing and OnPropertyChanging(PropertyChangingEventArgs)
// methods, as well as the partial On<PROPERTY_NAME>Changed and OnPropertyChanged(PropertyChangedEventArgs) methods.
if (propertyName == "Property")
{
return
propertyType.SpecialType == SpecialType.System_Object ||
propertyType.HasOrInheritsFromFullyQualifiedName("global::System.ComponentModel.PropertyChangedEventArgs") ||
propertyType.HasOrInheritsFromFullyQualifiedName("global::System.ComponentModel.PropertyChangingEventArgs");
}
return false;
}
/// <summary>
/// Tries to gather dependent properties from the given attribute.
/// </summary>
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="fieldSymbol"/>.</param>
/// <param name="propertyChangedNames">The target collection of dependent property names to populate.</param>
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
/// <returns>Whether or not <paramref name="attributeData"/> was an attribute containing any dependent properties.</returns>
private static bool TryGatherDependentPropertyChangedNames(
IFieldSymbol fieldSymbol,
AttributeData attributeData,
ImmutableArray<string>.Builder propertyChangedNames,
ImmutableArray<Diagnostic>.Builder diagnostics)
{
// Validates a property name using existing properties
bool IsPropertyNameValid(string propertyName)
{
return fieldSymbol.ContainingType.GetAllMembers(propertyName).OfType<IPropertySymbol>().Any();
}
// Validate a property name including generated properties too
bool IsPropertyNameValidWithGeneratedMembers(string propertyName)
{
foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers())
{
if (member is IFieldSymbol otherFieldSymbol &&
!SymbolEqualityComparer.Default.Equals(fieldSymbol, otherFieldSymbol) &&
otherFieldSymbol.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") &&
propertyName == GetGeneratedPropertyName(otherFieldSymbol))
{
return true;
}
}
return false;
}
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedForAttribute") == true)
{
foreach (string? dependentPropertyName in attributeData.GetConstructorArguments<string>())
{
// Each target must be a (not null and not empty) string matching the name of a property from the containing type
// of the annotated field being processed, or alternatively it must match the name of a property being generated.
if (dependentPropertyName is not (null or "") &&
(IsPropertyNameValid(dependentPropertyName) ||
IsPropertyNameValidWithGeneratedMembers(dependentPropertyName)))
{
propertyChangedNames.Add(dependentPropertyName);
}
else
{
diagnostics.Add(
NotifyPropertyChangedForInvalidTargetError,
fieldSymbol,
dependentPropertyName ?? "",
fieldSymbol.ContainingType);
}
}
return true;
}
return false;
}
/// <summary>
/// Tries to gather dependent commands from the given attribute.
/// </summary>
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="fieldSymbol"/>.</param>
/// <param name="notifiedCommandNames">The target collection of dependent command names to populate.</param>
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
/// <returns>Whether or not <paramref name="attributeData"/> was an attribute containing any dependent commands.</returns>
private static bool TryGatherDependentCommandNames(
IFieldSymbol fieldSymbol,
AttributeData attributeData,
ImmutableArray<string>.Builder notifiedCommandNames,
ImmutableArray<Diagnostic>.Builder diagnostics)
{
// Validates a command name using existing properties
bool IsCommandNameValid(string commandName, out bool shouldLookForGeneratedMembersToo)
{
// Each target must be a string matching the name of a property from the containing type of the annotated field, and the
// property must be of type IRelayCommand, or any type that implements that interface (to avoid generating invalid code).
if (fieldSymbol.ContainingType.GetAllMembers(commandName).OfType<IPropertySymbol>().FirstOrDefault() is IPropertySymbol propertySymbol)
{
// If there is a property member with the specified name, check that it's valid. If it isn't, the
// target is definitely not valid, and the additional checks below can just be skipped. The property
// is valid if it's of type IRelayCommand, or it has IRelayCommand in the set of all interfaces.
if (propertySymbol.Type is INamedTypeSymbol typeSymbol &&
(typeSymbol.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.IRelayCommand") ||
typeSymbol.HasInterfaceWithFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.IRelayCommand")))
{
shouldLookForGeneratedMembersToo = true;
return true;
}
// If a property with this name exists but is not valid, the search should stop immediately, as
// the target is already known not to be valid, so there is no reason to look for other members.
shouldLookForGeneratedMembersToo = false;
return false;
}
shouldLookForGeneratedMembersToo = true;
return false;
}
// Validate a command name including generated command too
bool IsCommandNameValidWithGeneratedMembers(string commandName)
{
foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers())
{
if (member is IMethodSymbol methodSymbol &&
methodSymbol.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.RelayCommandAttribute") &&
commandName == RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol).PropertyName)
{
return true;
}
}
return false;
}
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.NotifyCanExecuteChangedForAttribute") == true)
{
foreach (string? commandName in attributeData.GetConstructorArguments<string>())
{
// Each command must be a (not null and not empty) string matching the name of an existing command from the containing
// type (just like for properties), or it must match a generated command. The only caveat is the case where a property
// with the requested name does exist, but it is not of the right type. In that case the search should stop immediately.
if (commandName is not (null or "") &&
(IsCommandNameValid(commandName, out bool shouldLookForGeneratedMembersToo) ||
shouldLookForGeneratedMembersToo && IsCommandNameValidWithGeneratedMembers(commandName)))
{
notifiedCommandNames.Add(commandName);
}
else
{
diagnostics.Add(
NotifyCanExecuteChangedForInvalidTargetError,
fieldSymbol,
commandName ?? "",
fieldSymbol.ContainingType);
}
}
return true;
}
return false;
}
/// <summary>
/// Checks whether a given generated property should also notify recipients.
/// </summary>
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
/// <param name="isBroadcastTargetValid">Whether or not the the property is in a valid target that can notify recipients.</param>
/// <returns>Whether or not the generated property for <paramref name="fieldSymbol"/> is in a type annotated with <c>[NotifyPropertyChangedRecipients]</c>.</returns>
private static bool TryGetIsNotifyingRecipients(IFieldSymbol fieldSymbol, out bool isBroadcastTargetValid)
{
if (fieldSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute") == true)
{
// If the containing type is valid, track it
if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") ||
fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute"))
{
isBroadcastTargetValid = true;
return true;
}
// Otherwise, ignore the attribute but don't emit a diagnostic.
// The diagnostic for class-level attributes is handled separately.
isBroadcastTargetValid = false;
return true;
}
isBroadcastTargetValid = false;
return false;
}
/// <summary>
/// Checks whether a given generated property should also notify recipients.
/// </summary>
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="fieldSymbol"/>.</param>
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
/// <param name="hasOrInheritsClassLevelNotifyPropertyChangedRecipients">Indicates wether the containing type of <paramref name="fieldSymbol"/> has or inherits <c>[NotifyPropertyChangedRecipients]</c>.</param>
/// <param name="isBroadcastTargetValid">Whether or not the the property is in a valid target that can notify recipients.</param>
/// <returns>Whether or not the generated property for <paramref name="fieldSymbol"/> used <c>[NotifyPropertyChangedRecipients]</c>.</returns>
private static bool TryGetIsNotifyingRecipients(
IFieldSymbol fieldSymbol,
AttributeData attributeData,
ImmutableArray<Diagnostic>.Builder diagnostics,
bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients,
out bool isBroadcastTargetValid)
{
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute") == true)
{
// Emit a diagnostic if the attribute is unnecessary
if (hasOrInheritsClassLevelNotifyPropertyChangedRecipients)
{
diagnostics.Add(
UnnecessaryNotifyPropertyChangedRecipientsAttributeOnFieldWarning,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);
}
// If the containing type is valid, track it
if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") ||
fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute"))
{
isBroadcastTargetValid = true;
return true;
}
// Otherwise just emit the diagnostic and then ignore the attribute
diagnostics.Add(
InvalidContainingTypeForNotifyPropertyChangedRecipientsFieldError,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);
isBroadcastTargetValid = false;
return true;
}
isBroadcastTargetValid = false;
return false;
}
/// <summary>
/// Checks whether a given type using <c>[NotifyPropertyChangedRecipients]</c> is valid and creates a <see cref="Diagnostic"/> if not.
/// </summary>
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
/// <returns>The <see cref="Diagnostic"/> for <paramref name="typeSymbol"/>, if not a valid type.</returns>
public static Diagnostic? GetIsNotifyingRecipientsDiagnosticForType(INamedTypeSymbol typeSymbol)
{
// If the containing type is valid, track it
if (!typeSymbol.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") &&
!typeSymbol.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute"))
{
return Diagnostic.Create(
InvalidTypeForNotifyPropertyChangedRecipientsError,
typeSymbol.Locations.FirstOrDefault(),
typeSymbol);
}
return null;
}
/// <summary>
/// Checks whether a given generated property should also validate its value.
/// </summary>
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
/// <param name="isValidationTargetValid">Whether or not the the property is in a valid target that can validate values.</param>
/// <returns>Whether or not the generated property for <paramref name="fieldSymbol"/> used <c>[NotifyDataErrorInfo]</c>.</returns>
private static bool TryGetNotifyDataErrorInfo(IFieldSymbol fieldSymbol, out bool isValidationTargetValid)
{
if (fieldSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute") == true)
{
// If the containing type is valid, track it
if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
{
isValidationTargetValid = true;
return true;
}
// Otherwise, ignore the attribute but don't emit a diagnostic (same as above)
isValidationTargetValid = false;
return true;
}
isValidationTargetValid = false;
return false;
}
/// <summary>
/// Checks whether a given generated property should also validate its value.
/// </summary>
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="fieldSymbol"/>.</param>
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
/// <param name="hasOrInheritsClassLevelNotifyDataErrorInfo">Indicates wether the containing type of <paramref name="fieldSymbol"/> has or inherits <c>[NotifyDataErrorInfo]</c>.</param>
/// <param name="isValidationTargetValid">Whether or not the the property is in a valid target that can validate values.</param>
/// <returns>Whether or not the generated property for <paramref name="fieldSymbol"/> used <c>[NotifyDataErrorInfo]</c>.</returns>
private static bool TryGetNotifyDataErrorInfo(
IFieldSymbol fieldSymbol,
AttributeData attributeData,
ImmutableArray<Diagnostic>.Builder diagnostics,
bool hasOrInheritsClassLevelNotifyDataErrorInfo,
out bool isValidationTargetValid)
{
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute") == true)
{
// Emit a diagnostic if the attribute is unnecessary
if (hasOrInheritsClassLevelNotifyDataErrorInfo)
{
diagnostics.Add(
UnnecessaryNotifyDataErrorInfoAttributeOnFieldWarning,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);
}
// If the containing type is valid, track it
if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
{
isValidationTargetValid = true;
return true;
}
// Otherwise just emit the diagnostic and then ignore the attribute
diagnostics.Add(
MissingObservableValidatorInheritanceForNotifyDataErrorInfoError,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);
isValidationTargetValid = false;
return true;
}
isValidationTargetValid = false;
return false;
}
/// <summary>
/// Checks whether a given type using <c>[NotifyDataErrorInfo]</c> is valid and creates a <see cref="Diagnostic"/> if not.
/// </summary>
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
/// <returns>The <see cref="Diagnostic"/> for <paramref name="typeSymbol"/>, if not a valid type.</returns>
public static Diagnostic? GetIsNotifyDataErrorInfoDiagnosticForType(INamedTypeSymbol typeSymbol)
{
// If the containing type is valid, track it
if (!typeSymbol.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
{
return Diagnostic.Create(
InvalidTypeForNotifyDataErrorInfoError,
typeSymbol.Locations.FirstOrDefault(),
typeSymbol);
}
return null;
}
/// <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 GetPropertySyntax(PropertyInfo propertyInfo)
{
ImmutableArray<StatementSyntax>.Builder setterStatements = ImmutableArray.CreateBuilder<StatementSyntax>();
// Get the property type syntax
TypeSyntax propertyType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations);
// 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)
};
if (propertyInfo.NotifyPropertyChangedRecipients)
{
// If broadcasting changes are required, also store the old value.
// This code generates a statement as follows:
//
// <PROPERTY_TYPE> __oldValue = <FIELD_EXPRESSIONS>;
setterStatements.Add(
LocalDeclarationStatement(
VariableDeclaration(propertyType)
.AddVariables(
VariableDeclarator(Identifier("__oldValue"))
.WithInitializer(EqualsValueClause(fieldExpression)))));
}
// Add the OnPropertyChanging() call first:
//
// On<PROPERTY_NAME>Changing(value);
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing"))
.AddArgumentListArguments(Argument(IdentifierName("value")))));
// 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))))));
}
// Add the assignment statement:
//
// <FIELD_EXPRESSION> = value;
setterStatements.Add(
ExpressionStatement(
AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
fieldExpression,
IdentifierName("value"))));
// If validation is requested, add a call to ValidateProperty:
//
// ValidateProperty(value, <PROPERTY_NAME>);
if (propertyInfo.NotifyDataErrorInfo)
{
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName("ValidateProperty"))
.AddArgumentListArguments(
Argument(IdentifierName("value")),
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName))))));
}
// Add the OnPropertyChanged() call:
//
// On<PROPERTY_NAME>Changed(value);
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changed"))
.AddArgumentListArguments(Argument(IdentifierName("value")))));
// 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")))));
}
// Also broadcast the change, if requested
if (propertyInfo.NotifyPropertyChangedRecipients)
{
// This code generates a statement as follows:
//
// Broadcast(__oldValue, value, "<PROPERTY_NAME>");
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName("Broadcast"))
.AddArgumentListArguments(
Argument(IdentifierName("__oldValue")),
Argument(IdentifierName("value")),
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName))))));
}
// 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 forwarded attributes, if any
ImmutableArray<AttributeListSyntax> forwardedAttributes =
propertyInfo.ForwardedAttributes
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
.ToImmutableArray();
// Construct the generated property as follows:
//
// /// <inheritdoc cref="<FIELD_NAME>"/>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
// <FORWARDED_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()))))))
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <inheritdoc cref=\"{propertyInfo.FieldName}\"/>")), SyntaxKind.OpenBracketToken, TriviaList())),
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
.AddAttributeLists(forwardedAttributes.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 the <see cref="MemberDeclarationSyntax"/> instances for the <c>OnPropertyChanging</c> and <c>OnPropertyChanged</c> methods for the input field.
/// </summary>
/// <param name="propertyInfo">The input <see cref="PropertyInfo"/> instance to process.</param>
/// <returns>The generated <see cref="MemberDeclarationSyntax"/> instances for the <c>OnPropertyChanging</c> and <c>OnPropertyChanged</c> methods.</returns>
public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethodsSyntax(PropertyInfo propertyInfo)
{
// Get the property type syntax
TypeSyntax parameterType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations);
// Construct the generated method as follows:
//
// /// <summary>Executes the logic for when <see cref="<PROPERTY_NAME>"/> is changing.</summary>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// partial void On<PROPERTY_NAME>Changing(<PROPERTY_TYPE> value);
MemberDeclarationSyntax onPropertyChangingDeclaration =
MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changing"))
.AddModifiers(Token(SyntaxKind.PartialKeyword))
.AddParameterListParameters(Parameter(Identifier("value")).WithType(parameterType))
.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()))))))
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>Executes the logic for when <see cref=\"{propertyInfo.PropertyName}\"/> is changing.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
// Construct the generated method as follows:
//
// /// <summary>Executes the logic for when <see cref="<PROPERTY_NAME>"/> ust changed.</summary>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// partial void On<PROPERTY_NAME>Changed(<PROPERTY_TYPE> value);
MemberDeclarationSyntax onPropertyChangedDeclaration =
MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changed"))
.AddModifiers(Token(SyntaxKind.PartialKeyword))
.AddParameterListParameters(Parameter(Identifier("value")).WithType(parameterType))
.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()))))))
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>Executes the logic for when <see cref=\"{propertyInfo.PropertyName}\"/> just changed.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
return ImmutableArray.Create(onPropertyChangingDeclaration, onPropertyChangedDeclaration);
}
/// <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
// #nullable enable
// 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)),
Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), 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();
}
/// <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], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}";
}
}
}