src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs (366 lines of code) (raw):
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Markup.Parsers;
using Avalonia.Styling;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
using static Avalonia.Markup.Parsers.ContainerQueryGrammar;
using XamlLoadException = XamlX.XamlLoadException;
using XamlParseException = XamlX.XamlParseException;
class AvaloniaXamlIlQueryTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is not XamlAstObjectNode on ||
!context.GetAvaloniaTypes().Container.IsAssignableFrom(on.Type.GetClrType()))
return node;
var pn = on.Children.OfType<XamlAstXamlPropertyValueNode>()
.FirstOrDefault(p => p.Property.GetClrProperty().Name == "Query");
if (pn == null || pn.Property.GetClrProperty().Getter is not { } getter)
return node;
if (pn.Values.Count != 1)
throw new XamlParseException("Query property should should have exactly one value", node);
if (pn.Values[0] is XamlIlQueryNode)
//Deja vu. I've just been in this place before
return node;
if (!(pn.Values[0] is XamlAstTextNode tn))
throw new XamlParseException("Query property should be a text node", node);
var queryType = getter.ReturnType;
var initialNode = new XamlIlQueryInitialNode(node, queryType);
var avaloniaAttachedPropertyT = context.GetAvaloniaTypes().AvaloniaAttachedPropertyT;
XamlIlQueryNode Create(IEnumerable<ISyntax> syntax)
{
XamlIlQueryNode result = initialNode;
XamlIlOrQueryNode? results = null;
XamlIlAndQueryNode? andNode = null;
foreach (var i in syntax)
{
switch (i)
{
case ContainerQueryGrammar.WidthSyntax width:
result = new XamlIlWidthQuery(result, width);
break;
case ContainerQueryGrammar.HeightSyntax height:
result = new XamlIlHeightQuery(result, height);
break;
case ContainerQueryGrammar.OrSyntax or:
if (results == null)
results = new XamlIlOrQueryNode(node, queryType);
if (andNode != null && result == initialNode)
throw new XamlParseException($"Previously opened And node is not closed.", node);
results.Add(andNode ?? result);
result = initialNode;
andNode = null;
break;
case ContainerQueryGrammar.AndSyntax and:
if (andNode == null)
andNode = new XamlIlAndQueryNode(node, queryType);
andNode.Add(result);
result = initialNode;
break;
default:
throw new XamlParseException($"Unsupported query grammar '{i.GetType()}'.", node);
}
if (andNode != null && result != initialNode)
{
andNode.Add(result);
}
}
if (results != null && result != null)
{
results.Add(result);
}
return results ?? result ?? initialNode;
}
IEnumerable<ISyntax> parsed;
try
{
parsed = ContainerQueryGrammar.Parse(tn.Text);
}
catch (Exception e)
{
throw new XamlParseException("Unable to parse query: " + e.Message, node);
}
var query = Create(parsed);
pn.Values[0] = query;
return new AvaloniaXamlIlTargetTypeMetadataNode(on,
new XamlAstClrTypeReference(query, query.TargetType!, false),
AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Container);
}
}
abstract class XamlIlQueryNode : XamlAstNode, IXamlAstValueNode, IXamlAstEmitableNode<IXamlILEmitter, XamlILNodeEmitResult>
{
internal XamlIlQueryNode? Previous { get; }
public abstract IXamlType? TargetType { get; }
public XamlIlQueryNode(XamlIlQueryNode? previous,
IXamlLineInfo? info = null,
IXamlType? queryType = null) : base((info ?? previous)!)
{
Previous = previous;
Type = queryType == null ? previous!.Type : new XamlAstClrTypeReference(this, queryType, false);
}
public IXamlAstTypeReference Type { get; }
public virtual XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
if (Previous != null)
context.Emit(Previous, codeGen, Type.GetClrType());
DoEmit(context, codeGen);
return XamlILNodeEmitResult.Type(0, Type.GetClrType());
}
protected abstract void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen);
protected void EmitCall(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen, Func<IXamlMethod, bool> method)
{
var queries = context.Configuration.TypeSystem.GetType("Avalonia.Styling.StyleQueries");
var found = queries.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m));
if(found == null)
throw new XamlTypeSystemException(
$"Unable to find {TargetType} in Avalonia.Styling.StyleQueries");
codeGen.EmitCall(found);
}
}
class XamlIlQueryInitialNode : XamlIlQueryNode
{
public XamlIlQueryInitialNode(IXamlLineInfo info,
IXamlType queryType) : base(null, info, queryType)
{
}
public override IXamlType? TargetType => null;
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) => codeGen.Ldnull();
}
class XamlIlTypeQuery : XamlIlQueryNode
{
public bool Concrete { get; }
public XamlIlTypeQuery(XamlIlQueryNode previous, IXamlType type, bool concrete) : base(previous)
{
TargetType = type;
Concrete = concrete;
}
public override IXamlType TargetType { get; }
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
var name = Concrete ? "OfType" : "Is";
codeGen.Ldtype(TargetType);
EmitCall(context, codeGen,
m => m.Name == name && m.Parameters.Count == 2 && m.Parameters[1].FullName == "System.Type");
}
}
class XamlIlStringQuery : XamlIlQueryNode
{
public string String { get; set; }
public enum QueryType
{
Class,
Name
}
private QueryType _type;
public XamlIlStringQuery(XamlIlQueryNode previous, QueryType type, string s) : base(previous)
{
_type = type;
String = s;
}
public override IXamlType? TargetType => Previous?.TargetType;
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
codeGen.Ldstr(String);
var name = _type.ToString();
EmitCall(context, codeGen,
m => m.Name == name && m.Parameters.Count == 2 && m.Parameters[1].FullName == "System.String");
}
}
class XamlIlCombinatorQuery : XamlIlQueryNode
{
private readonly CombinatorQueryType _type;
public enum CombinatorQueryType
{
Child,
Descendant,
Template
}
public XamlIlCombinatorQuery(XamlIlQueryNode previous, CombinatorQueryType type) : base(previous)
{
_type = type;
}
public CombinatorQueryType QueryType => _type;
public override IXamlType? TargetType => null;
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
var name = _type.ToString();
EmitCall(context, codeGen,
m => m.Name == name && m.Parameters.Count == 1);
}
}
class XamlIlWidthQuery : XamlIlQueryNode
{
private ContainerQueryGrammar.WidthSyntax _argument;
public XamlIlWidthQuery(XamlIlQueryNode previous, ContainerQueryGrammar.WidthSyntax argument) : base(previous)
{
_argument = argument;
}
public override IXamlType? TargetType => Previous?.TargetType;
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
codeGen.Ldc_I4((int)_argument.Operator);
codeGen.Ldc_R8(_argument.Value);
EmitCall(context, codeGen,
m => m.Name == "Width" && m.Parameters.Count == 3);
}
}
class XamlIlHeightQuery : XamlIlQueryNode
{
private ContainerQueryGrammar.HeightSyntax _argument;
public XamlIlHeightQuery(XamlIlQueryNode previous, ContainerQueryGrammar.HeightSyntax argument) : base(previous)
{
_argument = argument;
}
public override IXamlType? TargetType => Previous?.TargetType;
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
codeGen.Ldc_I4((int)_argument.Operator);
codeGen.Ldc_R8(_argument.Value);
EmitCall(context, codeGen,
m => m.Name == "Height" && m.Parameters.Count == 3);
}
}
class XamlIlOrQueryNode : XamlIlQueryNode
{
List<XamlIlQueryNode> _queries = new List<XamlIlQueryNode>();
public XamlIlOrQueryNode(IXamlLineInfo info, IXamlType queryType) : base(null, info, queryType)
{
}
public void Add(XamlIlQueryNode node)
{
_queries.Add(node);
}
public override IXamlType? TargetType
{
get
{
IXamlType? result = null;
foreach (var query in _queries)
{
if (query.TargetType == null)
{
return null;
}
else if (result == null)
{
result = query.TargetType;
}
else
{
while (result?.IsAssignableFrom(query.TargetType) == false)
{
result = result.BaseType;
}
}
}
return result;
}
}
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
if (_queries.Count == 0)
throw new XamlLoadException("Invalid query count", this);
if (_queries.Count == 1)
{
_queries[0].Emit(context, codeGen);
return;
}
if (context.Configuration.TypeSystem.FindType("System.Collections.Generic.List`1") is not { } type)
return;
IXamlType listType = type.MakeGenericType(base.Type.GetClrType());
var add = listType.FindMethod("Add", context.Configuration.WellKnownTypes.Void, false, Type.GetClrType());
if (add == null)
return;
var ctor = listType.FindConstructor();
if (ctor == null)
{
return;
}
codeGen
.Newobj(ctor);
foreach (var s in _queries)
{
codeGen.Dup();
context.Emit(s, codeGen, Type.GetClrType());
codeGen.EmitCall(add, true);
}
EmitCall(context, codeGen,
m => m.Name == "Or" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList"));
}
}
class XamlIlAndQueryNode : XamlIlQueryNode
{
List<XamlIlQueryNode> _queries = new List<XamlIlQueryNode>();
public XamlIlAndQueryNode(IXamlLineInfo info, IXamlType queryType) : base(null, info, queryType)
{
}
public void Add(XamlIlQueryNode node)
{
_queries.Add(node);
}
public override IXamlType? TargetType
{
get
{
IXamlType? result = null;
foreach (var query in _queries)
{
if (query.TargetType == null)
{
return null;
}
else if (result == null)
{
result = query.TargetType;
}
else
{
while (result?.IsAssignableFrom(query.TargetType) == false)
{
result = result.BaseType;
}
}
}
return result;
}
}
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
if (_queries.Count == 0)
throw new XamlLoadException("Invalid query count", this);
if (_queries.Count == 1)
{
_queries[0].Emit(context, codeGen);
return;
}
if (context.Configuration.TypeSystem.FindType("System.Collections.Generic.List`1") is not { } type)
return;
IXamlType listType = type.MakeGenericType(base.Type.GetClrType());
var add = listType.FindMethod("Add", context.Configuration.WellKnownTypes.Void, false, Type.GetClrType());
if (add == null)
return;
var ctor = listType.FindConstructor();
if (ctor == null)
{
return;
}
codeGen
.Newobj(ctor);
foreach (var s in _queries)
{
codeGen.Dup();
context.Emit(s, codeGen, Type.GetClrType());
codeGen.EmitCall(add, true);
}
EmitCall(context, codeGen,
m => m.Name == "And" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList"));
}
}
}