diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 83afc96b..656a7697 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -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); @@ -80,7 +81,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) root, propertyDeclaration, fieldDeclaration, - defaultValue), + defaultValue, + defaultValueTypeFullyQualifiedMetadataName), equivalenceKey: "Use a partial property"), diagnostic); } @@ -122,12 +124,14 @@ private static bool TryGetGeneratedDependencyPropertyAttributeList( /// The original document being fixed. /// The instance for the current compilation. /// The expression for the default value of the property, if present + /// The fully qualified metadata name of the default value, if present. /// The updated attribute syntax. 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. @@ -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); @@ -190,6 +195,7 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis /// The for the property being updated. /// The for the declared property to remove. /// The expression for the default value of the property, if present + /// The fully qualified metadata name of the default value, if present. /// An updated document with the applied code fix, and being replaced with a partial property. private static async Task ConvertToPartialProperty( Document document, @@ -197,7 +203,8 @@ private static async Task ConvertToPartialProperty( SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration, FieldDeclarationSyntax fieldDeclaration, - string? defaultValueExpression) + string? defaultValueExpression, + string? defaultValueTypeFullyQualifiedMetadataName) { await Task.CompletedTask; @@ -217,7 +224,8 @@ private static async Task ConvertToPartialProperty( fieldDeclaration, generatedDependencyPropertyAttributeList, syntaxEditor, - defaultValueExpression); + defaultValueExpression, + defaultValueTypeFullyQualifiedMetadataName); RemoveLeftoverLeadingEndOfLines([fieldDeclaration], syntaxEditor); @@ -235,6 +243,7 @@ private static async Task ConvertToPartialProperty( /// The with the attribute to add. /// The instance to use. /// The expression for the default value of the property, if present + /// The fully qualified metadata name of the default value, if present. /// An updated document with the applied code fix, and being replaced with a partial property. private static void ConvertToPartialProperty( Document document, @@ -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 @@ -258,7 +268,8 @@ private static void ConvertToPartialProperty( document, semanticModel, generatedDependencyPropertyAttributeList, - defaultValueExpression); + defaultValueExpression, + defaultValueTypeFullyQualifiedMetadataName); // Start setting up the updated attribute lists SyntaxList attributeLists = propertyDeclaration.AttributeLists; @@ -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, @@ -467,7 +480,8 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider fieldDeclaration, generatedDependencyPropertyAttributeList, syntaxEditor, - defaultValue); + defaultValue, + defaultValueTypeFullyQualifiedMetadataName); fieldDeclarations.Add(fieldDeclaration); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index b7e9193d..b0468b88 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -57,6 +57,11 @@ public sealed class UseGeneratedDependencyPropertyOnManualPropertyAnalyzer : Dia /// public const string DefaultValuePropertyName = "DefaultValue"; + /// + /// The property name for the fully qualified metadata name of the default value, if present. + /// + public const string DefaultValueTypeFullyQualifiedMetadataNamePropertyName = "DefaultValueTypeFullyQualifiedMetadataName"; + /// public override ImmutableArray SupportedDiagnostics { get; } = [UseGeneratedDependencyPropertyForManualProperty]; @@ -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(); } } } @@ -552,7 +565,9 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla UseGeneratedDependencyPropertyForManualProperty, pair.Key.Locations.FirstOrDefault(), [fieldLocation], - ImmutableDictionary.Create().Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()), + ImmutableDictionary.Create() + .Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()) + .Add(DefaultValueTypeFullyQualifiedMetadataNamePropertyName, fieldFlags.DefaultValueTypeFullyQualifiedMetadataName), pair.Key)); } } @@ -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); @@ -647,6 +663,11 @@ private sealed class FieldFlags /// public TypedConstantInfo? DefaultValue; + /// + /// The fully qualified metadata name of the default value, if needed. + /// + public string? DefaultValueTypeFullyQualifiedMetadataName; + /// /// The location of the target field being initialized. /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index b284bb9f..5dc85149 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -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() {