Skip to content

Commit 5bbab53

Browse files
authored
Merge pull request #181 from CommunityToolkit/dev/icommand-overloads-diagnostics
Fix [ICommand] generator crashing with overloads, add diagnostics
2 parents 3b3c8b1 + f2ac314 commit 5bbab53

File tree

4 files changed

+89
-7
lines changed

4 files changed

+89
-7
lines changed

CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ MVVMTK0019 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
2727
MVVMTK0020 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
2828
MVVMTK0021 | CommunityToolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit/error
2929
MVVMTK0022 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
30+
MVVMTK0023 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error

CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ internal static class DiagnosticDescriptors
146146
/// Format: <c>"The CanExecute name must refer to a valid member, but "{0}" has no matches in type {1}"</c>.
147147
/// </para>
148148
/// </summary>
149-
public static readonly DiagnosticDescriptor InvalidCanExecuteMemberName = new DiagnosticDescriptor(
149+
public static readonly DiagnosticDescriptor InvalidCanExecuteMemberNameError = new DiagnosticDescriptor(
150150
id: "MVVMTK0009",
151151
title: "Invalid ICommand.CanExecute member name",
152152
messageFormat: "The CanExecute name must refer to a valid member, but \"{0}\" has no matches in type {1}",
@@ -162,7 +162,7 @@ internal static class DiagnosticDescriptors
162162
/// Format: <c>"The CanExecute name must refer to a single member, but "{0}" has multiple matches in type {1}"</c>.
163163
/// </para>
164164
/// </summary>
165-
public static readonly DiagnosticDescriptor MultipleCanExecuteMemberNameMatches = new DiagnosticDescriptor(
165+
public static readonly DiagnosticDescriptor MultipleCanExecuteMemberNameMatchesError = new DiagnosticDescriptor(
166166
id: "MVVMTK0010",
167167
title: "Multiple ICommand.CanExecute member name matches",
168168
messageFormat: "The CanExecute name must refer to a single member, but \"{0}\" has multiple matches in type {1}",
@@ -178,7 +178,7 @@ internal static class DiagnosticDescriptors
178178
/// Format: <c>"The CanExecute name must refer to a compatible member, but no valid members were found for "{0}" in type {1}"</c>.
179179
/// </para>
180180
/// </summary>
181-
public static readonly DiagnosticDescriptor InvalidCanExecuteMember = new DiagnosticDescriptor(
181+
public static readonly DiagnosticDescriptor InvalidCanExecuteMemberError = new DiagnosticDescriptor(
182182
id: "MVVMTK0011",
183183
title: "No valid ICommand.CanExecute member match",
184184
messageFormat: "The CanExecute name must refer to a compatible member, but no valid members were found for \"{0}\" in type {1}",
@@ -363,4 +363,20 @@ internal static class DiagnosticDescriptors
363363
isEnabledByDefault: true,
364364
description: "Fields annotated with [AlsoBroadcastChange] must be contained in a type that inherits from ObservableRecipient or that is annotated with [ObservableRecipient] (including base types).",
365365
helpLinkUri: "https://aka.ms/mvvmtoolkit");
366+
367+
/// <summary>
368+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a specified <c>[ICommand]</c> method has any overloads.
369+
/// <para>
370+
/// Format: <c>"The CanExecute name must refer to a single member, but "{0}" has multiple matches in type {1}"</c>.
371+
/// </para>
372+
/// </summary>
373+
public static readonly DiagnosticDescriptor MultipleICommandMethodOverloadsError = new DiagnosticDescriptor(
374+
id: "MVVMTK0023",
375+
title: "Multiple overloads for method annotated with ICommand",
376+
messageFormat: "The method {0}.{1} cannot be annotated with [ICommand], has it has multiple overloads (command methods must be unique within their containing type)",
377+
category: typeof(ICommandGenerator).FullName,
378+
defaultSeverity: DiagnosticSeverity.Error,
379+
isEnabledByDefault: true,
380+
description: "Methods with multiple overloads cannot be annotated with [ICommand], as command methods must be unique within their containing type.",
381+
helpLinkUri: "https://aka.ms/mvvmtoolkit");
366382
}

CommunityToolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.Execute.cs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ internal static class Execute
3636
{
3737
ImmutableArray<Diagnostic>.Builder builder = ImmutableArray.CreateBuilder<Diagnostic>();
3838

39+
// Validate the method definition is unique
40+
if (!IsCommandDefinitionUnique(methodSymbol, builder))
41+
{
42+
goto Failure;
43+
}
44+
3945
// Get the command field and property names
4046
(string fieldName, string propertyName) = GetGeneratedFieldAndPropertyNames(methodSymbol);
4147

@@ -300,6 +306,40 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
300306
return ImmutableArray.Create<MemberDeclarationSyntax>(fieldDeclaration, propertyDeclaration);
301307
}
302308

309+
/// <summary>
310+
/// Validates that a target method used as source for a command is unique within its containing type.
311+
/// </summary>
312+
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
313+
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
314+
/// <returns>Whether or not <paramref name="methodSymbol"/> was unique within its containing type.</returns>
315+
private static bool IsCommandDefinitionUnique(IMethodSymbol methodSymbol, ImmutableArray<Diagnostic>.Builder diagnostics)
316+
{
317+
foreach (ISymbol symbol in methodSymbol.ContainingType.GetMembers(methodSymbol.Name))
318+
{
319+
if (symbol is IMethodSymbol otherSymbol &&
320+
otherSymbol.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.ICommandAttribute"))
321+
{
322+
// If the first [ICommand] overload is the current symbol, return immediately. This makes it so
323+
// that if multiple overloads are present, only the ones after the first declared one will have
324+
// diagnostics generated for them, while the first one will remain valid and will keep working.
325+
if (SymbolEqualityComparer.Default.Equals(methodSymbol, otherSymbol))
326+
{
327+
return true;
328+
}
329+
330+
diagnostics.Add(
331+
MultipleICommandMethodOverloadsError,
332+
methodSymbol,
333+
methodSymbol.ContainingType,
334+
methodSymbol);
335+
336+
return false;
337+
}
338+
}
339+
340+
return true;
341+
}
342+
303343
/// <summary>
304344
/// Get the generated field and property names for the input method.
305345
/// </summary>
@@ -554,7 +594,7 @@ private static bool TryGetCanExecuteExpressionType(
554594

555595
if (memberName is null)
556596
{
557-
diagnostics.Add(InvalidCanExecuteMemberName, methodSymbol, memberName ?? string.Empty, methodSymbol.ContainingType);
597+
diagnostics.Add(InvalidCanExecuteMemberNameError, methodSymbol, memberName ?? string.Empty, methodSymbol.ContainingType);
558598

559599
goto Failure;
560600
}
@@ -571,11 +611,11 @@ private static bool TryGetCanExecuteExpressionType(
571611
return true;
572612
}
573613

574-
diagnostics.Add(InvalidCanExecuteMemberName, methodSymbol, memberName, methodSymbol.ContainingType);
614+
diagnostics.Add(InvalidCanExecuteMemberNameError, methodSymbol, memberName, methodSymbol.ContainingType);
575615
}
576616
else if (canExecuteSymbols.Length > 1)
577617
{
578-
diagnostics.Add(MultipleCanExecuteMemberNameMatches, methodSymbol, memberName, methodSymbol.ContainingType);
618+
diagnostics.Add(MultipleCanExecuteMemberNameMatchesError, methodSymbol, memberName, methodSymbol.ContainingType);
579619
}
580620
else if (TryGetCanExecuteExpressionFromSymbol(canExecuteSymbols[0], commandTypeArguments, out canExecuteExpressionType))
581621
{
@@ -585,7 +625,7 @@ private static bool TryGetCanExecuteExpressionType(
585625
}
586626
else
587627
{
588-
diagnostics.Add(InvalidCanExecuteMember, methodSymbol, memberName, methodSymbol.ContainingType);
628+
diagnostics.Add(InvalidCanExecuteMemberError, methodSymbol, memberName, methodSymbol.ContainingType);
589629
}
590630

591631
Failure:

tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,31 @@ public partial class MyViewModel : ObservableObject
10801080
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0022");
10811081
}
10821082

1083+
[TestMethod]
1084+
public void MultipleICommandMethodOverloads_WithOverloads()
1085+
{
1086+
string source = @"
1087+
using CommunityToolkit.Mvvm.Input;
1088+
1089+
namespace MyApp
1090+
{
1091+
public partial class SampleViewModel
1092+
{
1093+
[ICommand]
1094+
private void GreetUser()
1095+
{
1096+
}
1097+
1098+
[ICommand]
1099+
private void GreetUser(object value)
1100+
{
1101+
}
1102+
}
1103+
}";
1104+
1105+
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0023");
1106+
}
1107+
10831108
/// <summary>
10841109
/// Verifies the output of a source generator.
10851110
/// </summary>

0 commit comments

Comments
 (0)