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. ///