Skip to content

Commit b66fa4b

Browse files
committed
Add diagnostics for invalid target of [AlsoNotifyCanExecuteFor]
1 parent 48587bc commit b66fa4b

File tree

3 files changed

+68
-12
lines changed

3 files changed

+68
-12
lines changed

CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ MVVMTK0012 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error |
2020
MVVMTK0013 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
2121
MVVMTK0014 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
2222
MVVMTK0015 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
23+
MVVMTK0016 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error

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

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,16 @@ public static PropertyInfo GetInfo(IFieldSymbol fieldSymbol, out ImmutableArray<
7373
// Gather attributes info
7474
foreach (AttributeData attributeData in fieldSymbol.GetAttributes())
7575
{
76-
if (TryGatherDependentPropertyChangedNames(fieldSymbol, attributeData, propertyChangedNames, builder))
76+
// Gather dependent property and command names
77+
if (TryGatherDependentPropertyChangedNames(fieldSymbol, attributeData, propertyChangedNames, builder) ||
78+
TryGatherDependentCommandNames(fieldSymbol, attributeData, notifiedCommandNames, builder))
7779
{
80+
continue;
7881
}
79-
else if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute") == true)
80-
{
81-
// Add dependent relay command notifications, if needed
82-
foreach (string commandName in attributeData.GetConstructorArguments<string>())
83-
{
84-
notifiedCommandNames.Add(commandName);
85-
}
86-
}
87-
else if (attributeData.AttributeClass?.InheritsFrom("global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true)
82+
83+
// Track the current validation attribute, if applicable
84+
if (attributeData.AttributeClass?.InheritsFrom("global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true)
8885
{
89-
// Track the current validation attribute
9086
validationAttributes.Add(AttributeInfo.From(attributeData));
9187
}
9288
}
@@ -156,6 +152,49 @@ private static bool TryGatherDependentPropertyChangedNames(
156152
return false;
157153
}
158154

155+
/// <summary>
156+
/// Tries to gather dependent commands from the given attribute.
157+
/// </summary>
158+
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
159+
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="fieldSymbol"/>.</param>
160+
/// <param name="notifiedCommandNames">The target collection of dependent command names to populate.</param>
161+
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
162+
/// <returns>Whether or not <paramref name="attributeData"/> was an attribute containing any dependent commands.</returns>
163+
private static bool TryGatherDependentCommandNames(
164+
IFieldSymbol fieldSymbol,
165+
AttributeData attributeData,
166+
ImmutableArray<string>.Builder notifiedCommandNames,
167+
ImmutableArray<Diagnostic>.Builder diagnostics)
168+
{
169+
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute") == true)
170+
{
171+
foreach (string? commandName in attributeData.GetConstructorArguments<string>())
172+
{
173+
// Each target must be a string matching the name of a property from the containing type of the annotated field, and the
174+
// property must be of type IRelayCommand, or any type that implements that interface (to avoid generating invalid code).
175+
if (commandName is { Length: > 0 } &&
176+
fieldSymbol.ContainingType.GetMembers(commandName).OfType<IPropertySymbol>().FirstOrDefault() is IPropertySymbol propertySymbol &&
177+
propertySymbol is INamedTypeSymbol typeSymbol &&
178+
typeSymbol.InheritsFrom("global::CommunityToolkit.Mvvm.Input.IRelayCommand"))
179+
{
180+
notifiedCommandNames.Add(commandName);
181+
}
182+
else
183+
{
184+
diagnostics.Add(
185+
AlsoNotifyCanExecuteForInvalidTargetError,
186+
fieldSymbol,
187+
commandName ?? "",
188+
fieldSymbol.ContainingType);
189+
}
190+
}
191+
192+
return true;
193+
}
194+
195+
return false;
196+
}
197+
159198
/// <summary>
160199
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
161200
/// </summary>

CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,27 @@ internal static class DiagnosticDescriptors
244244
/// </summary>
245245
public static readonly DiagnosticDescriptor AlsoNotifyChangeForInvalidTargetError = new DiagnosticDescriptor(
246246
id: "MVVMTK0015",
247-
title: "Name collision for generated property",
247+
title: "Invalid target name for [AlsoNotifyChangeFor]",
248248
messageFormat: "The target(s) of [AlsoNotifyChangeFor] must be an accessible property, but \"{0}\" has no matches in type {1}",
249249
category: typeof(ObservablePropertyGenerator).FullName,
250250
defaultSeverity: DiagnosticSeverity.Error,
251251
isEnabledByDefault: true,
252252
description: "The target(s) of [AlsoNotifyChangeFor] must be an accessible property in its parent type.",
253253
helpLinkUri: "https://aka.ms/mvvmtoolkit");
254+
255+
/// <summary>
256+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when the specified target for <c>[AlsoNotifyCanExecuteFor]</c> is not valid.
257+
/// <para>
258+
/// Format: <c>"The target(s) of [AlsoNotifyCanExecuteFor] must be an accessible <c>IRelayCommand</c> property, but "{0}" has no matches in type {1}</c>.
259+
/// </para>
260+
/// </summary>
261+
public static readonly DiagnosticDescriptor AlsoNotifyCanExecuteForInvalidTargetError = new DiagnosticDescriptor(
262+
id: "MVVMTK0016",
263+
title: "Invalid target name for [AlsoNotifyCanExecuteFor]",
264+
messageFormat: "The target(s) of [AlsoNotifyCanExecuteFor] must be an accessible IRelayCommand property, but \"{0}\" has no matches in type {1}",
265+
category: typeof(ObservablePropertyGenerator).FullName,
266+
defaultSeverity: DiagnosticSeverity.Error,
267+
isEnabledByDefault: true,
268+
description: "The target(s) of [AlsoNotifyCanExecuteFor] must be an accessible IRelayCommand property in its parent type.",
269+
helpLinkUri: "https://aka.ms/mvvmtoolkit");
254270
}

0 commit comments

Comments
 (0)