I want to transform Scala XML literals with a macro. (Not a string literal with XML but actual XML literals). As far as I understand, XML literals are not actually built into the language on the AST level but are desugared in the parser. Interestingly though, this does work:
case q"<specificTag></specificTag>" => ... // succeeds for specificTag with no
// attributes and children
But obviously, this is totally useless because it is impossible to match arbitrary xml that way. Something like
case q"<$prefix:$label ..$attrs>$children</$prefix:$label>" => ...
can not work because we would have to bind the same variable twice in a pattern.
Printing out the tree of such an xml literal expression actually gives the desugared version. For example.
new _root_.scala.xml.Elem(null,"specificTag",_root_.scala.xml.Null,$scope,false)
But trying to match this fails:
case q"new _root_.scala.xml.Elem(..$params)" => ... // never succeeds
I am confused! My question is: Is there a way to reliably match arbitrary xml litarals in scala macros? Additionally: Why are they supported in quasiquotes for constant xml and not for the desugared value after all?
The xml is wrapped in blocks, macro invoked as rename( <top><bottom>hello</bottom></top> )
. I noticed that by looking at the incoming tree, not what's constructed by quasiquotes.
I had lodged this issue when I looked at your question previously; I don't know whether my SO is that; I tried bumping SS
in sbt. There's another SO issue that's probably unrelated.
class Normalizer(val c: Context) {
import c.universe._
def impl(e: c.Tree) = e match {
case Block(List(), Block(List(), x)) => x match {
case q"new scala.xml.Elem($prefix, $label, $attrs, $scope, $min, $t)" =>
Console println s"Childed tree is ${showRaw(e)}"
val b = t match {
case Typed(b, z) => c.untypecheck(b.duplicate)
case _ => EmptyTree
}
val Literal(Constant(tag: String)) = label
val x = c.eval(c.Expr[NodeBuffer](b))
//q"""<${tag.reverse}>..$x</${tag.reverse}>""" // SO
e
case q"new scala.xml.Elem($prefix, $label, $attrs, $scope, $min)" =>
Console println s"Childless tree is ${showRaw(e)}" ; e
case _ => Console println s"Tree is ${showRaw(e)}" ; e
}
case _ => Console println s"Nonblock is ${showRaw(e)}" ; e
}
}
Unfortunately quasiquotes don't natively support matching of xml literals and until today the only way to do it was to match on desugared tree as demonstrated by @som-snytt. But it's very easy to get it wrong and such manipulations may require so many AST nodes that they will blow up the pattern matcher.
To address this weakness we've just released a first milestone of scalamacros/xml, a library that turns this problem around: instead of working with AST of XML it lets you work with pure XML nodes instead:
scala> val q"${elem: xml.Elem}" = q"<foo><bar/></foo>"
elem: scala.xml.Elem = <foo><bar/></foo>
Here we use unlifting to convert code to value and than we can just process it as xml. In the end after processing you will probably want to convert it back to AST through lifting):
scala> q"$elem"
res4: org.scalamacros.xml.RuntimeLiftables.__universe.Tree =
new _root_.scala.xml.Elem(null, "foo", _root_.scala.xml.Null, $scope, false, ({
val $buf = new _root_.scala.xml.NodeBuffer();
$buf.$amp$plus(new _root_.scala.xml.Elem(null, "bar", _root_.scala.xml.Null, $scope, true));
$buf
}: _*))
In case your original AST case some code snippets they will be converted to special Unquote
node that contains such snippets:
scala> val q"${elem: xml.Elem}" = q"<foo>{x + y}</foo>"
elem: scala.xml.Elem = <foo>{x.+(y)}</foo>
scala> val <foo>{Unquote(q"x + y")}</foo> = elem
// matches
It's also easy to filter all unquote nodes through projection:
scala> elem \ "#UNQUOTE"
res6: scala.xml.NodeSeq = NodeSeq({x.+(y)})
You might also be interested in checking out example sbt project with simple macro that uses this library or have a deeper look at our test suite.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With