Skip to content

Commit

Permalink
Merge pull request #1020 from CommunityToolkit/dev/semi-auto-property…
Browse files Browse the repository at this point in the history
…-fixer

Add code fixer for semi-auto property to '[ObservableProperty]' partial property
  • Loading branch information
Sergio0694 authored Dec 11, 2024
2 parents 78348ca + 5766465 commit bcc6caf
Show file tree
Hide file tree
Showing 10 changed files with 1,870 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<Compile Include="$(MSBuildThisFileDirectory)AsyncVoidReturningRelayCommandMethodCodeFixer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)FieldReferenceForObservablePropertyFieldCodeFixer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePartialPropertyForSemiAutoPropertyCodeFixer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePartialPropertyForObservablePropertyCodeFixer.cs" />
</ItemGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf<FieldDeclarationSyntax>() is { Declaration.Variables: [{ Identifier.Text: string identifierName }] } fieldDeclaration &&
identifierName == fieldName)
{
// Register the code fix to update the class declaration to inherit from ObservableObject instead
// Register the code fix to convert the field declaration to a partial property
context.RegisterCodeFix(
CodeAction.Create(
title: "Use a partial property",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// 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.

#if ROSLYN_4_12_0_OR_GREATER

using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
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 converts semi-auto properties to partial properties using <c>[ObservableProperty]</c>.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp)]
[Shared]
public sealed class UsePartialPropertyForSemiAutoPropertyCodeFixer : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoPropertyId);

/// <inheritdoc/>
public override Microsoft.CodeAnalysis.CodeFixes.FixAllProvider? GetFixAllProvider()
{
return new FixAllProvider();
}

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

// This code fixer needs the semantic model, so check that first
if (!context.Document.SupportsSemanticModel)
{
return;
}

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

// Get the property declaration from the target diagnostic
if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration)
{
// Get the semantic model, as we need to resolve symbols
SemanticModel semanticModel = (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false))!;

// Make sure we can resolve the [ObservableProperty] attribute (as we want to add it in the fixed code)
if (semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol)
{
return;
}

// Register the code fix to update the semi-auto property to a partial property
context.RegisterCodeFix(
CodeAction.Create(
title: "Use a partial property",
createChangedDocument: token => ConvertToPartialProperty(context.Document, root, propertyDeclaration, observablePropertySymbol),
equivalenceKey: "Use a partial property"),
diagnostic);
}
}

/// <summary>
/// Applies the code fix to a target identifier 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="propertyDeclaration">The <see cref="PropertyDeclarationSyntax"/> for the property being updated.</param>
/// <param name="observablePropertySymbol">The <see cref="INamedTypeSymbol"/> for <c>[ObservableProperty]</c>.</param>
/// <returns>An updated document with the applied code fix, and <paramref name="propertyDeclaration"/> being replaced with a partial property.</returns>
private static async Task<Document> ConvertToPartialProperty(
Document document,
SyntaxNode root,
PropertyDeclarationSyntax propertyDeclaration,
INamedTypeSymbol observablePropertySymbol)
{
await Task.CompletedTask;

SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document);

// Create the attribute syntax for the new [ObservableProperty] attribute. Also
// annotate it to automatically add using directives to the document, if needed.
SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(observablePropertySymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation);
AttributeListSyntax observablePropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax);

// Create an editor to perform all mutations
SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services);

ConvertToPartialProperty(
propertyDeclaration,
observablePropertyAttributeList,
syntaxEditor);

// Create the new document with the single change
return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot());
}

/// <summary>
/// Applies the code fix to a target identifier and returns an updated document.
/// </summary>
/// <param name="propertyDeclaration">The <see cref="PropertyDeclarationSyntax"/> for the property being updated.</param>
/// <param name="observablePropertyAttributeList">The <see cref="AttributeListSyntax"/> with the attribute to add.</param>
/// <param name="syntaxEditor">The <see cref="SyntaxEditor"/> instance to use.</param>
/// <returns>An updated document with the applied code fix, and <paramref name="propertyDeclaration"/> being replaced with a partial property.</returns>
private static void ConvertToPartialProperty(
PropertyDeclarationSyntax propertyDeclaration,
AttributeListSyntax observablePropertyAttributeList,
SyntaxEditor syntaxEditor)
{
// Start setting up the updated attribute lists
SyntaxList<AttributeListSyntax> attributeLists = propertyDeclaration.AttributeLists;

if (attributeLists is [AttributeListSyntax firstAttributeListSyntax, ..])
{
// Remove the trivia from the original first attribute
attributeLists = attributeLists.Replace(
nodeInList: firstAttributeListSyntax,
newNode: firstAttributeListSyntax.WithoutTrivia());

// If the property has at least an attribute list, move the trivia from it to the new attribute
observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(firstAttributeListSyntax);

// Insert the new attribute
attributeLists = attributeLists.Insert(0, observablePropertyAttributeList);
}
else
{
// Otherwise (there are no attribute lists), transfer the trivia to the new (only) attribute list
observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(propertyDeclaration);

// Save the new attribute list
attributeLists = attributeLists.Add(observablePropertyAttributeList);
}

// Get a new property that is partial and with semicolon token accessors
PropertyDeclarationSyntax updatedPropertyDeclaration =
propertyDeclaration
.AddModifiers(Token(SyntaxKind.PartialKeyword))
.WithoutLeadingTrivia()
.WithAttributeLists(attributeLists)
.WithAdditionalAnnotations(Formatter.Annotation)
.WithAccessorList(AccessorList(List(
[
// Keep the accessors (so we can easily keep all trivia, modifiers, attributes, etc.) but make them semicolon only
propertyDeclaration.AccessorList!.Accessors[0]
.WithBody(null)
.WithExpressionBody(null)
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
.WithAdditionalAnnotations(Formatter.Annotation),
propertyDeclaration.AccessorList!.Accessors[1]
.WithBody(null)
.WithExpressionBody(null)
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
.WithTrailingTrivia(propertyDeclaration.AccessorList.Accessors[1].GetTrailingTrivia())
.WithAdditionalAnnotations(Formatter.Annotation)
])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia()));

syntaxEditor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration);

// Find the parent type for the property
TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf<TypeDeclarationSyntax>()!;

// Make sure it's partial (we create the updated node in the function to preserve the updated property declaration).
// If we created it separately and replaced it, the whole tree would also be replaced, and we'd lose the new property.
if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
{
syntaxEditor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true)));
}
}

/// <summary>
/// A custom <see cref="FixAllProvider"/> with the logic from <see cref="UsePartialPropertyForSemiAutoPropertyCodeFixer"/>.
/// </summary>
private sealed class FixAllProvider : DocumentBasedFixAllProvider
{
/// <inheritdoc/>
protected override async Task<Document?> FixAllAsync(FixAllContext fixAllContext, Document document, ImmutableArray<Diagnostic> diagnostics)
{
// Get the semantic model, as we need to resolve symbols
if (await document.GetSemanticModelAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SemanticModel semanticModel)
{
return document;
}

// Make sure we can resolve the [ObservableProperty] attribute here as well
if (semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol)
{
return document;
}

// Get the document root (this should always succeed)
if (await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SyntaxNode root)
{
return document;
}

SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document);

// Create the attribute syntax for the new [ObservableProperty] attribute here too
SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(observablePropertySymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation);
AttributeListSyntax observablePropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax);

// Create an editor to perform all mutations (across all edits in the file)
SyntaxEditor syntaxEditor = new(root, fixAllContext.Solution.Services);

foreach (Diagnostic diagnostic in diagnostics)
{
// Get the current property declaration for the diagnostic
if (root.FindNode(diagnostic.Location.SourceSpan) is not PropertyDeclarationSyntax propertyDeclaration)
{
continue;
}

ConvertToPartialProperty(
propertyDeclaration,
observablePropertyAttributeList,
syntaxEditor);
}

return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot());
}
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,4 @@ MVVMTK0052 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
MVVMTK0053 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0053
MVVMTK0054 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0054
MVVMTK0055 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0055
MVVMTK0056 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0056
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservableValidatorValidateAllPropertiesGenerator.Execute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AsyncVoidReturningRelayCommandMethodAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs" />
Expand Down Expand Up @@ -88,6 +89,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Helpers\EquatableArray{T}.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HashCode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ImmutableArrayBuilder{T}.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ObjectPool{T}.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Input\Models\CanExecuteExpressionType.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Input\Models\CommandInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Input\RelayCommandGenerator.cs" />
Expand Down
Loading

0 comments on commit bcc6caf

Please sign in to comment.