Skip to content

Commit

Permalink
Handle nested enum types in code fixer
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergio0694 committed Dec 28, 2024
1 parent df3c72d commit 0d01d5c
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)

// Retrieve the properties passed by the analyzer
string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName];
string? defaultValueTypeFullyQualifiedMetadataName = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeFullyQualifiedMetadataNamePropertyName];

SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

Expand All @@ -80,7 +81,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
root,
propertyDeclaration,
fieldDeclaration,
defaultValue),
defaultValue,
defaultValueTypeFullyQualifiedMetadataName),
equivalenceKey: "Use a partial property"),
diagnostic);
}
Expand Down Expand Up @@ -122,12 +124,14 @@ private static bool TryGetGeneratedDependencyPropertyAttributeList(
/// <param name="document">The original document being fixed.</param>
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current compilation.</param>
/// <param name="defaultValueExpression">The expression for the default value of the property, if present</param>
/// <param name="defaultValueTypeFullyQualifiedMetadataName">The fully qualified metadata name of the default value, if present.</param>
/// <returns>The updated attribute syntax.</returns>
private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeList(
Document document,
SemanticModel semanticModel,
AttributeListSyntax generatedDependencyPropertyAttributeList,
string? defaultValueExpression)
string? defaultValueExpression,
string? defaultValueTypeFullyQualifiedMetadataName)
{
// If we do have a default value expression, set it in the attribute.
// We extract the generated attribute so we can add the new argument.
Expand All @@ -139,18 +143,19 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis
// Special case values which are simple enum member accesses, like 'global::Windows.UI.Xaml.Visibility.Collapsed'
if (parsedExpression is MemberAccessExpressionSyntax { Expression: { } expressionSyntax, Name: IdentifierNameSyntax { Identifier.Text: { } memberName } })
{
string fullyQualifiedTypeName = expressionSyntax.ToFullString();
string fullyQualifiedMetadataName = defaultValueTypeFullyQualifiedMetadataName ?? expressionSyntax.ToFullString();

// Ensure we strip the global prefix, if present (it should always be present)
if (fullyQualifiedTypeName.StartsWith("global::"))
// Ensure we strip the global prefix, if present (it should always be present if we didn't have a metadata name).
// Note that using the fully qualified type name is just a fallback, as we should always have the metadata name.
if (fullyQualifiedMetadataName.StartsWith("global::"))
{
fullyQualifiedTypeName = fullyQualifiedTypeName["global::".Length..];
fullyQualifiedMetadataName = fullyQualifiedMetadataName["global::".Length..];
}

// Try to resolve the attribute type, if present. This API takes a fully qualified metadata name, not
// a fully qualified type name. However, for virtually all cases for enum types, the two should match.
// That is, they will be the same if the type is not nested, and not generic, which is what we expect.
if (semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedTypeName) is INamedTypeSymbol enumTypeSymbol)
if (semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) is INamedTypeSymbol enumTypeSymbol)
{
SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document);

Expand Down Expand Up @@ -190,14 +195,16 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis
/// <param name="propertyDeclaration">The <see cref="PropertyDeclarationSyntax"/> for the property being updated.</param>
/// <param name="fieldDeclaration">The <see cref="FieldDeclarationSyntax"/> for the declared property to remove.</param>
/// <param name="defaultValueExpression">The expression for the default value of the property, if present</param>
/// <param name="defaultValueTypeFullyQualifiedMetadataName">The fully qualified metadata name of the default value, if present.</param>
/// <returns>An updated document with the applied code fix, and <paramref name="propertyDeclaration"/> being replaced with a partial property.</returns>
private static async Task<Document> ConvertToPartialProperty(
Document document,
SemanticModel semanticModel,
SyntaxNode root,
PropertyDeclarationSyntax propertyDeclaration,
FieldDeclarationSyntax fieldDeclaration,
string? defaultValueExpression)
string? defaultValueExpression,
string? defaultValueTypeFullyQualifiedMetadataName)
{
await Task.CompletedTask;

Expand All @@ -217,7 +224,8 @@ private static async Task<Document> ConvertToPartialProperty(
fieldDeclaration,
generatedDependencyPropertyAttributeList,
syntaxEditor,
defaultValueExpression);
defaultValueExpression,
defaultValueTypeFullyQualifiedMetadataName);

RemoveLeftoverLeadingEndOfLines([fieldDeclaration], syntaxEditor);

Expand All @@ -235,6 +243,7 @@ private static async Task<Document> ConvertToPartialProperty(
/// <param name="generatedDependencyPropertyAttributeList">The <see cref="AttributeListSyntax"/> with the attribute to add.</param>
/// <param name="syntaxEditor">The <see cref="SyntaxEditor"/> instance to use.</param>
/// <param name="defaultValueExpression">The expression for the default value of the property, if present</param>
/// <param name="defaultValueTypeFullyQualifiedMetadataName">The fully qualified metadata name of the default value, if present.</param>
/// <returns>An updated document with the applied code fix, and <paramref name="propertyDeclaration"/> being replaced with a partial property.</returns>
private static void ConvertToPartialProperty(
Document document,
Expand All @@ -243,7 +252,8 @@ private static void ConvertToPartialProperty(
FieldDeclarationSyntax fieldDeclaration,
AttributeListSyntax generatedDependencyPropertyAttributeList,
SyntaxEditor syntaxEditor,
string? defaultValueExpression)
string? defaultValueExpression,
string? defaultValueTypeFullyQualifiedMetadataName)
{
// Replace the property with the partial property using the attribute. Note that it's important to use the
// lambda 'ReplaceNode' overload here, rather than creating a modifier property declaration syntax node and
Expand All @@ -258,7 +268,8 @@ private static void ConvertToPartialProperty(
document,
semanticModel,
generatedDependencyPropertyAttributeList,
defaultValueExpression);
defaultValueExpression,
defaultValueTypeFullyQualifiedMetadataName);

// Start setting up the updated attribute lists
SyntaxList<AttributeListSyntax> attributeLists = propertyDeclaration.AttributeLists;
Expand Down Expand Up @@ -459,6 +470,8 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider

// Retrieve the properties passed by the analyzer
string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName];
string? defaultValueTypeFullyQualifiedMetadataName = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeFullyQualifiedMetadataNamePropertyName];


ConvertToPartialProperty(
document,
Expand All @@ -467,7 +480,8 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider
fieldDeclaration,
generatedDependencyPropertyAttributeList,
syntaxEditor,
defaultValue);
defaultValue,
defaultValueTypeFullyQualifiedMetadataName);

fieldDeclarations.Add(fieldDeclaration);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public sealed class UseGeneratedDependencyPropertyOnManualPropertyAnalyzer : Dia
/// </summary>
public const string DefaultValuePropertyName = "DefaultValue";

/// <summary>
/// The property name for the fully qualified metadata name of the default value, if present.
/// </summary>
public const string DefaultValueTypeFullyQualifiedMetadataNamePropertyName = "DefaultValueTypeFullyQualifiedMetadataName";

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [UseGeneratedDependencyPropertyForManualProperty];

Expand Down Expand Up @@ -449,19 +454,27 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla
}
else if (TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue))
{
// We have found a valid constant. As an optimization, we check whether the constant was the value
// of some projected built-in WinRT enum type (ie. not any user-defined enum type). If that is the
// case, the XAML infrastructure can default that values automatically, meaning we can skip the
// overhead of instantiating a 'PropertyMetadata' instance in code, and marshalling default value.
if (conversionOperation.Operand.Type is { TypeKind: TypeKind.Enum } operandType &&
operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)))
// We have found a valid constant. If it's an enum type, we have a couple special cases to handle.
if (conversionOperation.Operand.Type is { TypeKind: TypeKind.Enum } operandType)
{
// Before actually enabling the optimization, validate that the default value is actually
// the same as the default value of the enum (ie. the value of its first declared field).
if (operandType.TryGetDefaultValueForEnumType(out object? defaultValue) &&
conversionOperation.Operand.ConstantValue.Value == defaultValue)
// As an optimization, we check whether the constant was the value
// of some projected built-in WinRT enum type (ie. not any user-defined enum type). If that is the
// case, the XAML infrastructure can default that values automatically, meaning we can skip the
// overhead of instantiating a 'PropertyMetadata' instance in code, and marshalling default value.
if (operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)))
{
// Before actually enabling the optimization, validate that the default value is actually
// the same as the default value of the enum (ie. the value of its first declared field).
if (operandType.TryGetDefaultValueForEnumType(out object? defaultValue) &&
conversionOperation.Operand.ConstantValue.Value == defaultValue)
{
fieldFlags.DefaultValue = null;
}
}
else if (operandType.ContainingType is not null)
{
fieldFlags.DefaultValue = null;
// If the enum is nested, we need to also
fieldFlags.DefaultValueTypeFullyQualifiedMetadataName = operandType.GetFullyQualifiedMetadataName();
}
}
}
Expand Down Expand Up @@ -552,7 +565,9 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla
UseGeneratedDependencyPropertyForManualProperty,
pair.Key.Locations.FirstOrDefault(),
[fieldLocation],
ImmutableDictionary.Create<string, string?>().Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()),
ImmutableDictionary.Create<string, string?>()
.Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString())
.Add(DefaultValueTypeFullyQualifiedMetadataNamePropertyName, fieldFlags.DefaultValueTypeFullyQualifiedMetadataName),
pair.Key));
}
}
Expand All @@ -573,6 +588,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla
fieldFlags.PropertyName = null;
fieldFlags.PropertyType = null;
fieldFlags.DefaultValue = null;
fieldFlags.DefaultValueTypeFullyQualifiedMetadataName = null;
fieldFlags.FieldLocation = null;

fieldFlagsStack.Push(fieldFlags);
Expand Down Expand Up @@ -647,6 +663,11 @@ private sealed class FieldFlags
/// </summary>
public TypedConstantInfo? DefaultValue;

/// <summary>
/// The fully qualified metadata name of the default value, if needed.
/// </summary>
public string? DefaultValueTypeFullyQualifiedMetadataName;

/// <summary>
/// The location of the target field being initialized.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,124 @@ public partial class MyControl : Control
await test.RunAsync();
}

[TestMethod]
public async Task SimpleProperty_WithExplicitValue_NestedEnumType()
{
const string original = """
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace MyApp;
public partial class MyControl : Control
{
public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
name: "Name",
propertyType: typeof(MyContainingType.MyEnum),
ownerType: typeof(MyControl),
typeMetadata: new PropertyMetadata(MyContainingType.MyEnum.B));
public MyContainingType.MyEnum [|Name|]
{
get => (MyContainingType.MyEnum)GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
}
public class MyContainingType
{
public enum MyEnum { A, B }
}
""";

const string @fixed = """
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace MyApp;
public partial class MyControl : Control
{
[GeneratedDependencyProperty(DefaultValue = MyContainingType.MyEnum.B)]
public partial MyContainingType.MyEnum {|CS9248:Name|} { get; set; }
}
public class MyContainingType
{
public enum MyEnum { A, B }
}
""";

CSharpCodeFixTest test = new(LanguageVersion.Preview)
{
TestCode = original,
FixedCode = @fixed
};

await test.RunAsync();
}

[TestMethod]
public async Task SimpleProperty_WithExplicitValue_NestedEnumType_WithUsingStatic()
{
const string original = """
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using static MyApp.MyContainingType;
namespace MyApp;
public partial class MyControl : Control
{
public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
name: "Name",
propertyType: typeof(MyEnum),
ownerType: typeof(MyControl),
typeMetadata: new PropertyMetadata(MyEnum.B));
public MyEnum [|Name|]
{
get => (MyEnum)GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
}
public class MyContainingType
{
public enum MyEnum { A, B }
}
""";

const string @fixed = """
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using static MyApp.MyContainingType;
namespace MyApp;
public partial class MyControl : Control
{
[GeneratedDependencyProperty(DefaultValue = MyEnum.B)]
public partial MyEnum {|CS9248:Name|} { get; set; }
}
public class MyContainingType
{
public enum MyEnum { A, B }
}
""";

CSharpCodeFixTest test = new(LanguageVersion.Preview)
{
TestCode = original,
FixedCode = @fixed
};

await test.RunAsync();
}

[TestMethod]
public async Task SimpleProperty_WithExplicitValue_NotDefault_AddsNamespace()
{
Expand Down

0 comments on commit 0d01d5c

Please sign in to comment.