Skip to content

Commit 162ae4f

Browse files
committed
Add 'UsePartialPropertyForSemiAutoPropertyCodeFixer'
1 parent 7c6eeb6 commit 162ae4f

File tree

4 files changed

+117
-2
lines changed

4 files changed

+117
-2
lines changed

src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<Compile Include="$(MSBuildThisFileDirectory)AsyncVoidReturningRelayCommandMethodCodeFixer.cs" />
1313
<Compile Include="$(MSBuildThisFileDirectory)ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs" />
1414
<Compile Include="$(MSBuildThisFileDirectory)FieldReferenceForObservablePropertyFieldCodeFixer.cs" />
15+
<Compile Include="$(MSBuildThisFileDirectory)UsePartialPropertyForSemiAutoPropertyCodeFixer.cs" />
1516
<Compile Include="$(MSBuildThisFileDirectory)UsePartialPropertyForObservablePropertyCodeFixer.cs" />
1617
</ItemGroup>
1718
<ItemGroup>

src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
100100
if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf<FieldDeclarationSyntax>() is { Declaration.Variables: [{ Identifier.Text: string identifierName }] } fieldDeclaration &&
101101
identifierName == fieldName)
102102
{
103-
// Register the code fix to update the class declaration to inherit from ObservableObject instead
103+
// Register the code fix to convert the field declaration to a partial property
104104
context.RegisterCodeFix(
105105
CodeAction.Create(
106106
title: "Use a partial property",
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
#if ROSLYN_4_12_0_OR_GREATER
6+
7+
using System.Collections.Immutable;
8+
using System.Composition;
9+
using System.Linq;
10+
using System.Threading.Tasks;
11+
using Microsoft.CodeAnalysis;
12+
using Microsoft.CodeAnalysis.CodeActions;
13+
using Microsoft.CodeAnalysis.CodeFixes;
14+
using Microsoft.CodeAnalysis.CSharp;
15+
using Microsoft.CodeAnalysis.CSharp.Syntax;
16+
using Microsoft.CodeAnalysis.Editing;
17+
using Microsoft.CodeAnalysis.Formatting;
18+
using Microsoft.CodeAnalysis.Text;
19+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
20+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
21+
22+
namespace CommunityToolkit.Mvvm.CodeFixers;
23+
24+
/// <summary>
25+
/// A code fixer that converts semi-auto properties to partial properties using <c>[ObservableProperty]</c>.
26+
/// </summary>
27+
[ExportCodeFixProvider(LanguageNames.CSharp)]
28+
[Shared]
29+
public sealed class UsePartialPropertyForSemiAutoPropertyCodeFixer : CodeFixProvider
30+
{
31+
/// <inheritdoc/>
32+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoPropertyId);
33+
34+
/// <inheritdoc/>
35+
public override FixAllProvider? GetFixAllProvider()
36+
{
37+
return WellKnownFixAllProviders.BatchFixer;
38+
}
39+
40+
/// <inheritdoc/>
41+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
42+
{
43+
Diagnostic diagnostic = context.Diagnostics[0];
44+
TextSpan diagnosticSpan = context.Span;
45+
46+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
47+
48+
// Get the property declaration from the target diagnostic
49+
if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration)
50+
{
51+
// Register the code fix to update the semi-auto property to a partial property
52+
context.RegisterCodeFix(
53+
CodeAction.Create(
54+
title: "Use a partial property",
55+
createChangedDocument: token => ConvertToPartialProperty(context.Document, root, propertyDeclaration),
56+
equivalenceKey: "Use a partial property"),
57+
diagnostic);
58+
}
59+
}
60+
61+
/// <summary>
62+
/// Applies the code fix to a target identifier and returns an updated document.
63+
/// </summary>
64+
/// <param name="document">The original document being fixed.</param>
65+
/// <param name="root">The original tree root belonging to the current document.</param>
66+
/// <param name="propertyDeclaration">The <see cref="PropertyDeclarationSyntax"/> for the property being updated.</param>
67+
/// <returns>An updated document with the applied code fix, and <paramref name="propertyDeclaration"/> being replaced with a partial property.</returns>
68+
private static async Task<Document> ConvertToPartialProperty(Document document, SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration)
69+
{
70+
await Task.CompletedTask;
71+
72+
// Get a new property that is partial and with semicolon token accessors
73+
PropertyDeclarationSyntax updatedPropertyDeclaration =
74+
propertyDeclaration
75+
.AddModifiers(Token(SyntaxKind.PartialKeyword))
76+
.AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ObservableProperty")))))
77+
.WithAdditionalAnnotations(Formatter.Annotation)
78+
.WithAccessorList(AccessorList(List(
79+
[
80+
// Keep the accessors (so we can easily keep all trivia, modifiers, attributes, etc.) but make them semicolon only
81+
propertyDeclaration.AccessorList!.Accessors[0]
82+
.WithBody(null)
83+
.WithExpressionBody(null)
84+
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
85+
.WithAdditionalAnnotations(Formatter.Annotation),
86+
propertyDeclaration.AccessorList!.Accessors[1]
87+
.WithBody(null)
88+
.WithExpressionBody(null)
89+
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
90+
.WithAdditionalAnnotations(Formatter.Annotation)
91+
])));
92+
93+
// Create an editor to perform all mutations
94+
SyntaxEditor editor = new(root, document.Project.Solution.Workspace.Services);
95+
96+
editor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration);
97+
98+
// Find the parent type for the property
99+
TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf<TypeDeclarationSyntax>()!;
100+
101+
// Make sure it's partial
102+
if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
103+
{
104+
TypeDeclarationSyntax updatedTypeDeclaration = typeDeclaration.AddModifiers(Token(SyntaxKind.PartialKeyword));
105+
106+
editor.ReplaceNode(typeDeclaration, updatedTypeDeclaration);
107+
}
108+
109+
return document.WithSyntaxRoot(editor.GetChangedRoot());
110+
}
111+
}
112+
113+
#endif

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,8 @@ public override void Initialize(AnalysisContext context)
212212
context.ReportDiagnostic(Diagnostic.Create(
213213
UseObservablePropertyOnSemiAutoProperty,
214214
pair.Key.Locations.FirstOrDefault(),
215-
pair.Key));
215+
pair.Key.ContainingType,
216+
pair.Key.Name));
216217
}
217218
}
218219
});

0 commit comments

Comments
 (0)