From e1bef09d8afe58d00aa0620ee1fd5e7ff92fe470 Mon Sep 17 00:00:00 2001 From: Den Shabalin Date: Thu, 8 Aug 2013 18:55:45 +0200 Subject: SI-6843 well-positioned syntax errors for quasiquotes This is achieved in a following way: 1. Similarly to toolbox quasiquotes can go away with wrapping for parsing purpose after introduction of `parseStats` and `parseRule` entry points. 2. In case of syntax error quasiquote computes equivalent corresponding position in the source code with the help of `corrrespondingPosition` mapper which relies on position data collected into `posMap` during code generation. --- .../scala/tools/nsc/ast/parser/TreeBuilder.scala | 2 +- .../scala/tools/reflect/quasiquotes/Parsers.scala | 94 ++++++++++++---------- .../tools/reflect/quasiquotes/Placeholders.scala | 21 ++++- .../tools/reflect/quasiquotes/Quasiquotes.scala | 2 +- .../neg/quasiquotes-syntax-error-position.check | 32 ++++++++ .../neg/quasiquotes-syntax-error-position.scala | 15 ++++ test/files/scalacheck/quasiquotes/ErrorProps.scala | 6 -- 7 files changed, 116 insertions(+), 56 deletions(-) create mode 100644 test/files/neg/quasiquotes-syntax-error-position.check create mode 100644 test/files/neg/quasiquotes-syntax-error-position.scala diff --git a/src/compiler/scala/tools/nsc/ast/parser/TreeBuilder.scala b/src/compiler/scala/tools/nsc/ast/parser/TreeBuilder.scala index 666f19851d..ed694023d7 100644 --- a/src/compiler/scala/tools/nsc/ast/parser/TreeBuilder.scala +++ b/src/compiler/scala/tools/nsc/ast/parser/TreeBuilder.scala @@ -39,7 +39,7 @@ abstract class TreeBuilder { * x becomes x @ _ * x: T becomes x @ (_: T) */ - private object patvarTransformer extends Transformer { + object patvarTransformer extends Transformer { override def transform(tree: Tree): Tree = tree match { case Ident(name) if (treeInfo.isVarPattern(tree) && name != nme.WILDCARD) => atPos(tree.pos)(Bind(name, atPos(tree.pos.focus) (Ident(nme.WILDCARD)))) diff --git a/src/compiler/scala/tools/reflect/quasiquotes/Parsers.scala b/src/compiler/scala/tools/reflect/quasiquotes/Parsers.scala index 9a6ba56c18..18a806e5ff 100644 --- a/src/compiler/scala/tools/reflect/quasiquotes/Parsers.scala +++ b/src/compiler/scala/tools/reflect/quasiquotes/Parsers.scala @@ -17,32 +17,38 @@ trait Parsers { self: Quasiquotes => abstract class Parser extends { val global: self.global.type = self.global } with ScalaParser { - /** Wraps given code to obtain a desired parser mode. - * This way we can just re-use standard parser entry point. - */ - def wrapCode(code: String): String = - s"object wrapper { self => $EOL $code $EOL }" - - def unwrapTree(wrappedTree: Tree): Tree = { - val PackageDef(_, List(ModuleDef(_, _, Template(_, _, _ :: parsed)))) = wrappedTree - parsed match { - case tree :: Nil => tree - case stats :+ tree => Block(stats, tree) - } - } - def parse(code: String): Tree = { try { - val wrapped = wrapCode(code) - debug(s"wrapped code\n=${wrapped}\n") - val file = new BatchSourceFile(nme.QUASIQUOTE_FILE, wrapped) - val tree = new QuasiquoteParser(file).parse() - unwrapTree(tree) + val file = new BatchSourceFile(nme.QUASIQUOTE_FILE, code) + new QuasiquoteParser(file).parseRule(entryPoint) } catch { - case mi: MalformedInput => c.abort(c.macroApplication.pos, s"syntax error: ${mi.msg}") + case mi: MalformedInput => c.abort(correspondingPosition(mi.offset), mi.msg) + } + } + + def correspondingPosition(offset: Int): Position = { + val posMapList = posMap.toList + def containsOffset(start: Int, end: Int) = start <= offset && offset <= end + def fallbackPosition = posMapList match { + case (pos1, (start1, end1)) :: _ if start1 > offset => pos1 + case _ :+ ((pos2, (start2, end2))) if offset > end2 => pos2.withPoint(pos2.point + (end2 - start2)) } + posMapList.sliding(2).collect { + case (pos1, (start1, end1)) :: _ if containsOffset(start1, end1) => (pos1, offset - start1) + case (pos1, (_, end1)) :: (_, (start2, _)) :: _ if containsOffset(end1, start2) => (pos1, end1) + case _ :: (pos2, (start2, end2)) :: _ if containsOffset(start2, end2) => (pos2, offset - start2) + }.map { case (pos, offset) => + pos.withPoint(pos.point + offset) + }.toList.headOption.getOrElse(fallbackPosition) } + override def token2string(token: Int): String = token match { + case EOF => "end of quote" + case _ => super.token2string(token) + } + + def entryPoint: QuasiquoteParser => Tree + class QuasiquoteParser(source0: SourceFile) extends SourceFileParser(source0) { override val treeBuilder = new ParserTreeBuilder { // q"(..$xs)" @@ -73,9 +79,11 @@ trait Parsers { self: Quasiquotes => } else super.caseClause() - def isHole = isIdent && holeMap.contains(in.name) + def isHole: Boolean = isIdent && holeMap.contains(in.name) - override def isAnnotation: Boolean = super.isAnnotation || (isHole && lookingAhead { isAnnotation }) + override def isAnnotation: Boolean = super.isAnnotation || (isHole && lookingAhead { isAnnotation }) + + override def isCaseDefStart: Boolean = super.isCaseDefStart || (in.token == EOF) override def isModifier: Boolean = super.isModifier || (isHole && lookingAhead { isModifier }) @@ -85,6 +93,12 @@ trait Parsers { self: Quasiquotes => override def isDclIntro: Boolean = super.isDclIntro || (isHole && lookingAhead { isDclIntro }) + override def isStatSep(token: Int) = token == EOF || super.isStatSep(token) + + override def expectedMsg(token: Int): String = + if (isHole) expectedMsgTemplate(token2string(token), "splicee") + else super.expectedMsg(token) + // $mods def foo // $mods T override def readAnnots(annot: => Tree): List[Tree] = in.token match { @@ -101,34 +115,26 @@ trait Parsers { self: Quasiquotes => } } - object TermParser extends Parser - - object CaseParser extends Parser { - override def wrapCode(code: String) = super.wrapCode("something match { case " + code + " }") - - override def unwrapTree(wrappedTree: Tree): Tree = { - val Match(_, head :: tail) = super.unwrapTree(wrappedTree) - if (tail.nonEmpty) - c.abort(c.macroApplication.pos, "Can't parse more than one casedef, consider generating a match tree instead") - head + object TermParser extends Parser { + def entryPoint = _.templateStats() match { + case Nil => EmptyTree + case tree :: Nil => tree + case stats :+ tree => Block(stats, tree) } } - object PatternParser extends Parser { - override def wrapCode(code: String) = super.wrapCode("something match { case " + code + " => }") - - override def unwrapTree(wrappedTree: Tree): Tree = { - val Match(_, List(CaseDef(pat, _, _))) = super.unwrapTree(wrappedTree) - pat - } + object TypeParser extends Parser { + def entryPoint = _.typ() } - object TypeParser extends Parser { - override def wrapCode(code: String) = super.wrapCode("type T = " + code) + object CaseParser extends Parser { + def entryPoint = _.caseClause() + } - override def unwrapTree(wrappedTree: Tree): Tree = { - val TypeDef(_, _, _, rhs) = super.unwrapTree(wrappedTree) - rhs + object PatternParser extends Parser { + def entryPoint = { parser => + val pat = parser.noSeq.pattern1() + parser.treeBuilder.patvarTransformer.transform(pat) } } } \ No newline at end of file diff --git a/src/compiler/scala/tools/reflect/quasiquotes/Placeholders.scala b/src/compiler/scala/tools/reflect/quasiquotes/Placeholders.scala index b680c25f76..b3ac1e293a 100644 --- a/src/compiler/scala/tools/reflect/quasiquotes/Placeholders.scala +++ b/src/compiler/scala/tools/reflect/quasiquotes/Placeholders.scala @@ -17,18 +17,31 @@ trait Placeholders { self: Quasiquotes => // Step 1: Transform Scala source with holes into vanilla Scala source lazy val holeMap = new HoleMap() + lazy val posMap = mutable.ListMap[Position, (Int, Int)]() lazy val code = { val sb = new StringBuilder() val sessionSuffix = randomUUID().toString.replace("-", "").substring(0, 8) + "$" - foreach2(args, parts.init) { (tree, p) => - val (part, cardinality) = parseDots(p) + def appendPart(value: String, pos: Position) = { + val start = sb.length + sb.append(value) + val end = sb.length + posMap += pos -> (start, end) + } + + def appendHole(tree: Tree, cardinality: Cardinality) = { val placeholderName = c.freshName(TermName(nme.QUASIQUOTE_PREFIX + sessionSuffix)) - sb.append(part) sb.append(placeholderName) holeMap(placeholderName) = Hole(tree, cardinality) } - sb.append(parts.last) + + foreach2(args, parts.init) { case (tree, (p, pos)) => + val (part, cardinality) = parseDots(p) + appendPart(part, pos) + appendHole(tree, cardinality) + } + val (p, pos) = parts.last + appendPart(p, pos) sb.toString } diff --git a/src/compiler/scala/tools/reflect/quasiquotes/Quasiquotes.scala b/src/compiler/scala/tools/reflect/quasiquotes/Quasiquotes.scala index fe954e0bfd..ee99a5e280 100644 --- a/src/compiler/scala/tools/reflect/quasiquotes/Quasiquotes.scala +++ b/src/compiler/scala/tools/reflect/quasiquotes/Quasiquotes.scala @@ -17,7 +17,7 @@ abstract class Quasiquotes extends Parsers lazy val (universe: Tree, args, parts, parse, reify) = c.macroApplication match { case Apply(Select(Select(Apply(Select(universe0, _), List(Apply(_, parts0))), interpolator0), method0), args0) => val parts1 = parts0.map { - case Literal(Constant(s: String)) => s + case lit @ Literal(Constant(s: String)) => s -> lit.pos case part => c.abort(part.pos, "Quasiquotes can only be used with literal strings") } val reify0 = method0 match { diff --git a/test/files/neg/quasiquotes-syntax-error-position.check b/test/files/neg/quasiquotes-syntax-error-position.check new file mode 100644 index 0000000000..3bd813b1bb --- /dev/null +++ b/test/files/neg/quasiquotes-syntax-error-position.check @@ -0,0 +1,32 @@ +quasiquotes-syntax-error-position.scala:5: error: '=' expected but identifier found. + q"def $a f" + ^ +quasiquotes-syntax-error-position.scala:6: error: illegal start of simple expression + q"$a(" + ^ +quasiquotes-syntax-error-position.scala:7: error: '}' expected but end of quote found. + q"class $t { def foo = $a" + ^ +quasiquotes-syntax-error-position.scala:8: error: '.' expected but splicee found. + q"import $t $t" + ^ +quasiquotes-syntax-error-position.scala:9: error: illegal start of definition + q"package p" + ^ +quasiquotes-syntax-error-position.scala:10: error: ';' expected but '@' found. + q"foo@$a" + ^ +quasiquotes-syntax-error-position.scala:11: error: case classes without a parameter list are not allowed; +use either case objects or case classes with an explicit `()' as a parameter list. + q"case class A" + ^ +quasiquotes-syntax-error-position.scala:12: error: identifier expected but ']' found. + tq"$t => $t $t]" + ^ +quasiquotes-syntax-error-position.scala:13: error: end of quote expected but 'case' found. + cq"pattern => body ; case pattern2 =>" + ^ +quasiquotes-syntax-error-position.scala:14: error: ')' expected but end of quote found. + pq"$a(bar" + ^ +10 errors found diff --git a/test/files/neg/quasiquotes-syntax-error-position.scala b/test/files/neg/quasiquotes-syntax-error-position.scala new file mode 100644 index 0000000000..b97af52cfc --- /dev/null +++ b/test/files/neg/quasiquotes-syntax-error-position.scala @@ -0,0 +1,15 @@ +import scala.reflect.runtime.universe._ +object test extends App { + val a = TermName("a") + val t = TypeName("t") + q"def $a f" + q"$a(" + q"class $t { def foo = $a" + q"import $t $t" + q"package p" + q"foo@$a" + q"case class A" + tq"$t => $t $t]" + cq"pattern => body ; case pattern2 =>" + pq"$a(bar" +} \ No newline at end of file diff --git a/test/files/scalacheck/quasiquotes/ErrorProps.scala b/test/files/scalacheck/quasiquotes/ErrorProps.scala index 044a332a04..b9e69e0e88 100644 --- a/test/files/scalacheck/quasiquotes/ErrorProps.scala +++ b/test/files/scalacheck/quasiquotes/ErrorProps.scala @@ -188,12 +188,6 @@ object ErrorProps extends QuasiquoteProperties("errors") { val q"$m1 $m2 def foo" = EmptyTree """) - property("can't parse more than one casedef") = fails( - "Can't parse more than one casedef, consider generating a match tree instead", - """ - cq"1 => 2 case 3 => 5" - """) - // // Make sure a nice error is reported in this case // { import Flag._; val mods = NoMods; q"lazy $mods val x: Int" } } \ No newline at end of file -- cgit v1.2.3