diff --git a/build.sbt b/build.sbt index d19e02686..73a5dde34 100644 --- a/build.sbt +++ b/build.sbt @@ -185,7 +185,9 @@ lazy val shared = projectMatrix coverageEnabled := false ) .defaultAxes(VirtualAxis.jvm) - .jvmPlatformFull(buildWithTargetVersions.map(_._2)) + // just compile shared with the latest patch for each minor Scala version + // to reduce the cardinality of the build + .jvmPlatform(buildScalaVersions) .disablePlugins(ScalafixPlugin) lazy val input = projectMatrix @@ -198,12 +200,14 @@ lazy val input = projectMatrix scalacOptions ++= warnUnused.value, // For RemoveUnusedTerms logLevel := Level.Error, // avoid flood of compiler warnings libraryDependencies ++= testsDependencies.value, - coverageEnabled := false + coverageEnabled := false, + // mimic dependsOn(shared) but allowing binary Scala version matching + Compile / internalDependencyClasspath ++= + resolve(shared, Compile / exportedProducts).value ) .defaultAxes(VirtualAxis.jvm) - .jvmPlatformFull(buildWithTargetVersions.map(_._2)) + .jvmPlatformTargets(buildScalaVersionsWithTargets.map(_._2)) .disablePlugins(ScalafixPlugin) - .dependsOn(shared) lazy val output = projectMatrix .in(file("scalafix-tests/output")) @@ -211,12 +215,21 @@ lazy val output = projectMatrix noPublishAndNoMima, scalacOptions --= warnUnusedImports.value, libraryDependencies ++= testsDependencies.value, - coverageEnabled := false + coverageEnabled := false, + // mimic dependsOn(shared) but allowing binary Scala version matching + Compile / internalDependencyClasspath ++= + resolve(shared, Compile / exportedProducts).value ) .defaultAxes(VirtualAxis.jvm) - .jvmPlatform(buildScalaVersions) + .jvmPlatformTargets( + buildScalaVersionsWithTargets + .map(_._2) + // don't compile output with old Scala patch versions to reduce the + // cardinality of the build: checking that it compiles with the + // latest patch of each minor Scala version is enough + .filter(axis => buildScalaVersions.contains(axis.scalaVersion)) + ) .disablePlugins(ScalafixPlugin) - .dependsOn(shared) lazy val unit = projectMatrix .in(file("scalafix-tests/unit")) @@ -359,7 +372,7 @@ lazy val expect = projectMatrix } ) .defaultAxes(VirtualAxis.jvm) - .jvmPlatformWithTargets(buildWithTargetVersions) + .jvmPlatformAgainstTargets(buildScalaVersionsWithTargets) .dependsOn(integration) lazy val docs = projectMatrix diff --git a/project/Dependencies.scala b/project/Dependencies.scala index d1a7ad2e1..9ed299376 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -2,8 +2,6 @@ import ScalafixBuild.autoImport.isScala2 import sbt.Keys.scalaVersion import sbt._ -import scala.util.Try - /* scalafmt: { maxColumn = 120 }*/ object Dependencies { @@ -11,20 +9,6 @@ object Dependencies { val scala213 = sys.props.getOrElse("scala213.nightly", "2.13.12") val scala3 = sys.props.getOrElse("scala3.nightly", "3.3.1") - val buildScalaVersions = Seq(scala212, scala213, scala3) - val buildWithTargetVersions: Seq[(String, String)] = { - val all = buildScalaVersions.map(sv => (sv, sv)) ++ - Seq(scala213, scala212).flatMap(sv => previousVersions(sv).map(prev => (sv, prev))) ++ - Seq(scala213, scala212).map(sv => (sv, scala3)) - - all.filter { - case (_, v) if System.getProperty("java.version") == "21" => - !Seq("2.12.16", "2.12.17", "2.13.10").contains(v) - case _ => - true - } - } - val bijectionCoreV = "0.9.7" val collectionCompatV = "2.11.0" val coursierV = "2.1.8" @@ -74,13 +58,4 @@ object Dependencies { val scalatest = "org.scalatest" %% "scalatest" % scalatestV val munit = "org.scalameta" %% "munit" % munitV val semanticdbScalacCore = "org.scalameta" % "semanticdb-scalac-core" % scalametaV cross CrossVersion.full - - private def previousVersions(scalaVersion: String): Seq[String] = { - val split = scalaVersion.split('.') - val binaryVersion = split.take(2).mkString(".") - val compilerVersion = Try(split.last.toInt).toOption - val previousPatchVersions = - compilerVersion.map(version => List.range(version - 2, version).filter(_ >= 0)).getOrElse(Nil) - previousPatchVersions.map(v => s"$binaryVersion.$v") - } } diff --git a/project/ScalafixBuild.scala b/project/ScalafixBuild.scala index b87349757..5a0ca61f6 100644 --- a/project/ScalafixBuild.scala +++ b/project/ScalafixBuild.scala @@ -15,6 +15,7 @@ import com.github.sbt.sbtghpages.GhpagesKeys import sbt.librarymanagement.ivy.IvyDependencyResolution import sbt.plugins.IvyPlugin import sbtversionpolicy.DependencyCheckReport +import scala.util.Try object ScalafixBuild extends AutoPlugin with GhpagesKeys { override def trigger = allRequirements @@ -30,6 +31,33 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys { publish / skip := true ) lazy val supportedScalaVersions = List(scala213, scala212) + lazy val buildScalaVersions = Seq(scala212, scala213, scala3) + lazy val buildScalaVersionsWithTargets: Seq[(String, TargetAxis)] = + buildScalaVersions.map(sv => (sv, TargetAxis(sv))) ++ + Seq(scala213, scala212).flatMap { sv => + def previousVersions(scalaVersion: String): Seq[String] = { + val split = scalaVersion.split('.') + val binaryVersion = split.take(2).mkString(".") + val compilerVersion = Try(split.last.toInt).toOption + val previousPatchVersions = + compilerVersion + .map(version => List.range(version - 2, version).filter(_ >= 0)) + .getOrElse(Nil) + previousPatchVersions + .map { patch => s"$binaryVersion.$patch" } + .filterNot { v => + System.getProperty("java.version").startsWith("21") && + Seq("2.12.16", "2.12.17", "2.13.10").contains(v) + } + } + + val prevVersions = previousVersions(sv).map(prev => TargetAxis(prev)) + val scala3FromScala2 = TargetAxis(scala3) + val xsource3 = TargetAxis(sv, xsource3 = true) + + (prevVersions :+ scala3FromScala2 :+ xsource3).map((sv, _)) + } + lazy val publishLocalTransitive = taskKey[Unit]("Run publishLocal on this project and its dependencies") lazy val isFullCrossVersion = Seq( @@ -123,32 +151,29 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys { taskKey[Unit]("run tests, excluding those incompatible with Windows") /** - * Lookup a setting key for the project of the same scala version in the - * given matrix + * Lookup the project with the closest Scala version, and resolve `key` */ def resolve[T]( matrix: ProjectMatrix, key: SettingKey[T] ): Def.Initialize[T] = Def.settingDyn { - val sv = scalaVersion.value - val project = matrix.jvm(sv) + val project = lookup(matrix, scalaVersion.value) Def.setting((project / key).value) } /** - * Lookup a task key for the project of the same scala version in the given - * matrix + * Lookup the project with the closest Scala version, and resolve `key` */ def resolve[T]( matrix: ProjectMatrix, key: TaskKey[T] ): Def.Initialize[Task[T]] = Def.taskDyn { - val sv = scalaVersion.value - val project = matrix.jvm(sv) + val project = lookup(matrix, scalaVersion.value) Def.task((project / key).value) } + } import autoImport._ @@ -317,4 +342,35 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys { } } ) + + /** + * Find the project matching the full Scala version when available or a binary + * one otherwise + */ + private def lookup(matrix: ProjectMatrix, scalaVersion: String): Project = { + val projects = matrix + .allProjects() + .collect { + case (project, projectVirtualAxes) + // CliSemanticSuite depends on classes compiled without -Xsource:3 + if !projectVirtualAxes.contains(Xsource3Axis) => + ( + projectVirtualAxes + .collectFirst { case x: VirtualAxis.ScalaVersionAxis => x } + .get + .value, + project + ) + } + .toMap + + val fullMatch = projects.get(scalaVersion) + + def binaryMatch = { + val scalaBinaryVersion = CrossVersion.binaryScalaVersion(scalaVersion) + projects.find(_._1.startsWith(scalaBinaryVersion)).map(_._2) + } + + fullMatch.orElse(binaryMatch).get + } } diff --git a/project/TargetAxis.scala b/project/TargetAxis.scala index 1506905d3..ecf784c87 100644 --- a/project/TargetAxis.scala +++ b/project/TargetAxis.scala @@ -1,94 +1,140 @@ import sbt._ +import sbt.Keys._ import sbt.internal.ProjectMatrix import sbtprojectmatrix.ProjectMatrixPlugin.autoImport._ +import scala.reflect.ClassTag -/** Use on ProjectMatrix rows to tag an affinity to a custom scalaVersion */ -case class TargetAxis(scalaVersion: String) extends VirtualAxis.WeakAxis { +/** + * Use on ProjectMatrix rows to tag an affinity to a project with a custom + * scalaVersion, with or without `-Xsource:3` + */ +case class TargetAxis(scalaVersion: String, xsource3: Boolean = false) + extends VirtualAxis.WeakAxis { - override val idSuffix = s"Target${scalaVersion.replace('.', '_')}" - override val directorySuffix = s"target$scalaVersion" + private val maybeSource3 = if (xsource3) "xsource3" else "" + + override val idSuffix = + s"Target${scalaVersion.replace('.', '_')}$maybeSource3" + override val directorySuffix = s"target$scalaVersion$maybeSource3" override val suffixOrder = VirtualAxis.scalaABIVersion("any").suffixOrder + 1 + + /** Axis values the targeted project should have */ + private def axisValues: Seq[VirtualAxis] = + VirtualAxis.ScalaVersionAxis(scalaVersion, scalaVersion) +: + (if (xsource3) Seq(Xsource3Axis) else Seq()) + + /** Settings the targeted project should have */ + private def settings: Seq[Setting[_]] = + if (xsource3) + Seq( + scalacOptions += "-Xsource:3", + Compile / unmanagedSourceDirectories ~= { + _.map { dir => file(s"${dir.getAbsolutePath}-xsource3") } + } + ) + else Seq() } -object TargetAxis { +/** Use on ProjectMatrix rows to mark usage of "-Xsource:3" */ +case object Xsource3Axis extends VirtualAxis.WeakAxis { + override val idSuffix = "xsource3" + override val directorySuffix = idSuffix + override val suffixOrder = VirtualAxis.scalaABIVersion("any").suffixOrder + 2 +} - def targetScalaVersion(virtualAxes: Seq[VirtualAxis]): Option[String] = - virtualAxes.collectFirst { case a: TargetAxis => a.scalaVersion } +object TargetAxis { /** - * When invoked on a ProjectMatrix with a TargetAxis, lookup the project - * generated by `matrix` with a scalaVersion matching the one declared in that - * TargetAxis, and resolve `key`. + * Lookup the project with axes best matching the TargetAxis, and resolve + * `key` */ def resolve[T]( matrix: ProjectMatrix, key: TaskKey[T] ): Def.Initialize[Task[T]] = Def.taskDyn { - val sv = targetScalaVersion(virtualAxes.value).get - val project = exactOrBinaryScalaVersionMatch(matrix, sv) + val project = lookup(matrix, virtualAxes.value) Def.task((project / key).value) } /** - * When invoked on a ProjectMatrix with a TargetAxis, lookup the project - * generated by `matrix` with a scalaVersion matching the one declared in that - * TargetAxis, and resolve `key`. + * Lookup the project with axes best matching the TargetAxis, and resolve + * `key` */ def resolve[T]( matrix: ProjectMatrix, key: SettingKey[T] ): Def.Initialize[T] = Def.settingDyn { - val sv = targetScalaVersion(virtualAxes.value).get - val project = exactOrBinaryScalaVersionMatch(matrix, sv) + val project = lookup(matrix, virtualAxes.value) Def.setting((project / key).value) } - private def exactOrBinaryScalaVersionMatch( + /** + * Find the project best matching the TargetAxis requests present in the + * provided virtualAxes: + * - presence or absence of the `-Xsource:3` flag + * - full Scala version when available or a binary one otherwise + */ + private def lookup( matrix: ProjectMatrix, - scalaVersion: String + virtualAxes: Seq[VirtualAxis] ): Project = { - val projectsWithAxisValues = matrix.allProjects().flatMap { - case (p, axisValues) => axisValues.map(v => (p, v)) + val TargetAxis(scalaVersion, xsource3) = + virtualAxes.collectFirst { case x: TargetAxis => x }.get + + val projects = matrix + .allProjects() + .collect { + case (project, projectVirtualAxes) + if projectVirtualAxes.contains(Xsource3Axis) == xsource3 => + ( + projectVirtualAxes + .collectFirst { case x: VirtualAxis.ScalaVersionAxis => x } + .get + .value, + project + ) + } + .toMap + + val fullMatch = projects.get(scalaVersion) + + def binaryMatch = { + val scalaBinaryVersion = CrossVersion.binaryScalaVersion(scalaVersion) + projects.find(_._1.startsWith(scalaBinaryVersion)).map(_._2) } - projectsWithAxisValues.collectFirst { - case (p, VirtualAxis.ScalaVersionAxis(_, value)) - if value == scalaVersion || - value == CrossVersion.binaryScalaVersion(scalaVersion) => - p - }.get + fullMatch.orElse(binaryMatch).get } implicit class TargetProjectMatrix(projectMatrix: ProjectMatrix) { - /** Like jvmPlatform but with the full scala version attached */ - def jvmPlatformFull(scalaVersions: Seq[String]): ProjectMatrix = { - scalaVersions.foldLeft(projectMatrix) { (acc, sv) => + /** + * Create one JVM project for each target, tagged and configured with the + * requests of that target + */ + def jvmPlatformTargets(targets: Seq[TargetAxis]): ProjectMatrix = { + targets.foldLeft(projectMatrix) { (acc, target) => acc.customRow( autoScalaLibrary = true, - axisValues = Seq( - VirtualAxis.jvm, - VirtualAxis.scalaVersionAxis(sv, sv) - ), - process = p => p + axisValues = target.axisValues :+ VirtualAxis.jvm, + process = { _.settings(target.settings) } ) } } /** - * Like jvmPlatform but adding a target axis with the scala version provided - * as the second element of the tuple + * Create one JVM project for each tuple (Scala version, affinity) */ - def jvmPlatformWithTargets( - buildWithTargetVersions: Seq[(String, String)] + def jvmPlatformAgainstTargets( + scalaVersionAgainstTarget: Seq[(String, TargetAxis)] ): ProjectMatrix = { - buildWithTargetVersions.foldLeft(projectMatrix) { - case (acc, (build, target)) => + scalaVersionAgainstTarget.foldLeft(projectMatrix) { + case (acc, (scalaVersion, target)) => acc.jvmPlatform( - scalaVersions = Seq(build), - axisValues = Seq(TargetAxis(target)), + scalaVersions = Seq(scalaVersion), + axisValues = Seq(target), settings = Seq() ) } diff --git a/scalafix-tests/input/src/main/scala-2-xsource3/test/removeUnused/RemoveUnusedImportsWildcards.scala b/scalafix-tests/input/src/main/scala-2-xsource3/test/removeUnused/RemoveUnusedImportsWildcards.scala new file mode 100644 index 000000000..fdc71b3df --- /dev/null +++ b/scalafix-tests/input/src/main/scala-2-xsource3/test/removeUnused/RemoveUnusedImportsWildcards.scala @@ -0,0 +1,16 @@ +/* +rules = RemoveUnused +*/ +package test.removeUnused + +import java.io.* +import java.util.concurrent._ + +import scala.collection.mutable +import scala.util._ +import scala.io.* + +object RemoveUnusedImportsWildcards { + new File("") + new ConcurrentHashMap[String, String]() +} \ No newline at end of file diff --git a/scalafix-tests/integration/src/test/scala/scalafix/tests/interfaces/ScalafixArgumentsSuite.scala b/scalafix-tests/integration/src/test/scala/scalafix/tests/interfaces/ScalafixArgumentsSuite.scala index 18905db5a..0eae51b18 100644 --- a/scalafix-tests/integration/src/test/scala/scalafix/tests/interfaces/ScalafixArgumentsSuite.scala +++ b/scalafix-tests/integration/src/test/scala/scalafix/tests/interfaces/ScalafixArgumentsSuite.scala @@ -442,69 +442,6 @@ class ScalafixArgumentsSuite extends AnyFunSuite with DiffAssertions { assert(obtainedError == ScalafixFileEvaluationError.ParseError) } - test("Scala 3 style wildcard import", SkipWindows) { - // https://github.com/scalacenter/scalafix/issues/1663 - - // Todo(i1680): Add another test for scala 3 that doesn't uses removeUnused or - // at least remove the if when removeUnused is supported in scala 3 - if (ScalaVersions.isScala3) { - cancel() - } - - val cwd = StringFS - .string2dir( - """|/src/Main.scala - |import scala.collection.mutable - |import scala.util._ - |import scala.io.* - | - |object Main - """.stripMargin, - charset - ) - .toNIO - val d = cwd.resolve("out") - val target = cwd.resolve("target") - val src = cwd.resolve("src") - Files.createDirectories(d) - val main = src.resolve("Main.scala") - - val scalacOptions = Array[String]( - "-Xsource:3", - removeUnused, - "-classpath", - s"${scalaLibrary.mkString(":")}", - "-d", - d.toString, - main.toString - ) ++ CompatSemanticdb.scalacOptions(src, target) - - val _ = CompatSemanticdb.runScalac(scalacOptions) - val result = api - .withRules( - Collections.singletonList(removeUnsuedRule().name.toString()) - ) - .withClasspath((scalaLibrary.map(_.toNIO) :+ target).asJava) - .withScalacOptions(Collections.singletonList(removeUnused)) - .withScalaVersion("2.13.8") - .withPaths(Seq(main).asJava) - .withSourceroot(src) - .evaluate() - - val error = result.getError - assert(!error.isPresent) - assert(result.isSuccessful) - assert(result.getFileEvaluations.length == 1) - val fileEvaluation = result.getFileEvaluations.head - assert(fileEvaluation.isSuccessful) - val expected = - """| - |object Main - |""".stripMargin - val obtained = fileEvaluation.previewPatches.get() - assertNoDiff(obtained, expected) - } - test("withScalaVersion: non-parsable scala version") { val run = Try(api.withScalaVersion("213")) val expectedErrorMessage = "Failed to parse the Scala version" diff --git a/scalafix-tests/output/src/main/scala-2-xsource3/test/removeUnused/RemoveUnusedImportsWildcards.scala b/scalafix-tests/output/src/main/scala-2-xsource3/test/removeUnused/RemoveUnusedImportsWildcards.scala new file mode 100644 index 000000000..cfefcbf3b --- /dev/null +++ b/scalafix-tests/output/src/main/scala-2-xsource3/test/removeUnused/RemoveUnusedImportsWildcards.scala @@ -0,0 +1,10 @@ +package test.removeUnused + +import java.io.* +import java.util.concurrent._ + + +object RemoveUnusedImportsWildcards { + new File("") + new ConcurrentHashMap[String, String]() +} \ No newline at end of file