Skip to content

Commit 96f3fad

Browse files
authored
Merge pull request #581 from CommunityToolkit/dev/no-symbols-in-ivp
Move two more diagnostics to analyzers
2 parents f9ee156 + 1690599 commit 96f3fad

File tree

8 files changed

+190
-83
lines changed

8 files changed

+190
-83
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservableValidatorValidateAllPropertiesGenerator.Execute.cs" />
4040
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.cs" />
4141
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
42+
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs" />
43+
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs" />
4244
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs" />
4345
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs" />
4446
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldReferenceForObservablePropertyFieldAnalyzer.cs" />

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

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -555,26 +555,6 @@ private static bool TryGetIsNotifyingRecipients(
555555
return false;
556556
}
557557

558-
/// <summary>
559-
/// Checks whether a given type using <c>[NotifyPropertyChangedRecipients]</c> is valid and creates a <see cref="Diagnostic"/> if not.
560-
/// </summary>
561-
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
562-
/// <returns>The <see cref="Diagnostic"/> for <paramref name="typeSymbol"/>, if not a valid type.</returns>
563-
public static Diagnostic? GetIsNotifyingRecipientsDiagnosticForType(INamedTypeSymbol typeSymbol)
564-
{
565-
// If the containing type is valid, track it
566-
if (!typeSymbol.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") &&
567-
!typeSymbol.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute"))
568-
{
569-
return Diagnostic.Create(
570-
InvalidTypeForNotifyPropertyChangedRecipientsError,
571-
typeSymbol.Locations.FirstOrDefault(),
572-
typeSymbol);
573-
}
574-
575-
return null;
576-
}
577-
578558
/// <summary>
579559
/// Checks whether a given generated property should also validate its value.
580560
/// </summary>
@@ -657,25 +637,6 @@ private static bool TryGetNotifyDataErrorInfo(
657637
return false;
658638
}
659639

660-
/// <summary>
661-
/// Checks whether a given type using <c>[NotifyDataErrorInfo]</c> is valid and creates a <see cref="Diagnostic"/> if not.
662-
/// </summary>
663-
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
664-
/// <returns>The <see cref="Diagnostic"/> for <paramref name="typeSymbol"/>, if not a valid type.</returns>
665-
public static Diagnostic? GetIsNotifyDataErrorInfoDiagnosticForType(INamedTypeSymbol typeSymbol)
666-
{
667-
// If the containing type is valid, track it
668-
if (!typeSymbol.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
669-
{
670-
return Diagnostic.Create(
671-
InvalidTypeForNotifyDataErrorInfoError,
672-
typeSymbol.Locations.FirstOrDefault(),
673-
typeSymbol);
674-
}
675-
676-
return null;
677-
}
678-
679640
/// <summary>
680641
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
681642
/// </summary>

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

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -113,32 +113,5 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
113113
context.AddSource("__KnownINotifyPropertyChangedArgs.g.cs", compilationUnit.GetText(Encoding.UTF8));
114114
}
115115
});
116-
117-
// Get all class declarations with at least one attribute
118-
IncrementalValuesProvider<INamedTypeSymbol> classSymbols =
119-
context.SyntaxProvider
120-
.CreateSyntaxProvider(
121-
static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
122-
static (context, _) => (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
123-
124-
// Filter only the type symbols with [NotifyPropertyChangedRecipients] and create diagnostics for them
125-
IncrementalValuesProvider<Diagnostic> notifyRecipientsErrors =
126-
classSymbols
127-
.Where(static item => item.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute"))
128-
.Select(static (item, _) => Execute.GetIsNotifyingRecipientsDiagnosticForType(item))
129-
.Where(static item => item is not null)!;
130-
131-
// Output the diagnostics for [NotifyPropertyChangedRecipients]
132-
context.ReportDiagnostics(notifyRecipientsErrors);
133-
134-
// Filter only the type symbols with [NotifyDataErrorInfo] and create diagnostics for them
135-
IncrementalValuesProvider<Diagnostic> notifyDataErrorInfoErrors =
136-
classSymbols
137-
.Where(static item => item.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute"))
138-
.Select(static (item, _) => Execute.GetIsNotifyDataErrorInfoDiagnosticForType(item))
139-
.Where(static item => item is not null)!;
140-
141-
// Output the diagnostics for [NotifyDataErrorInfo]
142-
context.ReportDiagnostics(notifyDataErrorInfoErrors);
143116
}
144117
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 class level <c>[NotifyDataErrorInfo]</c> use is detected.
16+
/// </summary>
17+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
18+
public sealed class InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer : DiagnosticAnalyzer
19+
{
20+
/// <inheritdoc/>
21+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidTypeForNotifyDataErrorInfoError);
22+
23+
/// <inheritdoc/>
24+
public override void Initialize(AnalysisContext context)
25+
{
26+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
27+
context.EnableConcurrentExecution();
28+
29+
context.RegisterCompilationStartAction(static context =>
30+
{
31+
// Get the symbols for [NotifyDataErrorInfo] and ObservableValidator
32+
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute") is not INamedTypeSymbol notifyDataErrorInfoAttributeSymbol ||
33+
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator") is not INamedTypeSymbol observableValidatorSymbol)
34+
{
35+
return;
36+
}
37+
38+
context.RegisterSymbolAction(context =>
39+
{
40+
// We're looking for all class declarations
41+
if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsImplicitlyDeclared: false } classSymbol)
42+
{
43+
return;
44+
}
45+
46+
// Emit a diagnostic for types that use [NotifyDataErrorInfo] but don't inherit from ObservableValidator
47+
if (classSymbol.HasAttributeWithType(notifyDataErrorInfoAttributeSymbol) &&
48+
!classSymbol.InheritsFromType(observableValidatorSymbol))
49+
{
50+
context.ReportDiagnostic(Diagnostic.Create(
51+
InvalidTypeForNotifyDataErrorInfoError,
52+
classSymbol.Locations.FirstOrDefault(),
53+
classSymbol));
54+
}
55+
}, SymbolKind.NamedType);
56+
});
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 class level <c>[NotifyPropertyChangedRecipients]</c> use is detected.
16+
/// </summary>
17+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
18+
public sealed class InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer : DiagnosticAnalyzer
19+
{
20+
/// <inheritdoc/>
21+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidTypeForNotifyPropertyChangedRecipientsError);
22+
23+
/// <inheritdoc/>
24+
public override void Initialize(AnalysisContext context)
25+
{
26+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
27+
context.EnableConcurrentExecution();
28+
29+
context.RegisterCompilationStartAction(static context =>
30+
{
31+
// Get the symbols for [NotifyPropertyChangedRecipients], ObservableRecipient and [ObservableRecipient]
32+
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute") is not INamedTypeSymbol notifyPropertyChangedRecipientsAttributeSymbol ||
33+
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") is not INamedTypeSymbol observableRecipientSymbol ||
34+
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute") is not INamedTypeSymbol observableRecipientAttributeSymbol)
35+
{
36+
return;
37+
}
38+
39+
context.RegisterSymbolAction(context =>
40+
{
41+
// We're looking for all class declarations
42+
if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsImplicitlyDeclared: false } classSymbol)
43+
{
44+
return;
45+
}
46+
47+
// Emit a diagnstic for types that use [NotifyPropertyChangedRecipients] but are neither inheriting from ObservableRecipient nor using [ObservableRecipient]
48+
if (classSymbol.HasAttributeWithType(notifyPropertyChangedRecipientsAttributeSymbol) &&
49+
!classSymbol.InheritsFromType(observableRecipientSymbol) &&
50+
!classSymbol.HasOrInheritsAttributeWithType(observableRecipientAttributeSymbol))
51+
{
52+
context.ReportDiagnostic(Diagnostic.Create(
53+
InvalidTypeForNotifyPropertyChangedRecipientsError,
54+
classSymbol.Locations.FirstOrDefault(),
55+
classSymbol));
56+
}
57+
}, SymbolKind.NamedType);
58+
});
59+
}
60+
}

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
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-
using System.Collections.Immutable;
65
#if !ROSLYN_4_3_1_OR_GREATER
76
using System.Diagnostics.CodeAnalysis;
87
#endif
@@ -54,9 +53,7 @@ public static bool HasFullyQualifiedName(this ISymbol symbol, string name)
5453
/// <returns>Whether or not <paramref name="symbol"/> has an attribute with the specified name.</returns>
5554
public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name)
5655
{
57-
ImmutableArray<AttributeData> attributes = symbol.GetAttributes();
58-
59-
foreach (AttributeData attribute in attributes)
56+
foreach (AttributeData attribute in symbol.GetAttributes())
6057
{
6158
if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true)
6259
{
@@ -67,6 +64,25 @@ public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbo
6764
return false;
6865
}
6966

67+
/// <summary>
68+
/// Checks whether or not a given symbol has an attribute with the specified fully qualified metadata name.
69+
/// </summary>
70+
/// <param name="symbol">The input <see cref="ISymbol"/> instance to check.</param>
71+
/// <param name="typeSymbol">The <see cref="ITypeSymbol"/> instance for the attribute type to look for.</param>
72+
/// <returns>Whether or not <paramref name="symbol"/> has an attribute with the specified type.</returns>
73+
public static bool HasAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol)
74+
{
75+
foreach (AttributeData attribute in symbol.GetAttributes())
76+
{
77+
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol))
78+
{
79+
return true;
80+
}
81+
}
82+
83+
return false;
84+
}
85+
7086
#if !ROSLYN_4_3_1_OR_GREATER
7187
/// <summary>
7288
/// Tries to get an attribute with the specified fully qualified metadata name.
@@ -77,9 +93,7 @@ public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbo
7793
/// <returns>Whether or not <paramref name="symbol"/> has an attribute with the specified name.</returns>
7894
public static bool TryGetAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name, [NotNullWhen(true)] out AttributeData? attributeData)
7995
{
80-
ImmutableArray<AttributeData> attributes = symbol.GetAttributes();
81-
82-
foreach (AttributeData attribute in attributes)
96+
foreach (AttributeData attribute in symbol.GetAttributes())
8397
{
8498
if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true)
8599
{

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS
4343
{
4444
INamedTypeSymbol? baseType = typeSymbol.BaseType;
4545

46-
while (baseType != null)
46+
while (baseType is not null)
4747
{
4848
if (baseType.HasFullyQualifiedMetadataName(name))
4949
{
@@ -56,6 +56,29 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS
5656
return false;
5757
}
5858

59+
/// <summary>
60+
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits from a specified type.
61+
/// </summary>
62+
/// <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>
64+
/// <returns>Whether or not <paramref name="typeSymbol"/> inherits from <paramref name="baseTypeSymbol"/>.</returns>
65+
public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
66+
{
67+
INamedTypeSymbol? currentBaseTypeSymbol = typeSymbol.BaseType;
68+
69+
while (currentBaseTypeSymbol is not null)
70+
{
71+
if (SymbolEqualityComparer.Default.Equals(currentBaseTypeSymbol, baseTypeSymbol))
72+
{
73+
return true;
74+
}
75+
76+
currentBaseTypeSymbol = currentBaseTypeSymbol.BaseType;
77+
}
78+
79+
return false;
80+
}
81+
5982
/// <summary>
6083
/// Checks whether or not a given <see cref="ITypeSymbol"/> implements an interface with a specified name.
6184
/// </summary>
@@ -113,6 +136,25 @@ public static bool HasOrInheritsAttributeWithFullyQualifiedMetadataName(this ITy
113136
return false;
114137
}
115138

139+
/// <summary>
140+
/// Checks whether or not a given <see cref="ITypeSymbol"/> has or inherits a specified attribute.
141+
/// </summary>
142+
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
143+
/// <param name="baseTypeSymbol">The <see cref="ITypeSymbol"/> instane to check for inheritance from.</param>
144+
/// <returns>Whether or not <paramref name="typeSymbol"/> has or inherits an attribute of type <paramref name="baseTypeSymbol"/>.</returns>
145+
public static bool HasOrInheritsAttributeWithType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
146+
{
147+
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
148+
{
149+
if (currentType.HasAttributeWithType(baseTypeSymbol))
150+
{
151+
return true;
152+
}
153+
}
154+
155+
return false;
156+
}
157+
116158
/// <summary>
117159
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits a specified attribute.
118160
/// If the type has no base type, this method will automatically handle that and return <see langword="false"/>.

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,27 +1337,27 @@ public partial class SampleViewModel : ObservableValidator
13371337
}
13381338

13391339
[TestMethod]
1340-
public void InvalidTypeForNotifyPropertyChangedRecipientsError()
1340+
public async Task InvalidTypeForNotifyPropertyChangedRecipientsError()
13411341
{
13421342
string source = """
13431343
using CommunityToolkit.Mvvm.ComponentModel;
13441344
13451345
namespace MyApp
13461346
{
13471347
[NotifyPropertyChangedRecipients]
1348-
public partial class MyViewModel : ObservableObject
1348+
public partial class {|MVVMTK0027:MyViewModel|} : ObservableObject
13491349
{
13501350
[ObservableProperty]
13511351
public int number;
13521352
}
13531353
}
13541354
""";
13551355

1356-
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0027");
1356+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer>(source, LanguageVersion.CSharp8);
13571357
}
13581358

13591359
[TestMethod]
1360-
public void InvalidTypeForNotifyDataErrorInfoError()
1360+
public async Task InvalidTypeForNotifyDataErrorInfoError()
13611361
{
13621362
string source = """
13631363
using System.ComponentModel.DataAnnotations;
@@ -1366,16 +1366,13 @@ public void InvalidTypeForNotifyDataErrorInfoError()
13661366
namespace MyApp
13671367
{
13681368
[NotifyDataErrorInfo]
1369-
public partial class SampleViewModel : ObservableObject
1369+
public partial class {|MVVMTK0028:SampleViewModel|} : ObservableObject
13701370
{
1371-
[ObservableProperty]
1372-
[Required]
1373-
private string name;
13741371
}
13751372
}
13761373
""";
13771374

1378-
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0006", "MVVMTK0028");
1375+
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer>(source, LanguageVersion.CSharp8);
13791376
}
13801377

13811378
[TestMethod]

0 commit comments

Comments
 (0)