Skip to content

Commit bcc6caf

Browse files
authored
Merge pull request #1020 from CommunityToolkit/dev/semi-auto-property-fixer
Add code fixer for semi-auto property to '[ObservableProperty]' partial property
2 parents 78348ca + 5766465 commit bcc6caf

10 files changed

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

src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,4 @@ MVVMTK0052 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
9797
MVVMTK0053 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0053
9898
MVVMTK0054 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0054
9999
MVVMTK0055 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0055
100+
MVVMTK0056 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0056

src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservableValidatorValidateAllPropertiesGenerator.Execute.cs" />
4040
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.cs" />
4141
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
42+
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs" />
4243
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AsyncVoidReturningRelayCommandMethodAnalyzer.cs" />
4344
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs" />
4445
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs" />
@@ -88,6 +89,7 @@
8889
<Compile Include="$(MSBuildThisFileDirectory)Helpers\EquatableArray{T}.cs" />
8990
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HashCode.cs" />
9091
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ImmutableArrayBuilder{T}.cs" />
92+
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ObjectPool{T}.cs" />
9193
<Compile Include="$(MSBuildThisFileDirectory)Input\Models\CanExecuteExpressionType.cs" />
9294
<Compile Include="$(MSBuildThisFileDirectory)Input\Models\CommandInfo.cs" />
9395
<Compile Include="$(MSBuildThisFileDirectory)Input\RelayCommandGenerator.cs" />

0 commit comments

Comments
 (0)