Skip to content

Commit 8524021

Browse files
committed
Add draft 'UseGeneratedDependencyPropertyOnManualPropertyCodeFixer'
1 parent a032d85 commit 8524021

File tree

1 file changed

+277
-0
lines changed

1 file changed

+277
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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.Diagnostics.CodeAnalysis;
8+
using System.Linq;
9+
using System.Threading.Tasks;
10+
using CommunityToolkit.GeneratedDependencyProperty.Constants;
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.Simplification;
19+
using Microsoft.CodeAnalysis.Text;
20+
using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors;
21+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
22+
23+
namespace CommunityToolkit.Mvvm.CodeFixers;
24+
25+
/// <summary>
26+
/// A code fixer that converts manual properties into partial properties using <c>[GeneratedDependencytProperty]</c>.
27+
/// </summary>
28+
[ExportCodeFixProvider(LanguageNames.CSharp)]
29+
[Shared]
30+
public sealed class UseGeneratedDependencyPropertyOnManualPropertyCodeFixer : CodeFixProvider
31+
{
32+
/// <inheritdoc/>
33+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = [UseGeneratedDependencyPropertyForManualPropertyId];
34+
35+
/// <inheritdoc/>
36+
public override Microsoft.CodeAnalysis.CodeFixes.FixAllProvider? GetFixAllProvider()
37+
{
38+
return new FixAllProvider();
39+
}
40+
41+
/// <inheritdoc/>
42+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
43+
{
44+
Diagnostic diagnostic = context.Diagnostics[0];
45+
TextSpan diagnosticSpan = context.Span;
46+
47+
// We can only possibly fix diagnostics with an additional location
48+
if (diagnostic.AdditionalLocations is not [{ } fieldLocation])
49+
{
50+
return;
51+
}
52+
53+
// This code fixer needs the semantic model, so check that first
54+
if (!context.Document.SupportsSemanticModel)
55+
{
56+
return;
57+
}
58+
59+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
60+
61+
// Get the property declaration and the field declaration from the target diagnostic
62+
if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration &&
63+
root.FindNode(fieldLocation.SourceSpan) is FieldDeclarationSyntax fieldDeclaration)
64+
{
65+
// Get the semantic model, as we need to resolve symbols
66+
SemanticModel semanticModel = (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false))!;
67+
68+
// Register the code fix to update the semi-auto property to a partial property
69+
context.RegisterCodeFix(
70+
CodeAction.Create(
71+
title: "Use a partial property",
72+
createChangedDocument: token => ConvertToPartialProperty(context.Document, semanticModel, root, propertyDeclaration, fieldDeclaration),
73+
equivalenceKey: "Use a partial property"),
74+
diagnostic);
75+
}
76+
}
77+
78+
/// <summary>
79+
/// Tries to get an <see cref="AttributeListSyntax"/> for the <c>[GeneratedDependencyProperty]</c> attribute.
80+
/// </summary>
81+
/// <param name="document">The original document being fixed.</param>
82+
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current compilation.</param>
83+
/// <param name="observablePropertyAttributeList">The resulting attribute list, if successfully retrieved.</param>
84+
/// <returns>Whether <paramref name="observablePropertyAttributeList"/> could be retrieved successfully.</returns>
85+
private static bool TryGetGeneratedObservablePropertyAttributeList(
86+
Document document,
87+
SemanticModel semanticModel,
88+
[NotNullWhen(true)] out AttributeListSyntax? observablePropertyAttributeList)
89+
{
90+
// Make sure we can resolve the '[GeneratedDependencyProperty]' attribute
91+
if (semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute) is not INamedTypeSymbol attributeSymbol)
92+
{
93+
observablePropertyAttributeList = null;
94+
95+
return false;
96+
}
97+
98+
SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document);
99+
100+
// Create the attribute syntax for the new '[GeneratedDependencyProperty]' attribute here too
101+
SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(attributeSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation);
102+
103+
observablePropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax);
104+
105+
return true;
106+
}
107+
108+
/// <summary>
109+
/// Applies the code fix to a target identifier and returns an updated document.
110+
/// </summary>
111+
/// <param name="document">The original document being fixed.</param>
112+
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current compilation.</param>
113+
/// <param name="root">The original tree root belonging to the current document.</param>
114+
/// <param name="propertyDeclaration">The <see cref="PropertyDeclarationSyntax"/> for the property being updated.</param>
115+
/// <param name="fieldDeclaration">The <see cref="FieldDeclarationSyntax"/> for the declared property to remove.</param>
116+
/// <returns>An updated document with the applied code fix, and <paramref name="propertyDeclaration"/> being replaced with a partial property.</returns>
117+
private static async Task<Document> ConvertToPartialProperty(
118+
Document document,
119+
SemanticModel semanticModel,
120+
SyntaxNode root,
121+
PropertyDeclarationSyntax propertyDeclaration,
122+
FieldDeclarationSyntax fieldDeclaration)
123+
{
124+
await Task.CompletedTask;
125+
126+
// If we can't generate the new attribute list, bail (this should never happen)
127+
if (!TryGetGeneratedObservablePropertyAttributeList(document, semanticModel, out AttributeListSyntax? observablePropertyAttributeList))
128+
{
129+
return document;
130+
}
131+
132+
// Create an editor to perform all mutations
133+
SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services);
134+
135+
ConvertToPartialProperty(
136+
propertyDeclaration,
137+
fieldDeclaration,
138+
observablePropertyAttributeList,
139+
syntaxEditor);
140+
141+
// Create the new document with the single change
142+
return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot());
143+
}
144+
145+
/// <summary>
146+
/// Applies the code fix to a target identifier and returns an updated document.
147+
/// </summary>
148+
/// <param name="propertyDeclaration">The <see cref="PropertyDeclarationSyntax"/> for the property being updated.</param>
149+
/// <param name="fieldDeclaration">The <see cref="FieldDeclarationSyntax"/> for the declared property to remove.</param>
150+
/// <param name="observablePropertyAttributeList">The <see cref="AttributeListSyntax"/> with the attribute to add.</param>
151+
/// <param name="syntaxEditor">The <see cref="SyntaxEditor"/> instance to use.</param>
152+
/// <returns>An updated document with the applied code fix, and <paramref name="propertyDeclaration"/> being replaced with a partial property.</returns>
153+
private static void ConvertToPartialProperty(
154+
PropertyDeclarationSyntax propertyDeclaration,
155+
FieldDeclarationSyntax fieldDeclaration,
156+
AttributeListSyntax observablePropertyAttributeList,
157+
SyntaxEditor syntaxEditor)
158+
{
159+
// Start setting up the updated attribute lists
160+
SyntaxList<AttributeListSyntax> attributeLists = propertyDeclaration.AttributeLists;
161+
162+
if (attributeLists is [AttributeListSyntax firstAttributeListSyntax, ..])
163+
{
164+
// Remove the trivia from the original first attribute
165+
attributeLists = attributeLists.Replace(
166+
nodeInList: firstAttributeListSyntax,
167+
newNode: firstAttributeListSyntax.WithoutTrivia());
168+
169+
// If the property has at least an attribute list, move the trivia from it to the new attribute
170+
observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(firstAttributeListSyntax);
171+
172+
// Insert the new attribute
173+
attributeLists = attributeLists.Insert(0, observablePropertyAttributeList);
174+
}
175+
else
176+
{
177+
// Otherwise (there are no attribute lists), transfer the trivia to the new (only) attribute list
178+
observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(propertyDeclaration);
179+
180+
// Save the new attribute list
181+
attributeLists = attributeLists.Add(observablePropertyAttributeList);
182+
}
183+
184+
// Get a new property that is partial and with semicolon token accessors
185+
PropertyDeclarationSyntax updatedPropertyDeclaration =
186+
propertyDeclaration
187+
.AddModifiers(Token(SyntaxKind.PartialKeyword))
188+
.WithoutLeadingTrivia()
189+
.WithAttributeLists(attributeLists)
190+
.WithAdditionalAnnotations(Formatter.Annotation)
191+
.WithAccessorList(AccessorList(List(
192+
[
193+
// Keep the accessors (so we can easily keep all trivia, modifiers, attributes, etc.) but make them semicolon only
194+
propertyDeclaration.AccessorList!.Accessors[0]
195+
.WithBody(null)
196+
.WithExpressionBody(null)
197+
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
198+
.WithAdditionalAnnotations(Formatter.Annotation),
199+
propertyDeclaration.AccessorList!.Accessors[1]
200+
.WithBody(null)
201+
.WithExpressionBody(null)
202+
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
203+
.WithTrailingTrivia(propertyDeclaration.AccessorList.Accessors[1].GetTrailingTrivia())
204+
.WithAdditionalAnnotations(Formatter.Annotation)
205+
])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia()));
206+
207+
syntaxEditor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration);
208+
209+
// Also remove the field declaration (it'll be generated now)
210+
syntaxEditor.RemoveNode(fieldDeclaration);
211+
212+
// Find the parent type for the property
213+
TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf<TypeDeclarationSyntax>()!;
214+
215+
// Make sure it's partial (we create the updated node in the function to preserve the updated property declaration).
216+
// If we created it separately and replaced it, the whole tree would also be replaced, and we'd lose the new property.
217+
if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
218+
{
219+
syntaxEditor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true)));
220+
}
221+
}
222+
223+
/// <summary>
224+
/// A custom <see cref="FixAllProvider"/> with the logic from <see cref="UsePartialPropertyForSemiAutoPropertyCodeFixer"/>.
225+
/// </summary>
226+
private sealed class FixAllProvider : DocumentBasedFixAllProvider
227+
{
228+
/// <inheritdoc/>
229+
protected override async Task<Document?> FixAllAsync(FixAllContext fixAllContext, Document document, ImmutableArray<Diagnostic> diagnostics)
230+
{
231+
// Get the semantic model, as we need to resolve symbols
232+
if (await document.GetSemanticModelAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SemanticModel semanticModel)
233+
{
234+
return document;
235+
}
236+
237+
// Get the document root (this should always succeed)
238+
if (await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SyntaxNode root)
239+
{
240+
return document;
241+
}
242+
243+
// If we can't generate the new attribute list, bail (this should never happen)
244+
if (!TryGetGeneratedObservablePropertyAttributeList(document, semanticModel, out AttributeListSyntax? observablePropertyAttributeList))
245+
{
246+
return document;
247+
}
248+
249+
// Create an editor to perform all mutations (across all edits in the file)
250+
SyntaxEditor syntaxEditor = new(root, fixAllContext.Solution.Services);
251+
252+
foreach (Diagnostic diagnostic in diagnostics)
253+
{
254+
// Get the current property declaration for the diagnostic
255+
if (root.FindNode(diagnostic.Location.SourceSpan) is not PropertyDeclarationSyntax propertyDeclaration)
256+
{
257+
continue;
258+
}
259+
260+
// Also check that we can find the target field to remove
261+
if (diagnostic.AdditionalLocations is not [{ } fieldLocation] ||
262+
root.FindNode(fieldLocation.SourceSpan) is not FieldDeclarationSyntax fieldDeclaration)
263+
{
264+
continue;
265+
}
266+
267+
ConvertToPartialProperty(
268+
propertyDeclaration,
269+
fieldDeclaration,
270+
observablePropertyAttributeList,
271+
syntaxEditor);
272+
}
273+
274+
return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot());
275+
}
276+
}
277+
}

0 commit comments

Comments
 (0)