Add incremental ICommandGenerator

This commit is contained in:
Sergio Pedri 2021-12-14 12:09:11 +01:00
Родитель 84c3e291ba
Коммит 28af36a269
8 изменённых файлов: 836 добавлений и 3 удалений

Просмотреть файл

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