Skip to content

Commit 4600382

Browse files
authored
Merge pull request #532 from CommunityToolkit/dev/observable-property-field-reference-analyzer
Add analyzer for references to fields with [ObservableProperty]
2 parents aa4ff30 + 9c35dc0 commit 4600382

File tree

7 files changed

+142
-5
lines changed

7 files changed

+142
-5
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ MVVMTK0030 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
4040
MVVMTK0031 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0031
4141
MVVMTK0032 | CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0032
4242
MVVMTK0033 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0033
43+
MVVMTK0034 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0034

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@
4848
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservableValidatorValidateAllPropertiesGenerator.Execute.cs" />
4949
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.cs" />
5050
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
51-
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs" />
5251
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs" />
52+
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs" />
53+
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldReferenceForObservablePropertyFieldAnalyzer.cs" />
5354
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\UnsupportedCSharpLanguageVersionAnalyzer.cs" />
5455
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Suppressors\ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs" />
5556
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\DiagnosticDescriptors.cs" />

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ public override void Initialize(AnalysisContext context)
4646
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
4747
context.EnableConcurrentExecution();
4848

49-
// Defer the callback registration to when the compilation starts, so we can execute more
50-
// preliminary checks and skip registering any kind of symbol analysis at all if not needed.
5149
context.RegisterSymbolAction(static context =>
5250
{
5351
// We're looking for class declarations
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
using Microsoft.CodeAnalysis.Operations;
9+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
10+
11+
namespace CommunityToolkit.Mvvm.SourceGenerators;
12+
13+
/// <summary>
14+
/// A diagnostic analyzer that generates a warning when accessing a field instead of a generated observable property.
15+
/// </summary>
16+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
17+
public sealed class FieldReferenceForObservablePropertyFieldAnalyzer : DiagnosticAnalyzer
18+
{
19+
/// <inheritdoc/>
20+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(FieldReferenceForObservablePropertyFieldWarning);
21+
22+
/// <inheritdoc/>
23+
public override void Initialize(AnalysisContext context)
24+
{
25+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
26+
context.EnableConcurrentExecution();
27+
28+
context.RegisterOperationAction(static context =>
29+
{
30+
// We're only looking for references to fields that could potentially be observable properties
31+
if (context.Operation is not IFieldReferenceOperation { Field: IFieldSymbol { IsStatic: false, IsConst: false, IsImplicitlyDeclared: false, ContainingType: INamedTypeSymbol } fieldSymbol })
32+
{
33+
return;
34+
}
35+
36+
foreach (AttributeData attribute in fieldSymbol.GetAttributes())
37+
{
38+
// Look for the [ObservableProperty] attribute (there can only ever be one per field)
39+
if (attribute.AttributeClass is { Name: "ObservablePropertyAttribute" } attributeClass &&
40+
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol attributeSymbol &&
41+
SymbolEqualityComparer.Default.Equals(attributeClass, attributeSymbol))
42+
{
43+
// Emit a warning to redirect users to access the generated property instead
44+
context.ReportDiagnostic(Diagnostic.Create(FieldReferenceForObservablePropertyFieldWarning, context.Operation.Syntax.GetLocation(), fieldSymbol));
45+
46+
return;
47+
}
48+
}
49+
}, OperationKind.FieldReference);
50+
}
51+
}

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
using System.ComponentModel;
66
using Microsoft.CodeAnalysis;
7-
using Microsoft.CodeAnalysis.CSharp;
87

98
#pragma warning disable IDE0090 // Use 'new DiagnosticDescriptor(...)'
109

@@ -543,4 +542,20 @@ internal static class DiagnosticDescriptors
543542
"Classes with no base types should prefer inheriting from ObservableObject instead of using attributes to generate INotifyPropertyChanged code, as that will " +
544543
"reduce the binary size of the application (the attributes are only meant to support cases where the annotated types are already inheriting from a different type).",
545544
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0032");
545+
546+
/// <summary>
547+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a field with <c>[ObservableProperty]</c> is being directly referenced.
548+
/// <para>
549+
/// Format: <c>"The field {0} is annotated with [ObservableProperty] and should not be directly referenced (use the generated property instead)"</c>.
550+
/// </para>
551+
/// </summary>
552+
public static readonly DiagnosticDescriptor FieldReferenceForObservablePropertyFieldWarning = new DiagnosticDescriptor(
553+
id: "MVVMTK0034",
554+
title: "Invalid task scheduler exception flow option usage",
555+
messageFormat: "The field {0} is annotated with [ObservableProperty] and should not be directly referenced (use the generated property instead)",
556+
category: typeof(ObservablePropertyGenerator).FullName,
557+
defaultSeverity: DiagnosticSeverity.Warning,
558+
isEnabledByDefault: true,
559+
description: "Fields with [ObservableProperty] should not be directly referenced, and the generated properties should be used instead.",
560+
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0033");
546561
}

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,6 +1531,77 @@ public partial class {|MVVMTK0033:SampleViewModel|}
15311531
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<ClassUsingAttributeInsteadOfInheritanceAnalyzer>(source, LanguageVersion.CSharp8);
15321532
}
15331533

1534+
// See https://github.com/CommunityToolkit/dotnet/issues/518
1535+
[TestMethod]
1536+
public async Task FieldReferenceToFieldWithObservablePropertyAttribute_Assignment()
1537+
{
1538+
string source = """
1539+
using CommunityToolkit.Mvvm.ComponentModel;
1540+
1541+
partial class MyViewModel : ObservableObject
1542+
{
1543+
[ObservableProperty]
1544+
int number;
1545+
1546+
void M1()
1547+
{
1548+
{|MVVMTK0034:number|} = 1;
1549+
}
1550+
1551+
void M2()
1552+
{
1553+
{|MVVMTK0034:this.number|} = 1;
1554+
}
1555+
}
1556+
""";
1557+
1558+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<FieldReferenceForObservablePropertyFieldAnalyzer>(source, LanguageVersion.CSharp8);
1559+
}
1560+
1561+
// See https://github.com/CommunityToolkit/dotnet/issues/518
1562+
[TestMethod]
1563+
public async Task FieldReferenceToFieldWithObservablePropertyAttribute_Read()
1564+
{
1565+
string source = """
1566+
using CommunityToolkit.Mvvm.ComponentModel;
1567+
1568+
partial class MyViewModel : ObservableObject
1569+
{
1570+
[ObservableProperty]
1571+
int number;
1572+
1573+
void M()
1574+
{
1575+
var temp = {|MVVMTK0034:number|};
1576+
}
1577+
}
1578+
""";
1579+
1580+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<FieldReferenceForObservablePropertyFieldAnalyzer>(source, LanguageVersion.CSharp8);
1581+
}
1582+
1583+
// See https://github.com/CommunityToolkit/dotnet/issues/518
1584+
[TestMethod]
1585+
public async Task FieldReferenceToFieldWithObservablePropertyAttribute_Ref()
1586+
{
1587+
string source = """
1588+
using CommunityToolkit.Mvvm.ComponentModel;
1589+
1590+
partial class MyViewModel : ObservableObject
1591+
{
1592+
[ObservableProperty]
1593+
int number;
1594+
1595+
void M()
1596+
{
1597+
ref var x = ref {|MVVMTK0034:number|};
1598+
}
1599+
}
1600+
""";
1601+
1602+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<FieldReferenceForObservablePropertyFieldAnalyzer>(source, LanguageVersion.CSharp8);
1603+
}
1604+
15341605
/// <summary>
15351606
/// Verifies the diagnostic errors for a given analyzer, and that all available source generators can run successfully with the input source (including subsequent compilation).
15361607
/// </summary>

tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
using CommunityToolkit.Mvvm.Messaging.Messages;
2222
using Microsoft.VisualStudio.TestTools.UnitTesting;
2323

24-
#pragma warning disable MVVMTK0032, MVVMTK0033
24+
#pragma warning disable MVVMTK0032, MVVMTK0033, MVVMTK0034
2525

2626
namespace CommunityToolkit.Mvvm.UnitTests;
2727

0 commit comments

Comments
 (0)