Skip to content

Commit 59e4306

Browse files
committed
Add 'UseFieldDeclarationCorrectlyCodeFixer' and tests
1 parent 53f7436 commit 59e4306

File tree

4 files changed

+340
-2
lines changed

4 files changed

+340
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.Threading.Tasks;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CodeActions;
10+
using Microsoft.CodeAnalysis.CodeFixes;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
12+
using Microsoft.CodeAnalysis.Editing;
13+
using Microsoft.CodeAnalysis.Text;
14+
using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors;
15+
16+
namespace CommunityToolkit.GeneratedDependencyProperty;
17+
18+
/// <summary>
19+
/// A code fixer that updates field declarations to ensure they follow the recommended rules for dependency properties.
20+
/// </summary>
21+
[ExportCodeFixProvider(LanguageNames.CSharp)]
22+
[Shared]
23+
public sealed class UseFieldDeclarationCorrectlyCodeFixer : CodeFixProvider
24+
{
25+
/// <inheritdoc/>
26+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = [IncorrectDependencyPropertyFieldDeclarationId];
27+
28+
/// <inheritdoc/>
29+
public override FixAllProvider? GetFixAllProvider()
30+
{
31+
return WellKnownFixAllProviders.BatchFixer;
32+
}
33+
34+
/// <inheritdoc/>
35+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
36+
{
37+
Diagnostic diagnostic = context.Diagnostics[0];
38+
TextSpan diagnosticSpan = context.Span;
39+
40+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
41+
42+
// Get the property declaration and the field declaration from the target diagnostic
43+
if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf<FieldDeclarationSyntax>() is { } fieldDeclaration)
44+
{
45+
// Register the code fix to update the field to be correctly declared
46+
context.RegisterCodeFix(
47+
CodeAction.Create(
48+
title: "Declare dependency property field correctly",
49+
createChangedDocument: token => FixDependencyPropertyFieldDeclaration(context.Document, root, fieldDeclaration),
50+
equivalenceKey: "Declare dependency property field correctly"),
51+
diagnostic);
52+
}
53+
}
54+
55+
/// <summary>
56+
/// Applies the code fix to a target field declaration and returns an updated document.
57+
/// </summary>
58+
/// <param name="document">The original document being fixed.</param>
59+
/// <param name="root">The original tree root belonging to the current document.</param>
60+
/// <param name="fieldDeclaration">The <see cref="FieldDeclarationSyntax"/> to update.</param>
61+
/// <returns>An updated document with the applied code fix.</returns>
62+
private static async Task<Document> FixDependencyPropertyFieldDeclaration(Document document, SyntaxNode root, FieldDeclarationSyntax fieldDeclaration)
63+
{
64+
await Task.CompletedTask;
65+
66+
SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services);
67+
68+
// We use the lambda overload mostly for convenient, so we can easily get a generator to use
69+
syntaxEditor.ReplaceNode(fieldDeclaration, (node, generator) =>
70+
{
71+
// Update the field to ensure it's declared as 'public static readonly'
72+
node = generator.WithAccessibility(node, Accessibility.Public);
73+
node = generator.WithModifiers(node, DeclarationModifiers.Static | DeclarationModifiers.ReadOnly);
74+
75+
// If the type is declared as nullable, unwrap it and remove the annotation.
76+
// We need to make sure to carry the space after the element type. When the
77+
// type is nullable, that space is attached to the question mark token.
78+
if (((FieldDeclarationSyntax)node).Declaration is { Type: NullableTypeSyntax { ElementType: { } fieldElementType } nullableType } variableDeclaration)
79+
{
80+
TypeSyntax typeDeclaration = fieldElementType.WithTrailingTrivia(nullableType.QuestionToken.TrailingTrivia);
81+
82+
node = ((FieldDeclarationSyntax)node).WithDeclaration(variableDeclaration.WithType(typeDeclaration));
83+
}
84+
85+
return node;
86+
});
87+
88+
// Create the new document with the single change
89+
return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot());
90+
}
91+
}

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis
182182
}
183183

184184
/// <summary>
185-
/// Applies the code fix to a target identifier and returns an updated document.
185+
/// Applies the code fix to a target property declaration and returns an updated document.
186186
/// </summary>
187187
/// <param name="document">The original document being fixed.</param>
188188
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current compilation.</param>

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ internal static class DiagnosticDescriptors
1616
/// </summary>
1717
public const string UseGeneratedDependencyPropertyForManualPropertyId = "WCTDP0017";
1818

19+
/// <summary>
20+
/// The diagnostic id for <see cref="IncorrectDependencyPropertyFieldDeclaration"/>.
21+
/// </summary>
22+
public const string IncorrectDependencyPropertyFieldDeclarationId = "WCTDP0020";
23+
1924
/// <summary>
2025
/// <c>"The property '{0}' cannot be used to generate a dependency property, as its declaration is not valid (it must be an instance (non static) partial property, with a getter and a setter that is not init-only)"</c>.
2126
/// </summary>
@@ -267,7 +272,7 @@ internal static class DiagnosticDescriptors
267272
/// <c>"The field '{0}' is a dependency property, but it is not declared correctly (all dependency property fields should be declared as 'public static readonly', and not be nullable)"</c>.
268273
/// </summary>
269274
public static readonly DiagnosticDescriptor IncorrectDependencyPropertyFieldDeclaration = new(
270-
id: "WCTDP0020",
275+
id: IncorrectDependencyPropertyFieldDeclarationId,
271276
title: "Incorrect dependency property field declaration",
272277
messageFormat: "The field '{0}' is a dependency property, but it is not declared correctly (all dependency property fields should be declared as 'public static readonly', and not be nullable)",
273278
category: typeof(DependencyPropertyGenerator).FullName,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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.Threading.Tasks;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.VisualStudio.TestTools.UnitTesting;
8+
using CSharpCodeFixTest = CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers.CSharpCodeFixTest<
9+
CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationCorrectlyAnalyzer,
10+
CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationCorrectlyCodeFixer>;
11+
12+
namespace CommunityToolkit.GeneratedDependencyProperty.Tests;
13+
14+
[TestClass]
15+
public class Test_UseFieldDeclarationCorrectlyCodeFixer
16+
{
17+
[TestMethod]
18+
[DataRow("private static readonly DependencyProperty")]
19+
[DataRow("public readonly DependencyProperty")]
20+
[DataRow("public static DependencyProperty")]
21+
[DataRow("public static volatile DependencyProperty")]
22+
[DataRow("public static readonly DependencyProperty?")]
23+
public async Task SingleField(string fieldDeclaration)
24+
{
25+
string original = $$"""
26+
using Windows.UI.Xaml;
27+
28+
#nullable enable
29+
30+
namespace MyApp;
31+
32+
public class MyObject : DependencyObject
33+
{
34+
{{fieldDeclaration}} [|TestProperty|];
35+
}
36+
""";
37+
38+
const string @fixed = """
39+
using Windows.UI.Xaml;
40+
41+
#nullable enable
42+
43+
namespace MyApp;
44+
45+
public class MyObject : DependencyObject
46+
{
47+
public static readonly DependencyProperty TestProperty;
48+
}
49+
""";
50+
51+
CSharpCodeFixTest test = new(LanguageVersion.Preview)
52+
{
53+
TestCode = original,
54+
FixedCode = @fixed
55+
};
56+
57+
await test.RunAsync();
58+
}
59+
60+
[TestMethod]
61+
[DataRow("private static readonly DependencyProperty")]
62+
[DataRow("public readonly DependencyProperty")]
63+
[DataRow("public static DependencyProperty")]
64+
[DataRow("public static volatile DependencyProperty")]
65+
[DataRow("public static readonly DependencyProperty?")]
66+
public async Task SingleField_WithInitializer(string fieldDeclaration)
67+
{
68+
string original = $$"""
69+
using Windows.UI.Xaml;
70+
71+
#nullable enable
72+
73+
namespace MyApp;
74+
75+
public class MyObject : DependencyObject
76+
{
77+
{{fieldDeclaration}} [|TestProperty|] = DependencyProperty.Register(
78+
"Test",
79+
typeof(string),
80+
typeof(MyObject),
81+
null);
82+
}
83+
""";
84+
85+
const string @fixed = """
86+
using Windows.UI.Xaml;
87+
88+
#nullable enable
89+
90+
namespace MyApp;
91+
92+
public class MyObject : DependencyObject
93+
{
94+
public static readonly DependencyProperty TestProperty = DependencyProperty.Register(
95+
"Test",
96+
typeof(string),
97+
typeof(MyObject),
98+
null);
99+
}
100+
""";
101+
102+
CSharpCodeFixTest test = new(LanguageVersion.Preview)
103+
{
104+
TestCode = original,
105+
FixedCode = @fixed
106+
};
107+
108+
await test.RunAsync();
109+
}
110+
111+
[TestMethod]
112+
public async Task MultipleFields()
113+
{
114+
string original = $$"""
115+
using Windows.UI.Xaml;
116+
117+
#nullable enable
118+
119+
namespace MyApp;
120+
121+
public class MyObject : DependencyObject
122+
{
123+
private static readonly DependencyProperty [|Test1Property|];
124+
public readonly DependencyProperty [|Test2Property|];
125+
public static DependencyProperty [|Test3Property|];
126+
public static readonly DependencyProperty Test4Property;
127+
public static volatile DependencyProperty [|Test5Property|];
128+
public static readonly DependencyProperty? [|Test6Property|];
129+
public static readonly DependencyProperty Test7Property;
130+
public static readonly DependencyProperty Test8Property;
131+
}
132+
""";
133+
134+
const string @fixed = """
135+
using Windows.UI.Xaml;
136+
137+
#nullable enable
138+
139+
namespace MyApp;
140+
141+
public class MyObject : DependencyObject
142+
{
143+
public static readonly DependencyProperty Test1Property;
144+
public static readonly DependencyProperty Test2Property;
145+
public static readonly DependencyProperty Test3Property;
146+
public static readonly DependencyProperty Test4Property;
147+
public static readonly DependencyProperty Test5Property;
148+
public static readonly DependencyProperty Test6Property;
149+
public static readonly DependencyProperty Test7Property;
150+
public static readonly DependencyProperty Test8Property;
151+
}
152+
""";
153+
154+
CSharpCodeFixTest test = new(LanguageVersion.Preview)
155+
{
156+
TestCode = original,
157+
FixedCode = @fixed
158+
};
159+
160+
await test.RunAsync();
161+
}
162+
163+
[TestMethod]
164+
public async Task MultipleFields_WithInitializers()
165+
{
166+
string original = $$"""
167+
using Windows.UI.Xaml;
168+
169+
#nullable enable
170+
171+
namespace MyApp;
172+
173+
public class MyObject : DependencyObject
174+
{
175+
private static readonly DependencyProperty [|Test1Property|] = DependencyProperty.Register(
176+
"Test1",
177+
typeof(string),
178+
typeof(MyObject),
179+
null);
180+
181+
public readonly DependencyProperty [|Test2Property|] = DependencyProperty.Register(
182+
"Test2",
183+
typeof(string),
184+
typeof(MyObject),
185+
null);
186+
187+
public static DependencyProperty [|Test3Property|];
188+
public static readonly DependencyProperty Test4Property = DependencyProperty.Register("Test4", typeof(string), typeof(MyObject), null);
189+
public static volatile DependencyProperty [|Test5Property|];
190+
public static readonly DependencyProperty? [|Test6Property|] = DependencyProperty.Register("Test6", typeof(string), typeof(MyObject), null);
191+
public static readonly DependencyProperty Test7Property;
192+
public static readonly DependencyProperty Test8Property = DependencyProperty.Register(
193+
"Test8",
194+
typeof(string),
195+
typeof(MyObject),
196+
null);
197+
}
198+
""";
199+
200+
const string @fixed = """
201+
using Windows.UI.Xaml;
202+
203+
#nullable enable
204+
205+
namespace MyApp;
206+
207+
public class MyObject : DependencyObject
208+
{
209+
public static readonly DependencyProperty Test1Property = DependencyProperty.Register(
210+
"Test1",
211+
typeof(string),
212+
typeof(MyObject),
213+
null);
214+
215+
public static readonly DependencyProperty Test2Property = DependencyProperty.Register(
216+
"Test2",
217+
typeof(string),
218+
typeof(MyObject),
219+
null);
220+
221+
public static readonly DependencyProperty Test3Property;
222+
public static readonly DependencyProperty Test4Property = DependencyProperty.Register("Test4", typeof(string), typeof(MyObject), null);
223+
public static readonly DependencyProperty Test5Property;
224+
public static readonly DependencyProperty Test6Property = DependencyProperty.Register("Test6", typeof(string), typeof(MyObject), null);
225+
public static readonly DependencyProperty Test7Property;
226+
public static readonly DependencyProperty Test8Property = DependencyProperty.Register(
227+
"Test8",
228+
typeof(string),
229+
typeof(MyObject),
230+
null);
231+
}
232+
""";
233+
234+
CSharpCodeFixTest test = new(LanguageVersion.Preview)
235+
{
236+
TestCode = original,
237+
FixedCode = @fixed
238+
};
239+
240+
await test.RunAsync();
241+
}
242+
}

0 commit comments

Comments
 (0)