1
0
mirror of https://github.com/chylex/.NET-Community-Toolkit.git synced 2025-02-24 15:46:02 +01:00

Add diagnostics for invalid [ObservableProperty] containing type

This commit is contained in:
Sergio Pedri 2022-03-13 19:36:11 +01:00
parent 914a7aca26
commit b632429c15
6 changed files with 85 additions and 37 deletions
CommunityToolkit.Mvvm.SourceGenerators
CommunityToolkit.Mvvm/ComponentModel/Attributes
tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests

View File

@ -23,3 +23,4 @@ ### New Rules
MVVMTK0016 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
MVVMTK0017 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
MVVMTK0018 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
MVVMTK0019 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error

View File

@ -34,7 +34,7 @@ public INotifyPropertyChangedGenerator()
{
static INotifyPropertyChangedInfo GetInfo(INamedTypeSymbol typeSymbol, AttributeData attributeData)
{
bool includeAdditionalHelperMethods = attributeData.GetNamedArgument<bool>("IncludeAdditionalHelperMethods", true);
bool includeAdditionalHelperMethods = attributeData.GetNamedArgument("IncludeAdditionalHelperMethods", true);
return new(includeAdditionalHelperMethods);
}

View File

@ -34,11 +34,19 @@ internal static class Execute
{
ImmutableArray<Diagnostic>.Builder builder = ImmutableArray.CreateBuilder<Diagnostic>();
// Check whether the containing type implements INotifyPropertyChanging and whether it inherits from ObservableValidator
bool isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObject");
bool isObservableValidator = fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator");
bool isNotifyPropertyChanging = fieldSymbol.ContainingType.AllInterfaces.Any(static i => i.HasFullyQualifiedName("global::System.ComponentModel.INotifyPropertyChanging"));
bool hasObservableObjectAttribute = fieldSymbol.ContainingType.GetAttributes().Any(static a => a.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute") == true);
// Validate the target type
if (!IsTargetTypeValid(fieldSymbol, out bool shouldInvokeOnPropertyChanging))
{
builder.Add(
InvalidContainingTypeForObservablePropertyFieldError,
fieldSymbol,
fieldSymbol.ContainingType,
fieldSymbol.Name);
diagnostics = builder.ToImmutable();
return null;
}
// Get the property type and name
string typeName = fieldSymbol.Type.GetFullyQualifiedName();
@ -69,7 +77,7 @@ internal static class Execute
ImmutableArray<AttributeInfo>.Builder validationAttributes = ImmutableArray.CreateBuilder<AttributeInfo>();
// Track the property changing event for the property, if the type supports it
if (isObservableObject || isNotifyPropertyChanging || hasObservableObjectAttribute)
if (shouldInvokeOnPropertyChanging)
{
propertyChangingNames.Add(propertyName);
}
@ -96,7 +104,7 @@ internal static class Execute
// Log the diagnostics if needed
if (validationAttributes.Count > 0 &&
!isObservableValidator)
!fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
{
builder.Add(
MissingObservableValidatorInheritanceError,
@ -124,6 +132,30 @@ internal static class Execute
validationAttributes.ToImmutable());
}
/// <summary>
/// Validates the containing type for a given field being annotated.
/// </summary>
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
/// <param name="shouldInvokeOnPropertyChanging">Whether or not property changing events should also be raised.</param>
/// <returns>Whether or not the containing type for <paramref name="fieldSymbol"/> is valid.</returns>
private static bool IsTargetTypeValid(
IFieldSymbol fieldSymbol,
out bool shouldInvokeOnPropertyChanging)
{
// The [ObservableProperty] attribute can only be used in types that are known to expose the necessary OnPropertyChanged and OnPropertyChanging methods.
// That means that the containing type for the field needs to match one of the following conditions:
// - It inherits from ObservableObject (in which case it also implements INotifyPropertyChanging).
// - It has the [ObservableObject] attribute (on itself or any of its base types).
// - It has the [INotifyPropertyChanged] attribute (on itself or any of its base types).
bool isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObject");
bool hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute");
bool hasINotifyPropertyChangedAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute");
shouldInvokeOnPropertyChanging = isObservableObject || hasObservableObjectAttribute;
return isObservableObject || hasObservableObjectAttribute || hasINotifyPropertyChangedAttribute;
}
/// <summary>
/// Tries to gather dependent properties from the given attribute.
/// </summary>

View File

@ -233,7 +233,7 @@ internal static class DiagnosticDescriptors
category: typeof(ObservablePropertyGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: $"The name of fields annotated with [ObservableProperty] should use \"lowerCamel\", \"_lowerCamel\" or \"m_lowerCamel\" pattern to avoid collisions with the generated properties.",
description: "The name of fields annotated with [ObservableProperty] should use \"lowerCamel\", \"_lowerCamel\" or \"m_lowerCamel\" pattern to avoid collisions with the generated properties.",
helpLinkUri: "https://aka.ms/mvvmtoolkit");
/// <summary>
@ -281,7 +281,7 @@ internal static class DiagnosticDescriptors
category: typeof(INotifyPropertyChangedGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: $"Cannot apply [INotifyPropertyChanged] to a type that already has this attribute or [ObservableObject] applied to it (including base types).",
description: "Cannot apply [INotifyPropertyChanged] to a type that already has this attribute or [ObservableObject] applied to it (including base types).",
helpLinkUri: "https://aka.ms/mvvmtoolkit");
/// <summary>
@ -297,6 +297,22 @@ internal static class DiagnosticDescriptors
category: typeof(ObservableObjectGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: $"Cannot apply [ObservableObject] to a type that already has this attribute or [INotifyPropertyChanged] applied to it (including base types).",
description: "Cannot apply [ObservableObject] to a type that already has this attribute or [INotifyPropertyChanged] applied to it (including base types).",
helpLinkUri: "https://aka.ms/mvvmtoolkit");
/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>[ObservableProperty]</c> is applied to a field in an invalid type.
/// <para>
/// Format: <c>"The field {0}.{1} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor InvalidContainingTypeForObservablePropertyFieldError = new DiagnosticDescriptor(
id: "MVVMTK0019",
title: $"Invalid containing type for [ObservableProperty] field",
messageFormat: $"The field {{0}}.{{1}} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]",
category: typeof(ObservablePropertyGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Fields annotated with [ObservableProperty] must be contained in a type that inherits from ObservableObject or that is annotated with [ObservableObject] or [INotifyPropertyChanged] (including base types).",
helpLinkUri: "https://aka.ms/mvvmtoolkit");
}

View File

@ -10,10 +10,11 @@ namespace CommunityToolkit.Mvvm.ComponentModel;
/// <summary>
/// An attribute that indicates that a given field should be wrapped by a generated observable property.
/// In order to use this attribute, the containing type has to implement the <see cref="INotifyPropertyChanged"/> interface
/// and expose a method with the same signature as <see cref="ObservableObject.OnPropertyChanged(string?)"/>. If the containing
/// type also implements the <see cref="INotifyPropertyChanging"/> interface and exposes a method with the same signature as
/// <see cref="ObservableObject.OnPropertyChanging(string?)"/>, then this method will be invoked as well by the property setter.
/// In order to use this attribute, the containing type has to inherit from <see cref="ObservableObject"/>, or it
/// must be using <see cref="ObservableObjectAttribute"/> or <see cref="INotifyPropertyChangedAttribute"/>.
/// If the containing type also implements the <see cref="INotifyPropertyChanging"/> (that is, if it either inherits from
/// <see cref="ObservableObject"/> or is using <see cref="ObservableObjectAttribute"/>), then the generated code will
/// also invoke <see cref="ObservableObject.OnPropertyChanging(PropertyChangingEventArgs)"/> to signal that event.
/// <para>
/// This attribute can be used as follows:
/// <code>

View File

@ -620,7 +620,6 @@ private async Task GreetUserAsync(User user)
public void NameCollisionForGeneratedObservableProperty()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MyApp
@ -639,7 +638,6 @@ public partial class SampleViewModel : ObservableObject
public void AlsoNotifyChangeForInvalidTargetError_Null()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MyApp
@ -659,7 +657,6 @@ public partial class SampleViewModel : ObservableObject
public void AlsoNotifyChangeForInvalidTargetError_SamePropertyAsGeneratedOneFromSelf()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MyApp
@ -679,7 +676,6 @@ public partial class SampleViewModel : ObservableObject
public void AlsoNotifyChangeForInvalidTargetError_Missing()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MyApp
@ -699,7 +695,6 @@ public partial class SampleViewModel : ObservableObject
public void AlsoNotifyChangeForInvalidTargetError_InvalidType()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MyApp
@ -723,7 +718,6 @@ public void Foo()
public void AlsoNotifyCanExecuteForInvalidTargetError_Null()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MyApp
@ -743,7 +737,6 @@ public partial class SampleViewModel : ObservableObject
public void AlsoNotifyCanExecuteForInvalidTargetError_Missing()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MyApp
@ -763,7 +756,6 @@ public partial class SampleViewModel : ObservableObject
public void AlsoNotifyCanExecuteForInvalidTargetError_InvalidMemberType()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MyApp
@ -787,7 +779,6 @@ public void Foo()
public void AlsoNotifyCanExecuteForInvalidTargetError_InvalidPropertyType()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MyApp
@ -809,7 +800,6 @@ public partial class SampleViewModel : ObservableObject
public void AlsoNotifyCanExecuteForInvalidTargetError_InvalidCommandType()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@ -832,9 +822,7 @@ public partial class SampleViewModel : ObservableObject
public void InvalidAttributeCombinationForINotifyPropertyChangedAttributeError_InheritingINotifyPropertyChangedAttribute()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MyApp
{
@ -856,9 +844,7 @@ public partial class B : A
public void InvalidAttributeCombinationForINotifyPropertyChangedAttributeError_InheritingObservableObjectAttribute()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MyApp
{
@ -880,9 +866,7 @@ public partial class B : A
public void InvalidAttributeCombinationForINotifyPropertyChangedAttributeError_WithAlsoObservableObjectAttribute()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MyApp
{
@ -900,9 +884,7 @@ public partial class A
public void InvalidAttributeCombinationForObservableObjectAttributeError_InheritingINotifyPropertyChangedAttribute()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MyApp
{
@ -924,9 +906,7 @@ public partial class B : A
public void InvalidAttributeCombinationForObservableObjectAttributeError_InheritingObservableObjectAttribute()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MyApp
{
@ -948,9 +928,7 @@ public partial class B : A
public void InvalidAttributeCombinationForObservableObjectAttributeError_WithAlsoINotifyPropertyChangedAttribute()
{
string source = @"
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MyApp
{
@ -964,6 +942,26 @@ public partial class A
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0018");
}
[TestMethod]
public void InvalidContainingTypeForObservablePropertyFieldError()
{
string source = @"
using CommunityToolkit.Mvvm.ComponentModel;
namespace MyApp
{
public partial class MyViewModel : INotifyPropertyChanged
{
[ObservableProperty]
public int number;
public event PropertyChangedEventHandler PropertyChanged;
}
}";
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0019");
}
/// <summary>
/// Verifies the output of a source generator.
/// </summary>