Skip to content

Commit a845ad4

Browse files
committed
Fix attributes gathering from partial command methods
1 parent 30c4ad4 commit a845ad4

File tree

2 files changed

+145
-29
lines changed

2 files changed

+145
-29
lines changed

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

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -961,52 +961,75 @@ private static void GatherForwardedAttributes(
961961
using ImmutableArrayBuilder<AttributeInfo> fieldAttributesInfo = ImmutableArrayBuilder<AttributeInfo>.Rent();
962962
using ImmutableArrayBuilder<AttributeInfo> propertyAttributesInfo = ImmutableArrayBuilder<AttributeInfo>.Rent();
963963

964-
foreach (SyntaxReference syntaxReference in methodSymbol.DeclaringSyntaxReferences)
964+
static void GatherForwardedAttributes(
965+
IMethodSymbol methodSymbol,
966+
SemanticModel semanticModel,
967+
CancellationToken token,
968+
in ImmutableArrayBuilder<DiagnosticInfo> diagnostics,
969+
in ImmutableArrayBuilder<AttributeInfo> fieldAttributesInfo,
970+
in ImmutableArrayBuilder<AttributeInfo> propertyAttributesInfo)
965971
{
966-
// Try to get the target method declaration syntax node
967-
if (syntaxReference.GetSyntax(token) is not MethodDeclarationSyntax methodDeclaration)
972+
foreach (SyntaxReference syntaxReference in methodSymbol.DeclaringSyntaxReferences)
968973
{
969-
continue;
970-
}
971-
972-
// Gather explicit forwarded attributes info
973-
foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists)
974-
{
975-
// Same as in the [ObservableProperty] generator, except we're also looking for fields here
976-
if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword))
974+
// Try to get the target method declaration syntax node
975+
if (syntaxReference.GetSyntax(token) is not MethodDeclarationSyntax methodDeclaration)
977976
{
978977
continue;
979978
}
980979

981-
foreach (AttributeSyntax attribute in attributeList.Attributes)
980+
// Gather explicit forwarded attributes info
981+
foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists)
982982
{
983-
// Get the symbol info for the attribute (once again just like in the [ObservableProperty] generator)
984-
if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol))
983+
// Same as in the [ObservableProperty] generator, except we're also looking for fields here
984+
if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword))
985985
{
986-
diagnostics.Add(
987-
InvalidFieldOrPropertyTargetedAttributeOnRelayCommandMethod,
988-
attribute,
989-
methodSymbol,
990-
attribute.Name);
991-
992986
continue;
993987
}
994988

995-
AttributeInfo attributeInfo = AttributeInfo.From(attributeTypeSymbol, semanticModel, attribute.ArgumentList?.Arguments ?? Enumerable.Empty<AttributeArgumentSyntax>(), token);
996-
997-
// Add the new attribute info to the right builder
998-
if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.FieldKeyword))
999-
{
1000-
fieldAttributesInfo.Add(attributeInfo);
1001-
}
1002-
else
989+
foreach (AttributeSyntax attribute in attributeList.Attributes)
1003990
{
1004-
propertyAttributesInfo.Add(attributeInfo);
991+
// Get the symbol info for the attribute (once again just like in the [ObservableProperty] generator)
992+
if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol))
993+
{
994+
diagnostics.Add(
995+
InvalidFieldOrPropertyTargetedAttributeOnRelayCommandMethod,
996+
attribute,
997+
methodSymbol,
998+
attribute.Name);
999+
1000+
continue;
1001+
}
1002+
1003+
AttributeInfo attributeInfo = AttributeInfo.From(attributeTypeSymbol, semanticModel, attribute.ArgumentList?.Arguments ?? Enumerable.Empty<AttributeArgumentSyntax>(), token);
1004+
1005+
// Add the new attribute info to the right builder
1006+
if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.FieldKeyword))
1007+
{
1008+
fieldAttributesInfo.Add(attributeInfo);
1009+
}
1010+
else
1011+
{
1012+
propertyAttributesInfo.Add(attributeInfo);
1013+
}
10051014
}
10061015
}
10071016
}
10081017
}
10091018

1019+
// Gather attributes from the method declaration
1020+
GatherForwardedAttributes(methodSymbol, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo);
1021+
1022+
// If the method is a partial definition, also gather attributes from the implementation part
1023+
if (methodSymbol is { IsPartialDefinition: true, PartialImplementationPart: { } partialImplementation })
1024+
{
1025+
GatherForwardedAttributes(partialImplementation, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo);
1026+
}
1027+
else if (methodSymbol is { IsPartialDefinition: false, PartialDefinitionPart: { } partialDefinition })
1028+
{
1029+
// If the method is a partial implementation, also gather attributes from the definition part
1030+
GatherForwardedAttributes(partialDefinition, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo);
1031+
}
1032+
10101033
fieldAttributes = fieldAttributesInfo.ToImmutable();
10111034
propertyAttributes = propertyAttributesInfo.ToImmutable();
10121035
}

tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,99 @@ partial class MyViewModel
268268
VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test.g.cs", result));
269269
}
270270

271+
// See https://github.com/CommunityToolkit/dotnet/issues/632
272+
[TestMethod]
273+
public void RelayCommandMethodWithForwardedAttributesOverPartialDeclarations_MergesAttributes()
274+
{
275+
string source = """
276+
using CommunityToolkit.Mvvm.Input;
277+
278+
#nullable enable
279+
280+
namespace MyApp;
281+
282+
partial class MyViewModel
283+
{
284+
[RelayCommand]
285+
[field: Value(0)]
286+
[property: Value(1)]
287+
private partial void Test1()
288+
{
289+
}
290+
291+
[field: Value(2)]
292+
[property: Value(3)]
293+
private partial void Test1();
294+
295+
[field: Value(0)]
296+
[property: Value(1)]
297+
private partial void Test2()
298+
{
299+
}
300+
301+
[RelayCommand]
302+
[field: Value(2)]
303+
[property: Value(3)]
304+
private partial void Test2();
305+
}
306+
307+
public class ValueAttribute : Attribute
308+
{
309+
public ValueAttribute(object value)
310+
{
311+
}
312+
}
313+
""";
314+
315+
string result1 = """
316+
// <auto-generated/>
317+
#pragma warning disable
318+
#nullable enable
319+
namespace MyApp
320+
{
321+
partial class MyViewModel
322+
{
323+
/// <summary>The backing field for <see cref="Test1Command"/>.</summary>
324+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
325+
[global::MyApp.ValueAttribute(0)]
326+
[global::MyApp.ValueAttribute(2)]
327+
private global::CommunityToolkit.Mvvm.Input.RelayCommand? test1Command;
328+
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IRelayCommand"/> instance wrapping <see cref="Test1"/>.</summary>
329+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
330+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
331+
[global::MyApp.ValueAttribute(1)]
332+
[global::MyApp.ValueAttribute(3)]
333+
public global::CommunityToolkit.Mvvm.Input.IRelayCommand Test1Command => test1Command ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test1));
334+
}
335+
}
336+
""";
337+
338+
string result2 = """
339+
// <auto-generated/>
340+
#pragma warning disable
341+
#nullable enable
342+
namespace MyApp
343+
{
344+
partial class MyViewModel
345+
{
346+
/// <summary>The backing field for <see cref="Test2Command"/>.</summary>
347+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
348+
[global::MyApp.ValueAttribute(2)]
349+
[global::MyApp.ValueAttribute(0)]
350+
private global::CommunityToolkit.Mvvm.Input.RelayCommand? test2Command;
351+
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IRelayCommand"/> instance wrapping <see cref="Test2"/>.</summary>
352+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
353+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
354+
[global::MyApp.ValueAttribute(3)]
355+
[global::MyApp.ValueAttribute(1)]
356+
public global::CommunityToolkit.Mvvm.Input.IRelayCommand Test2Command => test2Command ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test2));
357+
}
358+
}
359+
""";
360+
361+
VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test1.g.cs", result1), ("MyApp.MyViewModel.Test2.g.cs", result2));
362+
}
363+
271364
[TestMethod]
272365
public void ObservablePropertyWithinGenericAndNestedTypes()
273366
{

0 commit comments

Comments
 (0)