From 5eaaa84a1f20e110a009c62348cd2cf2b2c3591a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Dec 2024 11:53:18 -0800 Subject: [PATCH] draft --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 5 + .../ITypeSymbolExtensions - Copy.cs | 228 ++++++++++++++++++ .../Test_Analyzers.cs | 32 +-- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 74 +++++- 4 files changed, 321 insertions(+), 18 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions - Copy.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 2e898dc22..d4b9cf7bd 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 @@ -465,6 +466,10 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla } } } + else + { + + } // Find the parent field for the operation (we're guaranteed to only fine one) if (context.Operation.Syntax.FirstAncestor()?.GetLocation() is not Location fieldLocation) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions - Copy.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions - Copy.cs new file mode 100644 index 000000000..959425807 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions - Copy.cs @@ -0,0 +1,228 @@ +// 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 System; +using System.Diagnostics.CodeAnalysis; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for types. +/// +internal static class ITypeSymbolExtensions +{ + /// + /// Checks whether a given type has a default value of . + /// + /// The input instance to check. + /// Whether the default value of is . + public static bool IsDefaultValueNull(this ITypeSymbol symbol) + { + return symbol is { IsValueType: false } or INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + } + + /// + /// Tries to get the default value of a given enum type. + /// + /// The input instance to check. + /// The resulting default value for , if it was an enum type. + /// Whether was retrieved successfully. + public static bool TryGetDefaultValueForEnumType(this ITypeSymbol symbol, [NotNullWhen(true)] out object? value) + { + if (symbol.TypeKind is not TypeKind.Enum) + { + value = default; + + return false; + } + + // The default value of the enum is the value of its first constant field + foreach (ISymbol memberSymbol in symbol.GetMembers()) + { + if (memberSymbol is IFieldSymbol { IsConst: true, ConstantValue: object defaultValue }) + { + value = defaultValue; + + return true; + } + } + + value = default; + + return false; + } + + /// + /// Checks whether or not a given type symbol has a specified fully qualified metadata name. + /// + /// The input instance to check. + /// The full name to check. + /// Whether has a full name equals to . + public static bool HasFullyQualifiedMetadataName(this ITypeSymbol symbol, string name) + { + using ImmutableArrayBuilder builder = new(); + + symbol.AppendFullyQualifiedMetadataName(in builder); + + return builder.WrittenSpan.SequenceEqual(name.AsSpan()); + } + + /// + /// Checks whether or not a given inherits from a specified type. + /// + /// The target instance to check. + /// The instance to check for inheritance from. + /// Whether or not inherits from . + public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol) + { + INamedTypeSymbol? currentBaseTypeSymbol = typeSymbol.BaseType; + + while (currentBaseTypeSymbol is not null) + { + if (SymbolEqualityComparer.Default.Equals(currentBaseTypeSymbol, baseTypeSymbol)) + { + return true; + } + + currentBaseTypeSymbol = currentBaseTypeSymbol.BaseType; + } + + return false; + } + + /// + /// Checks whether or not a given inherits from a specified type. + /// + /// The target instance to check. + /// The full name of the type to check for inheritance. + /// Whether or not inherits from . + public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name) + { + INamedTypeSymbol? baseType = typeSymbol.BaseType; + + while (baseType is not null) + { + if (baseType.HasFullyQualifiedMetadataName(name)) + { + return true; + } + + baseType = baseType.BaseType; + } + + return false; + } + + /// + /// Gets the fully qualified metadata name for a given instance. + /// + /// The input instance. + /// The fully qualified metadata name for . + public static string GetFullyQualifiedMetadataName(this ITypeSymbol symbol) + { + using ImmutableArrayBuilder builder = new(); + + symbol.AppendFullyQualifiedMetadataName(in builder); + + return builder.ToString(); + } + + /// + /// Appends the fully qualified metadata name for a given symbol to a target builder. + /// + /// The input instance. + /// The target instance. + public static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, ref readonly ImmutableArrayBuilder builder) + { + static void BuildFrom(ISymbol? symbol, ref readonly ImmutableArrayBuilder builder) + { + switch (symbol) + { + // Namespaces that are nested also append a leading '.' + case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }: + BuildFrom(symbol.ContainingNamespace, in builder); + builder.Add('.'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Other namespaces (i.e. the one right before global) skip the leading '.' + case INamespaceSymbol { IsGlobalNamespace: false }: + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Types with no namespace just have their metadata name directly written + case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }: + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Types with a containing non-global namespace also append a leading '.' + case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }: + BuildFrom(namespaceSymbol, in builder); + builder.Add('.'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Nested types append a leading '+' + case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }: + BuildFrom(typeSymbol, in builder); + builder.Add('+'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + default: + break; + } + } + + BuildFrom(symbol, in builder); + } + + /// + /// Checks whether a given type is contained in a namespace with a specified name. + /// + /// The input instance. + /// The namespace to check. + /// Whether is contained within . + public static bool IsContainedInNamespace(this ITypeSymbol symbol, string? namespaceName) + { + static void BuildFrom(INamespaceSymbol? symbol, ref readonly ImmutableArrayBuilder builder) + { + switch (symbol) + { + // Namespaces that are nested also append a leading '.' + case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }: + BuildFrom(symbol.ContainingNamespace, in builder); + builder.Add('.'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Other namespaces (i.e. the one right before global) skip the leading '.' + case INamespaceSymbol { IsGlobalNamespace: false }: + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + default: + break; + } + } + + // Special case for no containing namespace + if (symbol.ContainingNamespace is not { } containingNamespace) + { + return namespaceName is null; + } + + // Special case if the type is directly in the global namespace + if (containingNamespace.IsGlobalNamespace) + { + return containingNamespace.MetadataName == namespaceName; + } + + using ImmutableArrayBuilder builder = new(); + + BuildFrom(containingNamespace, in builder); + + return builder.WrittenSpan.SequenceEqual(namespaceName.AsSpan()); + } +} 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 } """;