Skip to content

Commit

Permalink
Merge pull request #735 from CommunityToolkit/dev/field-targeted-obse…
Browse files Browse the repository at this point in the history
…rvableproperty

Add analyzer for [field: ObservableProperty] uses from auto-properties
  • Loading branch information
Sergio0694 authored Jul 22, 2023
2 parents 1d98dba + 4c8f27b commit e512411
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,11 @@ Rule ID | Category | Severity | Notes
MVVMTK0037 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0037
MVVMTK0038 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0038
MVVMTK0039 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0039

## Release 8.2.2

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
MVVMTK0040 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0040
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AsyncVoidReturningRelayCommandMethodAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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 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 an auto-property is using <c>[field: ObservableProperty]</c>.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(AutoPropertyBackingFieldObservableProperty);

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
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 =>
{
// Get the property symbol and the type symbol for the containing type
if (context.Symbol is not IPropertySymbol { ContainingType: INamedTypeSymbol typeSymbol } propertySymbol)
{
return;
}
foreach (ISymbol memberSymbol in typeSymbol.GetMembers())
{
// We're only looking for fields with an associated property
if (memberSymbol is not IFieldSymbol { AssociatedSymbol: IPropertySymbol associatedPropertySymbol })
{
continue;
}
// Check that this field is in fact the backing field for the target auto-property
if (!SymbolEqualityComparer.Default.Equals(associatedPropertySymbol, propertySymbol))
{
continue;
}
// If the field isn't using [ObservableProperty], this analyzer isn't applicable
if (!memberSymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? attributeData))
{
return;
}
// Report the diagnostic on the attribute location
context.ReportDiagnostic(Diagnostic.Create(
AutoPropertyBackingFieldObservableProperty,
attributeData.GetLocation(),
typeSymbol,
propertySymbol));
}
}, SymbolKind.Property);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -658,4 +658,20 @@ internal static class DiagnosticDescriptors
isEnabledByDefault: true,
description: "All asynchronous methods annotated with [RelayCommand] should return a Task type, to benefit from the additional support provided by AsyncRelayCommand and AsyncRelayCommand<T>.",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0039");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>[ObservableProperty]</c> is used on a generated field of an auto-property.
/// <para>
/// Format: <c>"The backing field for property {0}.{1} cannot be annotated with [ObservableProperty] (the attribute can only be used directly on fields, and the generator will then handle generating the corresponding property)"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor AutoPropertyBackingFieldObservableProperty = new DiagnosticDescriptor(
id: "MVVMTK0040",
title: "[ObservableProperty] on auto-property backing field",
messageFormat: "The backing field for property {0}.{1} cannot be annotated with [ObservableProperty] (the attribute can only be used directly on fields, and the generator will then handle generating the corresponding property)",
category: typeof(ObservablePropertyGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The backing fields of auto-properties cannot be annotated with [ObservableProperty] (the attribute can only be used directly on fields, and the generator will then handle generating the corresponding property).",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0040");
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ properties.Value.Value is T argumentValue &&
return false;
}

/// <summary>
/// Tries to get the location of the input <see cref="AttributeData"/> instance.
/// </summary>
/// <param name="attributeData">The input <see cref="AttributeData"/> instance to get the location for.</param>
/// <returns>The resulting location for <paramref name="attributeData"/>, if a syntax reference is available.</returns>
public static Location? GetLocation(this AttributeData attributeData)
{
if (attributeData.ApplicationSyntaxReference is { } syntaxReference)
{
return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span);
}

return null;
}

/// <summary>
/// Gets a given named argument value from an <see cref="AttributeData"/> instance, or a fallback value.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#if !ROSLYN_4_3_1_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
using Microsoft.CodeAnalysis;

namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
Expand Down Expand Up @@ -65,21 +63,37 @@ public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbo
}

/// <summary>
/// Checks whether or not a given symbol has an attribute with the specified fully qualified metadata name.
/// Checks whether or not a given symbol has an attribute with the specified type.
/// </summary>
/// <param name="symbol">The input <see cref="ISymbol"/> instance to check.</param>
/// <param name="typeSymbol">The <see cref="ITypeSymbol"/> instance for the attribute type to look for.</param>
/// <returns>Whether or not <paramref name="symbol"/> has an attribute with the specified type.</returns>
public static bool HasAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol)
{
return TryGetAttributeWithType(symbol, typeSymbol, out _);
}

/// <summary>
/// Tries to get an attribute with the specified type.
/// </summary>
/// <param name="symbol">The input <see cref="ISymbol"/> instance to check.</param>
/// <param name="typeSymbol">The <see cref="ITypeSymbol"/> instance for the attribute type to look for.</param>
/// <param name="attributeData">The resulting attribute, if it was found.</param>
/// <returns>Whether or not <paramref name="symbol"/> has an attribute with the specified type.</returns>
public static bool TryGetAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol, [NotNullWhen(true)] out AttributeData? attributeData)
{
foreach (AttributeData attribute in symbol.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol))
{
attributeData = attribute;

return true;
}
}

attributeData = null;

return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1820,6 +1820,66 @@ public partial class MyViewModel
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<AsyncVoidReturningRelayCommandMethodAnalyzer>(source, LanguageVersion.CSharp8);
}

[TestMethod]
public async Task FieldTargetedObservablePropertyAttribute_InstanceAutoProperty()
{
string source = """
using CommunityToolkit.Mvvm.ComponentModel;

namespace MyApp
{
public partial class SampleViewModel : ObservableObject
{
[field: {|MVVMTK0040:ObservableProperty|}]
public string Name { get; set; }
}
}
""";

await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp8);
}

[TestMethod]
public async Task FieldTargetedObservablePropertyAttribute_StaticAutoProperty()
{
string source = """
using CommunityToolkit.Mvvm.ComponentModel;

namespace MyApp
{
public partial class SampleViewModel : ObservableObject
{
[field: {|MVVMTK0040:ObservableProperty|}]
public static string Name { get; set; }
}
}
""";

await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp8);
}

[TestMethod]
public async Task FieldTargetedObservablePropertyAttribute_RecordPrimaryConstructorParameter()
{
string source = """
using CommunityToolkit.Mvvm.ComponentModel;

namespace MyApp
{
public partial record SampleViewModel([field: {|MVVMTK0040:ObservableProperty|}] string Name);
}

namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit
{
}
}
""";

await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp9);
}

/// <summary>
/// Verifies the diagnostic errors for a given analyzer, and that all available source generators can run successfully with the input source (including subsequent compilation).
/// </summary>
Expand Down

0 comments on commit e512411

Please sign in to comment.