Files
dotnet/CommunityToolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs
2021-12-10 00:58:55 +01:00

498 lines
26 KiB
C#

// 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.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
using System.Collections.Immutable;
namespace CommunityToolkit.Mvvm.SourceGenerators;
/// <summary>
/// A source generator for generating command properties from annotated methods.
/// </summary>
[Generator]
public sealed partial class ICommandGenerator : ISourceGenerator
{
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver());
}
/// <inheritdoc/>
public void Execute(GeneratorExecutionContext context)
{
// Get the syntax receiver with the candidate nodes
if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver ||
syntaxReceiver.GatheredInfo.Count == 0)
{
return;
}
// Validate the language version
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 })
{
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null));
}
foreach (IGrouping<INamedTypeSymbol, SyntaxReceiver.Item>? items in syntaxReceiver.GatheredInfo.GroupBy<SyntaxReceiver.Item, INamedTypeSymbol>(static item => item.MethodSymbol.ContainingType, SymbolEqualityComparer.Default))
{
if (items.Key.DeclaringSyntaxReferences.Length > 0 &&
items.Key.DeclaringSyntaxReferences.First().GetSyntax() is ClassDeclarationSyntax classDeclaration)
{
try
{
OnExecute(context, classDeclaration, items.Key, items);
}
catch
{
context.ReportDiagnostic(ICommandGeneratorError, classDeclaration, items.Key);
}
}
}
}
/// <summary>
/// Processes a given target type.
/// </summary>
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> node to process.</param>
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for <paramref name="classDeclaration"/>.</param>
/// <param name="items">The sequence of <see cref="IMethodSymbol"/> instances to process.</param>
private static void OnExecute(
GeneratorExecutionContext context,
ClassDeclarationSyntax classDeclaration,
INamedTypeSymbol classDeclarationSymbol,
IEnumerable<SyntaxReceiver.Item> items)
{
// Create the class declaration for the user type. This will produce a tree as follows:
//
// <MODIFIERS> <CLASS_NAME>
// {
// <MEMBERS>
// }
ClassDeclarationSyntax? classDeclarationSyntax =
ClassDeclaration(classDeclarationSymbol.Name)
.WithModifiers(classDeclaration.Modifiers)
.AddMembers(items.Select(item => CreateCommandMembers(context, classDeclarationSymbol, item.LeadingTrivia, item.MethodSymbol, item.AttributeData)).SelectMany(static g => g).ToArray());
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
// Add all parent types in ascending order, if any
foreach (TypeDeclarationSyntax? parentType in classDeclaration.Ancestors().OfType<TypeDeclarationSyntax>())
{
typeDeclarationSyntax = parentType
.WithMembers(SingletonList<MemberDeclarationSyntax>(typeDeclarationSyntax))
.WithConstraintClauses(List<TypeParameterConstraintClauseSyntax>())
.WithBaseList(null)
.WithAttributeLists(List<AttributeListSyntax>())
.WithoutTrivia();
}
// Create the compilation unit with the namespace and target member.
// From this, we can finally generate the source code to output.
string? namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new SymbolDisplayFormat(typeQualificationStyle: NameAndContainingTypesAndNamespaces));
// Create the final compilation unit to generate (with leading trivia)
string? source =
CompilationUnit().AddMembers(
NamespaceDeclaration(IdentifierName(namespaceName)).WithLeadingTrivia(TriviaList(
Comment("// <auto-generated/>"),
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))))
.AddMembers(typeDeclarationSyntax))
.NormalizeWhitespace()
.ToFullString();
// Add the partial type
context.AddSource($"{classDeclarationSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8));
}
/// <summary>
/// Creates the <see cref="MemberDeclarationSyntax"/> instances for a specified command.
/// </summary>
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for the parent type.</param>
/// <param name="leadingTrivia">The leading trivia for the field to process.</param>
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
/// <param name="attributeData">The <see cref="AttributeData"/> instance the method was annotated with.</param>
/// <returns>The <see cref="MemberDeclarationSyntax"/> instances for the input command.</returns>
[Pure]
private static IEnumerable<MemberDeclarationSyntax> CreateCommandMembers(
GeneratorExecutionContext context,
INamedTypeSymbol classDeclarationSymbol,
SyntaxTriviaList leadingTrivia,
IMethodSymbol methodSymbol,
AttributeData attributeData)
{
// Get the command member names
(string fieldName, string propertyName) = GetGeneratedFieldAndPropertyNames(context, methodSymbol);
// Get the command type symbols
if (!TryMapCommandTypesFromMethod(
context,
methodSymbol,
out INamedTypeSymbol? commandInterfaceTypeSymbol,
out INamedTypeSymbol? commandClassTypeSymbol,
out INamedTypeSymbol? delegateTypeSymbol))
{
context.ReportDiagnostic(InvalidICommandMethodSignatureError, methodSymbol, methodSymbol.ContainingType, methodSymbol);
return Array.Empty<MemberDeclarationSyntax>();
}
// Prepares the argument to pass the underlying method to invoke
List<ArgumentSyntax> commandCreationArguments = new()
{
Argument(
ObjectCreationExpression(IdentifierName(delegateTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))
.AddArgumentListArguments(Argument(IdentifierName(methodSymbol.Name))))
};
// Get the can execute member, if any
if (attributeData.TryGetNamedArgument("CanExecute", out string? memberName))
{
if (memberName is null)
{
context.ReportDiagnostic(InvalidCanExecuteMemberName, methodSymbol, memberName ?? string.Empty, methodSymbol.ContainingType);
}
else
{
ImmutableArray<ISymbol> canExecuteSymbols = classDeclarationSymbol.GetMembers(memberName);
if (canExecuteSymbols.IsEmpty)
{
context.ReportDiagnostic(InvalidCanExecuteMemberName, methodSymbol, memberName, methodSymbol.ContainingType);
}
else if (canExecuteSymbols.Length > 1)
{
context.ReportDiagnostic(MultipleCanExecuteMemberNameMatches, methodSymbol, memberName, methodSymbol.ContainingType);
}
else if (TryGetCanExecuteExpressionFromSymbol(context, memberName, canExecuteSymbols[0], commandInterfaceTypeSymbol, out ExpressionSyntax? canExecuteExpression))
{
commandCreationArguments.Add(Argument(canExecuteExpression));
}
else
{
context.ReportDiagnostic(InvalidCanExecuteMember, methodSymbol, memberName, methodSymbol.ContainingType);
}
}
}
// Construct the generated field as follows:
//
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
FieldDeclarationSyntax fieldDeclaration =
FieldDeclaration(
VariableDeclaration(NullableType(IdentifierName(commandClassTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))))
.AddVariables(VariableDeclarator(Identifier(fieldName))))
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
.AddAttributeLists(AttributeList(SingletonSeparatedList(
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString())))))));
SyntaxTriviaList summaryTrivia = SyntaxTriviaList.Empty;
// Parse the <summary> docs, if present
foreach (SyntaxTrivia trivia in leadingTrivia)
{
if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) ||
trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia))
{
string text = trivia.ToString();
Match match = Regex.Match(text, @"<summary>.*?<\/summary>", RegexOptions.Singleline);
if (match.Success)
{
summaryTrivia = TriviaList(Comment($"/// {match.Value}"));
break;
}
}
}
// 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 (commandClassTypeSymbol.Name is "AsyncRelayCommand" or "AsyncRelayCommand`1")
{
if (attributeData.TryGetNamedArgument("AllowConcurrentExecutions", out bool allowConcurrentExecutions) &&
!allowConcurrentExecutions)
{
commandCreationArguments.Add(Argument(LiteralExpression(SyntaxKind.FalseLiteralExpression)));
}
}
else if (attributeData.TryGetNamedArgument("AllowConcurrentExecutions", out bool _))
{
context.ReportDiagnostic(InvalidConcurrentExecutionsParameterError, methodSymbol, methodSymbol.ContainingType, methodSymbol);
}
// Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts):
//
// <METHOD_SUMMARY>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// [global::System.Diagnostics.DebuggerNonUserCode]
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
// public <COMMAND_TYPE> <COMMAND_PROPERTY_NAME> => <COMMAND_FIELD_NAME> ??= new <RELAY_COMMAND_TYPE>(new <DELEGATE_TYPE>(<METHOD_NAME>), <OPTIONAL_CAN_EXECUTE>, <OPTIONAL_ALLOW_CONCURRENT_EXECUTIONS>);
PropertyDeclarationSyntax propertyDeclaration =
PropertyDeclaration(
IdentifierName(commandInterfaceTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
Identifier(propertyName))
.AddModifiers(Token(SyntaxKind.PublicKeyword))
.AddAttributeLists(
AttributeList(SingletonSeparatedList(
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString()))))))
.WithOpenBracketToken(Token(summaryTrivia, SyntaxKind.OpenBracketToken, TriviaList())),
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
.WithExpressionBody(
ArrowExpressionClause(
AssignmentExpression(
SyntaxKind.CoalesceAssignmentExpression,
IdentifierName(fieldName),
ObjectCreationExpression(IdentifierName(commandClassTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))
.AddArgumentListArguments(commandCreationArguments.ToArray()))))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
return new MemberDeclarationSyntax[] { fieldDeclaration, propertyDeclaration };
}
/// <summary>
/// Get the generated field and property names for the input method.
/// </summary>
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
/// <returns>The generated field and property names for <paramref name="methodSymbol"/>.</returns>
[Pure]
private static (string FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(GeneratorExecutionContext context, IMethodSymbol methodSymbol)
{
string propertyName = methodSymbol.Name;
if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")) &&
methodSymbol.Name.EndsWith("Async"))
{
propertyName = propertyName.Substring(0, propertyName.Length - "Async".Length);
}
propertyName += "Command";
string fieldName = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1)}";
return (fieldName, propertyName);
}
/// <summary>
/// Gets the type symbols for the input method, if supported.
/// </summary>
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
/// <param name="commandInterfaceTypeSymbol">The command interface type symbol.</param>
/// <param name="commandClassTypeSymbol">The command class type symbol.</param>
/// <param name="delegateTypeSymbol">The delegate type symbol for the wrapped method.</param>
/// <returns>Whether or not <paramref name="methodSymbol"/> was valid and the requested types have been set.</returns>
private static bool TryMapCommandTypesFromMethod(
GeneratorExecutionContext context,
IMethodSymbol methodSymbol,
[NotNullWhen(true)] out INamedTypeSymbol? commandInterfaceTypeSymbol,
[NotNullWhen(true)] out INamedTypeSymbol? commandClassTypeSymbol,
[NotNullWhen(true)] out INamedTypeSymbol? delegateTypeSymbol)
{
// Map <void, void> to IRelayCommand, RelayCommand, Action
if (methodSymbol.ReturnsVoid && methodSymbol.Parameters.Length == 0)
{
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IRelayCommand")!;
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommand")!;
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Action")!;
return true;
}
// Map <T, void> to IRelayCommand<T>, RelayCommand<T>, Action<T>
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)
{
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IRelayCommand`1")!.Construct(parameter.Type);
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommand`1")!.Construct(parameter.Type);
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Action`1")!.Construct(parameter.Type);
return true;
}
if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!))
{
// Map <void, Task> to IAsyncRelayCommand, AsyncRelayCommand, Func<Task>
if (methodSymbol.Parameters.Length == 0)
{
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IAsyncRelayCommand")!;
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.AsyncRelayCommand")!;
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`1")!.Construct(context.Compilation.GetTypeByMetadataName("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 <CancellationToken, Task> to IAsyncRelayCommand, AsyncRelayCommand, Func<CancellationToken, Task>
if (SymbolEqualityComparer.Default.Equals(singleParameter.Type, context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!))
{
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IAsyncRelayCommand")!;
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.AsyncRelayCommand")!;
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`2")!.Construct(
context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!,
context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!);
return true;
}
// Map <T, Task> to IAsyncRelayCommand<T>, AsyncRelayCommand<T>, Func<T, Task>
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IAsyncRelayCommand`1")!.Construct(singleParameter.Type);
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.AsyncRelayCommand`1")!.Construct(singleParameter.Type);
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`2")!.Construct(
singleParameter.Type,
context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!);
return true;
}
// Map <T, CancellationToken, Task> to IAsyncRelayCommand<T>, AsyncRelayCommand<T>, Func<T, CancellationToken, Task>
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 &&
SymbolEqualityComparer.Default.Equals(secondParameter.Type, context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!))
{
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IAsyncRelayCommand`1")!.Construct(firstParameter.Type);
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.AsyncRelayCommand`1")!.Construct(firstParameter.Type);
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`3")!.Construct(
firstParameter.Type,
secondParameter.Type,
context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!);
return true;
}
}
commandInterfaceTypeSymbol = null;
commandClassTypeSymbol = null;
delegateTypeSymbol = null;
return false;
}
/// <summary>
/// Gets the expression for the can execute logic, if possible.
/// </summary>
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
/// <param name="canExecuteMemberName">The name of the can execute member name being used.</param>
/// <param name="canExecuteSymbol">The can execute member symbol (either a method or a property).</param>
/// <param name="commandInterfaceTypeSymbol">The command interface type symbol.</param>
/// <param name="canExecuteExpression">The resulting can execute expression, if available.</param>
/// <returns>Whether or not <paramref name="canExecuteExpression"/> was set and the input symbol was valid.</returns>
private static bool TryGetCanExecuteExpressionFromSymbol(
GeneratorExecutionContext context,
string canExecuteMemberName,
ISymbol canExecuteSymbol,
INamedTypeSymbol commandInterfaceTypeSymbol,
[NotNullWhen(true)] out ExpressionSyntax? canExecuteExpression)
{
if (canExecuteSymbol is IMethodSymbol canExecuteMethodSymbol)
{
// The return type must always be a bool
if (!SymbolEqualityComparer.Default.Equals(canExecuteMethodSymbol.ReturnType, context.Compilation.GetTypeByMetadataName("System.Boolean")))
{
goto Failure;
}
// Parameterless methods are always valid
if (canExecuteMethodSymbol.Parameters.IsEmpty)
{
// If the command is generic, the input value is ignored
if (commandInterfaceTypeSymbol.IsGenericType)
{
// Create a lambda expression ignoring the input value:
//
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, _ => <CAN_EXECUTE_METHOD>());
canExecuteExpression =
SimpleLambdaExpression(
Parameter(Identifier(TriviaList(), SyntaxKind.UnderscoreToken, "_", "_", TriviaList())))
.WithExpressionBody(InvocationExpression(IdentifierName(canExecuteMemberName)));
}
else
{
// Create a method groupd expression, which will become:
//
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, <CAN_EXECUTE_METHOD>);
canExecuteExpression = IdentifierName(canExecuteMemberName);
}
return true;
}
// If the method has parameters, it has to have a single one matching the command type
if (canExecuteMethodSymbol.Parameters.Length == 1 &&
commandInterfaceTypeSymbol.IsGenericType &&
SymbolEqualityComparer.Default.Equals(canExecuteMethodSymbol.Parameters[0].Type, commandInterfaceTypeSymbol.TypeArguments[0]))
{
// Create a method groupd expression again
canExecuteExpression = IdentifierName(canExecuteMemberName);
return true;
}
}
else if (canExecuteSymbol is IPropertySymbol { GetMethod: not null } canExecutePropertySymbol)
{
// The property type must always be a bool
if (!SymbolEqualityComparer.Default.Equals(canExecutePropertySymbol.Type, context.Compilation.GetTypeByMetadataName("System.Boolean")))
{
goto Failure;
}
if (commandInterfaceTypeSymbol.IsGenericType)
{
// Create a lambda expression again, but discarding the input value:
//
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, _ => <CAN_EXECUTE_PROPERTY>);
canExecuteExpression =
SimpleLambdaExpression(
Parameter(Identifier(TriviaList(), SyntaxKind.UnderscoreToken, "_", "_", TriviaList())))
.WithExpressionBody(IdentifierName(canExecuteMemberName));
}
else
{
// Create a lambda expression returning the property value:
//
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, () => <CAN_EXECUTE_PROPERTY>);
canExecuteExpression = ParenthesizedLambdaExpression().WithExpressionBody(IdentifierName(canExecuteMemberName));
}
return true;
}
Failure:
canExecuteExpression = null;
return false;
}
}