Skip to content

Commit 171b89b

Browse files
committed
Enable AlsoValidatePropertyAttribute generation
1 parent 488a762 commit 171b89b

File tree

5 files changed

+108
-33
lines changed

5 files changed

+108
-33
lines changed

CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ 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

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: 75 additions & 22 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,39 +120,48 @@ internal static class Execute
118120
continue;
119121
}
120122

121-
// Track the current attribute for forwarding if applicable. The following attributes are special cased:
122-
// - Validation attributes (System.ComponentModel.DataAnnotations.ValidationAttribute)
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
132+
if (attributeData.AttributeClass?.InheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true)
133+
{
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:
123140
// - Display attributes (System.ComponentModel.DataAnnotations.DisplayAttribute)
124141
// - UI hint attributes(System.ComponentModel.DataAnnotations.UIHintAttribute)
125142
// - Scaffold column attributes (System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute)
126143
// - Editable attributes (System.ComponentModel.DataAnnotations.EditableAttribute)
127144
// - Key attributes (System.ComponentModel.DataAnnotations.KeyAttribute)
128-
if (attributeData.AttributeClass?.InheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true ||
129-
attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.UIHintAttribute") == true ||
145+
if (attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.UIHintAttribute") == true ||
130146
attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true ||
131147
attributeData.AttributeClass?.HasFullyQualifiedName("global::System.ComponentModel.DataAnnotations.DisplayAttribute") == true ||
132148
attributeData.AttributeClass?.HasFullyQualifiedName("global::System.ComponentModel.DataAnnotations.EditableAttribute") == true ||
133149
attributeData.AttributeClass?.HasFullyQualifiedName("global::System.ComponentModel.DataAnnotations.KeyAttribute") == true)
134150
{
135-
validationAttributes.Add(AttributeInfo.From(attributeData));
151+
forwardedAttributes.Add(AttributeInfo.From(attributeData));
136152
}
137153
}
138154

139155
// Log the diagnostics if needed
140-
if (validationAttributes.Count > 0 &&
156+
if (hasAnyValidationAttributes &&
141157
!fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
142158
{
143159
builder.Add(
144-
MissingObservableValidatorInheritanceError,
160+
MissingObservableValidatorInheritanceForValidationAttributeError,
145161
fieldSymbol,
146162
fieldSymbol.ContainingType,
147163
fieldSymbol.Name,
148-
validationAttributes.Count);
149-
150-
// Remove all validation attributes so that the generated code doesn't cause a build error about the
151-
// "ValidateProperty" method not existing (as the type doesn't inherit from ObservableValidator). The
152-
// compilation will still fail due to this diagnostics, but with just this easier to understand error.
153-
validationAttributes.Clear();
164+
forwardedAttributes.Count);
154165
}
155166

156167
diagnostics = builder.ToImmutable();
@@ -163,7 +174,8 @@ internal static class Execute
163174
propertyChangedNames.ToImmutable(),
164175
notifiedCommandNames.ToImmutable(),
165176
alsoBroadcastChange,
166-
validationAttributes.ToImmutable());
177+
alsoValidateProperty,
178+
forwardedAttributes.ToImmutable());
167179
}
168180

169181
/// <summary>
@@ -423,6 +435,47 @@ private static bool TryGetIsBroadcastingChanges(
423435
return false;
424436
}
425437

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

519-
// If there are validation attributes, add a call to ValidateProperty:
572+
// If validation is requested, add a call to ValidateProperty:
520573
//
521574
// ValidateProperty(value, <PROPERTY_NAME>);
522-
if (propertyInfo.ValidationAttributes.Length > 0)
575+
if (propertyInfo.AlsoValidateProperty)
523576
{
524577
setterStatements.Add(
525578
ExpressionStatement(
@@ -605,9 +658,9 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
605658
Argument(IdentifierName("value")))),
606659
Block(setterStatements));
607660

608-
// Prepare the validation attributes, if any
609-
ImmutableArray<AttributeListSyntax> validationAttributes =
610-
propertyInfo.ValidationAttributes
661+
// Prepare the forwarded attributes, if any
662+
ImmutableArray<AttributeListSyntax> forwardedAttributes =
663+
propertyInfo.ForwardedAttributes
611664
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
612665
.ToImmutableArray();
613666

@@ -616,7 +669,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
616669
// /// <inheritdoc cref="<FIELD_NAME>"/>
617670
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
618671
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
619-
// <VALIDATION_ATTRIBUTES>
672+
// <FORWARDED_ATTRIBUTES>
620673
// public <FIELD_TYPE><NULLABLE_ANNOTATION?> <PROPERTY_NAME>
621674
// {
622675
// get => <FIELD_NAME>;
@@ -635,7 +688,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
635688
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString()))))))
636689
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <inheritdoc cref=\"{propertyInfo.FieldName}\"/>")), SyntaxKind.OpenBracketToken, TriviaList())),
637690
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
638-
.AddAttributeLists(validationAttributes.ToArray())
691+
.AddAttributeLists(forwardedAttributes.ToArray())
639692
.AddModifiers(Token(SyntaxKind.PublicKeyword))
640693
.AddAccessorListAccessors(
641694
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

CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,14 @@ internal static class DiagnosticDescriptors
101101
/// Format: <c>"The field {0}.{1} cannot be used to generate an observable property, as it has {2} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator"</c>.
102102
/// </para>
103103
/// </summary>
104-
public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceError = new DiagnosticDescriptor(
104+
public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceForValidationAttributeError = new DiagnosticDescriptor(
105105
id: "MVVMTK0006",
106106
title: "Missing ObservableValidator inheritance",
107107
messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as it has {2} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator",
108108
category: typeof(ObservablePropertyGenerator).FullName,
109109
defaultSeverity: DiagnosticSeverity.Error,
110110
isEnabledByDefault: true,
111-
description: $"Cannot apply [ObservableProperty] to fields with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.",
111+
description: "Cannot apply [ObservableProperty] to fields with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.",
112112
helpLinkUri: "https://aka.ms/mvvmtoolkit");
113113

114114
/// <summary>
@@ -319,17 +319,17 @@ internal static class DiagnosticDescriptors
319319
/// <summary>
320320
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>[ObservableProperty]</c> is applied to a field in an invalid type.
321321
/// <para>
322-
/// Format: <c>"The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [AlsoNotifyChangeFor], [AlsoNotifyCanExecuteFor] and [AlsoBroadcastChange]"</c>.
322+
/// Format: <c>"The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [AlsoNotifyChangeFor], [AlsoNotifyCanExecuteFor], [AlsoBroadcastChange] and [AlsoValidateProperty]"</c>.
323323
/// </para>
324324
/// </summary>
325325
public static readonly DiagnosticDescriptor FieldWithOrphanedDependentObservablePropertyAttributesError = new DiagnosticDescriptor(
326326
id: "MVVMTK0020",
327327
title: "Invalid use of attributes dependent on [ObservableProperty]",
328-
messageFormat: "The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [AlsoNotifyChangeFor], [AlsoNotifyCanExecuteFor] and [AlsoBroadcastChange]",
328+
messageFormat: "The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [AlsoNotifyChangeFor], [AlsoNotifyCanExecuteFor], [AlsoBroadcastChange] and [AlsoValidateProperty]",
329329
category: typeof(ObservablePropertyGenerator).FullName,
330330
defaultSeverity: DiagnosticSeverity.Error,
331331
isEnabledByDefault: true,
332-
description: "Fields not annotated with [ObservableProperty] cannot use [AlsoNotifyChangeFor], [AlsoNotifyCanExecuteFor] and [AlsoBroadcastChange].",
332+
description: "Fields not annotated with [ObservableProperty] cannot use [AlsoNotifyChangeFor], [AlsoNotifyCanExecuteFor], [AlsoBroadcastChange] and [AlsoValidateProperty].",
333333
helpLinkUri: "https://aka.ms/mvvmtoolkit");
334334

335335
/// <summary>
@@ -395,4 +395,20 @@ internal static class DiagnosticDescriptors
395395
isEnabledByDefault: true,
396396
description: "The fields annotated with [ObservableProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.",
397397
helpLinkUri: "https://aka.ms/mvvmtoolkit");
398+
399+
/// <summary>
400+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when the target type doesn't inherit from the <c>ObservableValidator</c> class.
401+
/// <para>
402+
/// Format: <c>"The field {0}.{1} cannot be annotated with [AlsoValidateProperty], as it is declared in a type that doesn't inherit from ObservableValidator"</c>.
403+
/// </para>
404+
/// </summary>
405+
public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceForAlsoValidatePropertyError = new DiagnosticDescriptor(
406+
id: "MVVMTK0025",
407+
title: "Missing ObservableValidator inheritance",
408+
messageFormat: "The field {0}.{1} cannot be annotated with [AlsoValidateProperty], as it is declared in a type that doesn't inherit from ObservableValidator",
409+
category: typeof(ObservablePropertyGenerator).FullName,
410+
defaultSeverity: DiagnosticSeverity.Error,
411+
isEnabledByDefault: true,
412+
description: "Cannot apply [AlsoValidateProperty] to field that are declared in a type that doesn't inherit from ObservableValidator.",
413+
helpLinkUri: "https://aka.ms/mvvmtoolkit");
398414
}

0 commit comments

Comments
 (0)