Add incremental ICommandGenerator
This commit is contained in:
Родитель
84c3e291ba
Коммит
28af36a269
|
@ -32,7 +32,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.11.0" PrivateAssets="all" Pack="false" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" Pack="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
// 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 Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="ISymbol"/> type.
|
||||
/// </summary>
|
||||
internal static class ISymbolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the fully qualified name for a given symbol.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The input <see cref="ISymbol"/> instance.</param>
|
||||
/// <returns>The fully qualified name for <paramref name="symbol"/>.</returns>
|
||||
public static string GetFullyQualifiedName(this ISymbol symbol)
|
||||
{
|
||||
return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether or not a given type symbol has a specified full name.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The input <see cref="ISymbol"/> instance to check.</param>
|
||||
/// <param name="name">The full name to check.</param>
|
||||
/// <returns>Whether <paramref name="symbol"/> has a full name equals to <paramref name="name"/>.</returns>
|
||||
public static bool HasFullyQualifiedName(this ISymbol symbol, string name)
|
||||
{
|
||||
return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,593 @@
|
|||
// 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.Linq;
|
||||
using System.Text;
|
||||
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 CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Models;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <summary>
|
||||
/// A source generator for generating command properties from annotated methods.
|
||||
/// </summary>
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public sealed partial class ICommandGenerator2 : IIncrementalGenerator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// Get all method declarations with at least one attribute
|
||||
IncrementalValuesProvider<IMethodSymbol> methodSymbols =
|
||||
context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax, AttributeLists.Count: > 0 },
|
||||
static (context, _) => (IMethodSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
|
||||
|
||||
// Filter the methods using [ICommand]
|
||||
IncrementalValuesProvider<(IMethodSymbol Symbol, AttributeData Attribute)> methodSymbolsWithAttributeData =
|
||||
methodSymbols
|
||||
.Select(static (item, _) => (
|
||||
item,
|
||||
Attribute: item.GetAttributes().FirstOrDefault(a => a.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.ICommandAttribute") == true)))
|
||||
.Where(static item => item.Attribute is not null)!;
|
||||
|
||||
// Gather info for all annotated command methods
|
||||
IncrementalValuesProvider<Result<CommandInfo?>> commandInfoWithErrors =
|
||||
methodSymbolsWithAttributeData
|
||||
.Select(static (item, _) =>
|
||||
{
|
||||
CommandInfo? commandInfo = Execute.GetInfo(item.Symbol, item.Attribute, out ImmutableArray<Diagnostic> diagnostics);
|
||||
|
||||
return new Result<CommandInfo?>(commandInfo, diagnostics);
|
||||
});
|
||||
|
||||
// Output the diagnostics
|
||||
context.ReportDiagnostics(commandInfoWithErrors.Select(static (item, _) => item.Errors));
|
||||
|
||||
// Get the filtered sequence to enable caching
|
||||
IncrementalValuesProvider<CommandInfo> commandInfo =
|
||||
commandInfoWithErrors
|
||||
.Select(static (item, _) => item.Value)
|
||||
.Where(static item => item is not null)!
|
||||
.WithComparer(CommandInfo.Comparer.Default);
|
||||
|
||||
// Generate the commands
|
||||
context.RegisterSourceOutput(commandInfo, static (context, item) =>
|
||||
{
|
||||
ImmutableArray<MemberDeclarationSyntax> memberDeclarations = Execute.GetSyntax(item);
|
||||
CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit(memberDeclarations);
|
||||
|
||||
context.AddSource(
|
||||
hintName: $"{item.Hierarchy.FilenameHint}.{item.MethodName}.cs",
|
||||
sourceText: SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A container for all the logic for <see cref="ICommandGenerator2"/>.
|
||||
/// </summary>
|
||||
private static class Execute
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes a given target method.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <param name="diagnostics">The resulting diagnostics from the processing operation.</param>
|
||||
/// <returns>The resulting <see cref="CommandInfo"/> instance for <paramref name="methodSymbol"/>, if available.</returns>
|
||||
public static CommandInfo? GetInfo(IMethodSymbol methodSymbol, AttributeData attributeData, out ImmutableArray<Diagnostic> diagnostics)
|
||||
{
|
||||
ImmutableArray<Diagnostic>.Builder builder = ImmutableArray.CreateBuilder<Diagnostic>();
|
||||
|
||||
// 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 ImmutableArray<string> commandTypeArguments,
|
||||
out ImmutableArray<string> delegateTypeArguments))
|
||||
{
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
// Check the switch to allow concurrent executions
|
||||
if (!TryGetAllowConcurrentExecutionsSwitch(
|
||||
methodSymbol,
|
||||
attributeData,
|
||||
commandClassType,
|
||||
builder,
|
||||
out bool allowConcurrentExecutions))
|
||||
{
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
// Get the CanExecute expression type, if any
|
||||
if (!TryGetCanExecuteExpressionType(
|
||||
methodSymbol,
|
||||
attributeData,
|
||||
commandTypeArguments,
|
||||
builder,
|
||||
out string? canExecuteMemberName,
|
||||
out CanExecuteExpressionType? canExecuteExpressionType))
|
||||
{
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return new(
|
||||
HierarchyInfo.From(methodSymbol.ContainingType),
|
||||
methodSymbol.Name,
|
||||
fieldName,
|
||||
propertyName,
|
||||
commandInterfaceType,
|
||||
commandClassType,
|
||||
delegateType,
|
||||
commandTypeArguments,
|
||||
delegateTypeArguments,
|
||||
canExecuteMemberName,
|
||||
canExecuteExpressionType,
|
||||
allowConcurrentExecutions);
|
||||
|
||||
Failure:
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="MemberDeclarationSyntax"/> instances for a specified command.
|
||||
/// </summary>
|
||||
/// <param name="commandInfo">The input <see cref="CommandInfo"/> instance with the info to generate the command.</param>
|
||||
/// <returns>The <see cref="MemberDeclarationSyntax"/> instances for the input command.</returns>
|
||||
public static ImmutableArray<MemberDeclarationSyntax> 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:
|
||||
//
|
||||
// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
|
||||
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(ICommandGenerator).FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString()))))))
|
||||
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{commandInfo.PropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())));
|
||||
|
||||
// Prepares the argument to pass the underlying method to invoke
|
||||
ImmutableArray<ArgumentSyntax>.Builder commandCreationArguments = ImmutableArray.CreateBuilder<ArgumentSyntax>();
|
||||
|
||||
// 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 <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, _ => <CAN_EXECUTE_METHOD>());
|
||||
CanExecuteExpressionType.MethodInvocationLambdaWithDiscard =>
|
||||
SimpleLambdaExpression(
|
||||
Parameter(Identifier(TriviaList(), SyntaxKind.UnderscoreToken, "_", "_", TriviaList())))
|
||||
.WithExpressionBody(InvocationExpression(IdentifierName(commandInfo.CanExecuteMemberName!))),
|
||||
|
||||
// Create a lambda expression returning the property value:
|
||||
//
|
||||
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, () => <CAN_EXECUTE_PROPERTY>);
|
||||
CanExecuteExpressionType.PropertyAccessLambda =>
|
||||
ParenthesizedLambdaExpression()
|
||||
.WithExpressionBody(IdentifierName(commandInfo.CanExecuteMemberName!)),
|
||||
|
||||
// Create a lambda expression again, but discarding the input value:
|
||||
//
|
||||
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, _ => <CAN_EXECUTE_PROPERTY>);
|
||||
CanExecuteExpressionType.PropertyAccessLambdaWithDiscard =>
|
||||
SimpleLambdaExpression(
|
||||
Parameter(Identifier(TriviaList(), SyntaxKind.UnderscoreToken, "_", "_", TriviaList())))
|
||||
.WithExpressionBody(IdentifierName(commandInfo.CanExecuteMemberName!)),
|
||||
|
||||
// Create a method groupd expression, which will become:
|
||||
//
|
||||
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, <CAN_EXECUTE_METHOD>);
|
||||
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));
|
||||
}
|
||||
|
||||
// Disable concurrent executions, if requested
|
||||
if (!commandInfo.AllowConcurrentExecutions)
|
||||
{
|
||||
commandCreationArguments.Add(Argument(LiteralExpression(SyntaxKind.FalseLiteralExpression)));
|
||||
}
|
||||
|
||||
// Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts):
|
||||
//
|
||||
// <summary>Gets an <see cref="<COMMAND_INTERFACE_TYPE>" instance wrapping <see cref="<METHOD_NAME>"/> and <see cref="<OPTIONAL_CAN_EXECUTE>"/>.</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>(<COMMAND_CREATION_ARGUMENTS>);
|
||||
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(ICommandGenerator).FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString()))))))
|
||||
.WithOpenBracketToken(Token(TriviaList(Comment(
|
||||
$"/// <summary>Gets an <see cref=\"{commandInterfaceTypeXmlName}\"/> instance wrapping <see cref=\"{commandInfo.MethodName}\"/>.</summary>")),
|
||||
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(commandInfo.FieldName),
|
||||
ObjectCreationExpression(IdentifierName(commandClassTypeName))
|
||||
.AddArgumentListArguments(commandCreationArguments.ToArray()))))
|
||||
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
|
||||
|
||||
return ImmutableArray.Create<MemberDeclarationSyntax>(fieldDeclaration, propertyDeclaration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the generated field and property names for the input method.
|
||||
/// </summary>
|
||||
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
|
||||
/// <returns>The generated field and property names for <paramref name="methodSymbol"/>.</returns>
|
||||
private static (string FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(IMethodSymbol methodSymbol)
|
||||
{
|
||||
string propertyName = methodSymbol.Name;
|
||||
|
||||
if (methodSymbol.ReturnType.HasFullyQualifiedName("global::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="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
|
||||
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
|
||||
/// <param name="commandInterfaceType">The command interface type name.</param>
|
||||
/// <param name="commandClassType">The command class type name.</param>
|
||||
/// <param name="delegateType">The delegate type name for the wrapped method.</param>
|
||||
/// <param name="commandTypeArguments">The type arguments for <paramref name="commandInterfaceType"/> and <paramref name="commandClassType"/>, if any.</param>
|
||||
/// <param name="delegateTypeArguments">The type arguments for <paramref name="delegateType"/>, if any.</param>
|
||||
/// <returns>Whether or not <paramref name="methodSymbol"/> was valid and the requested types have been set.</returns>
|
||||
private static bool TryMapCommandTypesFromMethod(
|
||||
IMethodSymbol methodSymbol,
|
||||
ImmutableArray<Diagnostic>.Builder diagnostics,
|
||||
[NotNullWhen(true)] out string? commandInterfaceType,
|
||||
[NotNullWhen(true)] out string? commandClassType,
|
||||
[NotNullWhen(true)] out string? delegateType,
|
||||
out ImmutableArray<string> commandTypeArguments,
|
||||
out ImmutableArray<string> delegateTypeArguments)
|
||||
{
|
||||
// Map <void, void> 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";
|
||||
commandTypeArguments = ImmutableArray<string>.Empty;
|
||||
delegateTypeArguments = ImmutableArray<string>.Empty;
|
||||
|
||||
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)
|
||||
{
|
||||
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IRelayCommand";
|
||||
commandClassType = "global::CommunityToolkit.Mvvm.Input.RelayCommand";
|
||||
delegateType = "global::System.Action";
|
||||
commandTypeArguments = ImmutableArray.Create(parameter.Type.GetFullyQualifiedName());
|
||||
delegateTypeArguments = ImmutableArray.Create(parameter.Type.GetFullyQualifiedName());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (methodSymbol.ReturnType.HasFullyQualifiedName("global::System.Threading.Tasks.Task"))
|
||||
{
|
||||
// Map <void, Task> to IAsyncRelayCommand, AsyncRelayCommand, Func<Task>
|
||||
if (methodSymbol.Parameters.Length == 0)
|
||||
{
|
||||
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
|
||||
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
|
||||
delegateType = "global::System.Func";
|
||||
commandTypeArguments = ImmutableArray<string>.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 <CancellationToken, Task> to IAsyncRelayCommand, AsyncRelayCommand, Func<CancellationToken, Task>
|
||||
if (singleParameter.Type.HasFullyQualifiedName("global::System.Threading.CancellationToken"))
|
||||
{
|
||||
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
|
||||
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
|
||||
delegateType = "global::System.Func";
|
||||
commandTypeArguments = ImmutableArray<string>.Empty;
|
||||
delegateTypeArguments = ImmutableArray.Create("global::System.Threading.CancellationToken", "global::System.Threading.Tasks.Task");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Map <T, Task> to IAsyncRelayCommand<T>, AsyncRelayCommand<T>, Func<T, Task>
|
||||
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
|
||||
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
|
||||
delegateType = "global::System.Func";
|
||||
commandTypeArguments = ImmutableArray.Create(singleParameter.Type.GetFullyQualifiedName());
|
||||
delegateTypeArguments = ImmutableArray.Create(singleParameter.Type.GetFullyQualifiedName(), "global::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 &&
|
||||
secondParameter.Type.HasFullyQualifiedName("global::System.Threading.CancellationToken"))
|
||||
{
|
||||
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
|
||||
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
|
||||
delegateType = "global::System.Func";
|
||||
commandTypeArguments = ImmutableArray.Create(firstParameter.Type.GetFullyQualifiedName());
|
||||
delegateTypeArguments = ImmutableArray.Create(firstParameter.Type.GetFullyQualifiedName(), secondParameter.Type.GetFullyQualifiedName(), "global::System.Threading.Tasks.Task");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.Add(InvalidICommandMethodSignatureError, methodSymbol, methodSymbol.ContainingType, methodSymbol);
|
||||
|
||||
commandInterfaceType = null;
|
||||
commandClassType = null;
|
||||
delegateType = null;
|
||||
commandTypeArguments = ImmutableArray<string>.Empty;
|
||||
delegateTypeArguments = ImmutableArray<string>.Empty;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether or not the user has requested to configure the handling of concurrent executions.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <param name="commandClassType">The command class type name.</param>
|
||||
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
|
||||
/// <param name="allowConcurrentExecutions">Whether or not concurrent executions have been disabled.</param>
|
||||
/// <returns>Whether or not a value for <paramref name="allowConcurrentExecutions"/> could be retrieved successfully.</returns>
|
||||
private static bool TryGetAllowConcurrentExecutionsSwitch(
|
||||
IMethodSymbol methodSymbol,
|
||||
AttributeData attributeData,
|
||||
string commandClassType,
|
||||
ImmutableArray<Diagnostic>.Builder diagnostics,
|
||||
out bool allowConcurrentExecutions)
|
||||
{
|
||||
// Try to get the custom switch for concurrent executions. If the switch is not present, the
|
||||
// default value is set to true, to avoid breaking backwards compatibility with the first release.
|
||||
if (!attributeData.TryGetNamedArgument("AllowConcurrentExecutions", out allowConcurrentExecutions))
|
||||
{
|
||||
allowConcurrentExecutions = true;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the expression type for the "CanExecute" property, if available.
|
||||
/// </summary>
|
||||
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
|
||||
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="methodSymbol"/>.</param>
|
||||
/// <param name="commandTypeArguments">The command type arguments, if any.</param>
|
||||
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
|
||||
/// <param name="canExecuteMemberName">The resulting can execute member name, if available.</param>
|
||||
/// <param name="canExecuteExpressionType">The resulting expression type, if available.</param>
|
||||
/// <returns>Whether or not a value for <paramref name="canExecuteMemberName"/> and <paramref name="canExecuteExpressionType"/> could be determined (may include <see langword="null"/>).</returns>
|
||||
private static bool TryGetCanExecuteExpressionType(
|
||||
IMethodSymbol methodSymbol,
|
||||
AttributeData attributeData,
|
||||
ImmutableArray<string> commandTypeArguments,
|
||||
ImmutableArray<Diagnostic>.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(InvalidCanExecuteMemberName, methodSymbol, memberName ?? string.Empty, methodSymbol.ContainingType);
|
||||
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
ImmutableArray<ISymbol> canExecuteSymbols = methodSymbol.ContainingType!.GetMembers(memberName);
|
||||
|
||||
if (canExecuteSymbols.IsEmpty)
|
||||
{
|
||||
diagnostics.Add(InvalidCanExecuteMemberName, methodSymbol, memberName, methodSymbol.ContainingType);
|
||||
}
|
||||
else if (canExecuteSymbols.Length > 1)
|
||||
{
|
||||
diagnostics.Add(MultipleCanExecuteMemberNameMatches, methodSymbol, memberName, methodSymbol.ContainingType);
|
||||
}
|
||||
else if (TryGetCanExecuteExpressionFromSymbol(canExecuteSymbols[0], commandTypeArguments, out canExecuteExpressionType))
|
||||
{
|
||||
canExecuteMemberName = memberName;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
diagnostics.Add(InvalidCanExecuteMember, methodSymbol, memberName, methodSymbol.ContainingType);
|
||||
}
|
||||
|
||||
Failure:
|
||||
canExecuteMemberName = null;
|
||||
canExecuteExpressionType = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expression type for the can execute logic, if possible.
|
||||
/// </summary>
|
||||
/// <param name="canExecuteSymbol">The can execute member symbol (either a method or a property).</param>
|
||||
/// <param name="commandTypeArguments">The type arguments for the command interface, if any.</param>
|
||||
/// <param name="canExecuteExpressionType">The resulting can execute expression type, if available.</param>
|
||||
/// <returns>Whether or not <paramref name="canExecuteExpressionType"/> was set and the input symbol was valid.</returns>
|
||||
private static bool TryGetCanExecuteExpressionFromSymbol(
|
||||
ISymbol canExecuteSymbol,
|
||||
ImmutableArray<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,7 +24,6 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
|
|||
/// <summary>
|
||||
/// A source generator for generating command properties from annotated methods.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed partial class ICommandGenerator : ISourceGenerator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// 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.
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A type describing the type of expression for the "CanExecute" property of a command.
|
||||
/// </summary>
|
||||
public enum CanExecuteExpressionType
|
||||
{
|
||||
/// <summary>
|
||||
/// A method invocation lambda with discard: <c>_ => Method()</c>.
|
||||
/// </summary>
|
||||
MethodInvocationLambdaWithDiscard,
|
||||
|
||||
/// <summary>
|
||||
/// A property access lambda: <c>() => Property</c>.
|
||||
/// </summary>
|
||||
PropertyAccessLambda,
|
||||
|
||||
/// <summary>
|
||||
/// A property access lambda with discard: <c>_ => Property</c>.
|
||||
/// </summary>
|
||||
PropertyAccessLambdaWithDiscard,
|
||||
|
||||
/// <summary>
|
||||
/// A method group expression: <c>Method</c>.
|
||||
/// </summary>
|
||||
MethodGroup
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
// 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.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Models;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A model with gathered info on a given command method.
|
||||
/// </summary>
|
||||
/// <param name="Hierarchy">The hierarchy info for the containing type.</param>
|
||||
/// <param name="MethodName">The name of the target method.</param>
|
||||
/// <param name="FieldName">The resulting field name for the generated command.</param>
|
||||
/// <param name="PropertyName">The resulting property name for the generated command.</param>
|
||||
/// <param name="CommandInterfaceType">The command interface type name.</param>
|
||||
/// <param name="CommandClassType">The command class type name.</param>
|
||||
/// <param name="DelegateType">The delegate type name for the wrapped method.</param>
|
||||
/// <param name="CommandTypeArguments">The type arguments for <paramref name="CommandInterfaceType"/> and <paramref name="CommandClassType"/>, if any.</param>
|
||||
/// <param name="DelegateTypeArguments">The type arguments for <paramref name="DelegateType"/>, if any.</param>
|
||||
/// <param name="CanExecuteMemberName">The member name for the can execute check, if available.</param>
|
||||
/// <param name="CanExecuteExpressionType">The can execute expression type, if available.</param>
|
||||
/// <param name="AllowConcurrentExecutions">Whether or not concurrent executions have been disabled.</param>
|
||||
internal sealed record CommandInfo(
|
||||
HierarchyInfo Hierarchy,
|
||||
string MethodName,
|
||||
string FieldName,
|
||||
string PropertyName,
|
||||
string CommandInterfaceType,
|
||||
string CommandClassType,
|
||||
string DelegateType,
|
||||
ImmutableArray<string> CommandTypeArguments,
|
||||
ImmutableArray<string> DelegateTypeArguments,
|
||||
string? CanExecuteMemberName,
|
||||
CanExecuteExpressionType? CanExecuteExpressionType,
|
||||
bool AllowConcurrentExecutions)
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IEqualityComparer{T}"/> implementation for <see cref="CommandInfo"/>.
|
||||
/// </summary>
|
||||
public sealed class Comparer : IEqualityComparer<CommandInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// The singleton <see cref="Comparer"/> instance.
|
||||
/// </summary>
|
||||
public static Comparer Default { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(CommandInfo x, CommandInfo y)
|
||||
{
|
||||
if (x is null && y is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x is null || y is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return
|
||||
HierarchyInfo.Comparer.Default.Equals(x.Hierarchy, y.Hierarchy) &&
|
||||
x.MethodName == y.MethodName &&
|
||||
x.FieldName == y.FieldName &&
|
||||
x.PropertyName == y.PropertyName &&
|
||||
x.CommandInterfaceType == y.CommandInterfaceType &&
|
||||
x.CommandClassType == y.CommandClassType &&
|
||||
x.DelegateType == y.DelegateType &&
|
||||
x.CommandTypeArguments.SequenceEqual(y.CommandTypeArguments) &&
|
||||
x.DelegateTypeArguments.SequenceEqual(y.CommandTypeArguments) &&
|
||||
x.CanExecuteMemberName == y.CanExecuteMemberName &&
|
||||
x.CanExecuteExpressionType == y.CanExecuteExpressionType &&
|
||||
x.AllowConcurrentExecutions == y.AllowConcurrentExecutions;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetHashCode(CommandInfo obj)
|
||||
{
|
||||
HashCode hashCode = default;
|
||||
|
||||
hashCode.Add(obj.Hierarchy, HierarchyInfo.Comparer.Default);
|
||||
hashCode.Add(obj.MethodName);
|
||||
hashCode.Add(obj.FieldName);
|
||||
hashCode.Add(obj.PropertyName);
|
||||
hashCode.Add(obj.CommandInterfaceType);
|
||||
hashCode.Add(obj.CommandClassType);
|
||||
hashCode.Add(obj.DelegateType);
|
||||
hashCode.AddRange(obj.CommandTypeArguments);
|
||||
hashCode.AddRange(obj.DelegateTypeArguments);
|
||||
hashCode.Add(obj.CanExecuteMemberName);
|
||||
hashCode.Add(obj.CanExecuteExpressionType);
|
||||
hashCode.Add(obj.AllowConcurrentExecutions);
|
||||
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// 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.
|
||||
|
||||
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Models;
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal sealed partial record HierarchyInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="CompilationUnitSyntax"/> instance wrapping the given members.
|
||||
/// </summary>
|
||||
/// <param name="memberDeclarations">The input <see cref="MemberDeclarationSyntax"/> instances to use.</param>
|
||||
/// <returns>A <see cref="CompilationUnitSyntax"/> object wrapping <paramref name="memberDeclarations"/>.</returns>
|
||||
public CompilationUnitSyntax GetCompilationUnit(ImmutableArray<MemberDeclarationSyntax> memberDeclarations)
|
||||
{
|
||||
// Create the partial type declaration with the given member declarations.
|
||||
// This code produces a class declaration as follows:
|
||||
//
|
||||
// partial class <TYPE_NAME>
|
||||
// {
|
||||
// <MEMBERS>
|
||||
// }
|
||||
ClassDeclarationSyntax classDeclarationSyntax =
|
||||
ClassDeclaration(Names[0])
|
||||
.AddModifiers(Token(SyntaxKind.PartialKeyword))
|
||||
.AddMembers(memberDeclarations.ToArray());
|
||||
|
||||
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
|
||||
|
||||
// Add all parent types in ascending order, if any
|
||||
foreach (string parentType in Names.AsSpan().Slice(1))
|
||||
{
|
||||
typeDeclarationSyntax =
|
||||
ClassDeclaration(parentType)
|
||||
.AddModifiers(Token(SyntaxKind.PartialKeyword))
|
||||
.AddMembers(typeDeclarationSyntax);
|
||||
}
|
||||
|
||||
// Create the compilation unit with disabled warnings, target namespace and generated type.
|
||||
// This will produce code as follows:
|
||||
//
|
||||
// <auto-generated/>
|
||||
// #pragma warning disable
|
||||
//
|
||||
// namespace <NAMESPACE>
|
||||
// {
|
||||
// <TYPE_HIERARCHY>
|
||||
// }
|
||||
return
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName(Namespace))
|
||||
.WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))))
|
||||
.AddMembers(typeDeclarationSyntax))
|
||||
.NormalizeWhitespace(eol: "\n");
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Models;
|
|||
/// <param name="MetadataName">The metadata name for the current type.</param>
|
||||
/// <param name="Namespace">Gets the namespace for the current type.</param>
|
||||
/// <param name="Names">Gets the sequence of type definitions containing the current type.</param>
|
||||
internal sealed record HierarchyInfo(string FilenameHint, string MetadataName, string Namespace, ImmutableArray<string> Names)
|
||||
internal sealed partial record HierarchyInfo(string FilenameHint, string MetadataName, string Namespace, ImmutableArray<string> Names)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HierarchyInfo"/> instance from a given <see cref="INamedTypeSymbol"/>.
|
||||
|
|
Загрузка…
Ссылка в новой задаче