// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using CommunityToolkit.Mvvm.SourceGenerators.Helpers; using CommunityToolkit.Mvvm.SourceGenerators.Input.Models; using CommunityToolkit.Mvvm.SourceGenerators.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace CommunityToolkit.Mvvm.SourceGenerators; /// partial class RelayCommandGenerator { /// /// A container for all the logic for . /// internal static class Execute { /// /// Processes a given annotated methods and produces command info, if possible. /// /// The input instance to process. /// The instance the method was annotated with. /// The resulting instance, if successfully generated. /// The resulting diagnostics from the processing operation. /// Whether a instance could be generated successfully. public static bool TryGetInfo( IMethodSymbol methodSymbol, AttributeData attributeData, [NotNullWhen(true)] out CommandInfo? commandInfo, out ImmutableArray diagnostics) { using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); // 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); // Get the command type symbols if (!TryMapCommandTypesFromMethod( methodSymbol, in builder, out string? commandInterfaceType, out string? commandClassType, out string? delegateType, out bool supportsCancellation, out ImmutableArray commandTypeArguments, out ImmutableArray commandTypeArgumentsWithNullabilityAnnotations, out ImmutableArray delegateTypeArgumentsWithNullabilityAnnotations)) { goto Failure; } // Check the switch to allow concurrent executions if (!TryGetAllowConcurrentExecutionsSwitch( methodSymbol, attributeData, commandClassType, in builder, out bool allowConcurrentExecutions)) { goto Failure; } // Check the switch to control exception flow if (!TryGetFlowExceptionsToTaskSchedulerSwitch( methodSymbol, attributeData, commandClassType, in builder, out bool flowExceptionsToTaskScheduler)) { goto Failure; } // Get the CanExecute expression type, if any if (!TryGetCanExecuteExpressionType( methodSymbol, attributeData, commandTypeArguments, in builder, out string? canExecuteMemberName, out CanExecuteExpressionType? canExecuteExpressionType)) { goto Failure; } // Get the option to include a cancel command, if any if (!TryGetIncludeCancelCommandSwitch( methodSymbol, attributeData, commandClassType, supportsCancellation, in builder, out bool generateCancelCommand)) { goto Failure; } commandInfo = new CommandInfo( methodSymbol.Name, fieldName, propertyName, commandInterfaceType, commandClassType, delegateType, commandTypeArgumentsWithNullabilityAnnotations, delegateTypeArgumentsWithNullabilityAnnotations, canExecuteMemberName, canExecuteExpressionType, allowConcurrentExecutions, flowExceptionsToTaskScheduler, generateCancelCommand); diagnostics = builder.ToImmutable(); return true; Failure: commandInfo = null; diagnostics = builder.ToImmutable(); return false; } /// /// Creates the instances for a specified command. /// /// The input instance with the info to generate the command. /// The instances for the input command. public static ImmutableArray GetSyntax(CommandInfo commandInfo) { // Prepare all necessary type names with type arguments string commandInterfaceTypeXmlName = commandInfo.CommandTypeArguments.IsEmpty ? commandInfo.CommandInterfaceType : commandInfo.CommandInterfaceType + "{T}"; string commandClassTypeName = commandInfo.CommandTypeArguments.IsEmpty ? commandInfo.CommandClassType : $"{commandInfo.CommandClassType}<{string.Join(", ", commandInfo.CommandTypeArguments)}>"; string commandInterfaceTypeName = commandInfo.CommandTypeArguments.IsEmpty ? commandInfo.CommandInterfaceType : $"{commandInfo.CommandInterfaceType}<{string.Join(", ", commandInfo.CommandTypeArguments)}>"; string delegateTypeName = commandInfo.DelegateTypeArguments.IsEmpty ? commandInfo.DelegateType : $"{commandInfo.DelegateType}<{string.Join(", ", commandInfo.DelegateTypeArguments)}>"; // Construct the generated field as follows: // // /// The backing field for // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // private ? ; FieldDeclarationSyntax fieldDeclaration = FieldDeclaration( VariableDeclaration(NullableType(IdentifierName(commandClassTypeName))) .AddVariables(VariableDeclarator(Identifier(commandInfo.FieldName)))) .AddModifiers(Token(SyntaxKind.PrivateKeyword)) .AddAttributeLists( AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList()))); // Prepares the argument to pass the underlying method to invoke using ImmutableArrayBuilder commandCreationArguments = ImmutableArrayBuilder.Rent(); // The first argument is the execute method, which is always present commandCreationArguments.Add( Argument( ObjectCreationExpression(IdentifierName(delegateTypeName)) .AddArgumentListArguments(Argument(IdentifierName(commandInfo.MethodName))))); // Get the can execute expression, if available ExpressionSyntax? canExecuteExpression = commandInfo.CanExecuteExpressionType switch { // Create a lambda expression ignoring the input value: // // new (, _ => ()); CanExecuteExpressionType.MethodInvocationLambdaWithDiscard => SimpleLambdaExpression( Parameter(Identifier(TriviaList(), SyntaxKind.UnderscoreToken, "_", "_", TriviaList()))) .WithExpressionBody(InvocationExpression(IdentifierName(commandInfo.CanExecuteMemberName!))), // Create a lambda expression returning the property value: // // new (, () => ); CanExecuteExpressionType.PropertyAccessLambda => ParenthesizedLambdaExpression() .WithExpressionBody(IdentifierName(commandInfo.CanExecuteMemberName!)), // Create a lambda expression again, but discarding the input value: // // new (, _ => ); CanExecuteExpressionType.PropertyAccessLambdaWithDiscard => SimpleLambdaExpression( Parameter(Identifier(TriviaList(), SyntaxKind.UnderscoreToken, "_", "_", TriviaList()))) .WithExpressionBody(IdentifierName(commandInfo.CanExecuteMemberName!)), // Create a method group expression, which will become: // // new (, ); CanExecuteExpressionType.MethodGroup => IdentifierName(commandInfo.CanExecuteMemberName!), _ => null }; // Add the can execute expression to the arguments, if available if (canExecuteExpression is not null) { commandCreationArguments.Add(Argument(canExecuteExpression)); } // Enable concurrent executions, if requested if (commandInfo.AllowConcurrentExecutions && !commandInfo.FlowExceptionsToTaskScheduler) { commandCreationArguments.Add( Argument(MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"), IdentifierName("AllowConcurrentExecutions")))); } else if (commandInfo.FlowExceptionsToTaskScheduler && !commandInfo.AllowConcurrentExecutions) { // Enable exception flow, if requested commandCreationArguments.Add( Argument(MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"), IdentifierName("FlowExceptionsToTaskScheduler")))); } else if (commandInfo.AllowConcurrentExecutions && commandInfo.FlowExceptionsToTaskScheduler) { // Enable both concurrency control and exception flow commandCreationArguments.Add( Argument(BinaryExpression( SyntaxKind.BitwiseOrExpression, MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"), IdentifierName("AllowConcurrentExecutions")), MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"), IdentifierName("FlowExceptionsToTaskScheduler"))))); } // Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts): // // /// Gets an and . // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // public => ??= new (); PropertyDeclarationSyntax propertyDeclaration = PropertyDeclaration( IdentifierName(commandInterfaceTypeName), Identifier(commandInfo.PropertyName)) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAttributeLists( AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString())))))) .WithOpenBracketToken(Token(TriviaList(Comment( $"/// Gets an instance wrapping .")), SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) .WithExpressionBody( ArrowExpressionClause( AssignmentExpression( SyntaxKind.CoalesceAssignmentExpression, IdentifierName(commandInfo.FieldName), ObjectCreationExpression(IdentifierName(commandClassTypeName)) .AddArgumentListArguments(commandCreationArguments.ToArray())))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); // Conditionally declare the additional members for the cancel commands if (commandInfo.IncludeCancelCommand) { // Prepare all necessary member and type names string cancelCommandFieldName = $"{commandInfo.FieldName.Substring(0, commandInfo.FieldName.Length - "Command".Length)}CancelCommand"; string cancelCommandPropertyName = $"{commandInfo.PropertyName.Substring(0, commandInfo.PropertyName.Length - "Command".Length)}CancelCommand"; // Construct the generated field for the cancel command as follows: // // /// The backing field for // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // private global::System.Windows.Input.ICommand? ; FieldDeclarationSyntax cancelCommandFieldDeclaration = FieldDeclaration( VariableDeclaration(NullableType(IdentifierName("global::System.Windows.Input.ICommand"))) .AddVariables(VariableDeclarator(Identifier(cancelCommandFieldName)))) .AddModifiers(Token(SyntaxKind.PrivateKeyword)) .AddAttributeLists( AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList()))); // Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts): // // /// Gets an . // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // public global::System.Windows.Input.ICommand => ??= global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommandExtensions.CreateCancelCommand(); PropertyDeclarationSyntax cancelCommandPropertyDeclaration = PropertyDeclaration( IdentifierName("global::System.Windows.Input.ICommand"), Identifier(cancelCommandPropertyName)) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAttributeLists( AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString())))))) .WithOpenBracketToken(Token(TriviaList(Comment( $"/// Gets an instance that can be used to cancel .")), SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) .WithExpressionBody( ArrowExpressionClause( AssignmentExpression( SyntaxKind.CoalesceAssignmentExpression, IdentifierName(cancelCommandFieldName), InvocationExpression( MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName("global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommandExtensions"), IdentifierName("CreateCancelCommand"))) .AddArgumentListArguments(Argument(IdentifierName(commandInfo.PropertyName)))))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); return ImmutableArray.Create(fieldDeclaration, propertyDeclaration, cancelCommandFieldDeclaration, cancelCommandPropertyDeclaration); } 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, in ImmutableArrayBuilder diagnostics) { // If a duplicate is present in any of the base types, always emit a diagnostic for the current method. // That is, there is no need to check the order: we assume the priority is top-down in the type hierarchy. // This check has to be done first, as otherwise there would always be a false positive for the current type. foreach (ISymbol symbol in methodSymbol.ContainingType.BaseType?.GetAllMembers(methodSymbol.Name) ?? Enumerable.Empty()) { if (symbol is IMethodSymbol otherSymbol && otherSymbol.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute")) { diagnostics.Add( MultipleRelayCommandMethodOverloadsError, methodSymbol, methodSymbol.ContainingType, methodSymbol); return false; } } // Check for duplicates in the containing type for the annotated method foreach (ISymbol symbol in methodSymbol.ContainingType.GetMembers(methodSymbol.Name)) { if (symbol is IMethodSymbol otherSymbol && otherSymbol.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute")) { // If the first [RelayCommand] 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( MultipleRelayCommandMethodOverloadsError, methodSymbol, methodSymbol.ContainingType, methodSymbol); return false; } } return true; } /// /// Get the generated field and property names for the input method. /// /// The input instance to process. /// The generated field and property names for . public static (string FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(IMethodSymbol methodSymbol) { string propertyName = methodSymbol.Name; // Strip the "On" prefix, if present. Only do this if the name is longer than 2 characters, // and if the 3rd character is not a lowercase letter. This is needed to avoid accidentally // stripping fales positives for "On" prefixes in names such as "Onboard". if (propertyName.Length > 2 && propertyName.StartsWith("On") && !char.IsLower(propertyName[2])) { propertyName = propertyName.Substring(2); } // Strip the "Async" suffix for methods returning a Task type if (methodSymbol.Name.EndsWith("Async") && methodSymbol.ReturnType.HasOrInheritsFromFullyQualifiedMetadataName("System.Threading.Tasks.Task")) { propertyName = propertyName.Substring(0, propertyName.Length - "Async".Length); } propertyName += "Command"; char firstCharacter = propertyName[0]; char loweredFirstCharacter = char.ToLower(firstCharacter, CultureInfo.InvariantCulture); // The field name is generated depending on whether the first character can be lowered: // - If it can, then the field name is just the property name starting in lowercase. // - If it can't (eg. starts with 'δΈ­'), then the '_' prefix is added to the property name. string fieldName = (firstCharacter == loweredFirstCharacter) switch { true => $"_{propertyName}", false => $"{loweredFirstCharacter}{propertyName.Substring(1)}" }; return (fieldName, propertyName); } /// /// Gets the type symbols for the input method, if supported. /// /// The input instance to process. /// The current collection of gathered diagnostics. /// The command interface type name. /// The command class type name. /// The delegate type name for the wrapped method. /// Indicates whether or not the resulting command supports cancellation. /// The type arguments for and , if any. /// Same as , but with nullability annotations. /// The type arguments for , if any, with nullability annotations. /// Whether or not was valid and the requested types have been set. private static bool TryMapCommandTypesFromMethod( IMethodSymbol methodSymbol, in ImmutableArrayBuilder diagnostics, [NotNullWhen(true)] out string? commandInterfaceType, [NotNullWhen(true)] out string? commandClassType, [NotNullWhen(true)] out string? delegateType, out bool supportsCancellation, out ImmutableArray commandTypeArguments, out ImmutableArray commandTypeArgumentsWithNullabilityAnnotations, out ImmutableArray delegateTypeArgumentsWithNullabilityAnnotations) { // Map to IRelayCommand, RelayCommand, Action if (methodSymbol.ReturnsVoid && methodSymbol.Parameters.Length == 0) { commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IRelayCommand"; commandClassType = "global::CommunityToolkit.Mvvm.Input.RelayCommand"; delegateType = "global::System.Action"; supportsCancellation = false; commandTypeArguments = ImmutableArray.Empty; commandTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Empty; delegateTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Empty; return true; } // Map to IRelayCommand, RelayCommand, Action if (methodSymbol.ReturnsVoid && methodSymbol.Parameters.Length == 1 && methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } parameter) { commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IRelayCommand"; commandClassType = "global::CommunityToolkit.Mvvm.Input.RelayCommand"; delegateType = "global::System.Action"; supportsCancellation = false; commandTypeArguments = ImmutableArray.Create(parameter.Type.GetFullyQualifiedName()); commandTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Create(parameter.Type.GetFullyQualifiedNameWithNullabilityAnnotations()); delegateTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Create(parameter.Type.GetFullyQualifiedNameWithNullabilityAnnotations()); return true; } // Map all Task-returning methods if (methodSymbol.ReturnType.HasOrInheritsFromFullyQualifiedMetadataName("System.Threading.Tasks.Task")) { // Map to IAsyncRelayCommand, AsyncRelayCommand, Func if (methodSymbol.Parameters.Length == 0) { commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand"; commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand"; delegateType = "global::System.Func"; supportsCancellation = false; commandTypeArguments = ImmutableArray.Empty; commandTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Empty; delegateTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Create("global::System.Threading.Tasks.Task"); return true; } if (methodSymbol.Parameters.Length == 1 && methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } singleParameter) { // Map to IAsyncRelayCommand, AsyncRelayCommand, Func if (singleParameter.Type.HasFullyQualifiedMetadataName("System.Threading.CancellationToken")) { commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand"; commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand"; delegateType = "global::System.Func"; supportsCancellation = true; commandTypeArguments = ImmutableArray.Empty; commandTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Empty; delegateTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Create("global::System.Threading.CancellationToken", "global::System.Threading.Tasks.Task"); return true; } // Map to IAsyncRelayCommand, AsyncRelayCommand, Func commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand"; commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand"; delegateType = "global::System.Func"; supportsCancellation = false; commandTypeArguments = ImmutableArray.Create(singleParameter.Type.GetFullyQualifiedName()); commandTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Create(singleParameter.Type.GetFullyQualifiedNameWithNullabilityAnnotations()); delegateTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Create(singleParameter.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), "global::System.Threading.Tasks.Task"); return true; } // Map to IAsyncRelayCommand, AsyncRelayCommand, Func if (methodSymbol.Parameters.Length == 2 && methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } firstParameter && methodSymbol.Parameters[1] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } secondParameter && secondParameter.Type.HasFullyQualifiedMetadataName("System.Threading.CancellationToken")) { commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand"; commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand"; delegateType = "global::System.Func"; supportsCancellation = true; commandTypeArguments = ImmutableArray.Create(firstParameter.Type.GetFullyQualifiedName()); commandTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Create(firstParameter.Type.GetFullyQualifiedNameWithNullabilityAnnotations()); delegateTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Create(firstParameter.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), "global::System.Threading.CancellationToken", "global::System.Threading.Tasks.Task"); return true; } } diagnostics.Add(InvalidRelayCommandMethodSignatureError, methodSymbol, methodSymbol.ContainingType, methodSymbol); commandInterfaceType = null; commandClassType = null; delegateType = null; supportsCancellation = false; commandTypeArguments = ImmutableArray.Empty; commandTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Empty; delegateTypeArgumentsWithNullabilityAnnotations = ImmutableArray.Empty; return false; } /// /// Checks whether or not the user has requested to configure the handling of concurrent executions. /// /// The input instance to process. /// The instance the method was annotated with. /// The command class type name. /// The current collection of gathered diagnostics. /// Whether or not concurrent executions have been enabled. /// Whether or not a value for could be retrieved successfully. private static bool TryGetAllowConcurrentExecutionsSwitch( IMethodSymbol methodSymbol, AttributeData attributeData, string commandClassType, in ImmutableArrayBuilder diagnostics, out bool allowConcurrentExecutions) { // Try to get the custom switch for concurrent executions (the default is false) if (!attributeData.TryGetNamedArgument("AllowConcurrentExecutions", out allowConcurrentExecutions)) { allowConcurrentExecutions = false; return true; } // If the current type is an async command type and concurrent execution is disabled, pass that value to the constructor. // If concurrent executions are allowed, there is no need to add any additional argument, as that is the default value. if (commandClassType is "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand") { return true; } else { diagnostics.Add(InvalidConcurrentExecutionsParameterError, methodSymbol, methodSymbol.ContainingType, methodSymbol); return false; } } /// /// Checks whether or not the user has requested to configure the task scheduler exception flow option. /// /// The input instance to process. /// The instance the method was annotated with. /// The command class type name. /// The current collection of gathered diagnostics. /// Whether or not task scheduler exception flow have been enabled. /// Whether or not a value for could be retrieved successfully. private static bool TryGetFlowExceptionsToTaskSchedulerSwitch( IMethodSymbol methodSymbol, AttributeData attributeData, string commandClassType, in ImmutableArrayBuilder diagnostics, out bool flowExceptionsToTaskScheduler) { // Try to get the custom switch for task scheduler exception flow (the default is false) if (!attributeData.TryGetNamedArgument("FlowExceptionsToTaskScheduler", out flowExceptionsToTaskScheduler)) { flowExceptionsToTaskScheduler = false; return true; } // Just like with the concurrency control option, check that the target command type is asynchronous if (commandClassType is "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand") { return true; } else { diagnostics.Add(InvalidFlowExceptionsToTaskSchedulerParameterError, methodSymbol, methodSymbol.ContainingType, methodSymbol); return false; } } /// /// Checks whether or not the user has requested to also generate a cancel command. /// /// The input instance to process. /// The instance the method was annotated with. /// The command class type name. /// Indicates whether or not the command supports cancellation. /// The current collection of gathered diagnostics. /// Whether or not concurrent executions have been enabled. /// Whether or not a value for could be retrieved successfully. private static bool TryGetIncludeCancelCommandSwitch( IMethodSymbol methodSymbol, AttributeData attributeData, string commandClassType, bool supportsCancellation, in ImmutableArrayBuilder diagnostics, out bool generateCancelCommand) { // Try to get the custom switch for cancel command generation (the default is false) if (!attributeData.TryGetNamedArgument("IncludeCancelCommand", out generateCancelCommand)) { generateCancelCommand = false; return true; } // If the current type is an async command type and cancellation is supported, pass that value to the constructor. // Otherwise, the current attribute use is not valid, so a diagnostic message should be produced. if (commandClassType is "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand" && supportsCancellation) { return true; } else { diagnostics.Add(InvalidIncludeCancelCommandParameterError, methodSymbol, methodSymbol.ContainingType, methodSymbol); return false; } } /// /// Tries to get the expression type for the "CanExecute" property, if available. /// /// The input instance to process. /// The instance for . /// The command type arguments, if any. /// The current collection of gathered diagnostics. /// The resulting can execute member name, if available. /// The resulting expression type, if available. /// Whether or not a value for and could be determined (may include ). private static bool TryGetCanExecuteExpressionType( IMethodSymbol methodSymbol, AttributeData attributeData, ImmutableArray commandTypeArguments, in ImmutableArrayBuilder diagnostics, out string? canExecuteMemberName, out CanExecuteExpressionType? canExecuteExpressionType) { // Get the can execute member, if any if (!attributeData.TryGetNamedArgument("CanExecute", out string? memberName)) { canExecuteMemberName = null; canExecuteExpressionType = null; return true; } if (memberName is null) { diagnostics.Add(InvalidCanExecuteMemberNameError, methodSymbol, memberName ?? string.Empty, methodSymbol.ContainingType); goto Failure; } ImmutableArray canExecuteSymbols = methodSymbol.ContainingType!.GetAllMembers(memberName).ToImmutableArray(); if (canExecuteSymbols.IsEmpty) { // Special case for when the target member is a generated property from [ObservableProperty] if (TryGetCanExecuteMemberFromGeneratedProperty(memberName, methodSymbol.ContainingType, commandTypeArguments, out canExecuteExpressionType)) { canExecuteMemberName = memberName; return true; } diagnostics.Add(InvalidCanExecuteMemberNameError, methodSymbol, memberName, methodSymbol.ContainingType); } else if (canExecuteSymbols.Length > 1) { diagnostics.Add(MultipleCanExecuteMemberNameMatchesError, methodSymbol, memberName, methodSymbol.ContainingType); } else if (TryGetCanExecuteExpressionFromSymbol(canExecuteSymbols[0], commandTypeArguments, out canExecuteExpressionType)) { canExecuteMemberName = memberName; return true; } else { diagnostics.Add(InvalidCanExecuteMemberError, methodSymbol, memberName, methodSymbol.ContainingType); } Failure: canExecuteMemberName = null; canExecuteExpressionType = null; return false; } /// /// Gets the expression type for the can execute logic, if possible. /// /// The can execute member symbol (either a method or a property). /// The type arguments for the command interface, if any. /// The resulting can execute expression type, if available. /// Whether or not was set and the input symbol was valid. private static bool TryGetCanExecuteExpressionFromSymbol( ISymbol canExecuteSymbol, ImmutableArray commandTypeArguments, [NotNullWhen(true)] out CanExecuteExpressionType? canExecuteExpressionType) { if (canExecuteSymbol is IMethodSymbol canExecuteMethodSymbol) { // The return type must always be a bool if (!canExecuteMethodSymbol.ReturnType.HasFullyQualifiedMetadataName("System.Boolean")) { goto Failure; } // Parameterless methods are always valid if (canExecuteMethodSymbol.Parameters.IsEmpty) { // If the command is generic, the input value is ignored if (commandTypeArguments.Length > 0) { canExecuteExpressionType = CanExecuteExpressionType.MethodInvocationLambdaWithDiscard; } else { canExecuteExpressionType = CanExecuteExpressionType.MethodGroup; } return true; } // If the method has parameters, it has to have a single one matching the command type if (canExecuteMethodSymbol.Parameters.Length == 1 && commandTypeArguments.Length == 1 && canExecuteMethodSymbol.Parameters[0].Type.HasFullyQualifiedName(commandTypeArguments[0])) { // Create a method group expression again canExecuteExpressionType = CanExecuteExpressionType.MethodGroup; return true; } } else if (canExecuteSymbol is IPropertySymbol { GetMethod: not null } canExecutePropertySymbol) { // The property type must always be a bool if (!canExecutePropertySymbol.Type.HasFullyQualifiedMetadataName("System.Boolean")) { goto Failure; } if (commandTypeArguments.Length > 0) { canExecuteExpressionType = CanExecuteExpressionType.PropertyAccessLambdaWithDiscard; } else { canExecuteExpressionType = CanExecuteExpressionType.PropertyAccessLambda; } return true; } Failure: canExecuteExpressionType = null; return false; } /// /// Gets the expression type for the can execute logic, if possible. /// /// The member name passed to [RelayCommand(CanExecute = ...)]. /// The containing type for the method annotated with [RelayCommand]. /// The type arguments for the command interface, if any. /// The resulting can execute expression type, if available. /// Whether or not was set and the input symbol was valid. private static bool TryGetCanExecuteMemberFromGeneratedProperty( string memberName, INamedTypeSymbol containingType, ImmutableArray commandTypeArguments, [NotNullWhen(true)] out CanExecuteExpressionType? canExecuteExpressionType) { foreach (ISymbol memberSymbol in containingType.GetAllMembers()) { // Only look for instance fields of bool type if (memberSymbol is not IFieldSymbol { IsStatic: false } fieldSymbol || !fieldSymbol.Type.HasFullyQualifiedMetadataName("System.Boolean")) { continue; } ImmutableArray attributes = memberSymbol.GetAttributes(); // Only filter fields with the [ObservableProperty] attribute if (memberSymbol is IFieldSymbol && !attributes.Any(static a => a.AttributeClass?.HasFullyQualifiedMetadataName( "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") == true)) { continue; } // Get the target property name either directly or matching the generated one string propertyName = ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol); // If the generated property name matches, get the right expression type if (memberName == propertyName) { if (commandTypeArguments.Length > 0) { canExecuteExpressionType = CanExecuteExpressionType.PropertyAccessLambdaWithDiscard; } else { canExecuteExpressionType = CanExecuteExpressionType.PropertyAccessLambda; } return true; } } canExecuteExpressionType = null; return false; } } }