Skip to content

Commit 974e819

Browse files
authored
Merge pull request #630 from CommunityToolkit/dev/relay-command-attribute-forwarding
Add [field:] and [property:] support for [RelayCommand] methods
2 parents ee0587e + 13ce990 commit 974e819

16 files changed

+592
-19
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/CommunityToolkit.Mvvm.SourceGenerators.projitems

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs" />
4646
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldReferenceForObservablePropertyFieldAnalyzer.cs" />
4747
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\UnsupportedCSharpLanguageVersionAnalyzer.cs" />
48+
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Suppressors\RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs" />
4849
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Suppressors\ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs" />
4950
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\DiagnosticDescriptors.cs" />
5051
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\SuppressionDescriptors.cs" />
@@ -54,10 +55,12 @@
5455
<Compile Include="$(MSBuildThisFileDirectory)Extensions\INamedTypeSymbolExtensions.cs" />
5556
<Compile Include="$(MSBuildThisFileDirectory)Extensions\IncrementalGeneratorInitializationContextExtensions.cs" />
5657
<Compile Include="$(MSBuildThisFileDirectory)Extensions\IncrementalValuesProviderExtensions.cs" />
58+
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SymbolInfoExtensions.cs" />
5759
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ISymbolExtensions.cs" />
5860
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SourceProductionContextExtensions.cs" />
5961
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ITypeSymbolExtensions.cs" />
6062
<Compile Include="$(MSBuildThisFileDirectory)Extensions\MemberDeclarationSyntaxExtensions.cs" />
63+
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SyntaxTokenExtensions.cs" />
6164
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SyntaxNodeExtensions.cs" />
6265
<Compile Include="$(MSBuildThisFileDirectory)Extensions\TypeDeclarationSyntaxExtensions.cs" />
6366
<Compile Include="$(MSBuildThisFileDirectory)Helpers\EquatableArray{T}.cs" />

src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public static bool TryGetInfo(
193193
// Only look for attribute lists explicitly targeting the (generated) property. Roslyn will normally emit a
194194
// CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic suppressor
195195
// that recognizes uses of this target specifically to support [ObservableProperty].
196-
if (attributeList.Target?.Identifier.Kind() is not SyntaxKind.PropertyKeyword)
196+
if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword))
197197
{
198198
continue;
199199
}
@@ -214,12 +214,7 @@ public static bool TryGetInfo(
214214
// There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the
215215
// generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the
216216
// lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway.
217-
SymbolInfo attributeSymbolInfo = semanticModel.GetSymbolInfo(attribute, token);
218-
219-
// Check if the attribute type can be resolved, and emit a diagnostic if that is not the case. This happens if eg. the
220-
// attribute type is spelled incorrectly, or if a user is missing the using directive for the attribute type being used.
221-
if ((attributeSymbolInfo.Symbol ?? attributeSymbolInfo.CandidateSymbols.SingleOrDefault()) is not ISymbol attributeSymbol ||
222-
(attributeSymbol as INamedTypeSymbol ?? attributeSymbol.ContainingType) is not INamedTypeSymbol attributeTypeSymbol)
217+
if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol))
223218
{
224219
builder.Add(
225220
InvalidPropertyTargetedAttributeOnObservablePropertyField,

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/Diagnostics/SuppressionDescriptors.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,18 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
1212
internal static class SuppressionDescriptors
1313
{
1414
/// <summary>
15-
/// Gets a <see cref="SuppressionDescriptor"/> for a field using [ObservableProperty] with on attribute list targeting a property.
15+
/// Gets a <see cref="SuppressionDescriptor"/> for a field using [ObservableProperty] with an attribute list targeting a property.
1616
/// </summary>
1717
public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new(
1818
id: "MVVMTKSPR0001",
1919
suppressedDiagnosticId: "CS0657",
2020
justification: "Fields using [ObservableProperty] can use [property:] attribute lists to forward attributes to the generated properties");
21+
22+
/// <summary>
23+
/// Gets a <see cref="SuppressionDescriptor"/> for a method using [RelayCommand] with an attribute list targeting a field or property.
24+
/// </summary>
25+
public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForRelayCommandMethod = new(
26+
id: "MVVMTKSPR0002",
27+
suppressedDiagnosticId: "CS0657",
28+
justification: "Methods using [RelayCommand] can use [field:] and [property:] attribute lists to forward attributes to the generated fields and properties");
2129
}

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Collections.Immutable;
6-
using System.Linq;
6+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
77
using Microsoft.CodeAnalysis;
88
using Microsoft.CodeAnalysis.CSharp;
99
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -19,7 +19,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
1919
/// <para>
2020
/// That is, this diagnostic suppressor will suppress the following diagnostic:
2121
/// <code>
22-
/// public class MyViewModel : ObservableObject
22+
/// public partial class MyViewModel : ObservableObject
2323
/// {
2424
/// [ObservableProperty]
2525
/// [property: JsonPropertyName("Name")]
@@ -53,7 +53,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)
5353
// Check if the field is using [ObservableProperty], in which case we should suppress the warning
5454
if (declaredSymbol is IFieldSymbol fieldSymbol &&
5555
semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol observablePropertySymbol &&
56-
fieldSymbol.GetAttributes().Select(attribute => attribute.AttributeClass).Contains(observablePropertySymbol, SymbolEqualityComparer.Default))
56+
fieldSymbol.HasAttributeWithType(observablePropertySymbol))
5757
{
5858
context.ReportSuppression(Suppression.Create(PropertyAttributeListForObservablePropertyField, diagnostic));
5959
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Immutable;
6+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.SuppressionDescriptors;
12+
13+
namespace CommunityToolkit.Mvvm.SourceGenerators;
14+
15+
/// <summary>
16+
/// <para>
17+
/// A diagnostic suppressor to suppress CS0657 warnings for methods with [RelayCommand] using a [field:] or [property:] attribute list.
18+
/// </para>
19+
/// <para>
20+
/// That is, this diagnostic suppressor will suppress the following diagnostic:
21+
/// <code>
22+
/// public partial class MyViewModel : ObservableObject
23+
/// {
24+
/// [RelayCommand]
25+
/// [field: JsonIgnore]
26+
/// [property: SomeOtherAttribute]
27+
/// private void DoSomething()
28+
/// {
29+
/// }
30+
/// }
31+
/// </code>
32+
/// </para>
33+
/// </summary>
34+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
35+
public sealed class RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor
36+
{
37+
/// <inheritdoc/>
38+
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForRelayCommandMethod);
39+
40+
/// <inheritdoc/>
41+
public override void ReportSuppressions(SuppressionAnalysisContext context)
42+
{
43+
foreach (Diagnostic diagnostic in context.ReportedDiagnostics)
44+
{
45+
SyntaxNode? syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan);
46+
47+
// Check that the target is effectively [field:] or [property:] over a method declaration, which is the case we're looking for
48+
if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: MethodDeclarationSyntax methodDeclaration, Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) })
49+
{
50+
SemanticModel semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree);
51+
52+
// Get the method symbol from the first variable declaration
53+
ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken);
54+
55+
// Check if the method is using [RelayCommand], in which case we should suppress the warning
56+
if (declaredSymbol is IMethodSymbol methodSymbol &&
57+
semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") is INamedTypeSymbol relayCommandSymbol &&
58+
methodSymbol.HasAttributeWithType(relayCommandSymbol))
59+
{
60+
context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForRelayCommandMethod, diagnostic));
61+
}
62+
}
63+
}
64+
}
65+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
using Microsoft.CodeAnalysis;
7+
8+
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
9+
10+
/// <summary>
11+
/// Extension methods for the <see cref="SymbolInfo"/> type.
12+
/// </summary>
13+
internal static class SymbolInfoExtensions
14+
{
15+
/// <summary>
16+
/// Tries to get the resolved attribute type symbol from a given <see cref="SymbolInfo"/> value.
17+
/// </summary>
18+
/// <param name="symbolInfo">The <see cref="SymbolInfo"/> value to check.</param>
19+
/// <param name="typeSymbol">The resulting attribute type symbol, if correctly resolved.</param>
20+
/// <returns>Whether <paramref name="symbolInfo"/> is resolved to a symbol.</returns>
21+
/// <remarks>
22+
/// This can be used to ensure users haven't eg. spelled names incorrecty or missed a using directive. Normally, code would just
23+
/// not compile if that was the case, but that doesn't apply for attributes using invalid targets. In that case, Roslyn will ignore
24+
/// any errors, meaning the generator has to validate the type symbols are correctly resolved on its own.
25+
/// </remarks>
26+
public static bool TryGetAttributeTypeSymbol(this SymbolInfo symbolInfo, [NotNullWhen(true)] out INamedTypeSymbol? typeSymbol)
27+
{
28+
ISymbol? attributeSymbol = symbolInfo.Symbol;
29+
30+
// If no symbol is selected and there is a single candidate symbol, use that
31+
if (attributeSymbol is null && symbolInfo.CandidateSymbols is [ISymbol candidateSymbol])
32+
{
33+
attributeSymbol = candidateSymbol;
34+
}
35+
36+
// Extract the symbol from either the current one or the containing type
37+
if ((attributeSymbol as INamedTypeSymbol ?? attributeSymbol?.ContainingType) is not INamedTypeSymbol resultingSymbol)
38+
{
39+
typeSymbol = null;
40+
41+
return false;
42+
}
43+
44+
typeSymbol = resultingSymbol;
45+
46+
return true;
47+
}
48+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
8+
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
9+
10+
/// <summary>
11+
/// Extension methods for the <see cref="SyntaxToken"/> type.
12+
/// </summary>
13+
internal static class SyntaxTokenExtensions
14+
{
15+
/// <summary>
16+
/// Deconstructs a <see cref="SyntaxToken"/> into its <see cref="SyntaxKind"/> value.
17+
/// </summary>
18+
/// <param name="syntaxToken">The input <see cref="SyntaxToken"/> value.</param>
19+
/// <param name="syntaxKind">The resulting <see cref="SyntaxKind"/> value for <paramref name="syntaxToken"/>.</param>
20+
public static void Deconstruct(this SyntaxToken syntaxToken, out SyntaxKind syntaxKind)
21+
{
22+
syntaxKind = syntaxToken.Kind();
23+
}
24+
}

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);

0 commit comments

Comments
 (0)