Skip to content

Commit 2b914e6

Browse files
committed
Add 'InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer'
1 parent 5e87b7d commit 2b914e6

File tree

6 files changed

+132
-24
lines changed

6 files changed

+132
-24
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\InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs" />
4344
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs" />
4445
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidTargetObservablePropertyAttributeAnalyzer.cs" />
4546
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs" />

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

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,6 @@ public static bool TryGetInfo(
180180
// Check for special cases that are explicitly not allowed
181181
if (IsGeneratedPropertyInvalid(propertyName, GetPropertyType(memberSymbol)))
182182
{
183-
builder.Add(
184-
InvalidObservablePropertyError,
185-
memberSymbol,
186-
memberSymbol.ContainingType,
187-
memberSymbol.Name);
188-
189183
propertyInfo = null;
190184
diagnostics = builder.ToImmutable();
191185

@@ -427,7 +421,7 @@ private static bool IsTargetTypeValid(ISymbol memberSymbol, out bool shouldInvok
427421
/// <param name="propertyName">The property name.</param>
428422
/// <param name="propertyType">The property type.</param>
429423
/// <returns>Whether the generated property is invalid.</returns>
430-
private static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol propertyType)
424+
public static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol propertyType)
431425
{
432426
// If the generated property name is called "Property" and the type is either object or it is PropertyChangedEventArgs or
433427
// PropertyChangingEventArgs (or a type derived from either of those two types), consider it invalid. This is needed because
@@ -1493,7 +1487,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
14931487
/// </summary>
14941488
/// <param name="memberSymbol">The input <see cref="ISymbol"/> instance to process.</param>
14951489
/// <returns>The type of <paramref name="memberSymbol"/>.</returns>
1496-
private static ITypeSymbol GetPropertyType(ISymbol memberSymbol)
1490+
public static ITypeSymbol GetPropertyType(ISymbol memberSymbol)
14971491
{
14981492
// Check if the member is a property first
14991493
if (memberSymbol is IPropertySymbol propertySymbol)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 field or property with <c>[ObservableProperty]</c> is not valid (special cases)
16+
/// </summary>
17+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
18+
public sealed class InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer
19+
{
20+
/// <inheritdoc/>
21+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidObservablePropertyError);
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] and the event args we need
32+
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol ||
33+
context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangedEventArgs") is not INamedTypeSymbol propertyChangedEventArgsSymbol ||
34+
context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangingEventArgs") is not INamedTypeSymbol propertyChangingEventArgsSymbol)
35+
{
36+
return;
37+
}
38+
39+
context.RegisterSymbolAction(context =>
40+
{
41+
// Validate that we do have a field or a property
42+
if (context.Symbol is not (IFieldSymbol or IPropertySymbol))
43+
{
44+
return;
45+
}
46+
47+
// Ensure we do have the [ObservableProperty] attribute
48+
if (!context.Symbol.HasAttributeWithType(observablePropertySymbol))
49+
{
50+
return;
51+
}
52+
53+
ITypeSymbol propertyType = ObservablePropertyGenerator.Execute.GetPropertyType(context.Symbol);
54+
string propertyName = ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(context.Symbol);
55+
56+
// Same logic as 'IsGeneratedPropertyInvalid' in the generator
57+
if (propertyName == "Property")
58+
{
59+
// Check for collisions with the generated helpers and the property, only happens with these 3 types
60+
if (propertyType.SpecialType == SpecialType.System_Object ||
61+
propertyType.HasOrInheritsFromType(propertyChangedEventArgsSymbol) ||
62+
propertyType.HasOrInheritsFromType(propertyChangingEventArgsSymbol))
63+
{
64+
context.ReportDiagnostic(Diagnostic.Create(
65+
InvalidObservablePropertyError,
66+
context.Symbol.Locations.FirstOrDefault(),
67+
context.Symbol.Kind.ToFieldOrPropertyKeyword(),
68+
context.Symbol.ContainingType,
69+
context.Symbol.Name));
70+
}
71+
}
72+
}, SymbolKind.Field, SymbolKind.Property);
73+
});
74+
}
75+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -407,17 +407,17 @@ internal static class DiagnosticDescriptors
407407
/// <summary>
408408
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a generated property created with <c>[ObservableProperty]</c> would cause conflicts with other generated members.
409409
/// <para>
410-
/// Format: <c>"The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members"</c>.
410+
/// Format: <c>"The {0} {1}.{2} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members"</c>.
411411
/// </para>
412412
/// </summary>
413413
public static readonly DiagnosticDescriptor InvalidObservablePropertyError = new DiagnosticDescriptor(
414414
id: "MVVMTK0024",
415415
title: "Invalid generated property declaration",
416-
messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members",
416+
messageFormat: "The {0} {1}.{2} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members",
417417
category: typeof(ObservablePropertyGenerator).FullName,
418418
defaultSeverity: DiagnosticSeverity.Error,
419419
isEnabledByDefault: true,
420-
description: "The fields annotated with [ObservableProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.",
420+
description: "The fields and properties annotated with [ObservableProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.",
421421
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0024");
422422

423423
/// <summary>

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,25 @@ public static bool HasOrInheritsFromFullyQualifiedMetadataName(this ITypeSymbol
3333
return false;
3434
}
3535

36+
/// <summary>
37+
/// Checks whether or not a given <see cref="ITypeSymbol"/> has or inherits from a specified type.
38+
/// </summary>
39+
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
40+
/// <param name="baseTypeSymbol">The type to check for inheritance.</param>
41+
/// <returns>Whether or not <paramref name="typeSymbol"/> is or inherits from <paramref name="baseTypeSymbol"/>.</returns>
42+
public static bool HasOrInheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
43+
{
44+
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
45+
{
46+
if (SymbolEqualityComparer.Default.Equals(currentType, baseTypeSymbol))
47+
{
48+
return true;
49+
}
50+
}
51+
52+
return false;
53+
}
54+
3655
/// <summary>
3756
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits from a specified type.
3857
/// </summary>
@@ -60,7 +79,7 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS
6079
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits from a specified type.
6180
/// </summary>
6281
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
63-
/// <param name="baseTypeSymbol">The <see cref="ITypeSymbol"/> instane to check for inheritance from.</param>
82+
/// <param name="baseTypeSymbol">The <see cref="ITypeSymbol"/> instance to check for inheritance from.</param>
6483
/// <returns>Whether or not <paramref name="typeSymbol"/> inherits from <paramref name="baseTypeSymbol"/>.</returns>
6584
public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
6685
{

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

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,7 +1306,7 @@ private void GreetUser(object value)
13061306
}
13071307

13081308
[TestMethod]
1309-
public void InvalidObservablePropertyError_Object()
1309+
public async Task InvalidObservablePropertyError_Object()
13101310
{
13111311
string source = """
13121312
using CommunityToolkit.Mvvm.ComponentModel;
@@ -1316,16 +1316,35 @@ namespace MyApp
13161316
public partial class MyViewModel : ObservableObject
13171317
{
13181318
[ObservableProperty]
1319-
public object property;
1319+
public object {|MVVMTK0024:property|};
13201320
}
13211321
}
13221322
""";
13231323

1324-
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0024");
1324+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp9);
13251325
}
13261326

13271327
[TestMethod]
1328-
public void InvalidObservablePropertyError_PropertyChangingEventArgs()
1328+
public async Task InvalidObservablePropertyError_Object_WithProperty()
1329+
{
1330+
string source = """
1331+
using CommunityToolkit.Mvvm.ComponentModel;
1332+
1333+
namespace MyApp
1334+
{
1335+
public partial class MyViewModel : ObservableObject
1336+
{
1337+
[ObservableProperty]
1338+
public object {|MVVMTK0024:Property|} { get; set; }
1339+
}
1340+
}
1341+
""";
1342+
1343+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer>(source, LanguageVersion.Preview);
1344+
}
1345+
1346+
[TestMethod]
1347+
public async Task InvalidObservablePropertyError_PropertyChangingEventArgs()
13291348
{
13301349
string source = """
13311350
using System.ComponentModel;
@@ -1336,16 +1355,16 @@ namespace MyApp
13361355
public partial class MyViewModel : ObservableObject
13371356
{
13381357
[ObservableProperty]
1339-
public PropertyChangingEventArgs property;
1358+
public PropertyChangingEventArgs {|MVVMTK0024:property|};
13401359
}
13411360
}
13421361
""";
13431362

1344-
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0024");
1363+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp9);
13451364
}
13461365

13471366
[TestMethod]
1348-
public void InvalidObservablePropertyError_PropertyChangedEventArgs()
1367+
public async Task InvalidObservablePropertyError_PropertyChangedEventArgs()
13491368
{
13501369
string source = """
13511370
using System.ComponentModel;
@@ -1356,16 +1375,16 @@ namespace MyApp
13561375
public partial class MyViewModel : ObservableObject
13571376
{
13581377
[ObservableProperty]
1359-
public PropertyChangedEventArgs property;
1378+
public PropertyChangedEventArgs {|MVVMTK0024:property|};
13601379
}
13611380
}
13621381
""";
13631382

1364-
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0024");
1383+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp9);
13651384
}
13661385

13671386
[TestMethod]
1368-
public void InvalidObservablePropertyError_CustomTypeDerivedFromPropertyChangedEventArgs()
1387+
public async Task InvalidObservablePropertyError_CustomTypeDerivedFromPropertyChangedEventArgs()
13691388
{
13701389
string source = """
13711390
using System.ComponentModel;
@@ -1384,12 +1403,12 @@ public MyPropertyChangedEventArgs(string propertyName)
13841403
public partial class MyViewModel : ObservableObject
13851404
{
13861405
[ObservableProperty]
1387-
public MyPropertyChangedEventArgs property;
1406+
public MyPropertyChangedEventArgs {|MVVMTK0024:property|};
13881407
}
13891408
}
13901409
""";
13911410

1392-
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0024");
1411+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer>(source, LanguageVersion.CSharp9);
13931412
}
13941413

13951414
[TestMethod]

0 commit comments

Comments
 (0)