Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fix attributes gathering from partial command methods
  • Loading branch information
Sergio0694 committed Mar 9, 2023
commit a845ad4b16789690ff31a6e99a1b59845f13ea8c
Original file line number Diff line number Diff line change
Expand Up @@ -961,52 +961,75 @@ private static void GatherForwardedAttributes(
using ImmutableArrayBuilder<AttributeInfo> fieldAttributesInfo = ImmutableArrayBuilder<AttributeInfo>.Rent();
using ImmutableArrayBuilder<AttributeInfo> propertyAttributesInfo = ImmutableArrayBuilder<AttributeInfo>.Rent();

foreach (SyntaxReference syntaxReference in methodSymbol.DeclaringSyntaxReferences)
static void GatherForwardedAttributes(
IMethodSymbol methodSymbol,
SemanticModel semanticModel,
CancellationToken token,
in ImmutableArrayBuilder<DiagnosticInfo> diagnostics,
in ImmutableArrayBuilder<AttributeInfo> fieldAttributesInfo,
in ImmutableArrayBuilder<AttributeInfo> propertyAttributesInfo)
{
// Try to get the target method declaration syntax node
if (syntaxReference.GetSyntax(token) is not MethodDeclarationSyntax methodDeclaration)
foreach (SyntaxReference syntaxReference in methodSymbol.DeclaringSyntaxReferences)
{
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))
// Try to get the target method declaration syntax node
if (syntaxReference.GetSyntax(token) is not MethodDeclarationSyntax methodDeclaration)
{
continue;
}

foreach (AttributeSyntax attribute in attributeList.Attributes)
// Gather explicit forwarded attributes info
foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists)
{
// Get the symbol info for the attribute (once again just like in the [ObservableProperty] generator)
if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol))
// 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))
{
diagnostics.Add(
InvalidFieldOrPropertyTargetedAttributeOnRelayCommandMethod,
attribute,
methodSymbol,
attribute.Name);

continue;
}

AttributeInfo attributeInfo = AttributeInfo.From(attributeTypeSymbol, semanticModel, attribute.ArgumentList?.Arguments ?? Enumerable.Empty<AttributeArgumentSyntax>(), token);

// Add the new attribute info to the right builder
if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.FieldKeyword))
{
fieldAttributesInfo.Add(attributeInfo);
}
else
foreach (AttributeSyntax attribute in attributeList.Attributes)
{
propertyAttributesInfo.Add(attributeInfo);
// 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<AttributeArgumentSyntax>(), 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);
}
}
}
}
}

// Gather attributes from the method declaration
GatherForwardedAttributes(methodSymbol, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo);

// If the method is a partial definition, also gather attributes from the implementation part
if (methodSymbol is { IsPartialDefinition: true, PartialImplementationPart: { } partialImplementation })
{
GatherForwardedAttributes(partialImplementation, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo);
}
else if (methodSymbol is { IsPartialDefinition: false, PartialDefinitionPart: { } partialDefinition })
{
// If the method is a partial implementation, also gather attributes from the definition part
GatherForwardedAttributes(partialDefinition, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo);
}

fieldAttributes = fieldAttributesInfo.ToImmutable();
propertyAttributes = propertyAttributesInfo.ToImmutable();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,99 @@ partial class MyViewModel
VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test.g.cs", result));
}

// See https://github.com/CommunityToolkit/dotnet/issues/632
[TestMethod]
public void RelayCommandMethodWithForwardedAttributesOverPartialDeclarations_MergesAttributes()
{
string source = """
using CommunityToolkit.Mvvm.Input;

#nullable enable

namespace MyApp;

partial class MyViewModel
{
[RelayCommand]
[field: Value(0)]
[property: Value(1)]
private partial void Test1()
{
}

[field: Value(2)]
[property: Value(3)]
private partial void Test1();

[field: Value(0)]
[property: Value(1)]
private partial void Test2()
{
}

[RelayCommand]
[field: Value(2)]
[property: Value(3)]
private partial void Test2();
}

public class ValueAttribute : Attribute
{
public ValueAttribute(object value)
{
}
}
""";

string result1 = """
// <auto-generated/>
#pragma warning disable
#nullable enable
namespace MyApp
{
partial class MyViewModel
{
/// <summary>The backing field for <see cref="Test1Command"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
[global::MyApp.ValueAttribute(0)]
[global::MyApp.ValueAttribute(2)]
private global::CommunityToolkit.Mvvm.Input.RelayCommand? test1Command;
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IRelayCommand"/> instance wrapping <see cref="Test1"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::MyApp.ValueAttribute(1)]
[global::MyApp.ValueAttribute(3)]
public global::CommunityToolkit.Mvvm.Input.IRelayCommand Test1Command => test1Command ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test1));
}
}
""";

string result2 = """
// <auto-generated/>
#pragma warning disable
#nullable enable
namespace MyApp
{
partial class MyViewModel
{
/// <summary>The backing field for <see cref="Test2Command"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
[global::MyApp.ValueAttribute(2)]
[global::MyApp.ValueAttribute(0)]
private global::CommunityToolkit.Mvvm.Input.RelayCommand? test2Command;
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IRelayCommand"/> instance wrapping <see cref="Test2"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::MyApp.ValueAttribute(3)]
[global::MyApp.ValueAttribute(1)]
public global::CommunityToolkit.Mvvm.Input.IRelayCommand Test2Command => test2Command ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test2));
}
}
""";

VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test1.g.cs", result1), ("MyApp.MyViewModel.Test2.g.cs", result2));
}

[TestMethod]
public void ObservablePropertyWithinGenericAndNestedTypes()
{
Expand Down