diff --git a/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md b/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md
index 172e2b6c8..4893436c9 100644
--- a/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md
+++ b/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -27,3 +27,4 @@ MVVMTK0019 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
MVVMTK0020 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
MVVMTK0021 | CommunityToolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit/error
MVVMTK0022 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
+MVVMTK0023 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
diff --git a/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
index 92d1175bf..449ab2f74 100644
--- a/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
+++ b/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
@@ -146,7 +146,7 @@ internal static class DiagnosticDescriptors
/// Format: "The CanExecute name must refer to a valid member, but "{0}" has no matches in type {1}".
///
///
- public static readonly DiagnosticDescriptor InvalidCanExecuteMemberName = new DiagnosticDescriptor(
+ public static readonly DiagnosticDescriptor InvalidCanExecuteMemberNameError = new DiagnosticDescriptor(
id: "MVVMTK0009",
title: "Invalid ICommand.CanExecute member name",
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
/// Format: "The CanExecute name must refer to a single member, but "{0}" has multiple matches in type {1}".
///
///
- public static readonly DiagnosticDescriptor MultipleCanExecuteMemberNameMatches = new DiagnosticDescriptor(
+ public static readonly DiagnosticDescriptor MultipleCanExecuteMemberNameMatchesError = new DiagnosticDescriptor(
id: "MVVMTK0010",
title: "Multiple ICommand.CanExecute member name matches",
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
/// Format: "The CanExecute name must refer to a compatible member, but no valid members were found for "{0}" in type {1}".
///
///
- public static readonly DiagnosticDescriptor InvalidCanExecuteMember = new DiagnosticDescriptor(
+ public static readonly DiagnosticDescriptor InvalidCanExecuteMemberError = new DiagnosticDescriptor(
id: "MVVMTK0011",
title: "No valid ICommand.CanExecute member match",
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
isEnabledByDefault: true,
description: "Fields annotated with [AlsoBroadcastChange] must be contained in a type that inherits from ObservableRecipient or that is annotated with [ObservableRecipient] (including base types).",
helpLinkUri: "https://aka.ms/mvvmtoolkit");
+
+ ///
+ /// Gets a indicating when a specified [ICommand] method has any overloads.
+ ///
+ /// Format: "The CanExecute name must refer to a single member, but "{0}" has multiple matches in type {1}".
+ ///
+ ///
+ public static readonly DiagnosticDescriptor MultipleICommandMethodOverloadsError = new DiagnosticDescriptor(
+ id: "MVVMTK0023",
+ title: "Multiple overloads for method annotated with ICommand",
+ messageFormat: "The method {0}.{1} cannot be annotated with [ICommand], has it has multiple overloads (command methods must be unique within their containing type)",
+ category: typeof(ICommandGenerator).FullName,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "Methods with multiple overloads cannot be annotated with [ICommand], as command methods must be unique within their containing type.",
+ helpLinkUri: "https://aka.ms/mvvmtoolkit");
}
diff --git a/CommunityToolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.Execute.cs b/CommunityToolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.Execute.cs
index 5f68df7c3..12d47aead 100644
--- a/CommunityToolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.Execute.cs
+++ b/CommunityToolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.Execute.cs
@@ -36,6 +36,12 @@ internal static class Execute
{
ImmutableArray.Builder builder = ImmutableArray.CreateBuilder();
+ // Validate the method definition is unique
+ if (!IsCommandDefinitionUnique(methodSymbol, builder))
+ {
+ goto Failure;
+ }
+
// Get the command field and property names
(string fieldName, string propertyName) = GetGeneratedFieldAndPropertyNames(methodSymbol);
@@ -300,6 +306,40 @@ public static ImmutableArray GetSyntax(CommandInfo comm
return ImmutableArray.Create(fieldDeclaration, propertyDeclaration);
}
+ ///
+ /// Validates that a target method used as source for a command is unique within its containing type.
+ ///
+ /// The input instance to process.
+ /// The current collection of gathered diagnostics.
+ /// Whether or not was unique within its containing type.
+ private static bool IsCommandDefinitionUnique(IMethodSymbol methodSymbol, ImmutableArray.Builder diagnostics)
+ {
+ foreach (ISymbol symbol in methodSymbol.ContainingType.GetMembers(methodSymbol.Name))
+ {
+ if (symbol is IMethodSymbol otherSymbol &&
+ otherSymbol.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.ICommandAttribute"))
+ {
+ // If the first [ICommand] overload is the current symbol, return immediately. This makes it so
+ // that if multiple overloads are present, only the ones after the first declared one will have
+ // diagnostics generated for them, while the first one will remain valid and will keep working.
+ if (SymbolEqualityComparer.Default.Equals(methodSymbol, otherSymbol))
+ {
+ return true;
+ }
+
+ diagnostics.Add(
+ MultipleICommandMethodOverloadsError,
+ methodSymbol,
+ methodSymbol.ContainingType,
+ methodSymbol);
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
///
/// Get the generated field and property names for the input method.
///
@@ -554,7 +594,7 @@ private static bool TryGetCanExecuteExpressionType(
if (memberName is null)
{
- diagnostics.Add(InvalidCanExecuteMemberName, methodSymbol, memberName ?? string.Empty, methodSymbol.ContainingType);
+ diagnostics.Add(InvalidCanExecuteMemberNameError, methodSymbol, memberName ?? string.Empty, methodSymbol.ContainingType);
goto Failure;
}
@@ -571,11 +611,11 @@ private static bool TryGetCanExecuteExpressionType(
return true;
}
- diagnostics.Add(InvalidCanExecuteMemberName, methodSymbol, memberName, methodSymbol.ContainingType);
+ diagnostics.Add(InvalidCanExecuteMemberNameError, methodSymbol, memberName, methodSymbol.ContainingType);
}
else if (canExecuteSymbols.Length > 1)
{
- diagnostics.Add(MultipleCanExecuteMemberNameMatches, methodSymbol, memberName, methodSymbol.ContainingType);
+ diagnostics.Add(MultipleCanExecuteMemberNameMatchesError, methodSymbol, memberName, methodSymbol.ContainingType);
}
else if (TryGetCanExecuteExpressionFromSymbol(canExecuteSymbols[0], commandTypeArguments, out canExecuteExpressionType))
{
@@ -585,7 +625,7 @@ private static bool TryGetCanExecuteExpressionType(
}
else
{
- diagnostics.Add(InvalidCanExecuteMember, methodSymbol, memberName, methodSymbol.ContainingType);
+ diagnostics.Add(InvalidCanExecuteMemberError, methodSymbol, memberName, methodSymbol.ContainingType);
}
Failure:
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
index 7a911234f..894a1af72 100644
--- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
@@ -1080,6 +1080,31 @@ public partial class MyViewModel : ObservableObject
VerifyGeneratedDiagnostics(source, "MVVMTK0022");
}
+ [TestMethod]
+ public void MultipleICommandMethodOverloads_WithOverloads()
+ {
+ string source = @"
+ using CommunityToolkit.Mvvm.Input;
+
+ namespace MyApp
+ {
+ public partial class SampleViewModel
+ {
+ [ICommand]
+ private void GreetUser()
+ {
+ }
+
+ [ICommand]
+ private void GreetUser(object value)
+ {
+ }
+ }
+ }";
+
+ VerifyGeneratedDiagnostics(source, "MVVMTK0023");
+ }
+
///
/// Verifies the output of a source generator.
///