diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
index 405e84cfd..91866191c 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
@@ -39,6 +39,8 @@
+
+
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
index 8041977f0..031a9d473 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
@@ -555,26 +555,6 @@ private static bool TryGetIsNotifyingRecipients(
return false;
}
- ///
- /// Checks whether a given type using [NotifyPropertyChangedRecipients] is valid and creates a if not.
- ///
- /// The input instance to process.
- /// The for , if not a valid type.
- public static Diagnostic? GetIsNotifyingRecipientsDiagnosticForType(INamedTypeSymbol typeSymbol)
- {
- // If the containing type is valid, track it
- if (!typeSymbol.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") &&
- !typeSymbol.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute"))
- {
- return Diagnostic.Create(
- InvalidTypeForNotifyPropertyChangedRecipientsError,
- typeSymbol.Locations.FirstOrDefault(),
- typeSymbol);
- }
-
- return null;
- }
-
///
/// Checks whether a given generated property should also validate its value.
///
@@ -657,25 +637,6 @@ private static bool TryGetNotifyDataErrorInfo(
return false;
}
- ///
- /// Checks whether a given type using [NotifyDataErrorInfo] is valid and creates a if not.
- ///
- /// The input instance to process.
- /// The for , if not a valid type.
- public static Diagnostic? GetIsNotifyDataErrorInfoDiagnosticForType(INamedTypeSymbol typeSymbol)
- {
- // If the containing type is valid, track it
- if (!typeSymbol.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
- {
- return Diagnostic.Create(
- InvalidTypeForNotifyDataErrorInfoError,
- typeSymbol.Locations.FirstOrDefault(),
- typeSymbol);
- }
-
- return null;
- }
-
///
/// Gets a instance with the cached args for property changing notifications.
///
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs
index 6dd813276..e97cfed29 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs
@@ -113,32 +113,5 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.AddSource("__KnownINotifyPropertyChangedArgs.g.cs", compilationUnit.GetText(Encoding.UTF8));
}
});
-
- // Get all class declarations with at least one attribute
- IncrementalValuesProvider classSymbols =
- context.SyntaxProvider
- .CreateSyntaxProvider(
- static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
- static (context, _) => (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
-
- // Filter only the type symbols with [NotifyPropertyChangedRecipients] and create diagnostics for them
- IncrementalValuesProvider notifyRecipientsErrors =
- classSymbols
- .Where(static item => item.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute"))
- .Select(static (item, _) => Execute.GetIsNotifyingRecipientsDiagnosticForType(item))
- .Where(static item => item is not null)!;
-
- // Output the diagnostics for [NotifyPropertyChangedRecipients]
- context.ReportDiagnostics(notifyRecipientsErrors);
-
- // Filter only the type symbols with [NotifyDataErrorInfo] and create diagnostics for them
- IncrementalValuesProvider notifyDataErrorInfoErrors =
- classSymbols
- .Where(static item => item.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute"))
- .Select(static (item, _) => Execute.GetIsNotifyDataErrorInfoDiagnosticForType(item))
- .Where(static item => item is not null)!;
-
- // Output the diagnostics for [NotifyDataErrorInfo]
- context.ReportDiagnostics(notifyDataErrorInfoErrors);
}
}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs
new file mode 100644
index 000000000..f4232ada0
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs
@@ -0,0 +1,58 @@
+// 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 System.Collections.Immutable;
+using System.Linq;
+using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators;
+
+///
+/// A diagnostic analyzer that generates an error when a class level [NotifyDataErrorInfo] use is detected.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer : DiagnosticAnalyzer
+{
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidTypeForNotifyDataErrorInfoError);
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(static context =>
+ {
+ // Get the symbols for [NotifyDataErrorInfo] and ObservableValidator
+ if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute") is not INamedTypeSymbol notifyDataErrorInfoAttributeSymbol ||
+ context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator") is not INamedTypeSymbol observableValidatorSymbol)
+ {
+ return;
+ }
+
+ context.RegisterSymbolAction(context =>
+ {
+ // We're looking for all class declarations
+ if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsImplicitlyDeclared: false } classSymbol)
+ {
+ return;
+ }
+
+ // Emit a diagnostic for types that use [NotifyDataErrorInfo] but don't inherit from ObservableValidator
+ if (classSymbol.HasAttributeWithType(notifyDataErrorInfoAttributeSymbol) &&
+ !classSymbol.InheritsFromType(observableValidatorSymbol))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidTypeForNotifyDataErrorInfoError,
+ classSymbol.Locations.FirstOrDefault(),
+ classSymbol));
+ }
+ }, SymbolKind.NamedType);
+ });
+ }
+}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs
new file mode 100644
index 000000000..d1062ba42
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs
@@ -0,0 +1,60 @@
+// 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 System.Collections.Immutable;
+using System.Linq;
+using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators;
+
+///
+/// A diagnostic analyzer that generates an error when a class level [NotifyPropertyChangedRecipients] use is detected.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer : DiagnosticAnalyzer
+{
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidTypeForNotifyPropertyChangedRecipientsError);
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(static context =>
+ {
+ // Get the symbols for [NotifyPropertyChangedRecipients], ObservableRecipient and [ObservableRecipient]
+ if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute") is not INamedTypeSymbol notifyPropertyChangedRecipientsAttributeSymbol ||
+ context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") is not INamedTypeSymbol observableRecipientSymbol ||
+ context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute") is not INamedTypeSymbol observableRecipientAttributeSymbol)
+ {
+ return;
+ }
+
+ context.RegisterSymbolAction(context =>
+ {
+ // We're looking for all class declarations
+ if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsImplicitlyDeclared: false } classSymbol)
+ {
+ return;
+ }
+
+ // Emit a diagnstic for types that use [NotifyPropertyChangedRecipients] but are neither inheriting from ObservableRecipient nor using [ObservableRecipient]
+ if (classSymbol.HasAttributeWithType(notifyPropertyChangedRecipientsAttributeSymbol) &&
+ !classSymbol.InheritsFromType(observableRecipientSymbol) &&
+ !classSymbol.HasOrInheritsAttributeWithType(observableRecipientAttributeSymbol))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidTypeForNotifyPropertyChangedRecipientsError,
+ classSymbol.Locations.FirstOrDefault(),
+ classSymbol));
+ }
+ }, SymbolKind.NamedType);
+ });
+ }
+}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs
index a54073fd1..a0fa4d315 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System.Collections.Immutable;
#if !ROSLYN_4_3_1_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
@@ -54,9 +53,7 @@ public static bool HasFullyQualifiedName(this ISymbol symbol, string name)
/// Whether or not has an attribute with the specified name.
public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name)
{
- ImmutableArray attributes = symbol.GetAttributes();
-
- foreach (AttributeData attribute in attributes)
+ foreach (AttributeData attribute in symbol.GetAttributes())
{
if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true)
{
@@ -67,6 +64,25 @@ public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbo
return false;
}
+ ///
+ /// Checks whether or not a given symbol has an attribute with the specified fully qualified metadata name.
+ ///
+ /// The input instance to check.
+ /// The instance for the attribute type to look for.
+ /// Whether or not has an attribute with the specified type.
+ public static bool HasAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol)
+ {
+ foreach (AttributeData attribute in symbol.GetAttributes())
+ {
+ if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
#if !ROSLYN_4_3_1_OR_GREATER
///
/// Tries to get an attribute with the specified fully qualified metadata name.
@@ -77,9 +93,7 @@ public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbo
/// Whether or not has an attribute with the specified name.
public static bool TryGetAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name, [NotNullWhen(true)] out AttributeData? attributeData)
{
- ImmutableArray attributes = symbol.GetAttributes();
-
- foreach (AttributeData attribute in attributes)
+ foreach (AttributeData attribute in symbol.GetAttributes())
{
if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true)
{
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs
index 52a813986..6e976501b 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs
@@ -43,7 +43,7 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS
{
INamedTypeSymbol? baseType = typeSymbol.BaseType;
- while (baseType != null)
+ while (baseType is not null)
{
if (baseType.HasFullyQualifiedMetadataName(name))
{
@@ -56,6 +56,29 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS
return false;
}
+ ///
+ /// Checks whether or not a given inherits from a specified type.
+ ///
+ /// The target instance to check.
+ /// The instane to check for inheritance from.
+ /// Whether or not inherits from .
+ public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
+ {
+ INamedTypeSymbol? currentBaseTypeSymbol = typeSymbol.BaseType;
+
+ while (currentBaseTypeSymbol is not null)
+ {
+ if (SymbolEqualityComparer.Default.Equals(currentBaseTypeSymbol, baseTypeSymbol))
+ {
+ return true;
+ }
+
+ currentBaseTypeSymbol = currentBaseTypeSymbol.BaseType;
+ }
+
+ return false;
+ }
+
///
/// Checks whether or not a given implements an interface with a specified name.
///
@@ -113,6 +136,25 @@ public static bool HasOrInheritsAttributeWithFullyQualifiedMetadataName(this ITy
return false;
}
+ ///
+ /// Checks whether or not a given has or inherits a specified attribute.
+ ///
+ /// The target instance to check.
+ /// The instane to check for inheritance from.
+ /// Whether or not has or inherits an attribute of type .
+ public static bool HasOrInheritsAttributeWithType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
+ {
+ for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
+ {
+ if (currentType.HasAttributeWithType(baseTypeSymbol))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
///
/// Checks whether or not a given inherits a specified attribute.
/// If the type has no base type, this method will automatically handle that and return .
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
index a2cd11c3e..6e15ce95e 100644
--- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
@@ -1337,7 +1337,7 @@ public partial class SampleViewModel : ObservableValidator
}
[TestMethod]
- public void InvalidTypeForNotifyPropertyChangedRecipientsError()
+ public async Task InvalidTypeForNotifyPropertyChangedRecipientsError()
{
string source = """
using CommunityToolkit.Mvvm.ComponentModel;
@@ -1345,7 +1345,7 @@ public void InvalidTypeForNotifyPropertyChangedRecipientsError()
namespace MyApp
{
[NotifyPropertyChangedRecipients]
- public partial class MyViewModel : ObservableObject
+ public partial class {|MVVMTK0027:MyViewModel|} : ObservableObject
{
[ObservableProperty]
public int number;
@@ -1353,11 +1353,11 @@ public partial class MyViewModel : ObservableObject
}
""";
- VerifyGeneratedDiagnostics(source, "MVVMTK0027");
+ await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8);
}
[TestMethod]
- public void InvalidTypeForNotifyDataErrorInfoError()
+ public async Task InvalidTypeForNotifyDataErrorInfoError()
{
string source = """
using System.ComponentModel.DataAnnotations;
@@ -1366,16 +1366,13 @@ public void InvalidTypeForNotifyDataErrorInfoError()
namespace MyApp
{
[NotifyDataErrorInfo]
- public partial class SampleViewModel : ObservableObject
+ public partial class {|MVVMTK0028:SampleViewModel|} : ObservableObject
{
- [ObservableProperty]
- [Required]
- private string name;
}
}
""";
- VerifyGeneratedDiagnostics(source, "MVVMTK0006", "MVVMTK0028");
+ await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8);
}
[TestMethod]