Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Immutable;
using System.Composition;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.SourceGenerators;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Text;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace CommunityToolkit.Mvvm.CodeFixers;

/// <summary>
/// A code fixer that automatically updates types using <c>[ObservableObject]</c> or <c>[INotifyPropertyChanged]</c>
/// that have no base type to inherit from <c>ObservableObject</c> instead.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp)]
[Shared]
public sealed class ClassUsingAttributeInsteadOfInheritanceCodeFixer : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
InheritFromObservableObjectInsteadOfUsingINotifyPropertyChangedAttributeId,
InheritFromObservableObjectInsteadOfUsingObservableObjectAttributeId);

/// <inheritdoc/>
public override FixAllProvider? GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

/// <inheritdoc/>
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Diagnostic diagnostic = context.Diagnostics[0];
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;

// Retrieve the property passed by the analyzer
if (diagnostic.Properties[ClassUsingAttributeInsteadOfInheritanceAnalyzer.TypeNameKey] is not string typeName ||
diagnostic.Properties[ClassUsingAttributeInsteadOfInheritanceAnalyzer.AttributeTypeNameKey] is not string attributeTypeName)
{
return;
}

SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

// Get the class declaration from the target diagnostic
if (root!.FindNode(diagnosticSpan) is ClassDeclarationSyntax { Identifier.Text: string identifierName } classDeclaration &&
identifierName == typeName)
{
// Register the code fix to update the class declaration to inherit from ObservableObject instead
context.RegisterCodeFix(
CodeAction.Create(
title: "Inherit from ObservableObject",
createChangedDocument: token => UpdateReference(context.Document, root, classDeclaration, attributeTypeName),
equivalenceKey: "Inherit from ObservableObject"),
diagnostic);

return;
}
}

/// <summary>
/// Applies the code fix to a target class declaration and returns an updated document.
/// </summary>
/// <param name="document">The original document being fixed.</param>
/// <param name="root">The original tree root belonging to the current document.</param>
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> to update.</param>
/// <param name="attributeTypeName">The name of the attribute that should be removed.</param>
/// <returns>An updated document with the applied code fix, and <paramref name="classDeclaration"/> inheriting from <c>ObservableObject</c>.</returns>
private static Task<Document> UpdateReference(Document document, SyntaxNode root, ClassDeclarationSyntax classDeclaration, string attributeTypeName)
{
// Insert ObservableObject always in first position in the base list. The type might have
// some interfaces in the base list, so we just copy them back after ObservableObject.
SyntaxGenerator generator = SyntaxGenerator.GetGenerator(document);
ClassDeclarationSyntax updatedClassDeclaration = (ClassDeclarationSyntax)generator.AddBaseType(classDeclaration, IdentifierName("ObservableObject"));

// Find the attribute list and attribute to remove
foreach (AttributeListSyntax attributeList in updatedClassDeclaration.AttributeLists)
{
foreach (AttributeSyntax attribute in attributeList.Attributes)
{
if (attribute.Name is IdentifierNameSyntax { Identifier.Text: string identifierName } &&
(identifierName == attributeTypeName || (identifierName + "Attribute") == attributeTypeName))
{
// We found the attribute to remove and the list to update
updatedClassDeclaration = (ClassDeclarationSyntax)generator.RemoveNode(updatedClassDeclaration, attribute);

break;
}
}
}

return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(classDeclaration, updatedClassDeclaration)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.SourceGenerators;
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;

namespace CommunityToolkit.Mvvm.CodeFixers;

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

/// <inheritdoc/>
public override FixAllProvider? GetFixAllProvider()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ClassUsingAttributeInsteadOfInheritanceAnalyzer : DiagnosticAnalyzer
{
/// <summary>
/// The key for the name of the target type to update.
/// </summary>
internal const string TypeNameKey = "TypeName";

/// <summary>
/// The key for the name of the attribute that was found and should be removed.
/// </summary>
internal const string AttributeTypeNameKey = "AttributeTypeName";

/// <summary>
/// The mapping of target attributes that will trigger the analyzer.
/// </summary>
Expand Down Expand Up @@ -67,7 +77,13 @@ public override void Initialize(AnalysisContext context)
if (classSymbol.BaseType is { SpecialType: SpecialType.System_Object })
{
// This type is using the attribute when it could just inherit from ObservableObject, which is preferred
context.ReportDiagnostic(Diagnostic.Create(GeneratorAttributeNamesToDiagnosticsMap[attributeClass.Name], context.Symbol.Locations.FirstOrDefault(), context.Symbol));
context.ReportDiagnostic(Diagnostic.Create(
GeneratorAttributeNamesToDiagnosticsMap[attributeClass.Name],
context.Symbol.Locations.FirstOrDefault(),
ImmutableDictionary.Create<string, string?>()
.Add(TypeNameKey, classSymbol.Name)
.Add(AttributeTypeNameKey, attributeName),
context.Symbol));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
/// </summary>
internal static class DiagnosticDescriptors
{
/// <summary>
/// The diagnostic id for <see cref="InheritFromObservableObjectInsteadOfUsingINotifyPropertyChangedAttributeWarning"/>.
/// </summary>
public const string InheritFromObservableObjectInsteadOfUsingINotifyPropertyChangedAttributeId = "MVVMTK0032";

/// <summary>
/// The diagnostic id for <see cref="InheritFromObservableObjectInsteadOfUsingObservableObjectAttributeWarning"/>.
/// </summary>
public const string InheritFromObservableObjectInsteadOfUsingObservableObjectAttributeId = "MVVMTK0033";

/// <summary>
/// The diagnostic id for <see cref="FieldReferenceForObservablePropertyFieldWarning"/>.
/// </summary>
Expand Down Expand Up @@ -519,7 +529,7 @@ internal static class DiagnosticDescriptors
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor InheritFromObservableObjectInsteadOfUsingINotifyPropertyChangedAttributeWarning = new DiagnosticDescriptor(
id: "MVVMTK0032",
id: InheritFromObservableObjectInsteadOfUsingINotifyPropertyChangedAttributeId,
title: "Inherit from ObservableObject instead of using [INotifyPropertyChanged]",
messageFormat: "The type {0} is using the [INotifyPropertyChanged] attribute while having no base type, and it should instead inherit from ObservableObject",
category: typeof(INotifyPropertyChangedGenerator).FullName,
Expand All @@ -537,7 +547,7 @@ internal static class DiagnosticDescriptors
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor InheritFromObservableObjectInsteadOfUsingObservableObjectAttributeWarning = new DiagnosticDescriptor(
id: "MVVMTK0033",
id: InheritFromObservableObjectInsteadOfUsingObservableObjectAttributeId,
title: "Inherit from ObservableObject instead of using [ObservableObject]",
messageFormat: "The type {0} is using the [ObservableObject] attribute while having no base type, and it should instead inherit from ObservableObject",
category: typeof(ObservableObjectGenerator).FullName,
Expand Down
Loading