From 233535ce75ca3a5b984d73fe9b4103fa7f18b4df Mon Sep 17 00:00:00 2001 From: deryaza <46353085+deryaza@users.noreply.github.com> Date: Wed, 21 Sep 2022 17:42:42 +0400 Subject: [PATCH] feature: Recognize Nodes with Generic Parameters (#88) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Recognize nodes with generic parameters * Move generic arguments to ResolvedName and add test Co-authored-by: デリャザ <デリャザ@DESKTOP-OBAKE1V> Co-authored-by: deryaza --- .../Views/View.cs | 11 ++++- .../Views/ViewWithGenericBaseView.xml | 13 +++++ .../XamlXClassResolverTests.cs | 4 +- .../XamlXNameResolverTests.cs | 25 +++++++++- .../Compiler/RoslynTypeSystem.cs | 8 +++- .../Domain/INameResolver.cs | 48 ++++++++++++++++++- .../InitializeComponentCodeGenerator.cs | 10 ++-- .../Generator/OnlyPropertiesCodeGenerator.cs | 4 +- .../Generator/XamlXNameResolver.cs | 9 +++- 9 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 src/Avalonia.NameGenerator.Tests/Views/ViewWithGenericBaseView.xml diff --git a/src/Avalonia.NameGenerator.Tests/Views/View.cs b/src/Avalonia.NameGenerator.Tests/Views/View.cs index f0280191f9..37091e950b 100644 --- a/src/Avalonia.NameGenerator.Tests/Views/View.cs +++ b/src/Avalonia.NameGenerator.Tests/Views/View.cs @@ -3,7 +3,9 @@ using System.ComponentModel; using System.IO; using System.Linq; using System.Threading.Tasks; + using Avalonia.Controls; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -22,7 +24,8 @@ public static class View public const string AttachedProps = "AttachedProps.xml"; public const string FieldModifier = "FieldModifier.xml"; public const string ControlWithoutWindow = "ControlWithoutWindow.xml"; - + public const string ViewWithGenericBaseView = "ViewWithGenericBaseView.xml"; + public static async Task Load(string viewName) { var assembly = typeof(XamlXNameResolverTests).Assembly; @@ -66,4 +69,10 @@ public static class View " public class CustomTextBox : TextBox { }" + " public class EvilControl { }" + "}")); + + public static CSharpCompilation WithBaseView(this CSharpCompilation compilation) => + compilation.AddSyntaxTrees( + CSharpSyntaxTree.ParseText( + "using Avalonia.Controls;" + + "namespace Sample.App { public class BaseView : UserControl { } }")); } \ No newline at end of file diff --git a/src/Avalonia.NameGenerator.Tests/Views/ViewWithGenericBaseView.xml b/src/Avalonia.NameGenerator.Tests/Views/ViewWithGenericBaseView.xml new file mode 100644 index 0000000000..7b5994f784 --- /dev/null +++ b/src/Avalonia.NameGenerator.Tests/Views/ViewWithGenericBaseView.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/src/Avalonia.NameGenerator.Tests/XamlXClassResolverTests.cs b/src/Avalonia.NameGenerator.Tests/XamlXClassResolverTests.cs index f0a1114fe4..ce9aaa0d49 100644 --- a/src/Avalonia.NameGenerator.Tests/XamlXClassResolverTests.cs +++ b/src/Avalonia.NameGenerator.Tests/XamlXClassResolverTests.cs @@ -19,12 +19,14 @@ public class XamlXClassResolverTests [InlineData("Sample.App", "SignUpView", View.SignUpView)] [InlineData("Sample.App", "xNamedControl", View.XNamedControl)] [InlineData("Sample.App", "xNamedControls", View.XNamedControls)] + [InlineData("Sample.App", "ViewWithGenericBaseView", View.ViewWithGenericBaseView)] public async Task Should_Resolve_Base_Class_From_Xaml_File(string nameSpace, string className, string markup) { var xaml = await View.Load(markup); var compilation = View .CreateAvaloniaCompilation() - .WithCustomTextBox(); + .WithCustomTextBox() + .WithBaseView(); var types = new RoslynTypeSystem(compilation); var resolver = new XamlXViewResolver( diff --git a/src/Avalonia.NameGenerator.Tests/XamlXNameResolverTests.cs b/src/Avalonia.NameGenerator.Tests/XamlXNameResolverTests.cs index b6c4b133c2..90f0f64a50 100644 --- a/src/Avalonia.NameGenerator.Tests/XamlXNameResolverTests.cs +++ b/src/Avalonia.NameGenerator.Tests/XamlXNameResolverTests.cs @@ -61,6 +61,28 @@ public class XamlXNameResolverTests Assert.Equal("Controls.CustomTextBox", controls[2].TypeName); } + [Fact] + public async Task Should_Resolve_Types_From_Avalonia_Markup_File_When_Types_Contains_Generic_Arguments() + { + var xaml = await View.Load(View.ViewWithGenericBaseView); + var controls = ResolveNames(xaml); + Assert.Equal(2, controls.Count); + + var currentControl = controls[0]; + Assert.Equal("Root", currentControl.Name); + Assert.Equal("Sample.App.BaseView", currentControl.TypeName); + Assert.Equal(1, currentControl.GenericTypeArguments.Count); + Assert.Equal(typeof(string).FullName, currentControl.GenericTypeArguments[0]); + Assert.Equal("global::Sample.App.BaseView", currentControl.PrintableTypeName); + + currentControl = controls[1]; + Assert.Equal("NotAsRootNode", currentControl.Name); + Assert.Equal("Sample.App.BaseView", currentControl.TypeName); + Assert.Equal(1, currentControl.GenericTypeArguments.Count); + Assert.Equal(typeof(int).FullName, currentControl.GenericTypeArguments[0]); + Assert.Equal("global::Sample.App.BaseView", currentControl.PrintableTypeName); + } + [Fact] public async Task Should_Not_Resolve_Named_Controls_From_Avalonia_Markup_File_Without_Named_Controls() { @@ -107,7 +129,8 @@ public class XamlXNameResolverTests { var compilation = View.CreateAvaloniaCompilation() - .WithCustomTextBox(); + .WithCustomTextBox() + .WithBaseView(); var classResolver = new XamlXViewResolver( new RoslynTypeSystem(compilation), diff --git a/src/Avalonia.NameGenerator/Compiler/RoslynTypeSystem.cs b/src/Avalonia.NameGenerator/Compiler/RoslynTypeSystem.cs index 933072c1ea..fd47f6de5d 100644 --- a/src/Avalonia.NameGenerator/Compiler/RoslynTypeSystem.cs +++ b/src/Avalonia.NameGenerator/Compiler/RoslynTypeSystem.cs @@ -145,11 +145,15 @@ public class RoslynType : IXamlType public IReadOnlyList CustomAttributes { get; } = new List(); - public IReadOnlyList GenericArguments { get; } = new List(); + public IReadOnlyList GenericArguments { get; private set; } = new List(); public bool IsAssignableFrom(IXamlType type) => type == this; - public IXamlType MakeGenericType(IReadOnlyList typeArguments) => this; + public IXamlType MakeGenericType(IReadOnlyList typeArguments) + { + GenericArguments = typeArguments; + return this; + } public IXamlType GenericTypeDefinition => this; diff --git a/src/Avalonia.NameGenerator/Domain/INameResolver.cs b/src/Avalonia.NameGenerator/Domain/INameResolver.cs index 2fdb1581f8..1ba2bf36e4 100644 --- a/src/Avalonia.NameGenerator/Domain/INameResolver.cs +++ b/src/Avalonia.NameGenerator/Domain/INameResolver.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; + using XamlX.Ast; namespace Avalonia.NameGenerator.Domain; @@ -8,4 +10,48 @@ internal interface INameResolver IReadOnlyList ResolveNames(XamlDocument xaml); } -internal record ResolvedName(string TypeName, string Name, string FieldModifier); \ No newline at end of file +internal class ResolvedName +{ + public ResolvedName(string typeName, string name, string fieldModifier, IReadOnlyList genericTypeArguments) + { + TypeName = typeName; + Name = name; + FieldModifier = fieldModifier; + GenericTypeArguments = genericTypeArguments; + } + + public string TypeName { get; } + public string Name { get; } + public string FieldModifier { get; } + public IReadOnlyList GenericTypeArguments { get; } + + public string PrintableTypeName => + GenericTypeArguments.Count == 0 + ? $"global::{TypeName}" + : $@"global::{TypeName}<{string.Join(", ", GenericTypeArguments.Select(arg => $"global::{arg}"))}>"; + + public void Deconstruct(out string typeName, out string name, out string fieldModifier) + { + typeName = TypeName; + name = Name; + fieldModifier = FieldModifier; + } + + public override bool Equals(object obj) + { + if (obj is not ResolvedName name) + { + return false; + } + + return name.TypeName == TypeName + && name.Name == Name + && name.FieldModifier == FieldModifier + && name.GenericTypeArguments.SequenceEqual(GenericTypeArguments); + } + + public override int GetHashCode() + { + return (TypeName, Name, FieldModifier).GetHashCode(); + } +} \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/Generator/InitializeComponentCodeGenerator.cs b/src/Avalonia.NameGenerator/Generator/InitializeComponentCodeGenerator.cs index 2f2698451c..e4ab64e36f 100644 --- a/src/Avalonia.NameGenerator/Generator/InitializeComponentCodeGenerator.cs +++ b/src/Avalonia.NameGenerator/Generator/InitializeComponentCodeGenerator.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Xml.Linq; + using Avalonia.NameGenerator.Domain; using XamlX.TypeSystem; @@ -28,10 +30,12 @@ internal class InitializeComponentCodeGenerator: ICodeGenerator { var properties = new List(); var initializations = new List(); - foreach (var (typeName, name, fieldModifier) in names) + foreach (var resolvedName in names) { - properties.Add($" {fieldModifier} global::{typeName} {name};"); - initializations.Add($" {name} = this.FindControl(\"{name}\");"); + var (_, name, fieldModifier) = resolvedName; + string typeName = resolvedName.PrintableTypeName; + properties.Add($" {fieldModifier} {typeName} {name};"); + initializations.Add($" {name} = this.FindControl<{typeName}>(\"{name}\");"); } var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType); diff --git a/src/Avalonia.NameGenerator/Generator/OnlyPropertiesCodeGenerator.cs b/src/Avalonia.NameGenerator/Generator/OnlyPropertiesCodeGenerator.cs index a45bf4ef0f..2aa5dcf2c5 100644 --- a/src/Avalonia.NameGenerator/Generator/OnlyPropertiesCodeGenerator.cs +++ b/src/Avalonia.NameGenerator/Generator/OnlyPropertiesCodeGenerator.cs @@ -11,8 +11,8 @@ internal class OnlyPropertiesCodeGenerator : ICodeGenerator { var namedControls = names .Select(info => " " + - $"{info.FieldModifier} global::{info.TypeName} {info.Name} => " + - $"this.FindControl(\"{info.Name}\");") + $"{info.FieldModifier} {info.PrintableTypeName} {info.Name} => " + + $"this.FindControl<{info.PrintableTypeName}>(\"{info.Name}\");") .ToList(); var lines = string.Join("\n", namedControls); return $@"// diff --git a/src/Avalonia.NameGenerator/Generator/XamlXNameResolver.cs b/src/Avalonia.NameGenerator/Generator/XamlXNameResolver.cs index dc2061076c..af0bc75403 100644 --- a/src/Avalonia.NameGenerator/Generator/XamlXNameResolver.cs +++ b/src/Avalonia.NameGenerator/Generator/XamlXNameResolver.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; + using Avalonia.NameGenerator.Domain; + using XamlX; using XamlX.Ast; @@ -47,8 +50,10 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor propertyValueNode.Values[0] is XamlAstTextNode text) { var fieldModifier = TryGetFieldModifier(objectNode); - var typeName = $@"{clrType.Namespace}.{clrType.Name}"; - var resolvedName = new ResolvedName(typeName, text.Text, fieldModifier); + string typeName = $@"{clrType.Namespace}.{clrType.Name}"; + IReadOnlyList typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList(); + + var resolvedName = new ResolvedName(typeName, text.Text, fieldModifier, typeAgs); if (_items.Contains(resolvedName)) continue; _items.Add(resolvedName);