Skip to content

Commit 1320475

Browse files
committed
Add support for forwarded field/property command attributes
1 parent 0b8ee5a commit 1320475

File tree

5 files changed

+142
-4
lines changed

5 files changed

+142
-4
lines changed

src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,11 @@ MVVMTK0032 | CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenera
4949
MVVMTK0033 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0033
5050
MVVMTK0034 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0034
5151
MVVMTK0035 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0035
52+
53+
## Release 8.2
54+
55+
### New Rules
56+
57+
Rule ID | Category | Severity | Notes
58+
--------|----------|----------|-------
59+
MVVMTK0036 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0036

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,4 +589,20 @@ internal static class DiagnosticDescriptors
589589
isEnabledByDefault: true,
590590
description: "All attributes targeting the generated property for a field annotated with [ObservableProperty] must correctly be resolved to valid types.",
591591
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0035");
592+
593+
/// <summary>
594+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a method with <c>[RelayCommand]</c> is using an invalid attribute targeting the field or property.
595+
/// <para>
596+
/// Format: <c>"The method {0} annotated with [RelayCommand] is using attribute "{1}" which was not recognized as a valid type (are you missing a using directive?)"</c>.
597+
/// </para>
598+
/// </summary>
599+
public static readonly DiagnosticDescriptor InvalidFieldOrPropertyTargetedAttributeOnRelayCommandMethod = new DiagnosticDescriptor(
600+
id: "MVVMTK0036",
601+
title: "Invalid field targeted attribute type",
602+
messageFormat: "The method {0} annotated with [RelayCommand] is using attribute \"{1}\" which was not recognized as a valid type (are you missing a using directive?)",
603+
category: typeof(RelayCommandGenerator).FullName,
604+
defaultSeverity: DiagnosticSeverity.Error,
605+
isEnabledByDefault: true,
606+
description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must correctly be resolved to valid types.",
607+
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0036");
592608
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
56
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
67

78
namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
@@ -22,6 +23,8 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
2223
/// <param name="AllowConcurrentExecutions">Whether or not concurrent executions have been enabled.</param>
2324
/// <param name="FlowExceptionsToTaskScheduler">Whether or not exceptions should flow to the task scheduler.</param>
2425
/// <param name="IncludeCancelCommand">Whether or not to also generate a cancel command.</param>
26+
/// <param name="ForwardedFieldAttributes">The sequence of forwarded attributes for the generated field.</param>
27+
/// <param name="ForwardedPropertyAttributes">The sequence of forwarded attributes for the generated property.</param>
2528
internal sealed record CommandInfo(
2629
string MethodName,
2730
string FieldName,
@@ -35,4 +38,6 @@ internal sealed record CommandInfo(
3538
CanExecuteExpressionType? CanExecuteExpressionType,
3639
bool AllowConcurrentExecutions,
3740
bool FlowExceptionsToTaskScheduler,
38-
bool IncludeCancelCommand);
41+
bool IncludeCancelCommand,
42+
EquatableArray<AttributeInfo> ForwardedFieldAttributes,
43+
EquatableArray<AttributeInfo> ForwardedPropertyAttributes);

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

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using System.Diagnostics.CodeAnalysis;
88
using System.Globalization;
99
using System.Linq;
10+
using System.Threading;
11+
using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
1012
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
1113
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
1214
using CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
@@ -32,12 +34,16 @@ internal static class Execute
3234
/// </summary>
3335
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
3436
/// <param name="attributeData">The <see cref="AttributeData"/> instance the method was annotated with.</param>
37+
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
38+
/// <param name="token">The cancellation token for the current operation.</param>
3539
/// <param name="commandInfo">The resulting <see cref="CommandInfo"/> instance, if successfully generated.</param>
3640
/// <param name="diagnostics">The resulting diagnostics from the processing operation.</param>
3741
/// <returns>Whether a <see cref="CommandInfo"/> instance could be generated successfully.</returns>
3842
public static bool TryGetInfo(
3943
IMethodSymbol methodSymbol,
4044
AttributeData attributeData,
45+
SemanticModel semanticModel,
46+
CancellationToken token,
4147
[NotNullWhen(true)] out CommandInfo? commandInfo,
4248
out ImmutableArray<DiagnosticInfo> diagnostics)
4349
{
@@ -113,6 +119,15 @@ public static bool TryGetInfo(
113119
goto Failure;
114120
}
115121

122+
// Get all forwarded attributes (don't stop in case of errors, just ignore faulting attributes)
123+
GatherForwardedAttributes(
124+
methodSymbol,
125+
semanticModel,
126+
token,
127+
in builder,
128+
out ImmutableArray<AttributeInfo> fieldAttributes,
129+
out ImmutableArray<AttributeInfo> propertyAttributes);
130+
116131
commandInfo = new CommandInfo(
117132
methodSymbol.Name,
118133
fieldName,
@@ -126,7 +141,9 @@ public static bool TryGetInfo(
126141
canExecuteExpressionType,
127142
allowConcurrentExecutions,
128143
flowExceptionsToTaskScheduler,
129-
generateCancelCommand);
144+
generateCancelCommand,
145+
fieldAttributes,
146+
propertyAttributes);
130147

131148
diagnostics = builder.ToImmutable();
132149

@@ -160,10 +177,23 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
160177
? commandInfo.DelegateType
161178
: $"{commandInfo.DelegateType}<{string.Join(", ", commandInfo.DelegateTypeArguments)}>";
162179

180+
// Prepare the forwarded field attributes, if any
181+
ImmutableArray<AttributeListSyntax> forwardedFieldAttributes =
182+
commandInfo.ForwardedFieldAttributes
183+
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
184+
.ToImmutableArray();
185+
186+
// Also prepare any forwarded property attributes
187+
ImmutableArray<AttributeListSyntax> forwardedPropertyAttributes =
188+
commandInfo.ForwardedPropertyAttributes
189+
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
190+
.ToImmutableArray();
191+
163192
// Construct the generated field as follows:
164193
//
165194
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
166195
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
196+
// <FORWARDED_ATTRIBUTES>
167197
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
168198
FieldDeclarationSyntax fieldDeclaration =
169199
FieldDeclaration(
@@ -176,7 +206,8 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
176206
.AddArgumentListArguments(
177207
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
178208
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
179-
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{commandInfo.PropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())));
209+
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{commandInfo.PropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
210+
.AddAttributeLists(forwardedFieldAttributes.ToArray());
180211

181212
// Prepares the argument to pass the underlying method to invoke
182213
using ImmutableArrayBuilder<ArgumentSyntax> commandCreationArguments = ImmutableArrayBuilder<ArgumentSyntax>.Rent();
@@ -265,6 +296,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
265296
// /// <summary>Gets an <see cref="<COMMAND_INTERFACE_TYPE>" instance wrapping <see cref="<METHOD_NAME>"/> and <see cref="<OPTIONAL_CAN_EXECUTE>"/>.</summary>
266297
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
267298
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
299+
// <FORWARDED_ATTRIBUTES>
268300
// public <COMMAND_TYPE> <COMMAND_PROPERTY_NAME> => <COMMAND_FIELD_NAME> ??= new <RELAY_COMMAND_TYPE>(<COMMAND_CREATION_ARGUMENTS>);
269301
PropertyDeclarationSyntax propertyDeclaration =
270302
PropertyDeclaration(
@@ -282,6 +314,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
282314
SyntaxKind.OpenBracketToken,
283315
TriviaList())),
284316
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
317+
.AddAttributeLists(forwardedPropertyAttributes.ToArray())
285318
.WithExpressionBody(
286319
ArrowExpressionClause(
287320
AssignmentExpression(
@@ -898,5 +931,75 @@ private static bool TryGetCanExecuteMemberFromGeneratedProperty(
898931

899932
return false;
900933
}
934+
935+
/// <summary>
936+
/// Gathers all forwarded attributes for the generated field and property.
937+
/// </summary>
938+
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
939+
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
940+
/// <param name="token">The cancellation token for the current operation.</param>
941+
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
942+
/// <param name="fieldAttributes">The resulting field attributes to forward.</param>
943+
/// <param name="propertyAttributes">The resulting property attributes to forward.</param>
944+
private static void GatherForwardedAttributes(
945+
IMethodSymbol methodSymbol,
946+
SemanticModel semanticModel,
947+
CancellationToken token,
948+
in ImmutableArrayBuilder<DiagnosticInfo> diagnostics,
949+
out ImmutableArray<AttributeInfo> fieldAttributes,
950+
out ImmutableArray<AttributeInfo> propertyAttributes)
951+
{
952+
using ImmutableArrayBuilder<AttributeInfo> fieldAttributesInfo = ImmutableArrayBuilder<AttributeInfo>.Rent();
953+
using ImmutableArrayBuilder<AttributeInfo> propertyAttributesInfo = ImmutableArrayBuilder<AttributeInfo>.Rent();
954+
955+
foreach (SyntaxReference syntaxReference in methodSymbol.DeclaringSyntaxReferences)
956+
{
957+
// Try to get the target method declaration syntax node
958+
if (syntaxReference.GetSyntax(token) is not MethodDeclarationSyntax methodDeclaration)
959+
{
960+
continue;
961+
}
962+
963+
// Gather explicit forwarded attributes info
964+
foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists)
965+
{
966+
// Same as in the [ObservableProperty] generator, except we're also looking for fields here
967+
if (attributeList.Target?.Identifier.Kind() is not (SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword))
968+
{
969+
continue;
970+
}
971+
972+
foreach (AttributeSyntax attribute in attributeList.Attributes)
973+
{
974+
// Get the symbol info for the attribute (once again just like in the [ObservableProperty] generator)
975+
if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol))
976+
{
977+
diagnostics.Add(
978+
InvalidFieldOrPropertyTargetedAttributeOnRelayCommandMethod,
979+
attribute,
980+
methodSymbol,
981+
attribute.Name);
982+
983+
continue;
984+
}
985+
986+
AttributeInfo attributeInfo = AttributeInfo.From(attributeTypeSymbol, semanticModel, attribute.ArgumentList?.Arguments ?? Enumerable.Empty<AttributeArgumentSyntax>(), token);
987+
988+
// Add the new attribute info to the right builder
989+
if (attributeList.Target?.Identifier.Kind() is SyntaxKind.FieldKeyword)
990+
{
991+
fieldAttributesInfo.Add(attributeInfo);
992+
}
993+
else
994+
{
995+
propertyAttributesInfo.Add(attributeInfo);
996+
}
997+
}
998+
}
999+
}
1000+
1001+
fieldAttributes = fieldAttributesInfo.ToImmutable();
1002+
propertyAttributes = propertyAttributesInfo.ToImmutable();
1003+
}
9011004
}
9021005
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
4040
// Get the hierarchy info for the target symbol, and try to gather the command info
4141
HierarchyInfo? hierarchy = HierarchyInfo.From(methodSymbol.ContainingType);
4242

43-
_ = Execute.TryGetInfo(methodSymbol, context.Attributes[0], out CommandInfo? commandInfo, out ImmutableArray<DiagnosticInfo> diagnostics);
43+
_ = Execute.TryGetInfo(
44+
methodSymbol,
45+
context.Attributes[0],
46+
context.SemanticModel,
47+
token,
48+
out CommandInfo? commandInfo,
49+
out ImmutableArray<DiagnosticInfo> diagnostics);
4450

4551
return (Hierarchy: hierarchy, new Result<CommandInfo?>(commandInfo, diagnostics));
4652
})

0 commit comments

Comments
 (0)