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]