// 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.Diagnostics;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using CommunityToolkit.Mvvm.SourceGenerators.Input.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 target method.
///
/// The input instance to process.
/// The instance the method was annotated with.
/// The resulting diagnostics from the processing operation.
/// The resulting instance for , if available.
public static CommandInfo? GetInfo(IMethodSymbol methodSymbol, AttributeData attributeData, out ImmutableArray diagnostics)
{
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);
// Get the command type symbols
if (!TryMapCommandTypesFromMethod(
methodSymbol,
builder,
out string? commandInterfaceType,
out string? commandClassType,
out string? delegateType,
out bool supportsCancellation,
out ImmutableArray commandTypeArguments,
out ImmutableArray delegateTypeArguments))
{
goto Failure;
}
// Check the switch to allow concurrent executions
if (!TryGetAllowConcurrentExecutionsSwitch(
methodSymbol,
attributeData,
commandClassType,
builder,
out bool allowConcurrentExecutions))
{
goto Failure;
}
// Check the switch to control exception flow
if (!TryGetFlowExceptionsToTaskSchedulerSwitch(
methodSymbol,
attributeData,
commandClassType,
builder,
out bool flowExceptionsToTaskScheduler))
{
goto Failure;
}
// Get the CanExecute expression type, if any
if (!TryGetCanExecuteExpressionType(
methodSymbol,
attributeData,
commandTypeArguments,
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,
builder,
out bool generateCancelCommand))
{
goto Failure;
}
diagnostics = builder.ToImmutable();
return new(
methodSymbol.Name,
fieldName,
propertyName,
commandInterfaceType,
commandClassType,
delegateType,
commandTypeArguments,
delegateTypeArguments,
canExecuteMemberName,
canExecuteExpressionType,
allowConcurrentExecutions,
flowExceptionsToTaskScheduler,
generateCancelCommand);
Failure:
diagnostics = builder.ToImmutable();
return null;
}
///
/// 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
ImmutableArray.Builder commandCreationArguments = ImmutableArray.CreateBuilder();
// 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, ImmutableArray.Builder 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.HasAttributeWithFullyQualifiedName("global::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.HasAttributeWithFullyQualifiedName("global::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.HasFullyQualifiedName("global::System.Threading.Tasks.Task") ||
methodSymbol.ReturnType.InheritsFromFullyQualifiedName("global::System.Threading.Tasks.Task")))
{
propertyName = propertyName.Substring(0, propertyName.Length - "Async".Length);
}
propertyName += "Command";
string fieldName = $"{char.ToLower(propertyName[0], CultureInfo.InvariantCulture)}{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.
/// The type arguments for , if any.
/// Whether or not was valid and the requested types have been set.
private static bool TryMapCommandTypesFromMethod(
IMethodSymbol methodSymbol,
ImmutableArray.Builder diagnostics,
[NotNullWhen(true)] out string? commandInterfaceType,
[NotNullWhen(true)] out string? commandClassType,
[NotNullWhen(true)] out string? delegateType,
out bool supportsCancellation,
out ImmutableArray commandTypeArguments,
out ImmutableArray delegateTypeArguments)
{
// 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;
delegateTypeArguments = 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.GetFullyQualifiedNameWithNullabilityAnnotations());
delegateTypeArguments = ImmutableArray.Create(parameter.Type.GetFullyQualifiedNameWithNullabilityAnnotations());
return true;
}
// Map all Task-returning methods
if (methodSymbol.ReturnType.HasFullyQualifiedName("global::System.Threading.Tasks.Task") ||
methodSymbol.ReturnType.InheritsFromFullyQualifiedName("global::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;
delegateTypeArguments = 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.HasFullyQualifiedName("global::System.Threading.CancellationToken"))
{
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
delegateType = "global::System.Func";
supportsCancellation = true;
commandTypeArguments = ImmutableArray.Empty;
delegateTypeArguments = 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.GetFullyQualifiedNameWithNullabilityAnnotations());
delegateTypeArguments = 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.HasFullyQualifiedName("global::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.GetFullyQualifiedNameWithNullabilityAnnotations());
delegateTypeArguments = 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;
delegateTypeArguments = 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,
ImmutableArray.Builder 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,
ImmutableArray.Builder 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,
ImmutableArray.Builder 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,
ImmutableArray.Builder 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.HasFullyQualifiedName("bool"))
{
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.HasFullyQualifiedName("bool"))
{
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.HasFullyQualifiedName("bool"))
{
continue;
}
ImmutableArray attributes = memberSymbol.GetAttributes();
// Only filter fields with the [ObservableProperty] attribute
if (memberSymbol is IFieldSymbol &&
!attributes.Any(static a => a.AttributeClass?.HasFullyQualifiedName(
"global::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;
}
}
}