Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add analyzer for [field: ObservableProperty] uses from auto-properties #735

Merged
merged 2 commits into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
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