diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..2d6c9651e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: test +on: + push: + branches: + - main + pull_request: +jobs: + test: + strategy: + fail-fast: false + matrix: + java: [8, 11, 17] + scala: [2.11.x, 2.12.x, 2.13.x] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: ${{matrix.java}} + cache: sbt + - name: Test + run: sbt ++${{matrix.scala}} test versionPolicyCheck package diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..d69ab7208 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,21 @@ +name: Release +on: + push: + tags: ["*"] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 8 + - run: sbt versionCheck ci-release + env: + PGP_PASSPHRASE: ${{secrets.PGP_PASSPHRASE}} + PGP_SECRET: ${{secrets.PGP_SECRET}} + SONATYPE_PASSWORD: ${{secrets.SONATYPE_PASSWORD}} + SONATYPE_USERNAME: ${{secrets.SONATYPE_USERNAME}} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9c90c4644..000000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: ~> 1.0 # needed for imports - -import: scala/scala-dev:travis/default.yml - -language: scala - -scala: - - 2.11.12 - - 2.12.10 - - 2.13.1 - -env: - - SCALAJS_VERSION= ADOPTOPENJDK=8 - - SCALAJS_VERSION=0.6.32 ADOPTOPENJDK=8 - - SCALAJS_VERSION=1.0.1 ADOPTOPENJDK=8 - - SCALAJS_VERSION= ADOPTOPENJDK=11 - -matrix: - exclude: - - scala: 2.11.12 - env: SCALAJS_VERSION=0.6.32 ADOPTOPENJDK=8 - - scala: 2.11.12 - env: SCALAJS_VERSION=1.0.1 ADOPTOPENJDK=8 - -install: - - git fetch --tags # get all tags for sbt-dynver - -script: ./build.sh diff --git a/build.sbt b/build.sbt index a35583347..cbd2c98ef 100644 --- a/build.sbt +++ b/build.sbt @@ -1,37 +1,55 @@ import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} +publish / skip := true // root project + +ThisBuild / startYear := Some(2002) +ThisBuild / licenses += (("Apache-2.0", url("https://www.apache.org/licenses/LICENSE-2.0"))) + +// because it doesn't declare it itself +ThisBuild / libraryDependencySchemes += "org.scala-js" %% "scalajs-library" % "semver-spec" +ThisBuild / apiURL := Some(url("https://javadoc.io/doc/org.scala-lang.modules/scala-xml_2.13/")) + +lazy val configSettings: Seq[Setting[?]] = Seq( + unmanagedSourceDirectories ++= { + unmanagedSourceDirectories.value.flatMap { dir => + def forVersion(version: String): File = file(dir.getPath ++ "-" ++ version) + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 13)) => Seq(forVersion("2.13")) + case _ => Seq(forVersion("2.11-2.12")) + } + } + } +) + lazy val xml = crossProject(JSPlatform, JVMPlatform) .withoutSuffixFor(JVMPlatform) .crossType(CrossType.Full) .in(file(".")) .settings(ScalaModulePlugin.scalaModuleSettings) - .jvmSettings(ScalaModulePlugin.scalaModuleSettingsJVM) + .jvmSettings(ScalaModulePlugin.scalaModuleOsgiSettings) .settings( name := "scala-xml", + scalaModuleAutomaticModuleName := Some("scala.xml"), + crossScalaVersions := Seq("2.13.11", "2.12.18", "2.11.12"), + scalaVersion := "2.12.18", - // Compiler team advised avoiding the -Xfuture option for releases. - // The output with -Xfuture should be periodically checked, though. - scalacOptions ++= "-deprecation:false -feature -Xlint:-stars-align,-nullary-unit,_".split("\\s+").to[Seq], - scalacOptions in Test += "-Xxml:coalescing", + scalacOptions ++= Seq("-deprecation:false", "-feature", "-Xlint:-stars-align,-nullary-unit,_"), - scalaModuleMimaPreviousVersion := { - if (System.getenv("SCALAJS_VERSION") == "1.0.1") None - else Some("1.2.0") - }, + Test / scalacOptions += "-Xxml:coalescing", - unmanagedSourceDirectories in Compile ++= { - (unmanagedSourceDirectories in Compile).value.map { dir => - val sv = scalaVersion.value - CrossVersion.partialVersion(sv) match { - case Some((2, 13)) => file(dir.getPath ++ "-2.13") - case _ => file(dir.getPath ++ "-2.11-2.12") - } - } - }, + headerLicense := Some(HeaderLicense.Custom( + s"""|Scala (https://www.scala-lang.org) + | + |Copyright EPFL and Lightbend, Inc. + | + |Licensed under Apache License 2.0 + |(http://www.apache.org/licenses/LICENSE-2.0). + | + |See the NOTICE file distributed with this work for + |additional information regarding copyright ownership. + |""".stripMargin)), - apiURL := Some( - url(s"""https://scala.github.io/scala-xml/api/${"-.*".r.replaceAllIn(version.value, "")}/""") - ), + versionPolicyIntention := Compatibility.BinaryAndSourceCompatible, apiMappings ++= scalaInstance.value.libraryJars.filter { file => file.getName.startsWith("scala-library") && file.getName.endsWith(".jar") @@ -47,7 +65,7 @@ lazy val xml = crossProject(JSPlatform, JVMPlatform) file(jarPath) -> url("http://docs.oracle.com/javase/8/docs/api") ) - } getOrElse { + }.getOrElse { // If everything fails, jam in Java 11 modules. Map( file("/modules/java.base") @@ -58,16 +76,20 @@ lazy val xml = crossProject(JSPlatform, JVMPlatform) } } ) + .settings( + inConfig(Compile)(configSettings) ++ inConfig(Test)(configSettings) + ) .jvmSettings( OsgiKeys.exportPackage := Seq(s"scala.xml.*;version=${version.value}"), - libraryDependencies += "junit" % "junit" % "4.13" % "test", - libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test", - libraryDependencies += "org.apache.commons" % "commons-lang3" % "3.9" % "test", - libraryDependencies += ("org.scala-lang" % "scala-compiler" % scalaVersion.value % "test").exclude("org.scala-lang.modules", s"scala-xml_${scalaBinaryVersion.value}") + libraryDependencies += "junit" % "junit" % "4.13.2" % Test, + libraryDependencies += "com.github.sbt" % "junit-interface" % "0.13.3" % Test, + libraryDependencies += "org.apache.commons" % "commons-lang3" % "3.12.0" % Test, + libraryDependencies += ("org.scala-lang" % "scala-compiler" % scalaVersion.value % Test).exclude("org.scala-lang.modules", s"scala-xml_${scalaBinaryVersion.value}") ) .jsSettings( + crossScalaVersions := crossScalaVersions.value.filterNot(_.startsWith("2.11")), // Scala.js cannot run forked tests - fork in Test := false + Test / fork := false ) - .jsConfigure(_.enablePlugins(ScalaJSJUnitPlugin)) + .jsEnablePlugins(ScalaJSJUnitPlugin) diff --git a/build.sh b/build.sh deleted file mode 100755 index 11ca2bb0a..000000000 --- a/build.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -set -e - -# Builds of tagged revisions are published to sonatype staging. - -# Travis runs a build on new revisions and on new tags, so a tagged revision is built twice. -# Builds for a tag have TRAVIS_TAG defined, which we use for identifying tagged builds. - -# sbt-dynver sets the version number from the tag -# sbt-travisci sets the Scala version from the travis job matrix - -# To back-publish an existing release for a new Scala / Scala.js / Scala Native version: -# - check out the tag for the version that needs to be published -# - change `.travis.yml` to adjust the version numbers and trim down the build matrix as necessary -# - commit the changes and tag this new revision with an arbitrary suffix after a hash, e.g., -# `v1.2.3#dotty-0.27` (the suffix is ignored, the version will be `1.2.3`) - -# We release on JDK 8 (for Scala 2.x and Dotty 0.x) -isReleaseJob() { - if [[ "$ADOPTOPENJDK" == "8" ]]; then - true - else - false - fi -} - -if [[ "$SCALAJS_VERSION" == "" ]]; then - projectPrefix="xml" -else - projectPrefix="xmlJS" -fi - -verPat="[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9-]+)?" -tagPat="^v$verPat(#.*)?$" - -if [[ "$TRAVIS_TAG" =~ $tagPat ]]; then - releaseTask="ci-release" - if ! isReleaseJob; then - echo "Not releasing on Java $ADOPTOPENJDK with Scala $TRAVIS_SCALA_VERSION" - exit 0 - fi -fi - -# default is +publishSigned; we cross-build with travis jobs, not sbt's crossScalaVersions -export CI_RELEASE="$projectPrefix/publishSigned" -export CI_SNAPSHOT_RELEASE="$projectPrefix/publish" - -# default is sonatypeBundleRelease, which closes and releases the staging repo -# see https://github.com/xerial/sbt-sonatype#commands -# for now, until we're confident in the new release scripts, just close the staging repo. -export CI_SONATYPE_RELEASE="; sonatypePrepare; sonatypeBundleUpload; sonatypeClose" - -sbt clean $projectPrefix/test $projectPrefix/publishLocal $releaseTask diff --git a/jvm/src/test/scala/scala/xml/CompilerErrors.scala b/jvm/src/test/scala/scala/xml/CompilerErrors.scala index 6096e5a60..995fd64ed 100644 --- a/jvm/src/test/scala/scala/xml/CompilerErrors.scala +++ b/jvm/src/test/scala/scala/xml/CompilerErrors.scala @@ -4,14 +4,20 @@ import org.junit.Test class CompilerErrors extends CompilerTesting { @Test - def t7185() = - expectXmlError("""|overloaded method value apply with alternatives: - | (f: scala.xml.Node => Boolean)scala.xml.NodeSeq - | (i: Int)scala.xml.Node - | cannot be applied to ()""".stripMargin, + def t7185() = { + // the error message here differs a bit by Scala version + import util.Properties.versionNumberString + val thing = + if (versionNumberString.startsWith("2.11") || versionNumberString.startsWith("2.12")) "method value" + else "method" + expectXmlError(s"""|overloaded $thing apply with alternatives: + | (f: scala.xml.Node => Boolean)scala.xml.NodeSeq + | (i: Int)scala.xml.Node + | cannot be applied to ()""".stripMargin, """|object Test { | () |}""") + } @Test def t1878_typer() = diff --git a/project/build.properties b/project/build.properties index a919a9b5f..875b706a8 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.3.8 +sbt.version=1.9.2 diff --git a/project/plugins.sbt b/project/plugins.sbt index c7107fa52..6f6beee39 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ -val scalaJSVersion = - Option(System.getenv("SCALAJS_VERSION")).filter(_.nonEmpty).getOrElse("0.6.32") - -addSbtPlugin("org.scala-lang.modules" % "sbt-scala-module" % "2.1.3") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion) +addSbtPlugin("org.scala-lang.modules" % "sbt-scala-module" % "3.1.0") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14") +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") diff --git a/shared/src/main/scala/scala/xml/Utility.scala b/shared/src/main/scala/scala/xml/Utility.scala index 9374ff990..4385e5807 100755 --- a/shared/src/main/scala/scala/xml/Utility.scala +++ b/shared/src/main/scala/scala/xml/Utility.scala @@ -9,6 +9,7 @@ package scala package xml +import scala.annotation.tailrec import scala.collection.mutable import scala.language.implicitConversions import scala.collection.Seq @@ -187,9 +188,7 @@ object Utility extends AnyRef with parsing.TokenTests { decodeEntities: Boolean = true, preserveWhitespace: Boolean = false, minimizeTags: Boolean = false): StringBuilder = - { serialize(x, pscope, sb, stripComments, decodeEntities, preserveWhitespace, if (minimizeTags) MinimizeMode.Always else MinimizeMode.Never) - } /** * Serialize an XML Node to a StringBuilder. @@ -206,35 +205,66 @@ object Utility extends AnyRef with parsing.TokenTests { stripComments: Boolean = false, decodeEntities: Boolean = true, preserveWhitespace: Boolean = false, - minimizeTags: MinimizeMode.Value = MinimizeMode.Default): StringBuilder = - { - x match { - case c: Comment => if (!stripComments) c buildString sb; sb - case s: SpecialNode => s buildString sb - case g: Group => - for (c <- g.nodes) serialize(c, g.scope, sb, stripComments, decodeEntities, preserveWhitespace, minimizeTags); sb - case el: Elem => - // print tag with namespace declarations - sb.append('<') - el.nameToString(sb) - if (el.attributes ne null) el.attributes.buildString(sb) - el.scope.buildString(sb, pscope) - if (el.child.isEmpty && - (minimizeTags == MinimizeMode.Always || - (minimizeTags == MinimizeMode.Default && el.minimizeEmpty))) { - // no children, so use short form: - sb.append("/>") - } else { - // children, so use long form: ... - sb.append('>') - sequenceToXML(el.child, el.scope, sb, stripComments, decodeEntities, preserveWhitespace, minimizeTags) - sb.append("') - } - case _ => throw new IllegalArgumentException("Don't know how to serialize a " + x.getClass.getName) - } + minimizeTags: MinimizeMode.Value = MinimizeMode.Default + ): StringBuilder = { + serializeImpl(List(x), pscope, false, stripComments, minimizeTags, sb) + sb + } + + private def serializeImpl( + ns: Seq[Node], + pscope: NamespaceBinding, + spaced: Boolean, + stripComments: Boolean, + minimizeTags: MinimizeMode.Value, + sb: StringBuilder + ): Unit = { + @tailrec def ser(nss: List[List[Node]], pscopes: List[NamespaceBinding], spaced: List[Boolean], toClose: List[Node]): Unit = nss match { + case List(Nil) => + case Nil :: rests => + if (toClose.head != null) { + sb.append("') + } + ser(rests, pscopes.tail, spaced.tail, toClose.tail) + case (n :: ns) :: r => + def sp(): Unit = if (ns.nonEmpty && spaced.head) sb.append(' ') + n match { + case c: Comment => + if (!stripComments) { + c.buildString(sb) + sp() + } + ser(ns :: r, pscopes, spaced, toClose) + case s: SpecialNode => + s.buildString(sb) + sp() + ser(ns :: r, pscopes, spaced, toClose) + case g: Group => + ser(g.nodes.toList :: ns :: r, g.scope :: pscopes, false :: spaced, null :: toClose) + case e: Elem => + sb.append('<') + e.nameToString(sb) + if (e.attributes.ne(null)) e.attributes.buildString(sb) + e.scope.buildString(sb, pscopes.head) + if (e.child.isEmpty && + (minimizeTags == MinimizeMode.Always || + (minimizeTags == MinimizeMode.Default && e.minimizeEmpty))) { + // no children, so use short form: + sb.append("/>") + sp() + ser(ns :: r, pscopes, spaced, toClose) + } else { + sb.append('>') + val csp = e.child.forall(isAtomAndNotText) + ser(e.child.toList :: ns :: r, e.scope :: pscopes, csp :: spaced, e :: toClose) + } + case n => throw new IllegalArgumentException("Don't know how to serialize a " + n.getClass.getName) + } } + ser(List(ns.toList), List(pscope), List(spaced), Nil) + } def sequenceToXML( children: Seq[Node], @@ -243,20 +273,11 @@ object Utility extends AnyRef with parsing.TokenTests { stripComments: Boolean = false, decodeEntities: Boolean = true, preserveWhitespace: Boolean = false, - minimizeTags: MinimizeMode.Value = MinimizeMode.Default): Unit = - { - if (children.isEmpty) return - else if (children forall isAtomAndNotText) { // add space - val it = children.iterator - val f = it.next() - serialize(f, pscope, sb, stripComments, decodeEntities, preserveWhitespace, minimizeTags) - while (it.hasNext) { - val x = it.next() - sb.append(' ') - serialize(x, pscope, sb, stripComments, decodeEntities, preserveWhitespace, minimizeTags) - } - } else children foreach { serialize(_, pscope, sb, stripComments, decodeEntities, preserveWhitespace, minimizeTags) } - } + minimizeTags: MinimizeMode.Value = MinimizeMode.Default + ): Unit = if (children.nonEmpty) { + val spaced = children.forall(isAtomAndNotText) + serializeImpl(children, pscope, spaced, stripComments, minimizeTags, sb) + } /** * Returns prefix of qualified name if any. diff --git a/shared/src/test/scala/scala/xml/UtilityTest.scala b/shared/src/test/scala/scala/xml/UtilityTest.scala index a27fb0b1a..b7eb95a80 100644 --- a/shared/src/test/scala/scala/xml/UtilityTest.scala +++ b/shared/src/test/scala/scala/xml/UtilityTest.scala @@ -217,4 +217,10 @@ class UtilityTest { val x =
{Text(" My name ")}{Text(" is ")}{Text(" Harry ")}
assertEquals(
My name is Harry
, Utility.trim(x)) } + + @Test + def toStringStackSafe(): Unit = { + val xml = (1 to 5000).foldRight() { case (_, n) => {n} } + xml.toString + } }