diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index f3dc16bc8..566094d56 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -27,11 +27,6 @@ partial class DependencyPropertyGenerator /// private static partial class Execute { - /// - /// Placeholder for . - /// - private static readonly DependencyPropertyDefaultValue.Null NullInfo = new(); - /// /// Generates the sources for the embedded types, for PrivateAssets="all" scenarios. /// @@ -274,7 +269,7 @@ public static DependencyPropertyDefaultValue GetDefaultValue( } // Invalid callback, the analyzer will emit an error - return NullInfo; + return DependencyPropertyDefaultValue.Null.Instance; } token.ThrowIfCancellationRequested(); @@ -313,7 +308,7 @@ public static DependencyPropertyDefaultValue GetDefaultValue( } // Otherwise, the value has been explicitly set to 'null', so let's respect that - return NullInfo; + return DependencyPropertyDefaultValue.Null.Instance; } token.ThrowIfCancellationRequested(); @@ -322,54 +317,14 @@ public static DependencyPropertyDefaultValue GetDefaultValue( // First we need to special case non nullable values, as for those we need 'default'. if (!propertySymbol.Type.IsDefaultValueNull()) { - string fullyQualifiedTypeName = propertySymbol.Type.GetFullyQualifiedName(); - - // There is a special case for this: if the type of the property is a built-in WinRT - // projected enum type or struct type (ie. some projected value type in general, except - // for 'Nullable' values), then we can just use 'null' and bypass creating the property - // metadata. The WinRT runtime will automatically instantiate a default value for us. - if (propertySymbol.Type.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)) || - propertySymbol.Type.IsContainedInNamespace("System.Numerics")) - { - return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); - } - - // Special case a few more well known value types that are mapped for WinRT - if (propertySymbol.Type.Name is "Point" or "Rect" or "Size" && - propertySymbol.Type.IsContainedInNamespace("Windows.Foundation")) - { - return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); - } - - // Special case two more system types - if (propertySymbol.Type is INamedTypeSymbol { MetadataName: "TimeSpan" or "DateTimeOffset", ContainingNamespace.MetadataName: "System" }) - { - return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); - } - - // Lastly, special case the well known primitive types - if (propertySymbol.Type.SpecialType is - SpecialType.System_Int32 or - SpecialType.System_Byte or - SpecialType.System_SByte or - SpecialType.System_Int16 or - SpecialType.System_UInt16 or - SpecialType.System_UInt32 or - SpecialType.System_Int64 or - SpecialType.System_UInt64 or - SpecialType.System_Char or - SpecialType.System_Single or - SpecialType.System_Double) - { - return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); - } - - // In all other cases, just use 'default(T)' here - return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: false); + // For non nullable types, we return 'default(T)', unless we can optimize for projected types + return new DependencyPropertyDefaultValue.Default( + TypeName: propertySymbol.Type.GetFullyQualifiedName(), + IsProjectedType: propertySymbol.Type.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml)); } // For all other ones, we can just use the 'null' placeholder again - return NullInfo; + return DependencyPropertyDefaultValue.Null.Instance; } /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 2e898dc22..10881e9f5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -65,6 +65,7 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(static context => { // Get the XAML mode to use @@ -393,7 +394,18 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla } // First, check if the metadata is 'null' (simplest case) - if (propertyMetadataArgument.Value.ConstantValue is not { HasValue: true, Value: null }) + if (propertyMetadataArgument.Value.ConstantValue is { HasValue: true, Value: null }) + { + // Here we need to special case non nullable value types that are not well known WinRT projected types. + // In this case, we cannot rely on XAML calling their default constructor. Rather, we need to preserve + // the explicit 'null' value that users had in their code. The analyzer will then warn on these cases + if (!propertyTypeSymbol.IsDefaultValueNull() && + !propertyTypeSymbol.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml)) + { + fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; + } + } + else { // Next, check if the argument is 'new PropertyMetadata(...)' with the default value for the property type if (propertyMetadataArgument.Value is not IObjectCreationOperation { Arguments: [{ } defaultValueArgument] } objectCreationOperation) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs new file mode 100644 index 000000000..de7983612 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for WinRT scenarios. +/// +internal static class WinRTExtensions +{ + /// + /// Checks whether a given type is a well known WinRT projected value type (ie. a type that XAML can default). + /// + /// The input instance to check. + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + /// Whether is a well known WinRT projected value type.. + public static bool IsWellKnownWinRTProjectedValueType(this ITypeSymbol symbol, bool useWindowsUIXaml) + { + // This method only cares about non nullable value types + if (symbol.IsDefaultValueNull()) + { + return false; + } + + // There is a special case for this: if the type of the property is a built-in WinRT + // projected enum type or struct type (ie. some projected value type in general, except + // for 'Nullable' values), then we can just use 'null' and bypass creating the property + // metadata. The WinRT runtime will automatically instantiate a default value for us. + if (symbol.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)) || + symbol.IsContainedInNamespace("System.Numerics")) + { + return true; + } + + // Special case a few more well known value types that are mapped for WinRT + if (symbol.Name is "Point" or "Rect" or "Size" && + symbol.IsContainedInNamespace("Windows.Foundation")) + { + return true; + } + + // Special case two more system types + if (symbol is INamedTypeSymbol { MetadataName: "TimeSpan" or "DateTimeOffset", ContainingNamespace.MetadataName: "System" }) + { + return true; + } + + // Lastly, special case the well known primitive types + if (symbol.SpecialType is + SpecialType.System_Int32 or + SpecialType.System_Byte or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_UInt16 or + SpecialType.System_UInt32 or + SpecialType.System_Int64 or + SpecialType.System_UInt64 or + SpecialType.System_Char or + SpecialType.System_Single or + SpecialType.System_Double) + { + return true; + } + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs index 29469028e..993c9d717 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs @@ -16,6 +16,11 @@ internal abstract partial record DependencyPropertyDefaultValue /// public sealed record Null : DependencyPropertyDefaultValue { + /// + /// The shared instance (the type is stateless). + /// + public static Null Instance { get; } = new(); + /// public override string ToString() { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs index 48accd4e1..427f25813 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs @@ -23,6 +23,11 @@ internal abstract partial record TypedConstantInfo /// public sealed record Null : TypedConstantInfo { + /// + /// The shared instance (the type is stateless). + /// + public static Null Instance { get; } = new(); + /// public override string ToString() { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index db6972429..b073ab716 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1382,7 +1382,6 @@ public string? Name [TestMethod] [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "global::System.TimeSpan.FromSeconds(1)")] [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "global::System.TimeSpan.FromSeconds(1)")] - [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_DoesNotWarn( string dependencyPropertyType, string propertyType, @@ -1425,21 +1424,21 @@ public enum MyEnum { A, B, C } [DataRow("object", "object?")] [DataRow("int", "int")] [DataRow("int?", "int?")] - [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "null")] - [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "default(global::System.TimeSpan?)")] - [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "null")] - [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "default(global::System.DateTimeOffset?)")] - [DataRow("global::System.Guid?", "global::System.Guid?", "default(global::System.Guid?)")] - [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "default(global::System.Collections.Generic.KeyValuePair?)")] - [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "null")] - [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct", "default(global::MyApp.MyStruct)")] - [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "null")] - [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "default(global::MyApp.MyStruct?)")] - [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "default(global::MyApp.MyEnum)")] - [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "null")] - [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum?)")] - [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "null")] - [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "default(global::MyApp.MyClass)")] + [DataRow("global::System.TimeSpan", "global::System.TimeSpan")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?")] + [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?")] + [DataRow("global::System.Guid?", "global::System.Guid?")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?" )] + [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_Warns( string dependencyPropertyType, string propertyType) @@ -1504,6 +1503,7 @@ public class MyClass { } [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "default(global::Windows.Foundation.Size)")] [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "default(global::Windows.UI.Xaml.Visibility)")] [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Visible")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")] [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "default(System.TimeSpan)")] [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "default(global::System.DateTimeOffset)")] [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "null")] diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 01cf0971a..5b7badbb1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -57,10 +57,10 @@ public class Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?")] [DataRow("global::System.Guid?", "global::System.Guid?")] [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?")] - [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")] [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")] - [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")] [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass?")] public async Task SimpleProperty(string dependencyPropertyType, string propertyType) { string original = $$""" @@ -86,6 +86,7 @@ public class MyControl : Control public struct MyStruct { public string X { get; set; } } public enum MyEnum { A, B, C } + public class MyClass { } """; string @fixed = $$""" @@ -101,6 +102,75 @@ public partial class MyControl : Control public partial {{propertyType}} {|CS9248:Name|} { get; set; } } + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + public class MyClass { } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + TestState = { AdditionalReferences = + { + MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), + MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) + }} + }; + + await test.RunAsync(); + } + + // These are custom value types, on properties where the metadata was set to 'null'. In this case, the + // default value would just be 'null', as XAML can't default initialize them. To preserve behavior, + // we must include an explicit default value. This will warn when the code is recompiled, but that + // is expected, because this specific scenario was (1) niche, and (2) kinda busted already anyway. + [TestMethod] + [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")] + public async Task SimpleProperty_ExplicitNull(string dependencyPropertyType, string propertyType) + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: nameof(Name), + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{propertyType}} [|Name|] + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = null)] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + public struct MyStruct { public string X { get; set; } } public enum MyEnum { A, B, C } """;