Skip to content

Commit a91e7a8

Browse files
committed
Handle adding using directives if needed
1 parent df714ea commit a91e7a8

File tree

2 files changed

+241
-12
lines changed

2 files changed

+241
-12
lines changed

src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs

Lines changed: 109 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Microsoft.CodeAnalysis.CSharp.Syntax;
1616
using Microsoft.CodeAnalysis.Editing;
1717
using Microsoft.CodeAnalysis.Formatting;
18+
using Microsoft.CodeAnalysis.Simplification;
1819
using Microsoft.CodeAnalysis.Text;
1920
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
2021
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
@@ -32,9 +33,9 @@ public sealed class UsePartialPropertyForSemiAutoPropertyCodeFixer : CodeFixProv
3233
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoPropertyId);
3334

3435
/// <inheritdoc/>
35-
public override FixAllProvider? GetFixAllProvider()
36+
public override Microsoft.CodeAnalysis.CodeFixes.FixAllProvider? GetFixAllProvider()
3637
{
37-
return WellKnownFixAllProviders.BatchFixer;
38+
return new FixAllProvider();
3839
}
3940

4041
/// <inheritdoc/>
@@ -43,16 +44,31 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
4344
Diagnostic diagnostic = context.Diagnostics[0];
4445
TextSpan diagnosticSpan = context.Span;
4546

47+
// This code fixer needs the semantic model, so check that first
48+
if (!context.Document.SupportsSemanticModel)
49+
{
50+
return;
51+
}
52+
4653
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
4754

4855
// Get the property declaration from the target diagnostic
4956
if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration)
5057
{
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+
5167
// Register the code fix to update the semi-auto property to a partial property
5268
context.RegisterCodeFix(
5369
CodeAction.Create(
5470
title: "Use a partial property",
55-
createChangedDocument: token => ConvertToPartialProperty(context.Document, root, propertyDeclaration),
71+
createChangedDocument: token => ConvertToPartialProperty(context.Document, root, propertyDeclaration, observablePropertySymbol),
5672
equivalenceKey: "Use a partial property"),
5773
diagnostic);
5874
}
@@ -64,14 +80,47 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
6480
/// <param name="document">The original document being fixed.</param>
6581
/// <param name="root">The original tree root belonging to the current document.</param>
6682
/// <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>
6784
/// <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)
85+
private static async Task<Document> ConvertToPartialProperty(
86+
Document document,
87+
SyntaxNode root,
88+
PropertyDeclarationSyntax propertyDeclaration,
89+
INamedTypeSymbol observablePropertySymbol)
6990
{
7091
await Task.CompletedTask;
7192

72-
// Prepare the [ObservableProperty] attribute, which is always inserted first
73-
AttributeListSyntax observablePropertyAttributeList = AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ObservableProperty"))));
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);
7499

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+
{
75124
// Start setting up the updated attribute lists
76125
SyntaxList<AttributeListSyntax> attributeLists = propertyDeclaration.AttributeLists;
77126

@@ -120,10 +169,7 @@ private static async Task<Document> ConvertToPartialProperty(Document document,
120169
.WithAdditionalAnnotations(Formatter.Annotation)
121170
])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia()));
122171

123-
// Create an editor to perform all mutations
124-
SyntaxEditor editor = new(root, document.Project.Solution.Workspace.Services);
125-
126-
editor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration);
172+
syntaxEditor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration);
127173

128174
// Find the parent type for the property
129175
TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf<TypeDeclarationSyntax>()!;
@@ -132,10 +178,61 @@ private static async Task<Document> ConvertToPartialProperty(Document document,
132178
// If we created it separately and replaced it, the whole tree would also be replaced, and we'd lose the new property.
133179
if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
134180
{
135-
editor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true)));
181+
syntaxEditor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true)));
136182
}
183+
}
137184

138-
return document.WithSyntaxRoot(editor.GetChangedRoot());
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+
}
139236
}
140237
}
141238

tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,57 @@ public partial class SampleViewModel : ObservableObject
127127
await test.RunAsync();
128128
}
129129

130+
[TestMethod]
131+
public async Task SimpleProperty_WithMissingUsingDirective()
132+
{
133+
string original = """
134+
namespace MyApp;
135+
136+
public class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
137+
{
138+
public string Name
139+
{
140+
get => field;
141+
set => SetProperty(ref field, value);
142+
}
143+
}
144+
""";
145+
146+
string @fixed = """
147+
using CommunityToolkit.Mvvm.ComponentModel;
148+
149+
namespace MyApp;
150+
151+
public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
152+
{
153+
[ObservableProperty]
154+
public partial string Name { get; set; }
155+
}
156+
""";
157+
158+
CSharpCodeFixTest test = new(LanguageVersion.Preview)
159+
{
160+
TestCode = original,
161+
FixedCode = @fixed,
162+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
163+
};
164+
165+
test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
166+
test.ExpectedDiagnostics.AddRange(new[]
167+
{
168+
// /0/Test0.cs(5,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
169+
CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 19, 5, 23).WithArguments("MyApp.SampleViewModel", "Name"),
170+
});
171+
172+
test.FixedState.ExpectedDiagnostics.AddRange(new[]
173+
{
174+
// /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part.
175+
DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"),
176+
});
177+
178+
await test.RunAsync();
179+
}
180+
130181
[TestMethod]
131182
public async Task SimpleProperty_WithLeadingTrivia()
132183
{
@@ -582,6 +633,87 @@ public partial class SampleViewModel : ObservableObject
582633
await test.RunAsync();
583634
}
584635

636+
[TestMethod]
637+
public async Task SimpleProperty_Multiple_WithMissingUsingDirective()
638+
{
639+
string original = """
640+
namespace MyApp;
641+
642+
public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
643+
{
644+
public string FirstName
645+
{
646+
get => field;
647+
set => SetProperty(ref field, value);
648+
}
649+
650+
public string LastName
651+
{
652+
get => field;
653+
set => SetProperty(ref field, value);
654+
}
655+
656+
public string PhoneNumber
657+
{
658+
get;
659+
set => SetProperty(ref field, value);
660+
}
661+
}
662+
""";
663+
664+
string @fixed = """
665+
using CommunityToolkit.Mvvm.ComponentModel;
666+
667+
namespace MyApp;
668+
669+
public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
670+
{
671+
[ObservableProperty]
672+
public partial string FirstName { get; set; }
673+
674+
[ObservableProperty]
675+
public partial string LastName { get; set; }
676+
677+
[ObservableProperty]
678+
public partial string PhoneNumber { get; set; }
679+
}
680+
""";
681+
682+
CSharpCodeFixTest test = new(LanguageVersion.Preview)
683+
{
684+
TestCode = original,
685+
FixedCode = @fixed,
686+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
687+
};
688+
689+
test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
690+
test.ExpectedDiagnostics.AddRange(new[]
691+
{
692+
// /0/Test0.cs(5,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
693+
CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 19, 5, 28).WithArguments("MyApp.SampleViewModel", "FirstName"),
694+
695+
// /0/Test0.cs(11,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
696+
CSharpCodeFixVerifier.Diagnostic().WithSpan(11, 19, 11, 27).WithArguments("MyApp.SampleViewModel", "LastName"),
697+
698+
// /0/Test0.cs(17,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.PhoneNumber can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
699+
CSharpCodeFixVerifier.Diagnostic().WithSpan(17, 19, 17, 30).WithArguments("MyApp.SampleViewModel", "PhoneNumber"),
700+
});
701+
702+
test.FixedState.ExpectedDiagnostics.AddRange(new[]
703+
{
704+
// /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part.
705+
DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"),
706+
707+
// /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part.
708+
DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 35).WithArguments("MyApp.SampleViewModel.LastName"),
709+
710+
// /0/Test0.cs(14,27): error CS9248: Partial property 'SampleViewModel.PhoneNumber' must have an implementation part.
711+
DiagnosticResult.CompilerError("CS9248").WithSpan(14, 27, 14, 38).WithArguments("MyApp.SampleViewModel.PhoneNumber"),
712+
});
713+
714+
await test.RunAsync();
715+
}
716+
585717
[TestMethod]
586718
public async Task SimpleProperty_WithinPartialType_Multiple_MixedScenario()
587719
{

0 commit comments

Comments
 (0)