Skip to content

Commit 94db183

Browse files
committed
Add FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer
1 parent 82ccd6c commit 94db183

File tree

1 file changed

+91
-0
lines changed

1 file changed

+91
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.Generic;
6+
using System.Collections.Immutable;
7+
using System.Linq;
8+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CSharp;
11+
using Microsoft.CodeAnalysis.Diagnostics;
12+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
13+
14+
namespace CommunityToolkit.Mvvm.SourceGenerators;
15+
16+
/// <summary>
17+
/// A diagnostic analyzer that generates an error whenever a field has an orphaned attribute that depends on <c>[ObservableProperty]</c>.
18+
/// </summary>
19+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
20+
public sealed class FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer : DiagnosticAnalyzer
21+
{
22+
/// <summary>
23+
/// The mapping of target attributes that will trigger the analyzer.
24+
/// </summary>
25+
private static readonly ImmutableDictionary<string, string> GeneratorAttributeNamesToFullyQualifiedNamesMap = ImmutableDictionary.CreateRange(new[]
26+
{
27+
new KeyValuePair<string, string>("NotifyCanExecuteChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyCanExecuteChangedForAttribute"),
28+
new KeyValuePair<string, string>("NotifyDataErrorInfoAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute"),
29+
new KeyValuePair<string, string>("NotifyPropertyChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedForAttribute"),
30+
new KeyValuePair<string, string>("NotifyPropertyChangedRecipientsAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute")
31+
});
32+
33+
/// <inheritdoc/>
34+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(FieldWithOrphanedDependentObservablePropertyAttributesError);
35+
36+
/// <inheritdoc/>
37+
public override void Initialize(AnalysisContext context)
38+
{
39+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
40+
context.EnableConcurrentExecution();
41+
42+
// Defer the registration so it can be skipped if C# 8.0 or more is not available.
43+
// That is because in that case source generators are not supported at all anyaway.
44+
context.RegisterCompilationStartAction(static context =>
45+
{
46+
if (!context.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
47+
{
48+
return;
49+
}
50+
51+
context.RegisterSymbolAction(static context =>
52+
{
53+
ImmutableArray<AttributeData> attributes = context.Symbol.GetAttributes();
54+
55+
// If the symbol has no attributes, there's nothing left to do
56+
if (attributes.IsEmpty)
57+
{
58+
return;
59+
}
60+
61+
foreach (AttributeData dependentAttribute in attributes)
62+
{
63+
// Go over each attribute on the target symbol, anche check if any of them matches one of the trigger attributes.
64+
// The logic here is the same as the one in UnsupportedCSharpLanguageVersionAnalyzer, to minimize retrieving symbols.
65+
if (dependentAttribute.AttributeClass is { Name: string attributeName } dependentAttributeClass &&
66+
GeneratorAttributeNamesToFullyQualifiedNamesMap.TryGetValue(attributeName, out string? fullyQualifiedDependentAttributeName) &&
67+
context.Compilation.GetTypeByMetadataName(fullyQualifiedDependentAttributeName) is INamedTypeSymbol dependentAttributeSymbol &&
68+
SymbolEqualityComparer.Default.Equals(dependentAttributeClass, dependentAttributeSymbol))
69+
{
70+
// If the attribute matches, iterate over the attributes to try to find [ObservableProperty]
71+
foreach (AttributeData attribute in attributes)
72+
{
73+
if (attribute.AttributeClass is { Name: "ObservablePropertyAttribute" } attributeSymbol &&
74+
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol observablePropertySymbol &&
75+
SymbolEqualityComparer.Default.Equals(attributeSymbol, observablePropertySymbol))
76+
{
77+
// If [ObservableProperty] is found, then this field is valid in that it doesn't have orphaned dependent attributes
78+
return;
79+
}
80+
}
81+
82+
context.ReportDiagnostic(Diagnostic.Create(FieldWithOrphanedDependentObservablePropertyAttributesError, context.Symbol.Locations.FirstOrDefault(), context.Symbol.ContainingType, context.Symbol.Name));
83+
84+
// Just like in UnsupportedCSharpLanguageVersionAnalyzer, stop if a diagnostic has been emitted for the current symbol
85+
return;
86+
}
87+
}
88+
}, SymbolKind.Field);
89+
});
90+
}
91+
}

0 commit comments

Comments
 (0)