Skip to content

Commit 864dc01

Browse files
allanrenucciolafurpg
authored andcommitted
Rewrite Xml Literals to Xml Interpolator (#146)
* Rewrite boilerplate * First basic tests pass * WIP * WIP * Fix #10: XML Interpolator rewrite * Skip failing test due to scalameta/issues/836 * Emit warning on `{{` * Address reviewers comments * Address reviewers comments: second pass * Failing test case * Bump scala-xml-quote version * Fix #150
1 parent 80ffc7d commit 864dc01

File tree

7 files changed

+260
-2
lines changed

7 files changed

+260
-2
lines changed

build.sbt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,12 @@ lazy val tests = project
228228
"; tests/it:testQuick" // hack to workaround cyclic dependencies in test.
229229
)(state.value),
230230
parallelExecution in Test := true,
231+
// TODO: Remove once scala-xml-quote is merged into scala-xml
232+
resolvers += Resolver.bintrayRepo("allanrenucci", "maven"),
231233
libraryDependencies ++= Seq(
232234
scalahost % Test,
233235
// integration property tests
236+
"org.renucci" %% "scala-xml-quote" % "0.1.1" % Test,
234237
"org.typelevel" %% "catalysts-platform" % "0.0.5" % Test,
235238
"com.typesafe.slick" %% "slick" % "3.2.0-M2" % Test,
236239
"com.chuusai" %% "shapeless" % "2.3.2" % Test,

project/Dependencies.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ object Dependencies {
55
val scalametaV = "1.8.0-650-890aeec1"
66
val paradiseV = "3.0.0-308-ec15a2f8"
77
val metaconfigV = "0.3.2"
8+
89
var testClasspath: String = "empty"
910
def scalahost: ModuleID = "org.scalameta" % s"scalahost" % scalametaV cross CrossVersion.full
1011
def scalatest: ModuleID = "org.scalatest" %% "scalatest" % "3.0.0"

project/build.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version=0.13.13
1+
sbt.version=0.13.15
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package scalafix
2+
package rewrite
3+
4+
import scala.meta._
5+
6+
/** Rewrite Xml Literal to Xml Interpolator
7+
*
8+
* e.g.
9+
* {{{
10+
* // before:
11+
* <div>{ "Hello" }</div>
12+
*
13+
* // after:
14+
* xml"<div>${ "Hello" }</div>"
15+
* }}}
16+
*/
17+
case object RemoveXmlLiterals extends Rewrite {
18+
19+
override def rewrite(ctx: RewriteCtx): Patch = {
20+
object Xml {
21+
def unapply(tree: Tree): Option[Seq[Lit]] =
22+
tree match {
23+
case Pat.Xml(parts, _) => Some(parts)
24+
case Term.Xml(parts, _) => Some(parts)
25+
case _ => None
26+
}
27+
}
28+
29+
def isMultiLine(xml: Tree) =
30+
xml.pos.start.line != xml.pos.end.line
31+
32+
/** Contains '"' or '\' */
33+
def containsEscapeSequence(xmlPart: Lit) = {
34+
val Lit(value: String) = xmlPart
35+
value.exists(c => c == '\"' || c == '\\')
36+
}
37+
38+
/** Rewrite xml literal to interpolator */
39+
def patchXml(xml: Tree, tripleQuoted: Boolean) = {
40+
41+
// We don't want to patch inner xml literals multiple times
42+
def removeSplices(tokens: Tokens) = {
43+
var depth = 0
44+
45+
tokens.filter {
46+
case Token.Xml.SpliceStart() =>
47+
depth += 1
48+
depth == 1
49+
case Token.Xml.SpliceEnd() =>
50+
depth -= 1
51+
depth == 0
52+
case _ =>
53+
depth == 0
54+
}
55+
}
56+
57+
/** Substitute {{ by { */
58+
def patchEscapedBraces(tok: Token.Xml.Part) = {
59+
ctx.reporter.warn(
60+
"""Single opening braces don't need be escaped with {{ inside the xml interpolator,
61+
|unlike xml literals. For example <x>{{</x> is identical to xml"<x>{</x>".
62+
|This Rewrite will replace all occurrences of {{. Make sure this is intended.
63+
""".stripMargin,
64+
tok.pos
65+
)
66+
ctx.replaceToken(tok, tok.value.replaceAllLiterally("{{", "{"))
67+
}
68+
69+
removeSplices(xml.tokens).collect {
70+
case tok @ Token.Xml.Start() =>
71+
val toAdd =
72+
if (tripleQuoted) "xml\"\"\""
73+
else "xml\""
74+
ctx.addLeft(tok, toAdd)
75+
76+
case tok @ Token.Xml.End() =>
77+
val toAdd =
78+
if (tripleQuoted) "\"\"\""
79+
else "\""
80+
ctx.addRight(tok, toAdd)
81+
82+
case tok @ Token.Xml.SpliceStart() =>
83+
ctx.addLeft(tok, "$")
84+
85+
case tok @ Token.Xml.Part(part) =>
86+
var patch = Patch.empty
87+
if (part.contains('$'))
88+
patch += ctx.replaceToken(tok, part.replaceAllLiterally("$", "$$"))
89+
if (part.contains("{{"))
90+
patch += patchEscapedBraces(tok)
91+
patch
92+
93+
}.asPatch
94+
}
95+
96+
/** add `import scala.xml.quote._` */
97+
def importXmlQuote = {
98+
val nextToken = {
99+
def loop(tree: Tree): Token = tree match {
100+
case Source(stat :: _) => loop(stat)
101+
case Pkg(_, stat :: _) => loop(stat)
102+
case els => els.tokens.head
103+
}
104+
loop(ctx.tree)
105+
}
106+
107+
ctx.addLeft(nextToken, "import scala.xml.quote._\n")
108+
}
109+
110+
val patch = ctx.tree.collect {
111+
case xml @ Xml(parts) =>
112+
val tripleQuoted = isMultiLine(xml) || parts.exists(
113+
containsEscapeSequence)
114+
patchXml(xml, tripleQuoted)
115+
}.asPatch
116+
117+
if (patch.nonEmpty) patch + importXmlQuote
118+
else patch
119+
}
120+
}

scalafix-core/src/main/scala/scalafix/rewrite/ScalafixRewrites.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ object ScalafixRewrites {
66
val syntax: List[Rewrite] = List(
77
ProcedureSyntax,
88
VolatileLazyVal,
9+
RemoveXmlLiterals,
910
ExplicitUnit
1011
)
1112
def semantic(mirror: Mirror): List[Rewrite] = List(

scalafix-testkit/src/main/scala/scalafix/testkit/SemanticRewriteSuite.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import java.net.URL
2222
import java.net.URLClassLoader
2323

2424
import metaconfig.ConfError
25+
import org.scalameta.logger
2526
import org.scalatest.FunSuite
2627

2728
/**
@@ -84,11 +85,11 @@ abstract class SemanticRewriteSuite(
8485
}
8586

8687
val fixed = fix(diffTest.wrapped(), diffTest.config)
88+
logger.elem(diffTest.unwrap(fixed))
8789
val obtained = parse(diffTest.unwrap(fixed))
8890
val expected = parse(expectedStr)
8991
try {
9092
typeChecks(diffTest.wrapped(fixed))
91-
checkMismatchesModuloDesugarings(obtained, expected)
9293
if (diffTest.checkSyntax) {
9394
assertNoDiff(obtained, expected)
9495
} else {
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
rewrites = [RemoveXmlLiterals]
2+
3+
<<< don't add import when not needed
4+
object A {}
5+
>>>
6+
object A {}
7+
8+
<<< single line, no splice
9+
object A {
10+
<div></div>
11+
}
12+
>>>
13+
import scala.xml.quote._
14+
object A {
15+
xml"<div></div>"
16+
}
17+
18+
<<< single line, triple quoted
19+
object A {
20+
val a = <div b="Hello"/>
21+
val b = <a>\</a>
22+
}
23+
>>>
24+
import scala.xml.quote._
25+
object A {
26+
val a = xml"""<div b="Hello"/>"""
27+
val b = xml"""<a>\</a>"""
28+
}
29+
30+
<<< single line, splice
31+
object A {
32+
val bar = "bar"
33+
<div>{bar}</div>
34+
}
35+
>>>
36+
import scala.xml.quote._
37+
object A {
38+
val bar = "bar"
39+
xml"<div>${bar}</div>"
40+
}
41+
42+
<<< multi-line, no splice
43+
object A {
44+
val foo =
45+
<div>
46+
<span>Hello</span>
47+
</div>
48+
}
49+
>>>
50+
import scala.xml.quote._
51+
object A {
52+
val foo =
53+
xml"""<div>
54+
<span>Hello</span>
55+
</div>"""
56+
}
57+
58+
<<< multi-line, splice
59+
object A {
60+
val foo =
61+
<div>
62+
<span>{"Hello"}</span>
63+
</div>
64+
}
65+
>>>
66+
import scala.xml.quote._
67+
object A {
68+
val foo =
69+
xml"""<div>
70+
<span>${"Hello"}</span>
71+
</div>"""
72+
}
73+
74+
<<< splice in attribute position
75+
object A {
76+
<foo bar={"Hello"}/>
77+
}
78+
>>>
79+
import scala.xml.quote._
80+
object A {
81+
xml"<foo bar=${"Hello"}/>"
82+
}
83+
84+
<<< nested xml literals
85+
object A {
86+
<a>{<a>{"Hello"}</a>}</a>
87+
}
88+
>>>
89+
import scala.xml.quote._
90+
object A {
91+
xml"<a>${xml"<a>${"Hello"}</a>"}</a>"
92+
}
93+
94+
<<< protect $
95+
object A {
96+
<div>$</div>
97+
}
98+
>>>
99+
import scala.xml.quote._
100+
object A {
101+
xml"<div>$$</div>"
102+
}
103+
104+
<<< protect curly brace
105+
object A {
106+
<div>{{</div>
107+
}
108+
>>>
109+
import scala.xml.quote._
110+
object A {
111+
xml"<div>{</div>"
112+
}
113+
114+
<<< SKIP protect curly brace 2
115+
object A {
116+
<div b="{{"/>
117+
}
118+
>>>
119+
import scala.xml.quote._
120+
object A {
121+
xml"""<div b="{{"/>"""
122+
}
123+
124+
<<< multiple splices
125+
object A {
126+
<a>{1}{2}</a>
127+
}
128+
>>>
129+
import scala.xml.quote._
130+
object A {
131+
xml"<a>${1}${2}</a>"
132+
}

0 commit comments

Comments
 (0)