Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support partial methods and forwarded attributes with [RelayCommand] #633

Merged
merged 5 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Extensions\INamedTypeSymbolExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\IncrementalGeneratorInitializationContextExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\IncrementalValuesProviderExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\MethodDeclarationSyntaxExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SymbolInfoExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ISymbolExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SourceProductionContextExtensions.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;

/// <summary>
/// Extension methods for the <see cref="MethodDeclarationSyntax"/> type.
/// </summary>
internal static class MethodDeclarationSyntaxExtensions
{
/// <summary>
/// Checks whether a given <see cref="MethodDeclarationSyntax"/> has or could potentially have any attribute lists.
/// </summary>
/// <param name="methodDeclaration">The input <see cref="MethodDeclarationSyntax"/> to check.</param>
/// <returns>Whether <paramref name="methodDeclaration"/> has or potentially has any attribute lists.</returns>
public static bool HasOrPotentiallyHasAttributeLists(this MethodDeclarationSyntax methodDeclaration)
{
// If the declaration has any attribute lists, there's nothing left to do
if (methodDeclaration.AttributeLists.Count > 0)
{
return true;
}

// If there are no attributes, check whether the method declaration has the partial keyword. If it
// does, there could potentially be attribute lists on the other partial definition/implementation.
return methodDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ internal static class SyntaxNodeExtensions
public static bool IsFirstSyntaxDeclarationForSymbol(this SyntaxNode syntaxNode, ISymbol symbol)
{
return
symbol.DeclaringSyntaxReferences.Length > 0 &&
symbol.DeclaringSyntaxReferences[0] is SyntaxReference syntaxReference &&
symbol.DeclaringSyntaxReferences is [SyntaxReference syntaxReference, ..] &&
syntaxReference.SyntaxTree == syntaxNode.SyntaxTree &&
syntaxReference.Span == syntaxNode.Span;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,15 @@ private static bool IsCommandDefinitionUnique(IMethodSymbol methodSymbol, in Imm
return true;
}

// If the two method symbols are partial and either is the implementation of the other one, this is allowed
if ((methodSymbol is { IsPartialDefinition: true, PartialImplementationPart: { } partialImplementation } &&
SymbolEqualityComparer.Default.Equals(otherSymbol, partialImplementation)) ||
(otherSymbol is { IsPartialDefinition: true, PartialImplementationPart: { } otherPartialImplementation } &&
SymbolEqualityComparer.Default.Equals(methodSymbol, otherPartialImplementation)))
{
continue;
}

diagnostics.Add(
MultipleRelayCommandMethodOverloadsError,
methodSymbol,
Expand Down Expand Up @@ -952,12 +961,24 @@ 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)
{
// Get the single syntax reference for the input method symbol (there should be only one)
if (methodSymbol.DeclaringSyntaxReferences is not [SyntaxReference syntaxReference])
{
return;
}

// Try to get the target method declaration syntax node
if (syntaxReference.GetSyntax(token) is not MethodDeclarationSyntax methodDeclaration)
{
continue;
return;
}

// Gather explicit forwarded attributes info
Expand Down Expand Up @@ -998,6 +1019,22 @@ private static void GatherForwardedAttributes(
}
}

// If the method is a partial definition, also gather attributes from the implementation part
if (methodSymbol is { IsPartialDefinition: true } or { PartialDefinitionPart: not null })
{
IMethodSymbol partialDefinition = methodSymbol.PartialDefinitionPart ?? methodSymbol;
IMethodSymbol partialImplementation = methodSymbol.PartialImplementationPart ?? methodSymbol;

// We always give priority to the partial definition, to ensure a predictable and testable ordering
GatherForwardedAttributes(partialDefinition, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo);
GatherForwardedAttributes(partialImplementation, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo);
}
else
{
// If the method is not a partial definition/implementation, just gather attributes from the method with no modifications
GatherForwardedAttributes(methodSymbol, 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 @@ -27,7 +27,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.SyntaxProvider
.ForAttributeWithMetadataName(
"CommunityToolkit.Mvvm.Input.RelayCommandAttribute",
static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax, AttributeLists.Count: > 0 },
static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax } methodDeclaration && methodDeclaration.HasOrPotentiallyHasAttributeLists(),
static (context, token) =>
{
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ public static IncrementalValuesProvider<T> ForAttributeWithMetadataName<T>(
return null;
}

// Edge case: if the symbol is a partial method, skip the implementation part and only process the partial method
// definition. This is needed because attributes will be reported as available on both the definition and the
// implementation part. To avoid generating duplicate files, we only give priority to the definition part.
// On Roslyn 4.3+, ForAttributeWithMetadataName will already only return the symbol the attribute was located on.
if (symbol is IMethodSymbol { IsPartialDefinition: false, PartialDefinitionPart: not null })
{
return null;
}

// Create the GeneratorAttributeSyntaxContext value to pass to the input transform. The attributes array
// will only ever have a single value, but that's fine with the attributes the various generators look for.
GeneratorAttributeSyntaxContext syntaxContext = new(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,169 @@ 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 RelayCommandMethodWithPartialDeclarations_TriggersCorrectly()
{
string source = """
using CommunityToolkit.Mvvm.Input;

#nullable enable

namespace MyApp;

partial class MyViewModel
{
[RelayCommand]
private partial void Test1()
{
}

private partial void Test1();

private partial void Test2()
{
}

[RelayCommand]
private partial void Test2();
}
""";

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")]
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]
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")]
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]
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));
}

// 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(2)]
[global::MyApp.ValueAttribute(0)]
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(3)]
[global::MyApp.ValueAttribute(1)]
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
Loading