Browse Source

feature: Recognize Nodes with Generic Parameters (#88)

* Recognize nodes with generic parameters
* Move generic arguments to ResolvedName and add test

Co-authored-by: デリャザ <デリャザ@DESKTOP-OBAKE1V>
Co-authored-by: deryaza <deryaza@DESKTOP-OBAKE1V>
pull/10407/head
deryaza 4 years ago
committed by GitHub
parent
commit
233535ce75
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      src/Avalonia.NameGenerator.Tests/Views/View.cs
  2. 13
      src/Avalonia.NameGenerator.Tests/Views/ViewWithGenericBaseView.xml
  3. 4
      src/Avalonia.NameGenerator.Tests/XamlXClassResolverTests.cs
  4. 25
      src/Avalonia.NameGenerator.Tests/XamlXNameResolverTests.cs
  5. 8
      src/Avalonia.NameGenerator/Compiler/RoslynTypeSystem.cs
  6. 48
      src/Avalonia.NameGenerator/Domain/INameResolver.cs
  7. 10
      src/Avalonia.NameGenerator/Generator/InitializeComponentCodeGenerator.cs
  8. 4
      src/Avalonia.NameGenerator/Generator/OnlyPropertiesCodeGenerator.cs
  9. 9
      src/Avalonia.NameGenerator/Generator/XamlXNameResolver.cs

11
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<string> 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<TViewModel> : UserControl { } }"));
}

13
src/Avalonia.NameGenerator.Tests/Views/ViewWithGenericBaseView.xml

@ -0,0 +1,13 @@
<local:BaseView
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.ViewWithGenericBaseView"
Design.Width="300"
xmlns:sys="clr-namespace:System"
x:TypeArguments="sys:String"
x:Name="Root"
xmlns:local="clr-namespace:Sample.App">
<Grid>
<local:BaseView x:Name="NotAsRootNode" x:TypeArguments="sys:Int32" />
</Grid>
</local:BaseView>

4
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(

25
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<global::System.String>", 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<global::System.Int32>", 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),

8
src/Avalonia.NameGenerator/Compiler/RoslynTypeSystem.cs

@ -145,11 +145,15 @@ public class RoslynType : IXamlType
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>();
public IReadOnlyList<IXamlType> GenericArguments { get; } = new List<IXamlType>();
public IReadOnlyList<IXamlType> GenericArguments { get; private set; } = new List<IXamlType>();
public bool IsAssignableFrom(IXamlType type) => type == this;
public IXamlType MakeGenericType(IReadOnlyList<IXamlType> typeArguments) => this;
public IXamlType MakeGenericType(IReadOnlyList<IXamlType> typeArguments)
{
GenericArguments = typeArguments;
return this;
}
public IXamlType GenericTypeDefinition => this;

48
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<ResolvedName> ResolveNames(XamlDocument xaml);
}
internal record ResolvedName(string TypeName, string Name, string FieldModifier);
internal class ResolvedName
{
public ResolvedName(string typeName, string name, string fieldModifier, IReadOnlyList<string> genericTypeArguments)
{
TypeName = typeName;
Name = name;
FieldModifier = fieldModifier;
GenericTypeArguments = genericTypeArguments;
}
public string TypeName { get; }
public string Name { get; }
public string FieldModifier { get; }
public IReadOnlyList<string> 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();
}
}

10
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<string>();
var initializations = new List<string>();
foreach (var (typeName, name, fieldModifier) in names)
foreach (var resolvedName in names)
{
properties.Add($" {fieldModifier} global::{typeName} {name};");
initializations.Add($" {name} = this.FindControl<global::{typeName}>(\"{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);

4
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<global::{info.TypeName}>(\"{info.Name}\");")
$"{info.FieldModifier} {info.PrintableTypeName} {info.Name} => " +
$"this.FindControl<{info.PrintableTypeName}>(\"{info.Name}\");")
.ToList();
var lines = string.Join("\n", namedControls);
return $@"// <auto-generated />

9
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<string> 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);

Loading…
Cancel
Save