diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
index 957d6b750..75abaf30d 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
@@ -49,3 +49,11 @@ MVVMTK0032 | CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenera
MVVMTK0033 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0033
MVVMTK0034 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0034
MVVMTK0035 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0035
+
+## Release 8.2
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+MVVMTK0036 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0036
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
index 5e950360e..b019f6d49 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
@@ -45,6 +45,7 @@
+
@@ -54,10 +55,12 @@
+
+
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
index d84c3f96e..c6d35b1d8 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
@@ -193,7 +193,7 @@ public static bool TryGetInfo(
// Only look for attribute lists explicitly targeting the (generated) property. Roslyn will normally emit a
// CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic suppressor
// that recognizes uses of this target specifically to support [ObservableProperty].
- if (attributeList.Target?.Identifier.Kind() is not SyntaxKind.PropertyKeyword)
+ if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword))
{
continue;
}
@@ -214,12 +214,7 @@ public static bool TryGetInfo(
// There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the
// generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the
// lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway.
- SymbolInfo attributeSymbolInfo = semanticModel.GetSymbolInfo(attribute, token);
-
- // Check if the attribute type can be resolved, and emit a diagnostic if that is not the case. This happens if eg. the
- // attribute type is spelled incorrectly, or if a user is missing the using directive for the attribute type being used.
- if ((attributeSymbolInfo.Symbol ?? attributeSymbolInfo.CandidateSymbols.SingleOrDefault()) is not ISymbol attributeSymbol ||
- (attributeSymbol as INamedTypeSymbol ?? attributeSymbol.ContainingType) is not INamedTypeSymbol attributeTypeSymbol)
+ if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol))
{
builder.Add(
InvalidPropertyTargetedAttributeOnObservablePropertyField,
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
index 993f959c1..fc28be102 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
@@ -589,4 +589,20 @@ internal static class DiagnosticDescriptors
isEnabledByDefault: true,
description: "All attributes targeting the generated property for a field annotated with [ObservableProperty] must correctly be resolved to valid types.",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0035");
+
+ ///
+ /// Gets a indicating when a method with [RelayCommand] is using an invalid attribute targeting the field or property.
+ ///
+ /// Format: "The method {0} annotated with [RelayCommand] is using attribute "{1}" which was not recognized as a valid type (are you missing a using directive?)".
+ ///
+ ///
+ public static readonly DiagnosticDescriptor InvalidFieldOrPropertyTargetedAttributeOnRelayCommandMethod = new DiagnosticDescriptor(
+ id: "MVVMTK0036",
+ title: "Invalid field targeted attribute type",
+ 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?)",
+ category: typeof(RelayCommandGenerator).FullName,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must correctly be resolved to valid types.",
+ helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0036");
}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs
index ad0000582..4a6c803a9 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs
@@ -12,10 +12,18 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
internal static class SuppressionDescriptors
{
///
- /// Gets a for a field using [ObservableProperty] with on attribute list targeting a property.
+ /// Gets a for a field using [ObservableProperty] with an attribute list targeting a property.
///
public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new(
id: "MVVMTKSPR0001",
suppressedDiagnosticId: "CS0657",
justification: "Fields using [ObservableProperty] can use [property:] attribute lists to forward attributes to the generated properties");
+
+ ///
+ /// Gets a for a method using [RelayCommand] with an attribute list targeting a field or property.
+ ///
+ public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForRelayCommandMethod = new(
+ id: "MVVMTKSPR0002",
+ suppressedDiagnosticId: "CS0657",
+ justification: "Methods using [RelayCommand] can use [field:] and [property:] attribute lists to forward attributes to the generated fields and properties");
}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs
index 5192e7412..f27f3969a 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs
@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
-using System.Linq;
+using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -19,7 +19,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
///
/// That is, this diagnostic suppressor will suppress the following diagnostic:
///
-/// public class MyViewModel : ObservableObject
+/// public partial class MyViewModel : ObservableObject
/// {
/// [ObservableProperty]
/// [property: JsonPropertyName("Name")]
@@ -53,7 +53,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)
// Check if the field is using [ObservableProperty], in which case we should suppress the warning
if (declaredSymbol is IFieldSymbol fieldSymbol &&
semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol observablePropertySymbol &&
- fieldSymbol.GetAttributes().Select(attribute => attribute.AttributeClass).Contains(observablePropertySymbol, SymbolEqualityComparer.Default))
+ fieldSymbol.HasAttributeWithType(observablePropertySymbol))
{
context.ReportSuppression(Suppression.Create(PropertyAttributeListForObservablePropertyField, diagnostic));
}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs
new file mode 100644
index 000000000..152d2f479
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs
@@ -0,0 +1,65 @@
+// 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.Collections.Immutable;
+using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.SuppressionDescriptors;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators;
+
+///
+///
+/// A diagnostic suppressor to suppress CS0657 warnings for methods with [RelayCommand] using a [field:] or [property:] attribute list.
+///
+///
+/// That is, this diagnostic suppressor will suppress the following diagnostic:
+///
+/// public partial class MyViewModel : ObservableObject
+/// {
+/// [RelayCommand]
+/// [field: JsonIgnore]
+/// [property: SomeOtherAttribute]
+/// private void DoSomething()
+/// {
+/// }
+/// }
+///
+///
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor
+{
+ ///
+ public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForRelayCommandMethod);
+
+ ///
+ public override void ReportSuppressions(SuppressionAnalysisContext context)
+ {
+ foreach (Diagnostic diagnostic in context.ReportedDiagnostics)
+ {
+ SyntaxNode? syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan);
+
+ // Check that the target is effectively [field:] or [property:] over a method declaration, which is the case we're looking for
+ if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: MethodDeclarationSyntax methodDeclaration, Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) })
+ {
+ SemanticModel semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree);
+
+ // Get the method symbol from the first variable declaration
+ ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken);
+
+ // Check if the method is using [RelayCommand], in which case we should suppress the warning
+ if (declaredSymbol is IMethodSymbol methodSymbol &&
+ semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") is INamedTypeSymbol relayCommandSymbol &&
+ methodSymbol.HasAttributeWithType(relayCommandSymbol))
+ {
+ context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForRelayCommandMethod, diagnostic));
+ }
+ }
+ }
+ }
+}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs
new file mode 100644
index 000000000..c87f9a8fb
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs
@@ -0,0 +1,48 @@
+// 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.Diagnostics.CodeAnalysis;
+using Microsoft.CodeAnalysis;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
+
+///
+/// Extension methods for the type.
+///
+internal static class SymbolInfoExtensions
+{
+ ///
+ /// Tries to get the resolved attribute type symbol from a given value.
+ ///
+ /// The value to check.
+ /// The resulting attribute type symbol, if correctly resolved.
+ /// Whether is resolved to a symbol.
+ ///
+ /// This can be used to ensure users haven't eg. spelled names incorrecty or missed a using directive. Normally, code would just
+ /// not compile if that was the case, but that doesn't apply for attributes using invalid targets. In that case, Roslyn will ignore
+ /// any errors, meaning the generator has to validate the type symbols are correctly resolved on its own.
+ ///
+ public static bool TryGetAttributeTypeSymbol(this SymbolInfo symbolInfo, [NotNullWhen(true)] out INamedTypeSymbol? typeSymbol)
+ {
+ ISymbol? attributeSymbol = symbolInfo.Symbol;
+
+ // If no symbol is selected and there is a single candidate symbol, use that
+ if (attributeSymbol is null && symbolInfo.CandidateSymbols is [ISymbol candidateSymbol])
+ {
+ attributeSymbol = candidateSymbol;
+ }
+
+ // Extract the symbol from either the current one or the containing type
+ if ((attributeSymbol as INamedTypeSymbol ?? attributeSymbol?.ContainingType) is not INamedTypeSymbol resultingSymbol)
+ {
+ typeSymbol = null;
+
+ return false;
+ }
+
+ typeSymbol = resultingSymbol;
+
+ return true;
+ }
+}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenExtensions.cs
new file mode 100644
index 000000000..70f725e19
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenExtensions.cs
@@ -0,0 +1,24 @@
+// 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;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
+
+///
+/// Extension methods for the type.
+///
+internal static class SyntaxTokenExtensions
+{
+ ///
+ /// Deconstructs a into its value.
+ ///
+ /// The input value.
+ /// The resulting value for .
+ public static void Deconstruct(this SyntaxToken syntaxToken, out SyntaxKind syntaxKind)
+ {
+ syntaxKind = syntaxToken.Kind();
+ }
+}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs
index 0721cb966..16ca48c0a 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
@@ -22,6 +23,8 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
/// Whether or not concurrent executions have been enabled.
/// Whether or not exceptions should flow to the task scheduler.
/// Whether or not to also generate a cancel command.
+/// The sequence of forwarded attributes for the generated field.
+/// The sequence of forwarded attributes for the generated property.
internal sealed record CommandInfo(
string MethodName,
string FieldName,
@@ -35,4 +38,6 @@ internal sealed record CommandInfo(
CanExecuteExpressionType? CanExecuteExpressionType,
bool AllowConcurrentExecutions,
bool FlowExceptionsToTaskScheduler,
- bool IncludeCancelCommand);
+ bool IncludeCancelCommand,
+ EquatableArray ForwardedFieldAttributes,
+ EquatableArray ForwardedPropertyAttributes);
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs
index 89b852953..33b2a752e 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs
@@ -7,6 +7,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
+using System.Threading;
+using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
using CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
@@ -32,12 +34,16 @@ internal static class Execute
///
/// The input instance to process.
/// The instance the method was annotated with.
+ /// The instance for the current run.
+ /// The cancellation token for the current operation.
/// The resulting instance, if successfully generated.
/// The resulting diagnostics from the processing operation.
/// Whether a instance could be generated successfully.
public static bool TryGetInfo(
IMethodSymbol methodSymbol,
AttributeData attributeData,
+ SemanticModel semanticModel,
+ CancellationToken token,
[NotNullWhen(true)] out CommandInfo? commandInfo,
out ImmutableArray diagnostics)
{
@@ -113,6 +119,15 @@ public static bool TryGetInfo(
goto Failure;
}
+ // Get all forwarded attributes (don't stop in case of errors, just ignore faulting attributes)
+ GatherForwardedAttributes(
+ methodSymbol,
+ semanticModel,
+ token,
+ in builder,
+ out ImmutableArray fieldAttributes,
+ out ImmutableArray propertyAttributes);
+
commandInfo = new CommandInfo(
methodSymbol.Name,
fieldName,
@@ -126,7 +141,9 @@ public static bool TryGetInfo(
canExecuteExpressionType,
allowConcurrentExecutions,
flowExceptionsToTaskScheduler,
- generateCancelCommand);
+ generateCancelCommand,
+ fieldAttributes,
+ propertyAttributes);
diagnostics = builder.ToImmutable();
@@ -160,10 +177,23 @@ public static ImmutableArray GetSyntax(CommandInfo comm
? commandInfo.DelegateType
: $"{commandInfo.DelegateType}<{string.Join(", ", commandInfo.DelegateTypeArguments)}>";
+ // Prepare the forwarded field attributes, if any
+ ImmutableArray forwardedFieldAttributes =
+ commandInfo.ForwardedFieldAttributes
+ .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
+ .ToImmutableArray();
+
+ // Also prepare any forwarded property attributes
+ ImmutableArray forwardedPropertyAttributes =
+ commandInfo.ForwardedPropertyAttributes
+ .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
+ .ToImmutableArray();
+
// Construct the generated field as follows:
//
// /// The backing field for
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
+ //
// private ? ;
FieldDeclarationSyntax fieldDeclaration =
FieldDeclaration(
@@ -176,7 +206,8 @@ public static ImmutableArray GetSyntax(CommandInfo comm
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
- .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList())));
+ .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList())))
+ .AddAttributeLists(forwardedFieldAttributes.ToArray());
// Prepares the argument to pass the underlying method to invoke
using ImmutableArrayBuilder commandCreationArguments = ImmutableArrayBuilder.Rent();
@@ -265,6 +296,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm
// /// Gets an and .
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ //
// public => ??= new ();
PropertyDeclarationSyntax propertyDeclaration =
PropertyDeclaration(
@@ -282,6 +314,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm
SyntaxKind.OpenBracketToken,
TriviaList())),
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
+ .AddAttributeLists(forwardedPropertyAttributes.ToArray())
.WithExpressionBody(
ArrowExpressionClause(
AssignmentExpression(
@@ -898,5 +931,75 @@ private static bool TryGetCanExecuteMemberFromGeneratedProperty(
return false;
}
+
+ ///
+ /// Gathers all forwarded attributes for the generated field and property.
+ ///
+ /// The input instance to process.
+ /// The instance for the current run.
+ /// The cancellation token for the current operation.
+ /// The current collection of gathered diagnostics.
+ /// The resulting field attributes to forward.
+ /// The resulting property attributes to forward.
+ private static void GatherForwardedAttributes(
+ IMethodSymbol methodSymbol,
+ SemanticModel semanticModel,
+ CancellationToken token,
+ in ImmutableArrayBuilder diagnostics,
+ out ImmutableArray fieldAttributes,
+ out ImmutableArray propertyAttributes)
+ {
+ using ImmutableArrayBuilder fieldAttributesInfo = ImmutableArrayBuilder.Rent();
+ using ImmutableArrayBuilder propertyAttributesInfo = ImmutableArrayBuilder.Rent();
+
+ foreach (SyntaxReference syntaxReference in methodSymbol.DeclaringSyntaxReferences)
+ {
+ // Try to get the target method declaration syntax node
+ if (syntaxReference.GetSyntax(token) is not MethodDeclarationSyntax methodDeclaration)
+ {
+ continue;
+ }
+
+ // Gather explicit forwarded attributes info
+ foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists)
+ {
+ // Same as in the [ObservableProperty] generator, except we're also looking for fields here
+ if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword))
+ {
+ continue;
+ }
+
+ foreach (AttributeSyntax attribute in attributeList.Attributes)
+ {
+ // Get the symbol info for the attribute (once again just like in the [ObservableProperty] generator)
+ if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol))
+ {
+ diagnostics.Add(
+ InvalidFieldOrPropertyTargetedAttributeOnRelayCommandMethod,
+ attribute,
+ methodSymbol,
+ attribute.Name);
+
+ continue;
+ }
+
+ AttributeInfo attributeInfo = AttributeInfo.From(attributeTypeSymbol, semanticModel, attribute.ArgumentList?.Arguments ?? Enumerable.Empty(), token);
+
+ // Add the new attribute info to the right builder
+ if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.FieldKeyword))
+ {
+ fieldAttributesInfo.Add(attributeInfo);
+ }
+ else
+ {
+ propertyAttributesInfo.Add(attributeInfo);
+ }
+ }
+ }
+ }
+
+ fieldAttributes = fieldAttributesInfo.ToImmutable();
+ propertyAttributes = propertyAttributesInfo.ToImmutable();
+ }
}
}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.cs
index 46b64a56c..e18f83b9c 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.cs
@@ -40,7 +40,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
// Get the hierarchy info for the target symbol, and try to gather the command info
HierarchyInfo? hierarchy = HierarchyInfo.From(methodSymbol.ContainingType);
- _ = Execute.TryGetInfo(methodSymbol, context.Attributes[0], out CommandInfo? commandInfo, out ImmutableArray diagnostics);
+ _ = Execute.TryGetInfo(
+ methodSymbol,
+ context.Attributes[0],
+ context.SemanticModel,
+ token,
+ out CommandInfo? commandInfo,
+ out ImmutableArray diagnostics);
return (Hierarchy: hierarchy, new Result(commandInfo, diagnostics));
})
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs
index 88567b938..fa76a7126 100644
--- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs
@@ -193,6 +193,81 @@ public object? A
VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result));
}
+ [TestMethod]
+ public void RelayCommandMethodWithForwardedAttributesWithNumberLiterals_PreservesType()
+ {
+ string source = """
+ using CommunityToolkit.Mvvm.Input;
+
+ #nullable enable
+
+ namespace MyApp;
+
+ partial class MyViewModel
+ {
+ const double MyDouble = 3.14;
+ const float MyFloat = 3.14f;
+
+ [RelayCommand]
+ [field: DefaultValue(0.0)]
+ [field: DefaultValue(1.24)]
+ [field: DefaultValue(0.0f)]
+ [field: DefaultValue(0.0f)]
+ [field: DefaultValue(MyDouble)]
+ [field: DefaultValue(MyFloat)]
+ [property: DefaultValue(0.0)]
+ [property: DefaultValue(1.24)]
+ [property: DefaultValue(0.0f)]
+ [property: DefaultValue(0.0f)]
+ [property: DefaultValue(MyDouble)]
+ [property: DefaultValue(MyFloat)]
+ private void Test()
+ {
+ }
+ }
+
+ public class DefaultValueAttribute : Attribute
+ {
+ public DefaultValueAttribute(object value)
+ {
+ }
+ }
+ """;
+
+ string result = """
+ //
+ #pragma warning disable
+ #nullable enable
+ namespace MyApp
+ {
+ partial class MyViewModel
+ {
+ /// The backing field for .
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
+ [global::MyApp.DefaultValueAttribute(0D)]
+ [global::MyApp.DefaultValueAttribute(1.24D)]
+ [global::MyApp.DefaultValueAttribute(0F)]
+ [global::MyApp.DefaultValueAttribute(0F)]
+ [global::MyApp.DefaultValueAttribute(3.14D)]
+ [global::MyApp.DefaultValueAttribute(3.14F)]
+ private global::CommunityToolkit.Mvvm.Input.RelayCommand? testCommand;
+ /// Gets an instance wrapping .
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ [global::MyApp.DefaultValueAttribute(0D)]
+ [global::MyApp.DefaultValueAttribute(1.24D)]
+ [global::MyApp.DefaultValueAttribute(0F)]
+ [global::MyApp.DefaultValueAttribute(0F)]
+ [global::MyApp.DefaultValueAttribute(3.14D)]
+ [global::MyApp.DefaultValueAttribute(3.14F)]
+ public global::CommunityToolkit.Mvvm.Input.IRelayCommand TestCommand => testCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test));
+ }
+ }
+ """;
+
+ VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test.g.cs", result));
+ }
+
[TestMethod]
public void ObservablePropertyWithinGenericAndNestedTypes()
{
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
index fcb059fb5..414aa6566 100644
--- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
@@ -1672,6 +1672,59 @@ public partial class MyViewModel : ObservableObject
VerifyGeneratedDiagnostics(source, "MVVMTK0035");
}
+ [TestMethod]
+ public void InvalidPropertyTargetedAttributeOnRelayCommandMethod_MissingUsingDirective()
+ {
+ string source = """
+ using System;
+ using CommunityToolkit.Mvvm.Input;
+
+ namespace MyApp
+ {
+ public partial class MyViewModel
+ {
+ [RelayCommand]
+ [property: MyTest]
+ private void Test()
+ {
+ }
+ }
+ }
+
+ namespace MyAttributes
+ {
+ [AttributeUsage(AttributeTargets.Property)]
+ public class MyTestAttribute : Attribute
+ {
+ }
+ }
+ """;
+
+ VerifyGeneratedDiagnostics(source, "MVVMTK0036");
+ }
+
+ [TestMethod]
+ public void InvalidPropertyTargetedAttributeOnRelayCommandMethod_TypoInAttributeName()
+ {
+ string source = """
+ using CommunityToolkit.Mvvm.Input;
+
+ namespace MyApp
+ {
+ public partial class MyViewModel
+ {
+ [RelayCommand]
+ [property: Fbuifbweif]
+ private void Test()
+ {
+ }
+ }
+ }
+ """;
+
+ VerifyGeneratedDiagnostics(source, "MVVMTK0036");
+ }
+
///
/// Verifies the diagnostic errors for a given analyzer, and that all available source generators can run successfully with the input source (including subsequent compilation).
///
diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs
index e9d5606d2..e485793df 100644
--- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs
+++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs
@@ -1611,13 +1611,13 @@ public partial class MyViewModelWithExplicitPropertyAttributes : ObservableValid
private int someComplexRandomAttribute;
}
- [AttributeUsage(AttributeTargets.Property)]
- private sealed class TestAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+ public sealed class TestAttribute : Attribute
{
}
- [AttributeUsage(AttributeTargets.Property)]
- private sealed class PropertyInfoAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+ public sealed class PropertyInfoAttribute : Attribute
{
public PropertyInfoAttribute(object? o, Type t, bool flag, double d, string[] names, object[] objects)
{
diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs
index 681f2fe08..3114cf1e5 100644
--- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs
+++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs
@@ -4,10 +4,13 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
+using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using System.Xml.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -565,6 +568,97 @@ public void Test_RelayCommandAttribute_CanExecuteWithNullabilityAnnotations()
Assert.IsTrue(model.DoSomething3Command.CanExecute((0, "Hello")));
}
+ [TestMethod]
+ public void Test_RelayCommandAttribute_WithExplicitAttributesForFieldAndProperty()
+ {
+ FieldInfo fooField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("fooCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
+
+ Assert.IsNotNull(fooField.GetCustomAttribute());
+ Assert.IsNotNull(fooField.GetCustomAttribute());
+ Assert.AreEqual(fooField.GetCustomAttribute()!.Length, 1);
+ Assert.IsNotNull(fooField.GetCustomAttribute());
+ Assert.AreEqual(fooField.GetCustomAttribute()!.Length, 100);
+
+ PropertyInfo fooProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("FooCommand")!;
+
+ Assert.IsNotNull(fooProperty.GetCustomAttribute());
+ Assert.IsNotNull(fooProperty.GetCustomAttribute());
+ Assert.AreEqual(fooProperty.GetCustomAttribute()!.Length, 1);
+ Assert.IsNotNull(fooProperty.GetCustomAttribute());
+ Assert.AreEqual(fooProperty.GetCustomAttribute()!.Length, 100);
+
+ PropertyInfo barProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("BarCommand")!;
+
+ Assert.IsNotNull(barProperty.GetCustomAttribute());
+ Assert.AreEqual(barProperty.GetCustomAttribute()!.Name, "bar");
+ Assert.IsNotNull(barProperty.GetCustomAttribute());
+
+ PropertyInfo bazProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("BazCommand")!;
+
+ Assert.IsNotNull(bazProperty.GetCustomAttribute());
+
+ static void ValidateTestAttribute(TestValidationAttribute testAttribute)
+ {
+ Assert.IsNotNull(testAttribute);
+ Assert.IsNull(testAttribute.O);
+ Assert.AreEqual(testAttribute.T, typeof(MyViewModelWithExplicitFieldAndPropertyAttributes));
+ Assert.AreEqual(testAttribute.Flag, true);
+ Assert.AreEqual(testAttribute.D, 6.28);
+ CollectionAssert.AreEqual(testAttribute.Names, new[] { "Bob", "Ross" });
+
+ object[]? nestedArray = (object[]?)testAttribute.NestedArray;
+
+ Assert.IsNotNull(nestedArray);
+ Assert.AreEqual(nestedArray!.Length, 3);
+ Assert.AreEqual(nestedArray[0], 1);
+ Assert.AreEqual(nestedArray[1], "Hello");
+ Assert.IsTrue(nestedArray[2] is int[]);
+ CollectionAssert.AreEqual((int[])nestedArray[2], new[] { 2, 3, 4 });
+
+ Assert.AreEqual(testAttribute.Animal, Test_ObservablePropertyAttribute.Animal.Llama);
+ }
+
+ FieldInfo fooBarField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("fooBarCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
+
+ ValidateTestAttribute(fooBarField.GetCustomAttribute()!);
+
+ PropertyInfo fooBarProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("FooBarCommand")!;
+
+ ValidateTestAttribute(fooBarProperty.GetCustomAttribute()!);
+
+ FieldInfo barBazField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("barBazCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
+
+ Assert.IsNotNull(barBazField.GetCustomAttribute());
+
+ PropertyInfo barBazCommand = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("BarBazCommand")!;
+
+ Assert.IsNotNull(barBazCommand.GetCustomAttribute());
+
+ Test_ObservablePropertyAttribute.PropertyInfoAttribute testAttribute2 = barBazCommand.GetCustomAttribute()!;
+
+ Assert.IsNotNull(testAttribute2);
+ Assert.IsNull(testAttribute2.O);
+ Assert.AreEqual(testAttribute2.T, typeof(MyViewModelWithExplicitFieldAndPropertyAttributes));
+ Assert.AreEqual(testAttribute2.Flag, true);
+ Assert.AreEqual(testAttribute2.D, 6.28);
+ Assert.IsNotNull(testAttribute2.Objects);
+ Assert.IsTrue(testAttribute2.Objects is object[]);
+ Assert.AreEqual(((object[])testAttribute2.Objects).Length, 1);
+ Assert.AreEqual(((object[])testAttribute2.Objects)[0], "Test");
+ CollectionAssert.AreEqual(testAttribute2.Names, new[] { "Bob", "Ross" });
+
+ object[]? nestedArray2 = (object[]?)testAttribute2.NestedArray;
+
+ Assert.IsNotNull(nestedArray2);
+ Assert.AreEqual(nestedArray2!.Length, 4);
+ Assert.AreEqual(nestedArray2[0], 1);
+ Assert.AreEqual(nestedArray2[1], "Hello");
+ Assert.AreEqual(nestedArray2[2], 42);
+ Assert.IsNull(nestedArray2[3]);
+
+ Assert.AreEqual(testAttribute2.Animal, (Test_ObservablePropertyAttribute.Animal)67);
+ }
+
#region Region
public class Region
{
@@ -1038,4 +1132,74 @@ private void DoSomething3((int A, string? B) parameter)
{
}
}
+
+ public partial class MyViewModelWithExplicitFieldAndPropertyAttributes
+ {
+ [RelayCommand]
+ [field: Required]
+ [field: MinLength(1)]
+ [field: MaxLength(100)]
+ [property: Required]
+ [property: MinLength(1)]
+ [property: MaxLength(100)]
+ private void Foo()
+ {
+ }
+
+ [RelayCommand]
+ [property: JsonPropertyName("bar")]
+ [property: XmlIgnore]
+ private void Bar()
+ {
+ }
+
+ [RelayCommand]
+ [property: Test_ObservablePropertyAttribute.Test]
+ private async Task BazAsync()
+ {
+ await Task.Yield();
+ }
+
+ [RelayCommand]
+ [field: TestValidation(null, typeof(MyViewModelWithExplicitFieldAndPropertyAttributes), true, 6.28, new[] { "Bob", "Ross" }, NestedArray = new object[] { 1, "Hello", new int[] { 2, 3, 4 } }, Animal = Test_ObservablePropertyAttribute.Animal.Llama)]
+ [property: TestValidation(null, typeof(MyViewModelWithExplicitFieldAndPropertyAttributes), true, 6.28, new[] { "Bob", "Ross" }, NestedArray = new object[] { 1, "Hello", new int[] { 2, 3, 4 } }, Animal = Test_ObservablePropertyAttribute.Animal.Llama)]
+ private void FooBar()
+ {
+ }
+
+ [RelayCommand]
+ [field: Test_ObservablePropertyAttribute.Test]
+ [property: Test_ObservablePropertyAttribute.Test]
+ [property: Test_ObservablePropertyAttribute.PropertyInfo(null, typeof(MyViewModelWithExplicitFieldAndPropertyAttributes), true, 6.28, new[] { "Bob", "Ross" }, new object[] { "Test" }, NestedArray = new object[] { 1, "Hello", 42, null }, Animal = (Test_ObservablePropertyAttribute.Animal)67)]
+ private void BarBaz()
+ {
+ }
+ }
+
+ // Copy of the attribute from Test_ObservablePropertyAttribute, to test nested types
+ private sealed class TestValidationAttribute : ValidationAttribute
+ {
+ public TestValidationAttribute(object? o, Type t, bool flag, double d, string[] names)
+ {
+ O = o;
+ T = t;
+ Flag = flag;
+ D = d;
+ Names = names;
+ }
+
+ public object? O { get; }
+
+ public Type T { get; }
+
+ public bool Flag { get; }
+
+ public double D { get; }
+
+ public string[] Names { get; }
+
+ public object? NestedArray { get; set; }
+
+ public Test_ObservablePropertyAttribute.Animal Animal { get; set; }
+ }
}