Skip to content

Commit 66f582b

Browse files
committed
Fix [RelayCommand] attribute over methods with no attributes
1 parent a925b19 commit 66f582b

File tree

5 files changed

+106
-3
lines changed

5 files changed

+106
-3
lines changed

src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
<Compile Include="$(MSBuildThisFileDirectory)Extensions\INamedTypeSymbolExtensions.cs" />
5656
<Compile Include="$(MSBuildThisFileDirectory)Extensions\IncrementalGeneratorInitializationContextExtensions.cs" />
5757
<Compile Include="$(MSBuildThisFileDirectory)Extensions\IncrementalValuesProviderExtensions.cs" />
58+
<Compile Include="$(MSBuildThisFileDirectory)Extensions\MethodDeclarationSyntaxExtensions.cs" />
5859
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SymbolInfoExtensions.cs" />
5960
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ISymbolExtensions.cs" />
6061
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SourceProductionContextExtensions.cs" />
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
9+
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
10+
11+
/// <summary>
12+
/// Extension methods for the <see cref="MethodDeclarationSyntax"/> type.
13+
/// </summary>
14+
internal static class MethodDeclarationSyntaxExtensions
15+
{
16+
/// <summary>
17+
/// Checks whether a given <see cref="MethodDeclarationSyntax"/> has or could potentially have any attribute lists.
18+
/// </summary>
19+
/// <param name="methodDeclaration">The input <see cref="MethodDeclarationSyntax"/> to check.</param>
20+
/// <returns>Whether <paramref name="methodDeclaration"/> has or potentially has any attribute lists.</returns>
21+
public static bool HasOrPotentiallyHasAttributeLists(this MethodDeclarationSyntax methodDeclaration)
22+
{
23+
// If the declaration has any attribute lists, there's nothing left to do
24+
if (methodDeclaration.AttributeLists.Count > 0)
25+
{
26+
return true;
27+
}
28+
29+
// If there are no attributes, check whether the method declaration has the partial keyword. If it
30+
// does, there could potentially be attribute lists on the other partial definition/implementation.
31+
return methodDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword);
32+
}
33+
}

src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ internal static class SyntaxNodeExtensions
2626
public static bool IsFirstSyntaxDeclarationForSymbol(this SyntaxNode syntaxNode, ISymbol symbol)
2727
{
2828
return
29-
symbol.DeclaringSyntaxReferences.Length > 0 &&
30-
symbol.DeclaringSyntaxReferences[0] is SyntaxReference syntaxReference &&
29+
symbol.DeclaringSyntaxReferences is [SyntaxReference syntaxReference, ..] &&
3130
syntaxReference.SyntaxTree == syntaxNode.SyntaxTree &&
3231
syntaxReference.Span == syntaxNode.Span;
3332
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
2727
context.SyntaxProvider
2828
.ForAttributeWithMetadataName(
2929
"CommunityToolkit.Mvvm.Input.RelayCommandAttribute",
30-
static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax, AttributeLists.Count: > 0 },
30+
static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax } methodDeclaration && methodDeclaration.HasOrPotentiallyHasAttributeLists(),
3131
static (context, token) =>
3232
{
3333
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,76 @@ 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 RelayCommandMethodWithPartialDeclarations_TriggersCorrectly()
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+
private partial void Test1()
286+
{
287+
}
288+
289+
private partial void Test1();
290+
291+
private partial void Test2()
292+
{
293+
}
294+
295+
[RelayCommand]
296+
private partial void Test2();
297+
}
298+
""";
299+
300+
string result1 = """
301+
// <auto-generated/>
302+
#pragma warning disable
303+
#nullable enable
304+
namespace MyApp
305+
{
306+
partial class MyViewModel
307+
{
308+
/// <summary>The backing field for <see cref="Test1Command"/>.</summary>
309+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
310+
private global::CommunityToolkit.Mvvm.Input.RelayCommand? test1Command;
311+
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IRelayCommand"/> instance wrapping <see cref="Test1"/>.</summary>
312+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
313+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
314+
public global::CommunityToolkit.Mvvm.Input.IRelayCommand Test1Command => test1Command ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test1));
315+
}
316+
}
317+
""";
318+
319+
string result2 = """
320+
// <auto-generated/>
321+
#pragma warning disable
322+
#nullable enable
323+
namespace MyApp
324+
{
325+
partial class MyViewModel
326+
{
327+
/// <summary>The backing field for <see cref="Test2Command"/>.</summary>
328+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
329+
private global::CommunityToolkit.Mvvm.Input.RelayCommand? test2Command;
330+
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IRelayCommand"/> instance wrapping <see cref="Test2"/>.</summary>
331+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
332+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
333+
public global::CommunityToolkit.Mvvm.Input.IRelayCommand Test2Command => test2Command ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test2));
334+
}
335+
}
336+
""";
337+
338+
VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test1.g.cs", result1), ("MyApp.MyViewModel.Test2.g.cs", result2));
339+
}
340+
271341
// See https://github.com/CommunityToolkit/dotnet/issues/632
272342
[TestMethod]
273343
public void RelayCommandMethodWithForwardedAttributesOverPartialDeclarations_MergesAttributes()

0 commit comments

Comments
 (0)