Skip to content

Commit 9127b61

Browse files
authored
Merge pull request #588 from CommunityToolkit/dev/attribute-inherit-fixer
Add fixer for ClassUsingAttributeInsteadOfInheritanceAnalyzer
2 parents 96f3fad + a421802 commit 9127b61

File tree

6 files changed

+461
-5
lines changed

6 files changed

+461
-5
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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.Composition;
7+
using System.Threading.Tasks;
8+
using CommunityToolkit.Mvvm.SourceGenerators;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CodeActions;
11+
using Microsoft.CodeAnalysis.CodeFixes;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
using Microsoft.CodeAnalysis.Editing;
14+
using Microsoft.CodeAnalysis.Text;
15+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
16+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
17+
18+
namespace CommunityToolkit.Mvvm.CodeFixers;
19+
20+
/// <summary>
21+
/// A code fixer that automatically updates types using <c>[ObservableObject]</c> or <c>[INotifyPropertyChanged]</c>
22+
/// that have no base type to inherit from <c>ObservableObject</c> instead.
23+
/// </summary>
24+
[ExportCodeFixProvider(LanguageNames.CSharp)]
25+
[Shared]
26+
public sealed class ClassUsingAttributeInsteadOfInheritanceCodeFixer : CodeFixProvider
27+
{
28+
/// <inheritdoc/>
29+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
30+
InheritFromObservableObjectInsteadOfUsingINotifyPropertyChangedAttributeId,
31+
InheritFromObservableObjectInsteadOfUsingObservableObjectAttributeId);
32+
33+
/// <inheritdoc/>
34+
public override FixAllProvider? GetFixAllProvider()
35+
{
36+
return WellKnownFixAllProviders.BatchFixer;
37+
}
38+
39+
/// <inheritdoc/>
40+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
41+
{
42+
Diagnostic diagnostic = context.Diagnostics[0];
43+
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
44+
45+
// Retrieve the property passed by the analyzer
46+
if (diagnostic.Properties[ClassUsingAttributeInsteadOfInheritanceAnalyzer.TypeNameKey] is not string typeName ||
47+
diagnostic.Properties[ClassUsingAttributeInsteadOfInheritanceAnalyzer.AttributeTypeNameKey] is not string attributeTypeName)
48+
{
49+
return;
50+
}
51+
52+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
53+
54+
// Get the class declaration from the target diagnostic
55+
if (root!.FindNode(diagnosticSpan) is ClassDeclarationSyntax { Identifier.Text: string identifierName } classDeclaration &&
56+
identifierName == typeName)
57+
{
58+
// Register the code fix to update the class declaration to inherit from ObservableObject instead
59+
context.RegisterCodeFix(
60+
CodeAction.Create(
61+
title: "Inherit from ObservableObject",
62+
createChangedDocument: token => UpdateReference(context.Document, root, classDeclaration, attributeTypeName),
63+
equivalenceKey: "Inherit from ObservableObject"),
64+
diagnostic);
65+
66+
return;
67+
}
68+
}
69+
70+
/// <summary>
71+
/// Applies the code fix to a target class declaration and returns an updated document.
72+
/// </summary>
73+
/// <param name="document">The original document being fixed.</param>
74+
/// <param name="root">The original tree root belonging to the current document.</param>
75+
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> to update.</param>
76+
/// <param name="attributeTypeName">The name of the attribute that should be removed.</param>
77+
/// <returns>An updated document with the applied code fix, and <paramref name="classDeclaration"/> inheriting from <c>ObservableObject</c>.</returns>
78+
private static Task<Document> UpdateReference(Document document, SyntaxNode root, ClassDeclarationSyntax classDeclaration, string attributeTypeName)
79+
{
80+
// Insert ObservableObject always in first position in the base list. The type might have
81+
// some interfaces in the base list, so we just copy them back after ObservableObject.
82+
SyntaxGenerator generator = SyntaxGenerator.GetGenerator(document);
83+
ClassDeclarationSyntax updatedClassDeclaration = (ClassDeclarationSyntax)generator.AddBaseType(classDeclaration, IdentifierName("ObservableObject"));
84+
85+
// Find the attribute list and attribute to remove
86+
foreach (AttributeListSyntax attributeList in updatedClassDeclaration.AttributeLists)
87+
{
88+
foreach (AttributeSyntax attribute in attributeList.Attributes)
89+
{
90+
if (attribute.Name is IdentifierNameSyntax { Identifier.Text: string identifierName } &&
91+
(identifierName == attributeTypeName || (identifierName + "Attribute") == attributeTypeName))
92+
{
93+
// We found the attribute to remove and the list to update
94+
updatedClassDeclaration = (ClassDeclarationSyntax)generator.RemoveNode(updatedClassDeclaration, attribute);
95+
96+
break;
97+
}
98+
}
99+
}
100+
101+
return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(classDeclaration, updatedClassDeclaration)));
102+
}
103+
}

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)