Skip to content

Commit fc423c5

Browse files
committed
Improve code fixer for known enum members
1 parent 4873405 commit fc423c5

File tree

3 files changed

+148
-14
lines changed

3 files changed

+148
-14
lines changed

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

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,71 @@ private static bool TryGetGeneratedDependencyPropertyAttributeList(
114114
return true;
115115
}
116116

117+
/// <summary>
118+
/// Updates an <see cref="AttributeListSyntax"/> for the <c>[GeneratedDependencyProperty]</c> attribute with the right default value.
119+
/// </summary>
120+
/// <param name="document">The original document being fixed.</param>
121+
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current compilation.</param>
122+
/// <param name="defaultValueExpression">The expression for the default value of the property, if present</param>
123+
/// <returns>The updated attribute syntax.</returns>
124+
private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeList(
125+
Document document,
126+
SemanticModel semanticModel,
127+
AttributeListSyntax generatedDependencyPropertyAttributeList,
128+
string? defaultValueExpression)
129+
{
130+
// If we do have a default value expression, set it in the attribute.
131+
// We extract the generated attribute so we can add the new argument.
132+
// It's important to reuse it, as it has the "add usings" annotation.
133+
if (defaultValueExpression is not null)
134+
{
135+
ExpressionSyntax parsedExpression = ParseExpression(defaultValueExpression);
136+
137+
// Special case values which are simple enum member accesses, like 'global::Windows.UI.Xaml.Visibility.Collapsed'
138+
if (parsedExpression is MemberAccessExpressionSyntax { Expression: { } expressionSyntax, Name: IdentifierNameSyntax { Identifier.Text: { } memberName } })
139+
{
140+
string fullyQualifiedTypeName = expressionSyntax.ToFullString();
141+
142+
// Ensure we strip the global prefix, if present (it should always be present)
143+
if (fullyQualifiedTypeName.StartsWith("global::"))
144+
{
145+
fullyQualifiedTypeName = fullyQualifiedTypeName["global::".Length..];
146+
}
147+
148+
// Try to resolve the attribute type, if present. This API takes a fully qualified metadata name, not
149+
// a fully qualified type name. However, for virtually all cases for enum types, the two should match.
150+
// That is, they will be the same if the type is not nested, and not generic, which is what we expect.
151+
if (semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedTypeName) is INamedTypeSymbol enumTypeSymbol)
152+
{
153+
SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document);
154+
155+
// Create the identifier syntax for the enum type, with the right annotations
156+
SyntaxNode enumTypeSyntax = syntaxGenerator.TypeExpression(enumTypeSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation);
157+
158+
// Create the member access expression for the target enum type
159+
SyntaxNode enumMemberAccessExpressionSyntax = syntaxGenerator.MemberAccessExpression(enumTypeSyntax, memberName);
160+
161+
// Create the attribute argument to insert
162+
SyntaxNode attributeArgumentSyntax = syntaxGenerator.AttributeArgument("DefaultValue", enumMemberAccessExpressionSyntax);
163+
164+
// Actually add the argument to the existing attribute syntax
165+
return (AttributeListSyntax)syntaxGenerator.AddAttributeArguments(generatedDependencyPropertyAttributeList, [attributeArgumentSyntax]);
166+
}
167+
}
168+
169+
// Otherwise, just add the new default value normally
170+
return
171+
AttributeList(SingletonSeparatedList(
172+
generatedDependencyPropertyAttributeList.Attributes[0]
173+
.AddArgumentListArguments(
174+
AttributeArgument(ParseExpression(defaultValueExpression))
175+
.WithNameEquals(NameEquals(IdentifierName("DefaultValue"))))));
176+
}
177+
178+
// If we have no value expression, we can just reuse the attribute with no changes
179+
return generatedDependencyPropertyAttributeList;
180+
}
181+
117182
/// <summary>
118183
/// Applies the code fix to a target identifier and returns an updated document.
119184
/// </summary>
@@ -144,6 +209,8 @@ private static async Task<Document> ConvertToPartialProperty(
144209
SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services);
145210

146211
ConvertToPartialProperty(
212+
document,
213+
semanticModel,
147214
propertyDeclaration,
148215
fieldDeclaration,
149216
generatedDependencyPropertyAttributeList,
@@ -157,31 +224,29 @@ private static async Task<Document> ConvertToPartialProperty(
157224
/// <summary>
158225
/// Applies the code fix to a target identifier and returns an updated document.
159226
/// </summary>
227+
/// <param name="document">The original document being fixed.</param>
228+
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current compilation.</param>
160229
/// <param name="propertyDeclaration">The <see cref="PropertyDeclarationSyntax"/> for the property being updated.</param>
161230
/// <param name="fieldDeclaration">The <see cref="FieldDeclarationSyntax"/> for the declared property to remove.</param>
162231
/// <param name="generatedDependencyPropertyAttributeList">The <see cref="AttributeListSyntax"/> with the attribute to add.</param>
163232
/// <param name="syntaxEditor">The <see cref="SyntaxEditor"/> instance to use.</param>
164233
/// <param name="defaultValueExpression">The expression for the default value of the property, if present</param>
165234
/// <returns>An updated document with the applied code fix, and <paramref name="propertyDeclaration"/> being replaced with a partial property.</returns>
166235
private static void ConvertToPartialProperty(
236+
Document document,
237+
SemanticModel semanticModel,
167238
PropertyDeclarationSyntax propertyDeclaration,
168239
FieldDeclarationSyntax fieldDeclaration,
169240
AttributeListSyntax generatedDependencyPropertyAttributeList,
170241
SyntaxEditor syntaxEditor,
171242
string? defaultValueExpression)
172243
{
173-
// If we do have a default value expression, set it in the attribute.
174-
// We extract the generated attribute so we can add the new argument.
175-
// It's important to reuse it, as it has the "add usings" annotation.
176-
if (defaultValueExpression is not null)
177-
{
178-
generatedDependencyPropertyAttributeList =
179-
AttributeList(SingletonSeparatedList(
180-
generatedDependencyPropertyAttributeList.Attributes[0]
181-
.AddArgumentListArguments(
182-
AttributeArgument(ParseExpression(defaultValueExpression))
183-
.WithNameEquals(NameEquals(IdentifierName("DefaultValue"))))));
184-
}
244+
// Update the attribute to insert with the default value, if present
245+
generatedDependencyPropertyAttributeList = UpdateGeneratedDependencyPropertyAttributeList(
246+
document,
247+
semanticModel,
248+
generatedDependencyPropertyAttributeList,
249+
defaultValueExpression);
185250

186251
// Start setting up the updated attribute lists
187252
SyntaxList<AttributeListSyntax> attributeLists = propertyDeclaration.AttributeLists;
@@ -295,6 +360,8 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider
295360
string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName];
296361

297362
ConvertToPartialProperty(
363+
document,
364+
semanticModel,
298365
propertyDeclaration,
299366
fieldDeclaration,
300367
generatedDependencyPropertyAttributeList,

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public static bool TryGetEnumFieldName(this ITypeSymbol symbol, object value, [N
7979
continue;
8080
}
8181

82-
if (fieldValue == value)
82+
if (fieldValue.Equals(value))
8383
{
8484
fieldName = fieldSymbol.Name;
8585

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,9 @@ public partial class MyControl : Control
290290
[DataRow("int", "int", "42")]
291291
[DataRow("int?", "int?", "0")]
292292
[DataRow("int?", "int?", "42")]
293-
[DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")]
293+
[DataRow("Visibility", "Visibility", "Visibility.Collapsed")]
294+
[DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "(global::MyApp.MyEnum)5")]
295+
[DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "(global::MyApp.MyEnum)(-5)")]
294296
public async Task SimpleProperty_WithExplicitValue_NotDefault(
295297
string dependencyPropertyType,
296298
string propertyType,
@@ -318,6 +320,8 @@ public partial class MyControl : Control
318320
set => SetValue(NameProperty, value);
319321
}
320322
}
323+
324+
public enum MyEnum { A }
321325
""";
322326

323327
string @fixed = $$"""
@@ -334,6 +338,69 @@ public partial class MyControl : Control
334338
[GeneratedDependencyProperty(DefaultValue = {{defaultValueExpression}})]
335339
public partial {{propertyType}} {|CS9248:Name|} { get; set; }
336340
}
341+
342+
public enum MyEnum { A }
343+
""";
344+
345+
CSharpCodeFixTest test = new(LanguageVersion.Preview)
346+
{
347+
TestCode = original,
348+
FixedCode = @fixed,
349+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
350+
TestState = { AdditionalReferences =
351+
{
352+
MetadataReference.CreateFromFile(typeof(Point).Assembly.Location),
353+
MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location),
354+
MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location),
355+
MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)
356+
}}
357+
};
358+
359+
await test.RunAsync();
360+
}
361+
362+
[TestMethod]
363+
public async Task SimpleProperty_WithExplicitValue_NotDefault_AddsNamespace()
364+
{
365+
string original = $$"""
366+
using Windows.UI.Xaml;
367+
using Windows.UI.Xaml.Controls;
368+
369+
#nullable enable
370+
371+
namespace MyApp;
372+
373+
public partial class MyControl : Control
374+
{
375+
public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
376+
name: "Name",
377+
propertyType: typeof(Windows.UI.Xaml.Automation.AnnotationType),
378+
ownerType: typeof(MyControl),
379+
typeMetadata: new PropertyMetadata(Windows.UI.Xaml.Automation.AnnotationType.TrackChanges));
380+
381+
public Windows.UI.Xaml.Automation.AnnotationType [|Name|]
382+
{
383+
get => (Windows.UI.Xaml.Automation.AnnotationType)GetValue(NameProperty);
384+
set => SetValue(NameProperty, value);
385+
}
386+
}
387+
""";
388+
389+
string @fixed = $$"""
390+
using CommunityToolkit.WinUI;
391+
using Windows.UI.Xaml;
392+
using Windows.UI.Xaml.Automation;
393+
using Windows.UI.Xaml.Controls;
394+
395+
#nullable enable
396+
397+
namespace MyApp;
398+
399+
public partial class MyControl : Control
400+
{
401+
[GeneratedDependencyProperty(DefaultValue = AnnotationType.TrackChanges)]
402+
public partial Windows.UI.Xaml.Automation.AnnotationType {|CS9248:Name|} { get; set; }
403+
}
337404
""";
338405

339406
CSharpCodeFixTest test = new(LanguageVersion.Preview)

0 commit comments

Comments
 (0)