Skip to content

Commit 63a6676

Browse files
committed
Add IncludeCancelCommand option to generator model
1 parent 8ae1006 commit 63a6676

File tree

5 files changed

+97
-5
lines changed

5 files changed

+97
-5
lines changed

CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ MVVMTK0009 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error |
1717
MVVMTK0010 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
1818
MVVMTK0011 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
1919
MVVMTK0012 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
20+
MVVMTK0013 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error

CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,20 @@ internal static class DiagnosticDescriptors
203203
isEnabledByDefault: true,
204204
description: "Cannot apply the [ICommand] attribute specifying a concurrency control setting to methods mapping to non-asynchronous command types.",
205205
helpLinkUri: "https://aka.ms/mvvmtoolkit");
206+
207+
/// <summary>
208+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>ICommandAttribute.IncludeCancelCommandParameter</c> is being set for an invalid method.
209+
/// <para>
210+
/// Format: <c>"The method {0}.{1} cannot be annotated with the [ICommand] attribute specifying to include a cancel command, as it does not map to an asynchronous command type taking a cancellation token"</c>.
211+
/// </para>
212+
/// </summary>
213+
public static readonly DiagnosticDescriptor InvalidIncludeCancelCommandParameterError = new DiagnosticDescriptor(
214+
id: "MVVMTK0013",
215+
title: "Invalid concurrency control setting usage",
216+
messageFormat: "The method {0}.{1} cannot be annotated with the [ICommand] attribute specifying to include a cancel command, as it does not map to an asynchronous command type taking a cancellation token",
217+
category: typeof(ICommandGenerator).FullName,
218+
defaultSeverity: DiagnosticSeverity.Error,
219+
isEnabledByDefault: true,
220+
description: "Cannot apply the [ICommand] attribute specifying to include a cancel command to methods not mapping to an asynchronous command type accepting a cancellation token.",
221+
helpLinkUri: "https://aka.ms/mvvmtoolkit");
206222
}

CommunityToolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.Execute.cs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ private static class Execute
4646
out string? commandInterfaceType,
4747
out string? commandClassType,
4848
out string? delegateType,
49+
out bool supportsCancellation,
4950
out ImmutableArray<string> commandTypeArguments,
5051
out ImmutableArray<string> delegateTypeArguments))
5152
{
@@ -75,6 +76,18 @@ private static class Execute
7576
goto Failure;
7677
}
7778

79+
// Get the option to include a cancel command, if any
80+
if (!TryGetIncludeCancelCommandSwitch(
81+
methodSymbol,
82+
attributeData,
83+
commandClassType,
84+
supportsCancellation,
85+
builder,
86+
out bool generateCancelCommand))
87+
{
88+
goto Failure;
89+
}
90+
7891
diagnostics = builder.ToImmutable();
7992

8093
return new(
@@ -88,7 +101,8 @@ private static class Execute
88101
delegateTypeArguments,
89102
canExecuteMemberName,
90103
canExecuteExpressionType,
91-
allowConcurrentExecutions);
104+
allowConcurrentExecutions,
105+
generateCancelCommand);
92106

93107
Failure:
94108
diagnostics = builder.ToImmutable();
@@ -255,6 +269,7 @@ private static (string FieldName, string PropertyName) GetGeneratedFieldAndPrope
255269
/// <param name="commandInterfaceType">The command interface type name.</param>
256270
/// <param name="commandClassType">The command class type name.</param>
257271
/// <param name="delegateType">The delegate type name for the wrapped method.</param>
272+
/// <param name="supportsCancellation">Indicates whether or not the resulting command supports cancellation.</param>
258273
/// <param name="commandTypeArguments">The type arguments for <paramref name="commandInterfaceType"/> and <paramref name="commandClassType"/>, if any.</param>
259274
/// <param name="delegateTypeArguments">The type arguments for <paramref name="delegateType"/>, if any.</param>
260275
/// <returns>Whether or not <paramref name="methodSymbol"/> was valid and the requested types have been set.</returns>
@@ -264,6 +279,7 @@ private static bool TryMapCommandTypesFromMethod(
264279
[NotNullWhen(true)] out string? commandInterfaceType,
265280
[NotNullWhen(true)] out string? commandClassType,
266281
[NotNullWhen(true)] out string? delegateType,
282+
out bool supportsCancellation,
267283
out ImmutableArray<string> commandTypeArguments,
268284
out ImmutableArray<string> delegateTypeArguments)
269285
{
@@ -273,6 +289,7 @@ private static bool TryMapCommandTypesFromMethod(
273289
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IRelayCommand";
274290
commandClassType = "global::CommunityToolkit.Mvvm.Input.RelayCommand";
275291
delegateType = "global::System.Action";
292+
supportsCancellation = false;
276293
commandTypeArguments = ImmutableArray<string>.Empty;
277294
delegateTypeArguments = ImmutableArray<string>.Empty;
278295

@@ -287,6 +304,7 @@ private static bool TryMapCommandTypesFromMethod(
287304
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IRelayCommand";
288305
commandClassType = "global::CommunityToolkit.Mvvm.Input.RelayCommand";
289306
delegateType = "global::System.Action";
307+
supportsCancellation = false;
290308
commandTypeArguments = ImmutableArray.Create(parameter.Type.GetFullyQualifiedName());
291309
delegateTypeArguments = ImmutableArray.Create(parameter.Type.GetFullyQualifiedName());
292310

@@ -301,6 +319,7 @@ private static bool TryMapCommandTypesFromMethod(
301319
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
302320
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
303321
delegateType = "global::System.Func";
322+
supportsCancellation = false;
304323
commandTypeArguments = ImmutableArray<string>.Empty;
305324
delegateTypeArguments = ImmutableArray.Create("global::System.Threading.Tasks.Task");
306325

@@ -316,8 +335,9 @@ private static bool TryMapCommandTypesFromMethod(
316335
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
317336
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
318337
delegateType = "global::System.Func";
338+
supportsCancellation = true;
319339
commandTypeArguments = ImmutableArray<string>.Empty;
320-
delegateTypeArguments = ImmutableArray.Create("global::System.Threading.CancellationToken", "global::System.Threading.Tasks.Task");
340+
delegateTypeArguments = ImmutableArray.Create(singleParameter.Type.GetFullyQualifiedName(), "global::System.Threading.Tasks.Task");
321341

322342
return true;
323343
}
@@ -326,6 +346,7 @@ private static bool TryMapCommandTypesFromMethod(
326346
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
327347
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
328348
delegateType = "global::System.Func";
349+
supportsCancellation = false;
329350
commandTypeArguments = ImmutableArray.Create(singleParameter.Type.GetFullyQualifiedName());
330351
delegateTypeArguments = ImmutableArray.Create(singleParameter.Type.GetFullyQualifiedName(), "global::System.Threading.Tasks.Task");
331352

@@ -341,6 +362,7 @@ private static bool TryMapCommandTypesFromMethod(
341362
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
342363
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
343364
delegateType = "global::System.Func";
365+
supportsCancellation = true;
344366
commandTypeArguments = ImmutableArray.Create(firstParameter.Type.GetFullyQualifiedName());
345367
delegateTypeArguments = ImmutableArray.Create(firstParameter.Type.GetFullyQualifiedName(), secondParameter.Type.GetFullyQualifiedName(), "global::System.Threading.Tasks.Task");
346368

@@ -353,6 +375,7 @@ private static bool TryMapCommandTypesFromMethod(
353375
commandInterfaceType = null;
354376
commandClassType = null;
355377
delegateType = null;
378+
supportsCancellation = false;
356379
commandTypeArguments = ImmutableArray<string>.Empty;
357380
delegateTypeArguments = ImmutableArray<string>.Empty;
358381

@@ -397,6 +420,47 @@ private static bool TryGetAllowConcurrentExecutionsSwitch(
397420
}
398421
}
399422

423+
/// <summary>
424+
/// Checks whether or not the user has requested to also generate a cancel command.
425+
/// </summary>
426+
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
427+
/// <param name="attributeData">The <see cref="AttributeData"/> instance the method was annotated with.</param>
428+
/// <param name="commandClassType">The command class type name.</param>
429+
/// <param name="supportsCancellation">Indicates whether or not the command supports cancellation.</param>
430+
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
431+
/// <param name="generateCancelCommand">Whether or not concurrent executions have been enabled.</param>
432+
/// <returns>Whether or not a value for <paramref name="generateCancelCommand"/> could be retrieved successfully.</returns>
433+
private static bool TryGetIncludeCancelCommandSwitch(
434+
IMethodSymbol methodSymbol,
435+
AttributeData attributeData,
436+
string commandClassType,
437+
bool supportsCancellation,
438+
ImmutableArray<Diagnostic>.Builder diagnostics,
439+
out bool generateCancelCommand)
440+
{
441+
// Try to get the custom switch for cancel command generation (the default is false)
442+
if (!attributeData.TryGetNamedArgument("IncludeCancelCommand", out generateCancelCommand))
443+
{
444+
generateCancelCommand = false;
445+
446+
return true;
447+
}
448+
449+
// If the current type is an async command type and cancellation is supported, pass that value to the constructor.
450+
// Otherwise, the current attribute use is not valid, so a diagnostic message should be produced.
451+
if (commandClassType is "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand" &&
452+
supportsCancellation)
453+
{
454+
return true;
455+
}
456+
else
457+
{
458+
diagnostics.Add(InvalidIncludeCancelCommandParameterError, methodSymbol, methodSymbol.ContainingType, methodSymbol);
459+
460+
return false;
461+
}
462+
}
463+
400464
/// <summary>
401465
/// Tries to get the expression type for the "CanExecute" property, if available.
402466
/// </summary>

CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
2424
/// <param name="DelegateTypeArguments">The type arguments for <paramref name="DelegateType"/>, if any.</param>
2525
/// <param name="CanExecuteMemberName">The member name for the can execute check, if available.</param>
2626
/// <param name="CanExecuteExpressionType">The can execute expression type, if available.</param>
27-
/// <param name="AllowConcurrentExecutions">Whether or not concurrent executions have been disabled.</param>
27+
/// <param name="AllowConcurrentExecutions">Whether or not concurrent executions have been enabled.</param>
28+
/// <param name="IncludeCancelCommand">Whether or not to also generate a cancel command.</param>
2829
internal sealed record CommandInfo(
2930
string MethodName,
3031
string FieldName,
@@ -36,7 +37,8 @@ internal sealed record CommandInfo(
3637
ImmutableArray<string> DelegateTypeArguments,
3738
string? CanExecuteMemberName,
3839
CanExecuteExpressionType? CanExecuteExpressionType,
39-
bool AllowConcurrentExecutions)
40+
bool AllowConcurrentExecutions,
41+
bool IncludeCancelCommand)
4042
{
4143
/// <summary>
4244
/// An <see cref="IEqualityComparer{T}"/> implementation for <see cref="CommandInfo"/>.
@@ -57,6 +59,7 @@ protected override void AddToHashCode(ref HashCode hashCode, CommandInfo obj)
5759
hashCode.Add(obj.CanExecuteMemberName);
5860
hashCode.Add(obj.CanExecuteExpressionType);
5961
hashCode.Add(obj.AllowConcurrentExecutions);
62+
hashCode.Add(obj.IncludeCancelCommand);
6063
}
6164

6265
/// <inheritdoc/>
@@ -73,7 +76,8 @@ protected override bool AreEqual(CommandInfo x, CommandInfo y)
7376
x.DelegateTypeArguments.SequenceEqual(y.CommandTypeArguments) &&
7477
x.CanExecuteMemberName == y.CanExecuteMemberName &&
7578
x.CanExecuteExpressionType == y.CanExecuteExpressionType &&
76-
x.AllowConcurrentExecutions == y.AllowConcurrentExecutions;
79+
x.AllowConcurrentExecutions == y.AllowConcurrentExecutions &&
80+
x.IncludeCancelCommand == y.IncludeCancelCommand;
7781
}
7882
}
7983
}

CommunityToolkit.Mvvm/Input/Attributes/ICommandAttribute.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,12 @@ public sealed class ICommandAttribute : Attribute
7878
/// when an execution is invoked while a previous one is still running. It is the same as creating an instance of
7979
/// these command types with a constructor such as <see cref="AsyncRelayCommand(Func{System.Threading.Tasks.Task}, bool)"/>.
8080
/// </summary>
81+
/// <remarks>Using this property is not valid if the target command doesn't map to an asynchronous command.</remarks>
8182
public bool AllowConcurrentExecutions { get; init; }
83+
84+
/// <summary>
85+
/// Gets or sets a value indicating whether a cancel command should also be generated for an asynchronous command.
86+
/// </summary>
87+
/// <remarks>Using this property is not valid if the target command doesn't map to a cancellable asynchronous command.</remarks>
88+
public bool IncludeCancelCommand { get; init; }
8289
}

0 commit comments

Comments
 (0)