Skip to content

Commit 5e87b7d

Browse files
committed
Add 'PropertyNameCollisionObservablePropertyAttributeAnalyzer'
1 parent 9299a57 commit 5e87b7d

File tree

5 files changed

+107
-10
lines changed

5 files changed

+107
-10
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.cs" />
4141
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
4242
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AsyncVoidReturningRelayCommandMethodAnalyzer.cs" />
43+
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs" />
4344
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidTargetObservablePropertyAttributeAnalyzer.cs" />
4445
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs" />
4546
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs" />

src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,6 @@ public static bool TryGetInfo(
166166
// Check for name collisions (only for fields)
167167
if (fieldName == propertyName && memberSyntax.IsKind(SyntaxKind.FieldDeclaration))
168168
{
169-
builder.Add(
170-
ObservablePropertyNameCollisionError,
171-
memberSymbol,
172-
memberSymbol.ContainingType,
173-
memberSymbol.Name);
174-
175169
propertyInfo = null;
176170
diagnostics = builder.ToImmutable();
177171

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public override void Initialize(AnalysisContext context)
4646
}
4747

4848
// Ensure we do have the [ObservableProperty] attribute
49-
if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? attributeDataobservablePropertyAttribute))
49+
if (!context.Symbol.HasAttributeWithType(observablePropertySymbol))
5050
{
5151
return;
5252
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 System.Linq;
7+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.Diagnostics;
10+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
11+
12+
namespace CommunityToolkit.Mvvm.SourceGenerators;
13+
14+
/// <summary>
15+
/// A diagnostic analyzer that generates an error when a generated property from <c>[ObservableProperty]</c> would collide with the field name.
16+
/// </summary>
17+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
18+
public sealed class PropertyNameCollisionObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer
19+
{
20+
/// <inheritdoc/>
21+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(ObservablePropertyNameCollisionError);
22+
23+
/// <inheritdoc/>
24+
public override void Initialize(AnalysisContext context)
25+
{
26+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
27+
context.EnableConcurrentExecution();
28+
29+
context.RegisterCompilationStartAction(static context =>
30+
{
31+
// Get the symbol for [ObservableProperty]
32+
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol)
33+
{
34+
return;
35+
}
36+
37+
context.RegisterSymbolAction(context =>
38+
{
39+
// Ensure we do have a valid field
40+
if (context.Symbol is not IFieldSymbol fieldSymbol)
41+
{
42+
return;
43+
}
44+
45+
// We only care if the field has [ObservableProperty]
46+
if (!fieldSymbol.HasAttributeWithType(observablePropertySymbol))
47+
{
48+
return;
49+
}
50+
51+
// Emit the diagnostic if there is a name collision
52+
if (fieldSymbol.Name == ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol))
53+
{
54+
context.ReportDiagnostic(Diagnostic.Create(
55+
ObservablePropertyNameCollisionError,
56+
fieldSymbol.Locations.FirstOrDefault(),
57+
fieldSymbol.ContainingType,
58+
fieldSymbol.Name));
59+
}
60+
}, SymbolKind.Field);
61+
});
62+
}
63+
}

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

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ private async Task GreetUserAsync(User user)
664664
}
665665

666666
[TestMethod]
667-
public void NameCollisionForGeneratedObservableProperty()
667+
public async Task NameCollisionForGeneratedObservableProperty_PascalCaseField_Warns()
668668
{
669669
string source = """
670670
using CommunityToolkit.Mvvm.ComponentModel;
@@ -674,12 +674,51 @@ namespace MyApp
674674
public partial class SampleViewModel : ObservableObject
675675
{
676676
[ObservableProperty]
677-
private string Name;
677+
private string {|MVVMTK0014:Name|};
678678
}
679679
}
680680
""";
681681

682-
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0014");
682+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<PropertyNameCollisionObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp8);
683+
}
684+
685+
[TestMethod]
686+
public async Task NameCollisionForGeneratedObservableProperty_CamelCaseField_DoesNotWarn()
687+
{
688+
string source = """
689+
using CommunityToolkit.Mvvm.ComponentModel;
690+
691+
namespace MyApp
692+
{
693+
public partial class SampleViewModel : ObservableObject
694+
{
695+
[ObservableProperty]
696+
private string name;
697+
}
698+
}
699+
""";
700+
701+
// Using C# 9 here because the generated code will emit [MemberNotNull] on the property setter, which requires C# 9
702+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<PropertyNameCollisionObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp9);
703+
}
704+
705+
[TestMethod]
706+
public async Task NameCollisionForGeneratedObservableProperty_PascalCaseProperty_DoesNotWarn()
707+
{
708+
string source = """
709+
using CommunityToolkit.Mvvm.ComponentModel;
710+
711+
namespace MyApp
712+
{
713+
public partial class SampleViewModel : ObservableObject
714+
{
715+
[ObservableProperty]
716+
private string Name { get; set; }
717+
}
718+
}
719+
""";
720+
721+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<PropertyNameCollisionObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp8);
683722
}
684723

685724
[TestMethod]

0 commit comments

Comments
 (0)