diff --git a/.travis.yml b/.travis.yml index 1594d962..7742a4fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,13 @@ sudo: false language: scala +addons: + hosts: + - myshorthost + hostname: myshorthost + env: global: - - PUBLISH_JDK=openjdk6 # PGP_PASSPHRASE - secure: "SkBtn/6OjEldoikn0MFuyeLT/pau27kwKSDYTVQeJ4BKDzdWLwLE5Q3RukLGttIfNdhOvRoocpQSW9GkZfibTHmwrRnAokucfZCqTsKbwoOp1xIoOh5GrrVrB6gcP7WBTKinqFdBgSvLOrP7GviImz4ZuB9wq1r+mToGG4pDrXc=" # SONA_USER @@ -18,7 +22,6 @@ script: admin/build.sh jdk: - openjdk6 - - openjdk7 - oraclejdk8 notifications: diff --git a/admin/README.md b/admin/README.md index 55ae9c8a..7f38379a 100644 --- a/admin/README.md +++ b/admin/README.md @@ -19,7 +19,7 @@ To configure tag driven releases from Travis CI. Edit `.travis.yml` as prompted. 4. Edit `.travis.yml` to use `./admin/build.sh` as the build script, and edit that script to use the tasks required for this project. - 5. Edit `.travis.yml` to select which JDK will be used for publishing. + 5. Edit `build.sbt` to select which JDK will be used for publishing. It is important to add comments in .travis.yml to identify the name of each environment variable encoded in a `:secure` section. @@ -30,7 +30,6 @@ form: language: scala env: global: - - PUBLISH_JDK=openjdk6 # PGP_PASSPHRASE - secure: "XXXXXX" # SONA_USER diff --git a/admin/build.sh b/admin/build.sh index ddd6d5e0..ce76249d 100755 --- a/admin/build.sh +++ b/admin/build.sh @@ -7,7 +7,7 @@ set -e # git on travis does not fetch tags, but we have TRAVIS_TAG # headTag=$(git describe --exact-match ||:) -if [ "$TRAVIS_JDK_VERSION" == "$PUBLISH_JDK" ] && [[ "$TRAVIS_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9-]+)? ]]; then +if [[ "$TRAVIS_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9-]+)? ]]; then echo "Going to release from tag $TRAVIS_TAG!" myVer=$(echo $TRAVIS_TAG | sed -e s/^v//) publishVersion='set every version := "'$myVer'"' diff --git a/build.sbt b/build.sbt index 3ff98463..7b11f41e 100644 --- a/build.sbt +++ b/build.sbt @@ -1,19 +1,34 @@ scalaVersion in ThisBuild := crossScalaVersions.value.head crossScalaVersions in ThisBuild := { + val v211 = List("2.11.8") + val v212 = List("2.12.0-RC1") + val javaVersion = System.getProperty("java.version") - val isJDK6Or7 = - javaVersion.startsWith("1.6.") || javaVersion.startsWith("1.7.") - if (isJDK6Or7) - Seq("2.11.7") - else - Seq("2.11.7", "2.12.0-M3") + val isTravisPublishing = !util.Properties.envOrElse("TRAVIS_TAG", "").trim.isEmpty + + if (isTravisPublishing) { + if (javaVersion.startsWith("1.6.")) v211 + else if (javaVersion.startsWith("1.8.")) v212 + else Nil + } else if (javaVersion.startsWith("1.6.") || javaVersion.startsWith("1.7.")) { + v211 + } else if (javaVersion.startsWith("1.8.") || javaVersion.startsWith("9")) { + v211 ++ v212 + } else { + sys.error(s"Unsupported java version: $javaVersion.") + } } lazy val `scala-parser-combinators` = crossProject.in(file(".")). settings(scalaModuleSettings: _*). + settings( + name := "scala-parser-combinators-root" + ). jvmSettings( - name := "scala-parser-combinators-jvm" + // Mima uses the name of the jvm project in the artifactId + // when resolving previous versions (so no "-jvm" project) + name := "scala-parser-combinators" ). jsSettings( name := "scala-parser-combinators-js" @@ -38,10 +53,10 @@ lazy val `scala-parser-combinators` = crossProject.in(file(".")). ). jsConfigure(_.enablePlugins(ScalaJSJUnitPlugin)). jvmSettings( - libraryDependencies += "junit" % "junit" % "4.11" % "test", - libraryDependencies += "com.novocode" % "junit-interface" % "0.10" % "test" + libraryDependencies += "junit" % "junit" % "4.12" % "test", + libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test" ). - settings( + jvmSettings( mimaPreviousVersion := None ) diff --git a/js/src/main/scala/scala/util/parsing/input/PositionCache.scala b/js/src/main/scala/scala/util/parsing/input/PositionCache.scala new file mode 100644 index 00000000..ff9f144f --- /dev/null +++ b/js/src/main/scala/scala/util/parsing/input/PositionCache.scala @@ -0,0 +1,14 @@ +package scala.util.parsing.input + +import java.lang.CharSequence +import java.util.{AbstractMap, Collections} + +private[input] trait PositionCache { + private[input] lazy val indexCache: java.util.Map[CharSequence,Array[Int]] = new AbstractMap[CharSequence, Array[Int]] { + + override def entrySet() = Collections.emptySet() + + // the /dev/null of Maps + override def put(ch: CharSequence, a: Array[Int]) = null + } +} diff --git a/jvm/src/main/scala/scala/util/parsing/input/PositionCache.scala b/jvm/src/main/scala/scala/util/parsing/input/PositionCache.scala new file mode 100644 index 00000000..84b1edfb --- /dev/null +++ b/jvm/src/main/scala/scala/util/parsing/input/PositionCache.scala @@ -0,0 +1,17 @@ +package scala.util.parsing.input + +import java.lang.{CharSequence, ThreadLocal} +import java.util.WeakHashMap + +/** + * @author Tomáš Janoušek + */ +private[input] trait PositionCache { + private lazy val indexCacheTL = + // not DynamicVariable as that would share the map from parent to child :-( + new ThreadLocal[java.util.Map[CharSequence, Array[Int]]] { + override def initialValue = new WeakHashMap[CharSequence, Array[Int]] + } + + private[input] def indexCache = indexCacheTL.get +} diff --git a/jvm/src/test/scala/scala/util/parsing/combinator/t9010.scala b/jvm/src/test/scala/scala/util/parsing/combinator/t9010.scala new file mode 100644 index 00000000..61fafd3d --- /dev/null +++ b/jvm/src/test/scala/scala/util/parsing/combinator/t9010.scala @@ -0,0 +1,51 @@ +import scala.util.parsing.combinator._ +import scala.util.DynamicVariable + +import org.junit.Test + +class t9010 { + @Test + def test: Unit = { + val p = new grammar + val lastNoSuccessVar = getLastNoSuccessVar(p) + import p._ + + val res1 = parse(x, "x") + assert(res1.successful) + assert(lastNoSuccessVar.value == None) + + val res2 = parse(x, "y") + assert(!res2.successful) + assert(lastNoSuccessVar.value == None) + + val res3 = parseAll(x, "x") + assert(res3.successful) + assert(lastNoSuccessVar.value == None) + + val res4 = parseAll(x, "y") + assert(!res4.successful) + assert(lastNoSuccessVar.value == None) + } + + private def getLastNoSuccessVar(p: Parsers): DynamicVariable[Option[_]] = { + // use java reflection instead of scala (see below) because of + // https://issues.scala-lang.org/browse/SI-9306 + val fn = "scala$util$parsing$combinator$Parsers$$lastNoSuccessVar" + val f = p.getClass.getDeclaredMethod(fn) + f.setAccessible(true) + f.invoke(p).asInstanceOf[DynamicVariable[Option[_]]] + + /* + val ru = scala.reflect.runtime.universe + val mirror = ru.runtimeMirror(getClass.getClassLoader) + val lastNoSuccessVarField = + ru.typeOf[Parsers].decl(ru.TermName("lastNoSuccessVar")).asTerm.accessed.asTerm + mirror.reflect(p).reflectField(lastNoSuccessVarField).get. + asInstanceOf[DynamicVariable[Option[_]]] + */ + } + + private final class grammar extends RegexParsers { + val x: Parser[String] = "x" + } +} diff --git a/project/build.properties b/project/build.properties index a6e117b6..35c88bab 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.8 +sbt.version=0.13.12 diff --git a/project/plugins.sbt b/project/plugins.sbt index 9eb822e3..983683a9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,3 @@ -addSbtPlugin("org.scala-lang.modules" % "scala-module-plugin" % "1.0.3") +addSbtPlugin("org.scala-lang.modules" % "scala-module-plugin" % "1.0.4") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.6") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.12") diff --git a/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala b/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala index 9fd60b3b..882b8970 100644 --- a/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala +++ b/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala @@ -156,14 +156,19 @@ trait Parsers { val successful = true } - private lazy val lastNoSuccessVar = new DynamicVariable[Option[NoSuccess]](None) + /* two layers of Option: + * outer Option is None if lastNoSuccess tracking is disabled (outside of + * phrase) and Some if tracking is enabled + * inner Option is None if NoSuccess hasn't been seen yet, Some otherwise + * this is necessary to avoid leaking NoSuccesses in thread locals */ + private lazy val lastNoSuccessVar = new DynamicVariable[Option[Option[NoSuccess]]](None) /** A common super-class for unsuccessful parse results. */ sealed abstract class NoSuccess(val msg: String, override val next: Input) extends ParseResult[Nothing] { // when we don't care about the difference between Failure and Error val successful = false - if (lastNoSuccessVar.value forall (v => !(next.pos < v.next.pos))) - lastNoSuccessVar.value = Some(this) + if (lastNoSuccessVar.value exists (_ forall (v => !(next.pos < v.next.pos)))) + lastNoSuccessVar.value = Some(Some(this)) def map[U](f: Nothing => U) = this def mapPartial[U](f: PartialFunction[Nothing, U], error: Nothing => String): ParseResult[U] = this @@ -590,7 +595,7 @@ trait Parsers { * @return a `tParser` that succeeds if `e` is the next available input. */ - implicit def accept(e: Elem): Parser[Elem] = acceptIf(_ == e)("`"+e+"' expected but " + _ + " found") + implicit def accept(e: Elem): Parser[Elem] = acceptIf(_ == e)("'"+e+"' expected but " + _ + " found") /** A parser that matches only the given list of element `es`. * @@ -908,14 +913,14 @@ trait Parsers { * if `p` consumed all the input. */ def phrase[T](p: Parser[T]) = new Parser[T] { - def apply(in: Input) = lastNoSuccessVar.withValue(None) { + def apply(in: Input) = lastNoSuccessVar.withValue(Some(None)) { p(in) match { - case s @ Success(out, in1) => - if (in1.atEnd) - s - else - lastNoSuccessVar.value filterNot { _.next.pos < in1.pos } getOrElse Failure("end of input expected", in1) - case ns => lastNoSuccessVar.value.getOrElse(ns) + case s @ Success(out, in1) => + if (in1.atEnd) + s + else + lastNoSuccessVar.value flatMap (_ filterNot { _.next.pos < in1.pos }) getOrElse Failure("end of input expected", in1) + case ns => lastNoSuccessVar.value.flatten.getOrElse(ns) } } } diff --git a/shared/src/main/scala/scala/util/parsing/input/OffsetPosition.scala b/shared/src/main/scala/scala/util/parsing/input/OffsetPosition.scala index 23f79c74..c69fc4d9 100644 --- a/shared/src/main/scala/scala/util/parsing/input/OffsetPosition.scala +++ b/shared/src/main/scala/scala/util/parsing/input/OffsetPosition.scala @@ -10,6 +10,8 @@ package scala package util.parsing.input import scala.collection.mutable.ArrayBuffer +import java.lang.{CharSequence, ThreadLocal} +import java.util.WeakHashMap /** `OffsetPosition` is a standard class for positions * represented as offsets into a source ``document''. @@ -19,10 +21,20 @@ import scala.collection.mutable.ArrayBuffer * * @author Martin Odersky */ -case class OffsetPosition(source: java.lang.CharSequence, offset: Int) extends Position { +case class OffsetPosition(source: CharSequence, offset: Int) extends Position { /** An index that contains all line starts, including first line, and eof. */ private lazy val index: Array[Int] = { + Option(OffsetPosition.indexCache.get(source)) match { + case Some(index) => index + case None => + val index = genIndex + OffsetPosition.indexCache.put(source, index) + index + } + } + + private def genIndex: Array[Int] = { val lineStarts = new ArrayBuffer[Int] lineStarts += 0 for (i <- 0 until source.length) @@ -50,8 +62,14 @@ case class OffsetPosition(source: java.lang.CharSequence, offset: Int) extends P * * @return the line at `offset` (not including a newline) */ - def lineContents: String = - source.subSequence(index(line - 1), index(line)).toString + def lineContents: String = { + val endIndex = if (source.charAt(index(line) - 1) == '\n') { + index(line) - 1 + } else { + index(line) + } + source.subSequence(index(line - 1), endIndex).toString + } /** Returns a string representation of the `Position`, of the form `line.column`. */ override def toString = line+"."+column @@ -71,3 +89,7 @@ case class OffsetPosition(source: java.lang.CharSequence, offset: Int) extends P this.line == that.line && this.column < that.column } } + +/** An object holding the index cache. + */ +object OffsetPosition extends scala.runtime.AbstractFunction2[CharSequence,Int,OffsetPosition] with PositionCache diff --git a/shared/src/main/scala/scala/util/parsing/input/PagedSeqReader.scala b/shared/src/main/scala/scala/util/parsing/input/PagedSeqReader.scala index 500c80c7..51e66943 100644 --- a/shared/src/main/scala/scala/util/parsing/input/PagedSeqReader.scala +++ b/shared/src/main/scala/scala/util/parsing/input/PagedSeqReader.scala @@ -30,10 +30,10 @@ object PagedSeqReader { * @author Martin Odersky */ class PagedSeqReader(seq: PagedSeq[Char], - override val offset: Int) extends Reader[Char] { + override val offset: Int) extends Reader[Char] { outer => import PagedSeqReader._ - override lazy val source: java.lang.CharSequence = seq + override val source: java.lang.CharSequence = seq /** Construct a `PagedSeqReader` with its first element at * `source(0)` and position `(1,1)`. @@ -51,7 +51,9 @@ class PagedSeqReader(seq: PagedSeq[Char], * otherwise, it's a `PagedSeqReader` containing the rest of input. */ def rest: PagedSeqReader = - if (seq.isDefinedAt(offset)) new PagedSeqReader(seq, offset + 1) + if (seq.isDefinedAt(offset)) new PagedSeqReader(seq, offset + 1) { + override val source: java.lang.CharSequence = outer.source + } else this /** The position of the first element in the reader. @@ -67,5 +69,7 @@ class PagedSeqReader(seq: PagedSeq[Char], * `n` elements. */ override def drop(n: Int): PagedSeqReader = - new PagedSeqReader(seq, offset + n) + new PagedSeqReader(seq, offset + n) { + override val source: java.lang.CharSequence = outer.source + } } diff --git a/shared/src/test/scala/scala/util/parsing/combinator/PackratParsersTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/PackratParsersTest.scala index 5a7e6d2e..aec44bc0 100644 --- a/shared/src/test/scala/scala/util/parsing/combinator/PackratParsersTest.scala +++ b/shared/src/test/scala/scala/util/parsing/combinator/PackratParsersTest.scala @@ -122,7 +122,7 @@ class PackratParsersTest { val failure = parseResult.asInstanceOf[Failure] assertEquals(expectedFailureMsg, failure.msg) } - assertFailure("``b'' expected but `c' found", "a a a a b b b c c c c") + assertFailure("'`b'' expected but `c' found", "a a a a b b b c c c c") assertFailure("end of input", "a a a a b b b b c c c") } diff --git a/shared/src/test/scala/scala/util/parsing/combinator/gh45.scala b/shared/src/test/scala/scala/util/parsing/combinator/gh45.scala new file mode 100644 index 00000000..e184719a --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/gh45.scala @@ -0,0 +1,46 @@ +package scala.util.parsing.combinator + +import scala.util.parsing.input._ +import scala.collection.immutable.PagedSeq + +import org.junit.Test +import org.junit.Assert.assertTrue + +import scala.util.parsing.combinator.syntactical.StandardTokenParsers + +class gh45 { + + @Test + def test4: Unit = { + def check(rd: Reader[Char]): Unit = { + val g = new grammar + val p = g.phrase(g.script) + val parseResult = p(new g.lexical.Scanner(rd)) + assertTrue(parseResult.isInstanceOf[g.Success[_]]) + } + + val str = "x once y" + check(new CharSequenceReader(str)) + /* Note that this only tests PagedSeq.rest since neither + * PackratReader nor lexical.Scanner override/use the drop method. + */ + check(new PagedSeqReader(PagedSeq.fromStrings(List(str)))) + } + +} + +private final class grammar extends StandardTokenParsers with PackratParsers { + lexical.reserved ++= List("x", "y", "z", "once") + + var onceCnt: Int = 0 + lazy val once: PackratParser[String] = memo("once") ^? { + case s if onceCnt == 0 => + onceCnt += 1 + s + } + + lazy val script: PackratParser[Any] = + ( "x" ~ once ~ "z" + | "x" ~ once ~ "y" + ) +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/gh56.scala b/shared/src/test/scala/scala/util/parsing/combinator/gh56.scala new file mode 100644 index 00000000..34e1d551 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/gh56.scala @@ -0,0 +1,57 @@ +package scala.util.parsing.combinator + +import org.junit.Assert.{assertEquals, assertTrue} +import org.junit.Test + +import scala.util.parsing.combinator.syntactical.StandardTokenParsers + +/** + * Test for issue 56: https://github.com/scala/scala-parser-combinators/issues/56 + * + * Makes sure that lineContents (and thus longString) in the Position trait doesn't + * include a newline + */ +class gh56 { + private object grammar extends StandardTokenParsers with PackratParsers { + lazy val term = (numericLit | stringLit | ident)+ + } + + @Test + def test1: Unit = { + import grammar._ + + val expr = + """/* an unclosed comment + |of multiple lines + |just to check longString/lineContents + """.stripMargin + + val fail = + """[1.1] failure: identifier expected + | + |/* an unclosed comment + |^""".stripMargin + + val parseResult = phrase(term)(new lexical.Scanner(expr)) + assertTrue(parseResult.isInstanceOf[Failure]) + assertEquals(fail, parseResult.toString) + } + + + @Test + def test2: Unit = { + import grammar._ + + val expr = "/* an unclosed comment without newline" + + val fail = + """[1.1] failure: identifier expected + | + |/* an unclosed comment without newline + |^""".stripMargin + + val parseResult = phrase(term)(new lexical.Scanner(expr)) + assertTrue(parseResult.isInstanceOf[Failure]) + assertEquals(fail, parseResult.toString) + } +}