From 0b5dfb2d242382e61e03971313ecb3417b6924eb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 27 Oct 2024 15:13:05 -0700 Subject: [PATCH 1/3] Add 'InvalidTargetObservablePropertyAttributeAnalyzer' --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 7 -- ...rgetObservablePropertyAttributeAnalyzer.cs | 72 +++++++++++++++++++ .../Test_SourceGeneratorsDiagnostics.cs | 65 ++++++++++++++++- 4 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 988d87a15..103d7cfbb 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -40,6 +40,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 6dc483e6b..b8d0167ff 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -145,13 +145,6 @@ public static bool TryGetInfo( // Validate the target type if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging)) { - builder.Add( - InvalidContainingTypeForObservablePropertyMemberError, - memberSymbol, - memberSyntax.Kind().ToFieldOrPropertyKeyword(), - memberSymbol.ContainingType, - memberSymbol.Name); - propertyInfo = null; diagnostics = builder.ToImmutable(); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..bad2a67b9 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,72 @@ +// 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 field or property with [ObservableProperty] is not a valid target. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidTargetObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidContainingTypeForObservablePropertyMemberError); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the required symbols for the analyzer + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject") is not INamedTypeSymbol observableObjectSymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute") is not INamedTypeSymbol observableObjectAttributeSymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute") is not INamedTypeSymbol notifyPropertyChangedAttributeSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Validate that we do have a field or a property + if (context.Symbol is not (IFieldSymbol or IPropertySymbol)) + { + return; + } + + // Ensure we do have the [ObservableProperty] attribute + if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? attributeDataobservablePropertyAttribute)) + { + return; + } + + // Same logic as in 'IsTargetTypeValid' in the generator + bool isObservableObject = context.Symbol.ContainingType.InheritsFromType(observableObjectSymbol); + bool hasObservableObjectAttribute = context.Symbol.ContainingType.HasOrInheritsAttributeWithType(observableObjectAttributeSymbol); + bool hasINotifyPropertyChangedAttribute = context.Symbol.ContainingType.HasOrInheritsAttributeWithType(notifyPropertyChangedAttributeSymbol); + + // Emit the diagnostic if the target is not valid + if (!isObservableObject && !hasObservableObjectAttribute && !hasINotifyPropertyChangedAttribute) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidContainingTypeForObservablePropertyMemberError, + context.Symbol.Locations.FirstOrDefault(), + context.Symbol.Kind.ToFieldOrPropertyKeyword(), + context.Symbol.ContainingType, + context.Symbol.Name)); + } + }, SymbolKind.Field, SymbolKind.Property); + }); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 605651520..41d4dfb5b 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1006,24 +1006,85 @@ public partial class A } [TestMethod] - public void InvalidContainingTypeForObservablePropertyFieldError() + public async Task InvalidContainingTypeForObservableProperty_OnField_Warns() { string source = """ + using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp { public partial class MyViewModel : INotifyPropertyChanged + { + [ObservableProperty] + public int {|MVVMTK0019:number|}; + + public event PropertyChangedEventHandler PropertyChanged; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task InvalidContainingTypeForObservableProperty_OnField_InValidType_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject { [ObservableProperty] public int number; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task InvalidContainingTypeForObservableProperty_OnPartialProperty_Warns() + { + string source = """ + using System.ComponentModel; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : INotifyPropertyChanged + { + [ObservableProperty] + public int {|MVVMTK0019:Number|} { get; set; } public event PropertyChangedEventHandler PropertyChanged; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0019"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task InvalidContainingTypeForObservableProperty_OnPartialProperty_InValidType_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public int Number { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); } [TestMethod] From ead5b1163bc0421516c58a5aeff910c9d919a29c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 27 Oct 2024 15:23:13 -0700 Subject: [PATCH 2/3] Add 'PropertyNameCollisionObservablePropertyAttributeAnalyzer' --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 6 -- ...rgetObservablePropertyAttributeAnalyzer.cs | 2 +- ...sionObservablePropertyAttributeAnalyzer.cs | 63 +++++++++++++++++++ .../Test_SourceGeneratorsDiagnostics.cs | 45 ++++++++++++- 5 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 103d7cfbb..c76af85f1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -40,6 +40,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index b8d0167ff..64ff45bcb 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -166,12 +166,6 @@ public static bool TryGetInfo( // Check for name collisions (only for fields) if (fieldName == propertyName && memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) { - builder.Add( - ObservablePropertyNameCollisionError, - memberSymbol, - memberSymbol.ContainingType, - memberSymbol.Name); - propertyInfo = null; diagnostics = builder.ToImmutable(); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs index bad2a67b9..70c645227 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs @@ -46,7 +46,7 @@ public override void Initialize(AnalysisContext context) } // Ensure we do have the [ObservableProperty] attribute - if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? attributeDataobservablePropertyAttribute)) + if (!context.Symbol.HasAttributeWithType(observablePropertySymbol)) { return; } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..06894ac25 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,63 @@ +// 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 generated property from [ObservableProperty] would collide with the field name. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PropertyNameCollisionObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(ObservablePropertyNameCollisionError); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure we do have a valid field + if (context.Symbol is not IFieldSymbol fieldSymbol) + { + return; + } + + // We only care if the field has [ObservableProperty] + if (!fieldSymbol.HasAttributeWithType(observablePropertySymbol)) + { + return; + } + + // Emit the diagnostic if there is a name collision + if (fieldSymbol.Name == ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + ObservablePropertyNameCollisionError, + fieldSymbol.Locations.FirstOrDefault(), + fieldSymbol.ContainingType, + fieldSymbol.Name)); + } + }, SymbolKind.Field); + }); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 41d4dfb5b..cf6bf92dc 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -664,7 +664,7 @@ private async Task GreetUserAsync(User user) } [TestMethod] - public void NameCollisionForGeneratedObservableProperty() + public async Task NameCollisionForGeneratedObservableProperty_PascalCaseField_Warns() { string source = """ using CommunityToolkit.Mvvm.ComponentModel; @@ -674,12 +674,51 @@ namespace MyApp public partial class SampleViewModel : ObservableObject { [ObservableProperty] - private string Name; + private string {|MVVMTK0014:Name|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0014"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task NameCollisionForGeneratedObservableProperty_CamelCaseField_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + // Using C# 9 here because the generated code will emit [MemberNotNull] on the property setter, which requires C# 9 + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); + } + + [TestMethod] + public async Task NameCollisionForGeneratedObservableProperty_PascalCaseProperty_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string Name { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); } [TestMethod] From 143df8e1014d63309798684d302ee92c32d60898 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 27 Oct 2024 15:39:12 -0700 Subject: [PATCH 3/3] Add 'InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer' --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 10 +-- ...ertyObservablePropertyAttributeAnalyzer.cs | 75 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 6 +- .../Extensions/ITypeSymbolExtensions.cs | 21 +++++- .../Test_SourceGeneratorsDiagnostics.cs | 43 ++++++++--- 6 files changed, 132 insertions(+), 24 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index c76af85f1..86d4254a3 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -40,6 +40,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 64ff45bcb..ce3c5d98c 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -180,12 +180,6 @@ public static bool TryGetInfo( // Check for special cases that are explicitly not allowed if (IsGeneratedPropertyInvalid(propertyName, GetPropertyType(memberSymbol))) { - builder.Add( - InvalidObservablePropertyError, - memberSymbol, - memberSymbol.ContainingType, - memberSymbol.Name); - propertyInfo = null; diagnostics = builder.ToImmutable(); @@ -427,7 +421,7 @@ private static bool IsTargetTypeValid(ISymbol memberSymbol, out bool shouldInvok /// The property name. /// The property type. /// Whether the generated property is invalid. - private static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol propertyType) + public static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol propertyType) { // If the generated property name is called "Property" and the type is either object or it is PropertyChangedEventArgs or // PropertyChangingEventArgs (or a type derived from either of those two types), consider it invalid. This is needed because @@ -1493,7 +1487,7 @@ public static ImmutableArray GetOnPropertyChangeMethods /// /// The input instance to process. /// The type of . - private static ITypeSymbol GetPropertyType(ISymbol memberSymbol) + public static ITypeSymbol GetPropertyType(ISymbol memberSymbol) { // Check if the member is a property first if (memberSymbol is IPropertySymbol propertySymbol) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..e960cf47e --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,75 @@ +// 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 field or property with [ObservableProperty] is not valid (special cases) +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidObservablePropertyError); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] and the event args we need + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangedEventArgs") is not INamedTypeSymbol propertyChangedEventArgsSymbol || + context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangingEventArgs") is not INamedTypeSymbol propertyChangingEventArgsSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Validate that we do have a field or a property + if (context.Symbol is not (IFieldSymbol or IPropertySymbol)) + { + return; + } + + // Ensure we do have the [ObservableProperty] attribute + if (!context.Symbol.HasAttributeWithType(observablePropertySymbol)) + { + return; + } + + ITypeSymbol propertyType = ObservablePropertyGenerator.Execute.GetPropertyType(context.Symbol); + string propertyName = ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(context.Symbol); + + // Same logic as 'IsGeneratedPropertyInvalid' in the generator + if (propertyName == "Property") + { + // Check for collisions with the generated helpers and the property, only happens with these 3 types + if (propertyType.SpecialType == SpecialType.System_Object || + propertyType.HasOrInheritsFromType(propertyChangedEventArgsSymbol) || + propertyType.HasOrInheritsFromType(propertyChangingEventArgsSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyError, + context.Symbol.Locations.FirstOrDefault(), + context.Symbol.Kind.ToFieldOrPropertyKeyword(), + context.Symbol.ContainingType, + context.Symbol.Name)); + } + } + }, SymbolKind.Field, SymbolKind.Property); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 74986d3ce..6ac4c7950 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -407,17 +407,17 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when a generated property created with [ObservableProperty] would cause conflicts with other generated members. /// - /// Format: "The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members". + /// Format: "The {0} {1}.{2} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members". /// /// public static readonly DiagnosticDescriptor InvalidObservablePropertyError = new DiagnosticDescriptor( id: "MVVMTK0024", title: "Invalid generated property declaration", - messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members", + messageFormat: "The {0} {1}.{2} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "The fields annotated with [ObservableProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.", + description: "The fields and properties annotated with [ObservableProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0024"); /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index 6e976501b..b79f743b3 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -33,6 +33,25 @@ public static bool HasOrInheritsFromFullyQualifiedMetadataName(this ITypeSymbol return false; } + /// + /// Checks whether or not a given has or inherits from a specified type. + /// + /// The target instance to check. + /// The type to check for inheritance. + /// Whether or not is or inherits from . + public static bool HasOrInheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol) + { + for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(currentType, baseTypeSymbol)) + { + return true; + } + } + + return false; + } + /// /// Checks whether or not a given inherits from a specified type. /// @@ -60,7 +79,7 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS /// Checks whether or not a given inherits from a specified type. /// /// The target instance to check. - /// The instane to check for inheritance from. + /// The instance to check for inheritance from. /// Whether or not inherits from . public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol) { diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index cf6bf92dc..bef6d9c35 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1306,7 +1306,7 @@ private void GreetUser(object value) } [TestMethod] - public void InvalidObservablePropertyError_Object() + public async Task InvalidObservablePropertyError_Object() { string source = """ using CommunityToolkit.Mvvm.ComponentModel; @@ -1316,16 +1316,35 @@ namespace MyApp public partial class MyViewModel : ObservableObject { [ObservableProperty] - public object property; + public object {|MVVMTK0024:property|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } [TestMethod] - public void InvalidObservablePropertyError_PropertyChangingEventArgs() + public async Task InvalidObservablePropertyError_Object_WithProperty() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public object {|MVVMTK0024:Property|} { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidObservablePropertyError_PropertyChangingEventArgs() { string source = """ using System.ComponentModel; @@ -1336,16 +1355,16 @@ namespace MyApp public partial class MyViewModel : ObservableObject { [ObservableProperty] - public PropertyChangingEventArgs property; + public PropertyChangingEventArgs {|MVVMTK0024:property|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } [TestMethod] - public void InvalidObservablePropertyError_PropertyChangedEventArgs() + public async Task InvalidObservablePropertyError_PropertyChangedEventArgs() { string source = """ using System.ComponentModel; @@ -1356,16 +1375,16 @@ namespace MyApp public partial class MyViewModel : ObservableObject { [ObservableProperty] - public PropertyChangedEventArgs property; + public PropertyChangedEventArgs {|MVVMTK0024:property|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } [TestMethod] - public void InvalidObservablePropertyError_CustomTypeDerivedFromPropertyChangedEventArgs() + public async Task InvalidObservablePropertyError_CustomTypeDerivedFromPropertyChangedEventArgs() { string source = """ using System.ComponentModel; @@ -1384,12 +1403,12 @@ public MyPropertyChangedEventArgs(string propertyName) public partial class MyViewModel : ObservableObject { [ObservableProperty] - public MyPropertyChangedEventArgs property; + public MyPropertyChangedEventArgs {|MVVMTK0024:property|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } [TestMethod]