Skip to content

Commit 9a0113f

Browse files
committed
Add ClassUsingAttributeInsteadOfInheritanceCodeFixer
1 parent 96f3fad commit 9a0113f

File tree

4 files changed

+166
-5
lines changed

4 files changed

+166
-5
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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;
6+
using System.Collections.Immutable;
7+
using System.Composition;
8+
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using CommunityToolkit.Mvvm.SourceGenerators;
12+
using Microsoft.CodeAnalysis;
13+
using Microsoft.CodeAnalysis.CodeActions;
14+
using Microsoft.CodeAnalysis.CodeFixes;
15+
using Microsoft.CodeAnalysis.CSharp.Syntax;
16+
using Microsoft.CodeAnalysis.Text;
17+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
18+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
19+
20+
namespace CommunityToolkit.Mvvm.CodeFixers;
21+
22+
/// <summary>
23+
/// A code fixer that automatically updates types using <c>[ObservableObject]</c> or <c>[INotifyPropertyChanged]</c>
24+
/// that have no base type to inherit from <c>ObservableObject</c> instead.
25+
/// </summary>
26+
[ExportCodeFixProvider(LanguageNames.CSharp)]
27+
[Shared]
28+
public sealed class ClassUsingAttributeInsteadOfInheritanceCodeFixer : CodeFixProvider
29+
{
30+
/// <inheritdoc/>
31+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
32+
InheritFromObservableObjectInsteadOfUsingINotifyPropertyChangedAttributeId,
33+
InheritFromObservableObjectInsteadOfUsingObservableObjectAttributeId);
34+
35+
/// <inheritdoc/>
36+
public override FixAllProvider? GetFixAllProvider()
37+
{
38+
return WellKnownFixAllProviders.BatchFixer;
39+
}
40+
41+
/// <inheritdoc/>
42+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
43+
{
44+
Diagnostic diagnostic = context.Diagnostics[0];
45+
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
46+
47+
// Retrieve the property passed by the analyzer
48+
if (diagnostic.Properties[ClassUsingAttributeInsteadOfInheritanceAnalyzer.TypeNameKey] is not string typeName ||
49+
diagnostic.Properties[ClassUsingAttributeInsteadOfInheritanceAnalyzer.AttributeTypeNameKey] is not string attributeTypeName)
50+
{
51+
return;
52+
}
53+
54+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
55+
56+
foreach (SyntaxNode syntaxNode in root!.FindNode(diagnosticSpan).DescendantNodesAndSelf())
57+
{
58+
// Find the first descendant node from the source of the diagnostic that is a class declaration with the target name
59+
if (syntaxNode is ClassDeclarationSyntax { Identifier.Text: string identifierName } classDeclaration &&
60+
identifierName == typeName)
61+
{
62+
// Register the code fix to update the class declaration to inherit from ObservableObject instead
63+
context.RegisterCodeFix(
64+
CodeAction.Create(
65+
title: "Inherit from ObservableObject",
66+
createChangedDocument: token => UpdateReference(context.Document, classDeclaration, attributeTypeName, token),
67+
equivalenceKey: "Inherit from ObservableObject"),
68+
diagnostic);
69+
70+
return;
71+
}
72+
}
73+
}
74+
75+
/// <summary>
76+
/// Applies the code fix to a target class declaration and returns an updated document.
77+
/// </summary>
78+
/// <param name="document">The original document being fixed.</param>
79+
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> to update.</param>
80+
/// <param name="attributeTypeName">The name of the attribute that should be removed.</param>
81+
/// <param name="cancellationToken">The cancellation token for the operation.</param>
82+
/// <returns>An updated document with the applied code fix, and <paramref name="classDeclaration"/> inheriting from <c>ObservableObject</c>.</returns>
83+
private static async Task<Document> UpdateReference(Document document, ClassDeclarationSyntax classDeclaration, string attributeTypeName, CancellationToken cancellationToken)
84+
{
85+
// Insert ObservableObject always in first position in the base list. The type might have
86+
// some interfaces in the base list, so we just copy them back after ObservableObject.
87+
ClassDeclarationSyntax updatedClassDeclaration =
88+
classDeclaration.WithBaseList(BaseList(SingletonSeparatedList(
89+
(BaseTypeSyntax)SimpleBaseType(IdentifierName("ObservableObject"))))
90+
.AddTypes(classDeclaration.BaseList?.Types.ToArray() ?? Array.Empty<BaseTypeSyntax>()));
91+
92+
AttributeListSyntax? targetAttributeList = null;
93+
AttributeSyntax? targetAttribute = null;
94+
95+
// Find the attribute list and attribute to remove
96+
foreach (AttributeListSyntax attributeList in updatedClassDeclaration.AttributeLists)
97+
{
98+
foreach (AttributeSyntax attribute in attributeList.Attributes)
99+
{
100+
if (attribute.Name is IdentifierNameSyntax { Identifier.Text: string identifierName } &&
101+
identifierName == attributeTypeName)
102+
{
103+
// We found the attribute to remove and the list to update
104+
targetAttributeList = attributeList;
105+
targetAttribute = attribute;
106+
107+
break;
108+
}
109+
}
110+
}
111+
112+
// If we found an attribute to remove, do that
113+
if (targetAttribute is not null)
114+
{
115+
// If the target list has more than one attribute, keep it and just remove the target one
116+
if (targetAttributeList!.Attributes.Count > 1)
117+
{
118+
updatedClassDeclaration =
119+
updatedClassDeclaration.ReplaceNode(
120+
targetAttributeList,
121+
targetAttributeList.RemoveNode(targetAttribute, SyntaxRemoveOptions.KeepNoTrivia)!);
122+
}
123+
else
124+
{
125+
// Otherwise, remove the entire attribute list
126+
updatedClassDeclaration = updatedClassDeclaration.RemoveNode(targetAttributeList, SyntaxRemoveOptions.KeepExteriorTrivia)!;
127+
}
128+
}
129+
130+
SyntaxNode originalRoot = await classDeclaration.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
131+
SyntaxTree updatedTree = originalRoot.ReplaceNode(classDeclaration, updatedClassDeclaration).SyntaxTree;
132+
133+
return document.WithSyntaxRoot(await updatedTree.GetRootAsync(cancellationToken).ConfigureAwait(false));
134+
}
135+
}

src/CommunityToolkit.Mvvm.CodeFixers/FieldReferenceForObservablePropertyFieldFixer.cs renamed to src/CommunityToolkit.Mvvm.CodeFixers/FieldReferenceForObservablePropertyFieldCodeFixer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
using System.Threading;
88
using System.Threading.Tasks;
99
using CommunityToolkit.Mvvm.SourceGenerators;
10-
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
1110
using Microsoft.CodeAnalysis;
1211
using Microsoft.CodeAnalysis.CodeActions;
1312
using Microsoft.CodeAnalysis.CodeFixes;
1413
using Microsoft.CodeAnalysis.CSharp;
1514
using Microsoft.CodeAnalysis.CSharp.Syntax;
1615
using Microsoft.CodeAnalysis.Text;
16+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
1717

1818
namespace CommunityToolkit.Mvvm.CodeFixers;
1919

@@ -25,7 +25,7 @@ namespace CommunityToolkit.Mvvm.CodeFixers;
2525
public sealed class FieldReferenceForObservablePropertyFieldCodeFixer : CodeFixProvider
2626
{
2727
/// <inheritdoc/>
28-
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticDescriptors.FieldReferenceForObservablePropertyFieldId);
28+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(FieldReferenceForObservablePropertyFieldId);
2929

3030
/// <inheritdoc/>
3131
public override FixAllProvider? GetFixAllProvider()

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
1717
[DiagnosticAnalyzer(LanguageNames.CSharp)]
1818
public sealed class ClassUsingAttributeInsteadOfInheritanceAnalyzer : DiagnosticAnalyzer
1919
{
20+
/// <summary>
21+
/// The key for the name of the target type to update.
22+
/// </summary>
23+
internal const string TypeNameKey = "TypeName";
24+
25+
/// <summary>
26+
/// The key for the name of the attribute that was found and should be removed.
27+
/// </summary>
28+
internal const string AttributeTypeNameKey = "AttributeTypeName";
29+
2030
/// <summary>
2131
/// The mapping of target attributes that will trigger the analyzer.
2232
/// </summary>
@@ -67,7 +77,13 @@ public override void Initialize(AnalysisContext context)
6777
if (classSymbol.BaseType is { SpecialType: SpecialType.System_Object })
6878
{
6979
// This type is using the attribute when it could just inherit from ObservableObject, which is preferred
70-
context.ReportDiagnostic(Diagnostic.Create(GeneratorAttributeNamesToDiagnosticsMap[attributeClass.Name], context.Symbol.Locations.FirstOrDefault(), context.Symbol));
80+
context.ReportDiagnostic(Diagnostic.Create(
81+
GeneratorAttributeNamesToDiagnosticsMap[attributeClass.Name],
82+
context.Symbol.Locations.FirstOrDefault(),
83+
ImmutableDictionary.Create<string, string?>()
84+
.Add(TypeNameKey, classSymbol.Name)
85+
.Add(AttributeTypeNameKey, attributeName),
86+
context.Symbol));
7187
}
7288
}
7389
}

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
1414
/// </summary>
1515
internal static class DiagnosticDescriptors
1616
{
17+
/// <summary>
18+
/// The diagnostic id for <see cref="InheritFromObservableObjectInsteadOfUsingINotifyPropertyChangedAttributeWarning"/>.
19+
/// </summary>
20+
public const string InheritFromObservableObjectInsteadOfUsingINotifyPropertyChangedAttributeId = "MVVMTK0032";
21+
22+
/// <summary>
23+
/// The diagnostic id for <see cref="InheritFromObservableObjectInsteadOfUsingObservableObjectAttributeWarning"/>.
24+
/// </summary>
25+
public const string InheritFromObservableObjectInsteadOfUsingObservableObjectAttributeId = "MVVMTK0033";
26+
1727
/// <summary>
1828
/// The diagnostic id for <see cref="FieldReferenceForObservablePropertyFieldWarning"/>.
1929
/// </summary>
@@ -519,7 +529,7 @@ internal static class DiagnosticDescriptors
519529
/// </para>
520530
/// </summary>
521531
public static readonly DiagnosticDescriptor InheritFromObservableObjectInsteadOfUsingINotifyPropertyChangedAttributeWarning = new DiagnosticDescriptor(
522-
id: "MVVMTK0032",
532+
id: InheritFromObservableObjectInsteadOfUsingINotifyPropertyChangedAttributeId,
523533
title: "Inherit from ObservableObject instead of using [INotifyPropertyChanged]",
524534
messageFormat: "The type {0} is using the [INotifyPropertyChanged] attribute while having no base type, and it should instead inherit from ObservableObject",
525535
category: typeof(INotifyPropertyChangedGenerator).FullName,
@@ -537,7 +547,7 @@ internal static class DiagnosticDescriptors
537547
/// </para>
538548
/// </summary>
539549
public static readonly DiagnosticDescriptor InheritFromObservableObjectInsteadOfUsingObservableObjectAttributeWarning = new DiagnosticDescriptor(
540-
id: "MVVMTK0033",
550+
id: InheritFromObservableObjectInsteadOfUsingObservableObjectAttributeId,
541551
title: "Inherit from ObservableObject instead of using [ObservableObject]",
542552
messageFormat: "The type {0} is using the [ObservableObject] attribute while having no base type, and it should instead inherit from ObservableObject",
543553
category: typeof(ObservableObjectGenerator).FullName,

0 commit comments

Comments
 (0)