Skip to content

Commit 914a7ac

Browse files
committed
Add diagnostics for combining [INPC] and [ObservableObject]
1 parent 0fb8a62 commit 914a7ac

File tree

6 files changed

+209
-0
lines changed

6 files changed

+209
-0
lines changed

CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ MVVMTK0013 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error |
2121
MVVMTK0014 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
2222
MVVMTK0015 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
2323
MVVMTK0016 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
24+
MVVMTK0017 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
25+
MVVMTK0018 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ protected override bool ValidateTargetType(INamedTypeSymbol typeSymbol, INotifyP
5757
return false;
5858
}
5959

60+
// Check if the type uses [INotifyPropertyChanged] or [ObservableObject] already (in the type hierarchy too)
61+
if (typeSymbol.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute") ||
62+
typeSymbol.InheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute"))
63+
{
64+
builder.Add(InvalidAttributeCombinationForINotifyPropertyChangedAttributeError, typeSymbol, typeSymbol);
65+
66+
diagnostics = builder.ToImmutable();
67+
68+
return false;
69+
}
70+
6071
diagnostics = builder.ToImmutable();
6172

6273
return true;

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ protected override bool ValidateTargetType(INamedTypeSymbol typeSymbol, object?
6060
return false;
6161
}
6262

63+
// Check if the type uses [INotifyPropertyChanged] or [ObservableObject] already (in the type hierarchy too)
64+
if (typeSymbol.InheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute") ||
65+
typeSymbol.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute"))
66+
{
67+
builder.Add(InvalidAttributeCombinationForObservableObjectAttributeError, typeSymbol, typeSymbol);
68+
69+
diagnostics = builder.ToImmutable();
70+
71+
return false;
72+
}
73+
6374
diagnostics = builder.ToImmutable();
6475

6576
return true;

CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,4 +267,36 @@ internal static class DiagnosticDescriptors
267267
isEnabledByDefault: true,
268268
description: "The target(s) of [AlsoNotifyCanExecuteFor] must be an accessible IRelayCommand property in its parent type.",
269269
helpLinkUri: "https://aka.ms/mvvmtoolkit");
270+
271+
/// <summary>
272+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>[INotifyPropertyChanged]</c> is applied to a type with an attribute already.
273+
/// <para>
274+
/// Format: <c>"Cannot apply [INotifyPropertyChanged] to type {0}, as it already has this attribute or [ObservableObject] applied to it (including base types)"</c>.
275+
/// </para>
276+
/// </summary>
277+
public static readonly DiagnosticDescriptor InvalidAttributeCombinationForINotifyPropertyChangedAttributeError = new DiagnosticDescriptor(
278+
id: "MVVMTK0017",
279+
title: $"Invalid target type for [INotifyPropertyChanged]",
280+
messageFormat: $"Cannot apply [INotifyPropertyChanged] to type {{0}}, as it already has this attribute or [ObservableObject] applied to it (including base types)",
281+
category: typeof(INotifyPropertyChangedGenerator).FullName,
282+
defaultSeverity: DiagnosticSeverity.Error,
283+
isEnabledByDefault: true,
284+
description: $"Cannot apply [INotifyPropertyChanged] to a type that already has this attribute or [ObservableObject] applied to it (including base types).",
285+
helpLinkUri: "https://aka.ms/mvvmtoolkit");
286+
287+
/// <summary>
288+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>[ObservableObject]</c> is applied to a type with an attribute already.
289+
/// <para>
290+
/// Format: <c>"Cannot apply [ObservableObject] to type {0}, as it already has this attribute or [INotifyPropertyChanged] applied to it (including base types)"</c>.
291+
/// </para>
292+
/// </summary>
293+
public static readonly DiagnosticDescriptor InvalidAttributeCombinationForObservableObjectAttributeError = new DiagnosticDescriptor(
294+
id: "MVVMTK0018",
295+
title: $"Invalid target type for [ObservableObject]",
296+
messageFormat: $"Cannot apply [ObservableObject] to type {{0}}, as it already has this attribute or [INotifyPropertyChanged] applied to it (including base types)",
297+
category: typeof(ObservableObjectGenerator).FullName,
298+
defaultSeverity: DiagnosticSeverity.Error,
299+
isEnabledByDefault: true,
300+
description: $"Cannot apply [ObservableObject] to a type that already has this attribute or [INotifyPropertyChanged] applied to it (including base types).",
301+
helpLinkUri: "https://aka.ms/mvvmtoolkit");
270302
}

CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,21 @@ public static bool HasOrInheritsAttributeWithFullyQualifiedName(this ITypeSymbol
9292

9393
return false;
9494
}
95+
96+
/// <summary>
97+
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits a specified attribute.
98+
/// If the type has no base type, this method will automatically handle that and return <see langword="false"/>.
99+
/// </summary>
100+
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
101+
/// <param name="name">The name of the attribute to look for.</param>
102+
/// <returns>Whether or not <paramref name="typeSymbol"/> has an attribute with the specified type name.</returns>
103+
public static bool InheritsAttributeWithFullyQualifiedName(this ITypeSymbol typeSymbol, string name)
104+
{
105+
if (typeSymbol.BaseType is INamedTypeSymbol baseTypeSymbol)
106+
{
107+
return HasOrInheritsAttributeWithFullyQualifiedName(baseTypeSymbol, name);
108+
}
109+
110+
return false;
111+
}
95112
}

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

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,142 @@ public partial class SampleViewModel : ObservableObject
828828
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0016");
829829
}
830830

831+
[TestMethod]
832+
public void InvalidAttributeCombinationForINotifyPropertyChangedAttributeError_InheritingINotifyPropertyChangedAttribute()
833+
{
834+
string source = @"
835+
using System.ComponentModel.DataAnnotations;
836+
using CommunityToolkit.Mvvm.ComponentModel;
837+
using CommunityToolkit.Mvvm.Input;
838+
839+
namespace MyApp
840+
{
841+
[INotifyPropertyChanged]
842+
public partial class A
843+
{
844+
}
845+
846+
[INotifyPropertyChanged]
847+
public partial class B : A
848+
{
849+
}
850+
}";
851+
852+
VerifyGeneratedDiagnostics<INotifyPropertyChangedGenerator>(source, "MVVMTK0017");
853+
}
854+
855+
[TestMethod]
856+
public void InvalidAttributeCombinationForINotifyPropertyChangedAttributeError_InheritingObservableObjectAttribute()
857+
{
858+
string source = @"
859+
using System.ComponentModel.DataAnnotations;
860+
using CommunityToolkit.Mvvm.ComponentModel;
861+
using CommunityToolkit.Mvvm.Input;
862+
863+
namespace MyApp
864+
{
865+
[ObservableObject]
866+
public partial class A
867+
{
868+
}
869+
870+
[INotifyPropertyChanged]
871+
public partial class B : A
872+
{
873+
}
874+
}";
875+
876+
VerifyGeneratedDiagnostics<INotifyPropertyChangedGenerator>(source, "MVVMTK0017");
877+
}
878+
879+
[TestMethod]
880+
public void InvalidAttributeCombinationForINotifyPropertyChangedAttributeError_WithAlsoObservableObjectAttribute()
881+
{
882+
string source = @"
883+
using System.ComponentModel.DataAnnotations;
884+
using CommunityToolkit.Mvvm.ComponentModel;
885+
using CommunityToolkit.Mvvm.Input;
886+
887+
namespace MyApp
888+
{
889+
[INotifyPropertyChanged]
890+
[ObservableObject]
891+
public partial class A
892+
{
893+
}
894+
}";
895+
896+
VerifyGeneratedDiagnostics<INotifyPropertyChangedGenerator>(source, "MVVMTK0017");
897+
}
898+
899+
[TestMethod]
900+
public void InvalidAttributeCombinationForObservableObjectAttributeError_InheritingINotifyPropertyChangedAttribute()
901+
{
902+
string source = @"
903+
using System.ComponentModel.DataAnnotations;
904+
using CommunityToolkit.Mvvm.ComponentModel;
905+
using CommunityToolkit.Mvvm.Input;
906+
907+
namespace MyApp
908+
{
909+
[INotifyPropertyChanged]
910+
public partial class A
911+
{
912+
}
913+
914+
[ObservableObject]
915+
public partial class B : A
916+
{
917+
}
918+
}";
919+
920+
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0018");
921+
}
922+
923+
[TestMethod]
924+
public void InvalidAttributeCombinationForObservableObjectAttributeError_InheritingObservableObjectAttribute()
925+
{
926+
string source = @"
927+
using System.ComponentModel.DataAnnotations;
928+
using CommunityToolkit.Mvvm.ComponentModel;
929+
using CommunityToolkit.Mvvm.Input;
930+
931+
namespace MyApp
932+
{
933+
[ObservableObject]
934+
public partial class A
935+
{
936+
}
937+
938+
[ObservableObject]
939+
public partial class B : A
940+
{
941+
}
942+
}";
943+
944+
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0018");
945+
}
946+
947+
[TestMethod]
948+
public void InvalidAttributeCombinationForObservableObjectAttributeError_WithAlsoINotifyPropertyChangedAttribute()
949+
{
950+
string source = @"
951+
using System.ComponentModel.DataAnnotations;
952+
using CommunityToolkit.Mvvm.ComponentModel;
953+
using CommunityToolkit.Mvvm.Input;
954+
955+
namespace MyApp
956+
{
957+
[INotifyPropertyChanged]
958+
[ObservableObject]
959+
public partial class A
960+
{
961+
}
962+
}";
963+
964+
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0018");
965+
}
966+
831967
/// <summary>
832968
/// Verifies the output of a source generator.
833969
/// </summary>

0 commit comments

Comments
 (0)