Skip to content

Commit e512411

Browse files
authored
Merge pull request #735 from CommunityToolkit/dev/field-targeted-observableproperty
Add analyzer for [field: ObservableProperty] uses from auto-properties
2 parents 1d98dba + 4c8f27b commit e512411

File tree

7 files changed

+191
-3
lines changed

7 files changed

+191
-3
lines changed

src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,11 @@ Rule ID | Category | Severity | Notes
6767
MVVMTK0037 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0037
6868
MVVMTK0038 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0038
6969
MVVMTK0039 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0039
70+
71+
## Release 8.2.2
72+
73+
### New Rules
74+
75+
Rule ID | Category | Severity | Notes
76+
--------|----------|----------|-------
77+
MVVMTK0040 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0040

src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
4242
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AsyncVoidReturningRelayCommandMethodAnalyzer.cs" />
4343
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs" />
44+
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs" />
4445
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs" />
4546
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs" />
4647
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Immutable;
6+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
10+
11+
namespace CommunityToolkit.Mvvm.SourceGenerators;
12+
13+
/// <summary>
14+
/// A diagnostic analyzer that generates an error when an auto-property is using <c>[field: ObservableProperty]</c>.
15+
/// </summary>
16+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
17+
public sealed class AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer
18+
{
19+
/// <inheritdoc/>
20+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(AutoPropertyBackingFieldObservableProperty);
21+
22+
/// <inheritdoc/>
23+
public override void Initialize(AnalysisContext context)
24+
{
25+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
26+
context.EnableConcurrentExecution();
27+
28+
context.RegisterCompilationStartAction(static context =>
29+
{
30+
// Get the symbol for [ObservableProperty]
31+
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol)
32+
{
33+
return;
34+
}
35+
36+
context.RegisterSymbolAction(context =>
37+
{
38+
// Get the property symbol and the type symbol for the containing type
39+
if (context.Symbol is not IPropertySymbol { ContainingType: INamedTypeSymbol typeSymbol } propertySymbol)
40+
{
41+
return;
42+
}
43+
44+
foreach (ISymbol memberSymbol in typeSymbol.GetMembers())
45+
{
46+
// We're only looking for fields with an associated property
47+
if (memberSymbol is not IFieldSymbol { AssociatedSymbol: IPropertySymbol associatedPropertySymbol })
48+
{
49+
continue;
50+
}
51+
52+
// Check that this field is in fact the backing field for the target auto-property
53+
if (!SymbolEqualityComparer.Default.Equals(associatedPropertySymbol, propertySymbol))
54+
{
55+
continue;
56+
}
57+
58+
// If the field isn't using [ObservableProperty], this analyzer isn't applicable
59+
if (!memberSymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? attributeData))
60+
{
61+
return;
62+
}
63+
64+
// Report the diagnostic on the attribute location
65+
context.ReportDiagnostic(Diagnostic.Create(
66+
AutoPropertyBackingFieldObservableProperty,
67+
attributeData.GetLocation(),
68+
typeSymbol,
69+
propertySymbol));
70+
}
71+
}, SymbolKind.Property);
72+
});
73+
}
74+
}

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,4 +658,20 @@ internal static class DiagnosticDescriptors
658658
isEnabledByDefault: true,
659659
description: "All asynchronous methods annotated with [RelayCommand] should return a Task type, to benefit from the additional support provided by AsyncRelayCommand and AsyncRelayCommand<T>.",
660660
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0039");
661+
662+
/// <summary>
663+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>[ObservableProperty]</c> is used on a generated field of an auto-property.
664+
/// <para>
665+
/// 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>.
666+
/// </para>
667+
/// </summary>
668+
public static readonly DiagnosticDescriptor AutoPropertyBackingFieldObservableProperty = new DiagnosticDescriptor(
669+
id: "MVVMTK0040",
670+
title: "[ObservableProperty] on auto-property backing field",
671+
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)",
672+
category: typeof(ObservablePropertyGenerator).FullName,
673+
defaultSeverity: DiagnosticSeverity.Error,
674+
isEnabledByDefault: true,
675+
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).",
676+
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0040");
661677
}

src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ properties.Value.Value is T argumentValue &&
3838
return false;
3939
}
4040

41+
/// <summary>
42+
/// Tries to get the location of the input <see cref="AttributeData"/> instance.
43+
/// </summary>
44+
/// <param name="attributeData">The input <see cref="AttributeData"/> instance to get the location for.</param>
45+
/// <returns>The resulting location for <paramref name="attributeData"/>, if a syntax reference is available.</returns>
46+
public static Location? GetLocation(this AttributeData attributeData)
47+
{
48+
if (attributeData.ApplicationSyntaxReference is { } syntaxReference)
49+
{
50+
return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span);
51+
}
52+
53+
return null;
54+
}
55+
4156
/// <summary>
4257
/// Gets a given named argument value from an <see cref="AttributeData"/> instance, or a fallback value.
4358
/// </summary>

src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
#if !ROSLYN_4_3_1_OR_GREATER
65
using System.Diagnostics.CodeAnalysis;
7-
#endif
86
using Microsoft.CodeAnalysis;
97

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

6765
/// <summary>
68-
/// Checks whether or not a given symbol has an attribute with the specified fully qualified metadata name.
66+
/// Checks whether or not a given symbol has an attribute with the specified type.
6967
/// </summary>
7068
/// <param name="symbol">The input <see cref="ISymbol"/> instance to check.</param>
7169
/// <param name="typeSymbol">The <see cref="ITypeSymbol"/> instance for the attribute type to look for.</param>
7270
/// <returns>Whether or not <paramref name="symbol"/> has an attribute with the specified type.</returns>
7371
public static bool HasAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol)
72+
{
73+
return TryGetAttributeWithType(symbol, typeSymbol, out _);
74+
}
75+
76+
/// <summary>
77+
/// Tries to get an attribute with the specified type.
78+
/// </summary>
79+
/// <param name="symbol">The input <see cref="ISymbol"/> instance to check.</param>
80+
/// <param name="typeSymbol">The <see cref="ITypeSymbol"/> instance for the attribute type to look for.</param>
81+
/// <param name="attributeData">The resulting attribute, if it was found.</param>
82+
/// <returns>Whether or not <paramref name="symbol"/> has an attribute with the specified type.</returns>
83+
public static bool TryGetAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol, [NotNullWhen(true)] out AttributeData? attributeData)
7484
{
7585
foreach (AttributeData attribute in symbol.GetAttributes())
7686
{
7787
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol))
7888
{
89+
attributeData = attribute;
90+
7991
return true;
8092
}
8193
}
8294

95+
attributeData = null;
96+
8397
return false;
8498
}
8599

tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,66 @@ public partial class MyViewModel
18201820
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<AsyncVoidReturningRelayCommandMethodAnalyzer>(source, LanguageVersion.CSharp8);
18211821
}
18221822

1823+
[TestMethod]
1824+
public async Task FieldTargetedObservablePropertyAttribute_InstanceAutoProperty()
1825+
{
1826+
string source = """
1827+
using CommunityToolkit.Mvvm.ComponentModel;
1828+
1829+
namespace MyApp
1830+
{
1831+
public partial class SampleViewModel : ObservableObject
1832+
{
1833+
[field: {|MVVMTK0040:ObservableProperty|}]
1834+
public string Name { get; set; }
1835+
}
1836+
}
1837+
""";
1838+
1839+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp8);
1840+
}
1841+
1842+
[TestMethod]
1843+
public async Task FieldTargetedObservablePropertyAttribute_StaticAutoProperty()
1844+
{
1845+
string source = """
1846+
using CommunityToolkit.Mvvm.ComponentModel;
1847+
1848+
namespace MyApp
1849+
{
1850+
public partial class SampleViewModel : ObservableObject
1851+
{
1852+
[field: {|MVVMTK0040:ObservableProperty|}]
1853+
public static string Name { get; set; }
1854+
}
1855+
}
1856+
""";
1857+
1858+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp8);
1859+
}
1860+
1861+
[TestMethod]
1862+
public async Task FieldTargetedObservablePropertyAttribute_RecordPrimaryConstructorParameter()
1863+
{
1864+
string source = """
1865+
using CommunityToolkit.Mvvm.ComponentModel;
1866+
1867+
namespace MyApp
1868+
{
1869+
public partial record SampleViewModel([field: {|MVVMTK0040:ObservableProperty|}] string Name);
1870+
}
1871+
1872+
namespace System.Runtime.CompilerServices
1873+
{
1874+
internal static class IsExternalInit
1875+
{
1876+
}
1877+
}
1878+
""";
1879+
1880+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp9);
1881+
}
1882+
18231883
/// <summary>
18241884
/// Verifies the diagnostic errors for a given analyzer, and that all available source generators can run successfully with the input source (including subsequent compilation).
18251885
/// </summary>

0 commit comments

Comments
 (0)