Skip to content

Commit cbafd72

Browse files
authored
Merge pull request #267 from CommunityToolkit/dev/validation-attribute-improvements
Add [AlsoValidateProperty] attribute
2 parents 7efbb55 + 0b15992 commit cbafd72

File tree

8 files changed

+324
-32
lines changed

8 files changed

+324
-32
lines changed

CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ MVVMTK0021 | CommunityToolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator
3131
MVVMTK0022 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
3232
MVVMTK0023 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
3333
MVVMTK0024 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
34+
MVVMTK0025 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
35+
MVVMTK0026 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
2121
/// <param name="PropertyChangedNames">The sequence of property changed properties to notify.</param>
2222
/// <param name="NotifiedCommandNames">The sequence of commands to notify.</param>
2323
/// <param name="AlsoBroadcastChange">Whether or not the generated property also broadcasts changes.</param>
24-
/// <param name="ValidationAttributes">The sequence of validation attributes for the generated property.</param>
24+
/// <param name="AlsoValidateProperty">Whether or not the generated property also validates its value.</param>
25+
/// <param name="ForwardedAttributes">The sequence of forwarded attributes for the generated property.</param>
2526
internal sealed record PropertyInfo(
2627
string TypeNameWithNullabilityAnnotations,
2728
string FieldName,
@@ -30,7 +31,8 @@ internal sealed record PropertyInfo(
3031
ImmutableArray<string> PropertyChangedNames,
3132
ImmutableArray<string> NotifiedCommandNames,
3233
bool AlsoBroadcastChange,
33-
ImmutableArray<AttributeInfo> ValidationAttributes)
34+
bool AlsoValidateProperty,
35+
ImmutableArray<AttributeInfo> ForwardedAttributes)
3436
{
3537
/// <summary>
3638
/// An <see cref="IEqualityComparer{T}"/> implementation for <see cref="PropertyInfo"/>.
@@ -47,7 +49,8 @@ protected override void AddToHashCode(ref HashCode hashCode, PropertyInfo obj)
4749
hashCode.AddRange(obj.PropertyChangedNames);
4850
hashCode.AddRange(obj.NotifiedCommandNames);
4951
hashCode.Add(obj.AlsoBroadcastChange);
50-
hashCode.AddRange(obj.ValidationAttributes, AttributeInfo.Comparer.Default);
52+
hashCode.Add(obj.AlsoValidateProperty);
53+
hashCode.AddRange(obj.ForwardedAttributes, AttributeInfo.Comparer.Default);
5154
}
5255

5356
/// <inheritdoc/>
@@ -61,7 +64,8 @@ protected override bool AreEqual(PropertyInfo x, PropertyInfo y)
6164
x.PropertyChangedNames.SequenceEqual(y.PropertyChangedNames) &&
6265
x.NotifiedCommandNames.SequenceEqual(y.NotifiedCommandNames) &&
6366
x.AlsoBroadcastChange == y.AlsoBroadcastChange &&
64-
x.ValidationAttributes.SequenceEqual(y.ValidationAttributes, AttributeInfo.Comparer.Default);
67+
x.AlsoValidateProperty == y.AlsoValidateProperty &&
68+
x.ForwardedAttributes.SequenceEqual(y.ForwardedAttributes, AttributeInfo.Comparer.Default);
6569
}
6670
}
6771
}

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,10 @@ internal static class Execute
8888
ImmutableArray<string>.Builder propertyChangedNames = ImmutableArray.CreateBuilder<string>();
8989
ImmutableArray<string>.Builder propertyChangingNames = ImmutableArray.CreateBuilder<string>();
9090
ImmutableArray<string>.Builder notifiedCommandNames = ImmutableArray.CreateBuilder<string>();
91-
ImmutableArray<AttributeInfo>.Builder validationAttributes = ImmutableArray.CreateBuilder<AttributeInfo>();
91+
ImmutableArray<AttributeInfo>.Builder forwardedAttributes = ImmutableArray.CreateBuilder<AttributeInfo>();
9292
bool alsoBroadcastChange = false;
93+
bool alsoValidateProperty = false;
94+
bool hasAnyValidationAttributes = false;
9395

9496
// Track the property changing event for the property, if the type supports it
9597
if (shouldInvokeOnPropertyChanging)
@@ -118,28 +120,58 @@ internal static class Execute
118120
continue;
119121
}
120122

121-
// Track the current validation attribute, if applicable
123+
// Check whether the property should also be validated
124+
if (TryGetIsValidatingProperty(fieldSymbol, attributeData, builder, out bool isValidationTargetValid))
125+
{
126+
alsoValidateProperty = isValidationTargetValid;
127+
128+
continue;
129+
}
130+
131+
// Track the current attribute for forwarding if it is a validation attribute
122132
if (attributeData.AttributeClass?.InheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true)
123133
{
124-
validationAttributes.Add(AttributeInfo.From(attributeData));
134+
hasAnyValidationAttributes = true;
135+
136+
forwardedAttributes.Add(AttributeInfo.From(attributeData));
137+
}
138+
139+
// Also track the current attribute for forwarding if it is of any of the following types:
140+
// - Display attributes (System.ComponentModel.DataAnnotations.DisplayAttribute)
141+
// - UI hint attributes(System.ComponentModel.DataAnnotations.UIHintAttribute)
142+
// - Scaffold column attributes (System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute)
143+
// - Editable attributes (System.ComponentModel.DataAnnotations.EditableAttribute)
144+
// - Key attributes (System.ComponentModel.DataAnnotations.KeyAttribute)
145+
if (attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.UIHintAttribute") == true ||
146+
attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true ||
147+
attributeData.AttributeClass?.HasFullyQualifiedName("global::System.ComponentModel.DataAnnotations.DisplayAttribute") == true ||
148+
attributeData.AttributeClass?.HasFullyQualifiedName("global::System.ComponentModel.DataAnnotations.EditableAttribute") == true ||
149+
attributeData.AttributeClass?.HasFullyQualifiedName("global::System.ComponentModel.DataAnnotations.KeyAttribute") == true)
150+
{
151+
forwardedAttributes.Add(AttributeInfo.From(attributeData));
125152
}
126153
}
127154

128-
// Log the diagnostics if needed
129-
if (validationAttributes.Count > 0 &&
155+
// Log the diagnostic for missing ObservableValidator, if needed
156+
if (hasAnyValidationAttributes &&
130157
!fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
131158
{
132159
builder.Add(
133-
MissingObservableValidatorInheritanceError,
160+
MissingObservableValidatorInheritanceForValidationAttributeError,
134161
fieldSymbol,
135162
fieldSymbol.ContainingType,
136163
fieldSymbol.Name,
137-
validationAttributes.Count);
164+
forwardedAttributes.Count);
165+
}
138166

139-
// Remove all validation attributes so that the generated code doesn't cause a build error about the
140-
// "ValidateProperty" method not existing (as the type doesn't inherit from ObservableValidator). The
141-
// compilation will still fail due to this diagnostics, but with just this easier to understand error.
142-
validationAttributes.Clear();
167+
// Log the diagnostic for missing validation attributes, if any
168+
if (alsoValidateProperty && !hasAnyValidationAttributes)
169+
{
170+
builder.Add(
171+
MissingValidationAttributesForAlsoValidatePropertyError,
172+
fieldSymbol,
173+
fieldSymbol.ContainingType,
174+
fieldSymbol.Name);
143175
}
144176

145177
diagnostics = builder.ToImmutable();
@@ -152,7 +184,8 @@ internal static class Execute
152184
propertyChangedNames.ToImmutable(),
153185
notifiedCommandNames.ToImmutable(),
154186
alsoBroadcastChange,
155-
validationAttributes.ToImmutable());
187+
alsoValidateProperty,
188+
forwardedAttributes.ToImmutable());
156189
}
157190

158191
/// <summary>
@@ -412,6 +445,47 @@ private static bool TryGetIsBroadcastingChanges(
412445
return false;
413446
}
414447

448+
/// <summary>
449+
/// Checks whether a given generated property should also validate its value.
450+
/// </summary>
451+
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
452+
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="fieldSymbol"/>.</param>
453+
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
454+
/// <param name="isValidationTargetValid">Whether or not the the property is in a valid target that can validate values.</param>
455+
/// <returns>Whether or not the generated property for <paramref name="fieldSymbol"/> used <c>[AlsoValidateProperty]</c>.</returns>
456+
private static bool TryGetIsValidatingProperty(
457+
IFieldSymbol fieldSymbol,
458+
AttributeData attributeData,
459+
ImmutableArray<Diagnostic>.Builder diagnostics,
460+
out bool isValidationTargetValid)
461+
{
462+
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoValidatePropertyAttribute") == true)
463+
{
464+
// If the containing type is valid, track it
465+
if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
466+
{
467+
isValidationTargetValid = true;
468+
469+
return true;
470+
}
471+
472+
// Otherwise just emit the diagnostic and then ignore the attribute
473+
diagnostics.Add(
474+
MissingObservableValidatorInheritanceForAlsoValidatePropertyError,
475+
fieldSymbol,
476+
fieldSymbol.ContainingType,
477+
fieldSymbol.Name);
478+
479+
isValidationTargetValid = false;
480+
481+
return true;
482+
}
483+
484+
isValidationTargetValid = false;
485+
486+
return false;
487+
}
488+
415489
/// <summary>
416490
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
417491
/// </summary>
@@ -505,10 +579,10 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
505579
fieldExpression,
506580
IdentifierName("value"))));
507581

508-
// If there are validation attributes, add a call to ValidateProperty:
582+
// If validation is requested, add a call to ValidateProperty:
509583
//
510584
// ValidateProperty(value, <PROPERTY_NAME>);
511-
if (propertyInfo.ValidationAttributes.Length > 0)
585+
if (propertyInfo.AlsoValidateProperty)
512586
{
513587
setterStatements.Add(
514588
ExpressionStatement(
@@ -594,9 +668,9 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
594668
Argument(IdentifierName("value")))),
595669
Block(setterStatements));
596670

597-
// Prepare the validation attributes, if any
598-
ImmutableArray<AttributeListSyntax> validationAttributes =
599-
propertyInfo.ValidationAttributes
671+
// Prepare the forwarded attributes, if any
672+
ImmutableArray<AttributeListSyntax> forwardedAttributes =
673+
propertyInfo.ForwardedAttributes
600674
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
601675
.ToImmutableArray();
602676

@@ -605,7 +679,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
605679
// /// <inheritdoc cref="<FIELD_NAME>"/>
606680
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
607681
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
608-
// <VALIDATION_ATTRIBUTES>
682+
// <FORWARDED_ATTRIBUTES>
609683
// public <FIELD_TYPE><NULLABLE_ANNOTATION?> <PROPERTY_NAME>
610684
// {
611685
// get => <FIELD_NAME>;
@@ -624,7 +698,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
624698
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString()))))))
625699
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <inheritdoc cref=\"{propertyInfo.FieldName}\"/>")), SyntaxKind.OpenBracketToken, TriviaList())),
626700
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
627-
.AddAttributeLists(validationAttributes.ToArray())
701+
.AddAttributeLists(forwardedAttributes.ToArray())
628702
.AddModifiers(Token(SyntaxKind.PublicKeyword))
629703
.AddAccessorListAccessors(
630704
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3838
fieldSymbols
3939
.Where(static item => item.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute"));
4040

41-
// Get diagnostics for fields using [AlsoNotifyChangeFor], [AlsoNotifyCanExecuteFor] and [AlsoBroadcastChange], but not [ObservableProperty]
41+
// Get diagnostics for fields using [AlsoNotifyChangeFor], [AlsoNotifyCanExecuteFor], [AlsoBroadcastChange] and [AlsoValidateProperty], but not [ObservableProperty]
4242
IncrementalValuesProvider<Diagnostic> fieldSymbolsWithOrphanedDependentAttributeWithErrors =
4343
fieldSymbols
4444
.Where(static item =>
4545
(item.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute") ||
4646
item.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute") ||
47-
item.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoBroadcastChangeAttribute")) &&
47+
item.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoBroadcastChangeAttribute") ||
48+
item.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoValidatePropertyAttribute")) &&
4849
!item.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute"))
4950
.Select(static (item, _) => Execute.GetDiagnosticForFieldWithOrphanedDependentAttributes(item));
5051

0 commit comments

Comments
 (0)