Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add 'InvalidTargetObservablePropertyAttributeAnalyzer'
  • Loading branch information
Sergio0694 committed Oct 28, 2024
commit 0b5dfb2d242382e61e03971313ecb3417b6924eb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AsyncVoidReturningRelayCommandMethodAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidTargetObservablePropertyAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A diagnostic analyzer that generates an error when a field or property with <c>[ObservableProperty]</c> is not a valid target.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class InvalidTargetObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidContainingTypeForObservablePropertyMemberError);

/// <inheritdoc/>
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);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<InvalidTargetObservablePropertyAttributeAnalyzer>(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<InvalidTargetObservablePropertyAttributeAnalyzer>(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<ObservablePropertyGenerator>(source, "MVVMTK0019");
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<InvalidTargetObservablePropertyAttributeAnalyzer>(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<InvalidTargetObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp8);
}

[TestMethod]
Expand Down