Skip to content

Commit 0d7ec7a

Browse files
committed
Add FlowExceptionsToTaskScheduler generator support
1 parent 799558c commit 0d7ec7a

File tree

5 files changed

+114
-5
lines changed

5 files changed

+114
-5
lines changed

CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ MVVMTK0027 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
3737
MVVMTK0028 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
3838
MVVMTK0029 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/error
3939
MVVMTK0030 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/error
40+
MVVMTK0031 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error

CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,17 +191,17 @@ internal static class DiagnosticDescriptors
191191
/// <summary>
192192
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>RelayCommandAttribute.AllowConcurrentExecutions</c> is being set for a non-asynchronous method.
193193
/// <para>
194-
/// Format: <c>"The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying a concurrency control setting, as it maps to a non-asynchronous command type"</c>.
194+
/// Format: <c>"The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying a concurrency control option, as it maps to a non-asynchronous command type"</c>.
195195
/// </para>
196196
/// </summary>
197197
public static readonly DiagnosticDescriptor InvalidConcurrentExecutionsParameterError = new DiagnosticDescriptor(
198198
id: "MVVMTK0012",
199-
title: "Invalid concurrency control setting usage",
200-
messageFormat: "The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying a concurrency control setting, as it maps to a non-asynchronous command type",
199+
title: "Invalid concurrency control option usage",
200+
messageFormat: "The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying a concurrency control option, as it maps to a non-asynchronous command type",
201201
category: typeof(RelayCommandGenerator).FullName,
202202
defaultSeverity: DiagnosticSeverity.Error,
203203
isEnabledByDefault: true,
204-
description: "Cannot apply the [RelayCommand] attribute specifying a concurrency control setting to methods mapping to non-asynchronous command types.",
204+
description: "Cannot apply the [RelayCommand] attribute specifying a concurrency control option to methods mapping to non-asynchronous command types.",
205205
helpLinkUri: "https://aka.ms/mvvmtoolkit");
206206

207207
/// <summary>
@@ -491,4 +491,20 @@ internal static class DiagnosticDescriptors
491491
isEnabledByDefault: true,
492492
description: "Annotating a field with [NotifyDataErrorInfo] is not necessary if the containing type has or inherits [NotifyDataErrorInfo] at the class-level.",
493493
helpLinkUri: "https://aka.ms/mvvmtoolkit");
494+
495+
/// <summary>
496+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>RelayCommandAttribute.FlowExceptionsToTaskScheduler</c> is being set for a non-asynchronous method.
497+
/// <para>
498+
/// Format: <c>"The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying an exception flow option, as it maps to a non-asynchronous command type"</c>.
499+
/// </para>
500+
/// </summary>
501+
public static readonly DiagnosticDescriptor InvalidFlowExceptionsToTaskSchedulerParameterError = new DiagnosticDescriptor(
502+
id: "MVVMTK0031",
503+
title: "Invalid task scheduler exception flow option usage",
504+
messageFormat: "The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying a task scheduler exception flow option, as it maps to a non-asynchronous command type",
505+
category: typeof(RelayCommandGenerator).FullName,
506+
defaultSeverity: DiagnosticSeverity.Error,
507+
isEnabledByDefault: true,
508+
description: "Cannot apply the [RelayCommand] attribute specifying a task scheduler exception flow option to methods mapping to non-asynchronous command types.",
509+
helpLinkUri: "https://aka.ms/mvvmtoolkit");
494510
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
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>
2727
/// <param name="AllowConcurrentExecutions">Whether or not concurrent executions have been enabled.</param>
28+
/// <param name="FlowExceptionsToTaskScheduler">Whether or not exceptions should flow to the task scheduler.</param>
2829
/// <param name="IncludeCancelCommand">Whether or not to also generate a cancel command.</param>
2930
internal sealed record CommandInfo(
3031
string MethodName,
@@ -38,6 +39,7 @@ internal sealed record CommandInfo(
3839
string? CanExecuteMemberName,
3940
CanExecuteExpressionType? CanExecuteExpressionType,
4041
bool AllowConcurrentExecutions,
42+
bool FlowExceptionsToTaskScheduler,
4143
bool IncludeCancelCommand)
4244
{
4345
/// <summary>
@@ -59,6 +61,7 @@ protected override void AddToHashCode(ref HashCode hashCode, CommandInfo obj)
5961
hashCode.Add(obj.CanExecuteMemberName);
6062
hashCode.Add(obj.CanExecuteExpressionType);
6163
hashCode.Add(obj.AllowConcurrentExecutions);
64+
hashCode.Add(obj.FlowExceptionsToTaskScheduler);
6265
hashCode.Add(obj.IncludeCancelCommand);
6366
}
6467

@@ -77,6 +80,7 @@ protected override bool AreEqual(CommandInfo x, CommandInfo y)
7780
x.CanExecuteMemberName == y.CanExecuteMemberName &&
7881
x.CanExecuteExpressionType == y.CanExecuteExpressionType &&
7982
x.AllowConcurrentExecutions == y.AllowConcurrentExecutions &&
83+
x.FlowExceptionsToTaskScheduler == y.FlowExceptionsToTaskScheduler &&
8084
x.IncludeCancelCommand == y.IncludeCancelCommand;
8185
}
8286
}

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

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ internal static class Execute
7171
goto Failure;
7272
}
7373

74+
// Check the switch to control exception flow
75+
if (!TryGetFlowExceptionsToTaskSchedulerSwitch(
76+
methodSymbol,
77+
attributeData,
78+
commandClassType,
79+
builder,
80+
out bool flowExceptionsToTaskScheduler))
81+
{
82+
goto Failure;
83+
}
84+
7485
// Get the CanExecute expression type, if any
7586
if (!TryGetCanExecuteExpressionType(
7687
methodSymbol,
@@ -109,6 +120,7 @@ internal static class Execute
109120
canExecuteMemberName,
110121
canExecuteExpressionType,
111122
allowConcurrentExecutions,
123+
flowExceptionsToTaskScheduler,
112124
generateCancelCommand);
113125

114126
Failure:
@@ -205,14 +217,38 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
205217
}
206218

207219
// Enable concurrent executions, if requested
208-
if (commandInfo.AllowConcurrentExecutions)
220+
if (commandInfo.AllowConcurrentExecutions && !commandInfo.FlowExceptionsToTaskScheduler)
209221
{
210222
commandCreationArguments.Add(
211223
Argument(MemberAccessExpression(
212224
SyntaxKind.SimpleMemberAccessExpression,
213225
IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"),
214226
IdentifierName("AllowConcurrentExecutions"))));
215227
}
228+
else if (commandInfo.FlowExceptionsToTaskScheduler && !commandInfo.AllowConcurrentExecutions)
229+
{
230+
// Enable exception flow, if requested
231+
commandCreationArguments.Add(
232+
Argument(MemberAccessExpression(
233+
SyntaxKind.SimpleMemberAccessExpression,
234+
IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"),
235+
IdentifierName("FlowExceptionsToTaskScheduler"))));
236+
}
237+
else if (commandInfo.AllowConcurrentExecutions && commandInfo.FlowExceptionsToTaskScheduler)
238+
{
239+
// Enable both concurrency control and exception flow
240+
commandCreationArguments.Add(
241+
Argument(BinaryExpression(
242+
SyntaxKind.BitwiseOrExpression,
243+
MemberAccessExpression(
244+
SyntaxKind.SimpleMemberAccessExpression,
245+
IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"),
246+
IdentifierName("AllowConcurrentExecutions")),
247+
MemberAccessExpression(
248+
SyntaxKind.SimpleMemberAccessExpression,
249+
IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"),
250+
IdentifierName("FlowExceptionsToTaskScheduler")))));
251+
}
216252

217253
// Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts):
218254
//
@@ -559,6 +595,43 @@ private static bool TryGetAllowConcurrentExecutionsSwitch(
559595
}
560596
}
561597

598+
/// <summary>
599+
/// Checks whether or not the user has requested to configure the task scheduler exception flow option.
600+
/// </summary>
601+
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
602+
/// <param name="attributeData">The <see cref="AttributeData"/> instance the method was annotated with.</param>
603+
/// <param name="commandClassType">The command class type name.</param>
604+
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
605+
/// <param name="flowExceptionsToTaskScheduler">Whether or not task scheduler exception flow have been enabled.</param>
606+
/// <returns>Whether or not a value for <paramref name="flowExceptionsToTaskScheduler"/> could be retrieved successfully.</returns>
607+
private static bool TryGetFlowExceptionsToTaskSchedulerSwitch(
608+
IMethodSymbol methodSymbol,
609+
AttributeData attributeData,
610+
string commandClassType,
611+
ImmutableArray<Diagnostic>.Builder diagnostics,
612+
out bool flowExceptionsToTaskScheduler)
613+
{
614+
// Try to get the custom switch for task scheduler exception flow (the default is false)
615+
if (!attributeData.TryGetNamedArgument("FlowExceptionsToTaskScheduler", out flowExceptionsToTaskScheduler))
616+
{
617+
flowExceptionsToTaskScheduler = false;
618+
619+
return true;
620+
}
621+
622+
// Just like with the concurrency control option, check that the target command type is asynchronous
623+
if (commandClassType is "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand")
624+
{
625+
return true;
626+
}
627+
else
628+
{
629+
diagnostics.Add(InvalidFlowExceptionsToTaskSchedulerParameterError, methodSymbol, methodSymbol.ContainingType, methodSymbol);
630+
631+
return false;
632+
}
633+
}
634+
562635
/// <summary>
563636
/// Checks whether or not the user has requested to also generate a cancel command.
564637
/// </summary>

CommunityToolkit.Mvvm/Input/Attributes/RelayCommandAttribute.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,30 @@ public sealed class RelayCommandAttribute : Attribute
7575

7676
/// <summary>
7777
/// Gets or sets a value indicating whether or not to allow concurrent executions for an asynchronous command.
78+
/// <para>
7879
/// When set for an attribute used on a method that would result in an <see cref="AsyncRelayCommand"/> or an
7980
/// <see cref="AsyncRelayCommand{T}"/> property to be generated, this will modify the behavior of these commands
8081
/// when an execution is invoked while a previous one is still running. It is the same as creating an instance of
8182
/// these command types with a constructor such as <see cref="AsyncRelayCommand(Func{System.Threading.Tasks.Task}, AsyncRelayCommandOptions)"/>
8283
/// and using the <see cref="AsyncRelayCommandOptions.AllowConcurrentExecutions"/> value.
84+
/// </para>
8385
/// </summary>
8486
/// <remarks>Using this property is not valid if the target command doesn't map to an asynchronous command.</remarks>
8587
public bool AllowConcurrentExecutions { get; init; }
8688

89+
/// <summary>
90+
/// Gets or sets a value indicating whether or not to exceptions should be propagated to <see cref="System.Threading.Tasks.TaskScheduler.UnobservedTaskException"/>.
91+
/// <para>
92+
/// When set for an attribute used on a method that would result in an <see cref="AsyncRelayCommand"/> or an
93+
/// <see cref="AsyncRelayCommand{T}"/> property to be generated, this will modify the behavior of these commands
94+
/// in case an exception is thrown by the underlying operation. It is the same as creating an instance of
95+
/// these command types with a constructor such as <see cref="AsyncRelayCommand(Func{System.Threading.Tasks.Task}, AsyncRelayCommandOptions)"/>
96+
/// and using the <see cref="AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler"/> value.
97+
/// </para>
98+
/// </summary>
99+
/// <remarks>Using this property is not valid if the target command doesn't map to an asynchronous command.</remarks>
100+
public bool FlowExceptionsToTaskScheduler { get; init; }
101+
87102
/// <summary>
88103
/// Gets or sets a value indicating whether a cancel command should also be generated for an asynchronous command.
89104
/// <para>

0 commit comments

Comments
 (0)