Skip to content

Commit e4a105f

Browse files
authored
Merge pull request #202 from CommunityToolkit/dev/fix-target-member-diagnostics
Fix incorrect MVVMTK0015 with inherited members
2 parents e0016b7 + 3324b52 commit e4a105f

File tree

3 files changed

+107
-6
lines changed

3 files changed

+107
-6
lines changed

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,21 +187,19 @@ private static bool TryGatherDependentPropertyChangedNames(
187187
// Validates a property name using existing properties
188188
bool IsPropertyNameValid(string propertyName)
189189
{
190-
return fieldSymbol.ContainingType.GetMembers(propertyName).OfType<IPropertySymbol>().Any();
190+
return fieldSymbol.ContainingType.GetAllMembers(propertyName).OfType<IPropertySymbol>().Any();
191191
}
192192

193193
// Validate a property name including generated properties too
194194
bool IsPropertyNameValidWithGeneratedMembers(string propertyName)
195195
{
196-
foreach (ISymbol member in fieldSymbol.ContainingType.GetMembers())
196+
foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers())
197197
{
198198
if (member is IFieldSymbol otherFieldSymbol &&
199199
!SymbolEqualityComparer.Default.Equals(fieldSymbol, otherFieldSymbol) &&
200200
otherFieldSymbol.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") &&
201201
propertyName == GetGeneratedPropertyName(otherFieldSymbol))
202202
{
203-
propertyChangedNames.Add(propertyName);
204-
205203
return true;
206204
}
207205
}
@@ -256,7 +254,7 @@ bool IsCommandNameValid(string commandName, out bool shouldLookForGeneratedMembe
256254
{
257255
// Each target must be a string matching the name of a property from the containing type of the annotated field, and the
258256
// property must be of type IRelayCommand, or any type that implements that interface (to avoid generating invalid code).
259-
if (fieldSymbol.ContainingType.GetMembers(commandName).OfType<IPropertySymbol>().FirstOrDefault() is IPropertySymbol propertySymbol)
257+
if (fieldSymbol.ContainingType.GetAllMembers(commandName).OfType<IPropertySymbol>().FirstOrDefault() is IPropertySymbol propertySymbol)
260258
{
261259
// If there is a property member with the specified name, check that it's valid. If it isn't, the
262260
// target is definitely not valid, and the additional checks below can just be skipped. The property
@@ -285,7 +283,7 @@ bool IsCommandNameValid(string commandName, out bool shouldLookForGeneratedMembe
285283
// Validate a command name including generated command too
286284
bool IsCommandNameValidWithGeneratedMembers(string commandName)
287285
{
288-
foreach (ISymbol member in fieldSymbol.ContainingType.GetMembers())
286+
foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers())
289287
{
290288
if (member is IMethodSymbol methodSymbol &&
291289
methodSymbol.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.ICommandAttribute") &&

CommunityToolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Collections.Generic;
56
using System.Text;
67
using Microsoft.CodeAnalysis;
78

@@ -39,4 +40,37 @@ static StringBuilder BuildFrom(ISymbol? symbol, StringBuilder builder)
3940
// those characters at the moment, see: https://github.com/dotnet/roslyn/issues/58476.
4041
return BuildFrom(symbol, new StringBuilder(256)).ToString().Replace('`', '-').Replace('+', '.');
4142
}
43+
44+
/// <summary>
45+
/// Gets all member symbols from a given <see cref="INamedTypeSymbol"/> instance, including inherited ones.
46+
/// </summary>
47+
/// <param name="symbol">The input <see cref="INamedTypeSymbol"/> instance.</param>
48+
/// <returns>A sequence of all member symbols for <paramref name="symbol"/>.</returns>
49+
public static IEnumerable<ISymbol> GetAllMembers(this INamedTypeSymbol symbol)
50+
{
51+
for (INamedTypeSymbol? currentSymbol = symbol; currentSymbol is { SpecialType: not SpecialType.System_Object }; currentSymbol = currentSymbol.BaseType)
52+
{
53+
foreach (ISymbol memberSymbol in currentSymbol.GetMembers())
54+
{
55+
yield return memberSymbol;
56+
}
57+
}
58+
}
59+
60+
/// <summary>
61+
/// Gets all member symbols from a given <see cref="INamedTypeSymbol"/> instance, including inherited ones.
62+
/// </summary>
63+
/// <param name="symbol">The input <see cref="INamedTypeSymbol"/> instance.</param>
64+
/// <param name="name">The name of the members to look for.</param>
65+
/// <returns>A sequence of all member symbols for <paramref name="symbol"/>.</returns>
66+
public static IEnumerable<ISymbol> GetAllMembers(this INamedTypeSymbol symbol, string name)
67+
{
68+
for (INamedTypeSymbol? currentSymbol = symbol; currentSymbol is not null; currentSymbol = currentSymbol.BaseType)
69+
{
70+
foreach (ISymbol memberSymbol in currentSymbol.GetMembers(name))
71+
{
72+
yield return memberSymbol;
73+
}
74+
}
75+
}
4276
}

tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,75 @@ public void Test_ObservableProperty_NullabilityAnnotations_Complex()
515515
}
516516
#endif
517517

518+
// See https://github.com/CommunityToolkit/dotnet/issues/201
519+
[TestMethod]
520+
public void Test_ObservableProperty_InheritedMembersAsAttributeTargets()
521+
{
522+
ConcreteViewModel model = new();
523+
524+
List<string?> propertyNames = new();
525+
List<object?> canExecuteChangedArgs = new();
526+
527+
model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
528+
model.DoSomethingCommand.CanExecuteChanged += (s, _) => canExecuteChangedArgs.Add(s);
529+
model.ManualCommand.CanExecuteChanged += (s, _) => canExecuteChangedArgs.Add(s);
530+
531+
model.A = nameof(model.A);
532+
model.B = nameof(model.B);
533+
model.C = nameof(model.C);
534+
model.D = nameof(model.D);
535+
536+
CollectionAssert.AreEqual(new[]
537+
{
538+
nameof(model.A),
539+
nameof(model.Content),
540+
nameof(model.B),
541+
nameof(model.SomeGeneratedProperty),
542+
nameof(model.C),
543+
nameof(model.D)
544+
}, propertyNames);
545+
546+
CollectionAssert.AreEqual(new[] { model.DoSomethingCommand, model.ManualCommand }, canExecuteChangedArgs);
547+
}
548+
549+
public abstract partial class BaseViewModel : ObservableObject
550+
{
551+
public string? Content { get; set; }
552+
553+
[ObservableProperty]
554+
private string? someGeneratedProperty;
555+
556+
[ICommand]
557+
private void DoSomething()
558+
{
559+
}
560+
561+
public IRelayCommand ManualCommand { get; } = new RelayCommand(() => { });
562+
}
563+
564+
public partial class ConcreteViewModel : BaseViewModel
565+
{
566+
// Inherited property
567+
[ObservableProperty]
568+
[AlsoNotifyChangeFor(nameof(Content))]
569+
private string? _a;
570+
571+
// Inherited generated property
572+
[ObservableProperty]
573+
[AlsoNotifyChangeFor(nameof(SomeGeneratedProperty))]
574+
private string? _b;
575+
576+
// Inherited generated command
577+
[ObservableProperty]
578+
[AlsoNotifyCanExecuteFor(nameof(DoSomethingCommand))]
579+
private string? _c;
580+
581+
// Inherited manual command
582+
[ObservableProperty]
583+
[AlsoNotifyCanExecuteFor(nameof(ManualCommand))]
584+
private string? _d;
585+
}
586+
518587
public partial class SampleModel : ObservableObject
519588
{
520589
/// <summary>

0 commit comments

Comments
 (0)