From 9fa372df7b22c5af47a34736388977c2a3b25c8b Mon Sep 17 00:00:00 2001 From: Alex Piskunov Date: Tue, 17 Sep 2024 18:57:43 +0300 Subject: [PATCH 1/2] Native image support --- .../main/scala/scala/build/FixArtifacts.scala | 34 +++ .../scala/scala/build/internal/Runner.scala | 2 + .../cli/commands/scalafix/Scalafix.scala | 236 ++++++++++++------ .../scala/cli/integration/TestInputs.scala | 4 +- 4 files changed, 194 insertions(+), 82 deletions(-) create mode 100644 modules/build/src/main/scala/scala/build/FixArtifacts.scala diff --git a/modules/build/src/main/scala/scala/build/FixArtifacts.scala b/modules/build/src/main/scala/scala/build/FixArtifacts.scala new file mode 100644 index 0000000000..5827ba619a --- /dev/null +++ b/modules/build/src/main/scala/scala/build/FixArtifacts.scala @@ -0,0 +1,34 @@ +package scala.build + +import coursier.cache.FileCache +import coursier.core.{Repository, Version} +import coursier.util.Task +import dependency.* + +import scala.build.EitherCps.{either, value} +import scala.build.errors.BuildException +import scala.build.internal.CsLoggerUtil.* + +final case class FixArtifacts( + artifacts: Seq[(String, os.Path)] +) + +object FixArtifacts { + def artifacts( + extraRepositories: Seq[Repository], + logger: Logger, + cache: FileCache[Task] + ): Either[BuildException, FixArtifacts] = either { + val scalafixDeps = Seq(dep"ch.epfl.scala:scalafix-cli_2.13.14:0.12.1") + val fixArtifacts = Artifacts.artifacts( + scalafixDeps.map(Positioned.none), + extraRepositories, + None, + logger, + cache.withMessage(s"Downloading 0.12.1") + ) + FixArtifacts( + artifacts = value(fixArtifacts) + ) + } +} diff --git a/modules/build/src/main/scala/scala/build/internal/Runner.scala b/modules/build/src/main/scala/scala/build/internal/Runner.scala index f1a6f7a780..3b3e40137a 100644 --- a/modules/build/src/main/scala/scala/build/internal/Runner.scala +++ b/modules/build/src/main/scala/scala/build/internal/Runner.scala @@ -176,6 +176,8 @@ object Runner { useManifest, scratchDirOpt ) + + println(command.mkString(" ")) if (allowExecve) maybeExec("java", command, logger, cwd = cwd, extraEnv = extraEnv) diff --git a/modules/cli/src/main/scala/scala/cli/commands/scalafix/Scalafix.scala b/modules/cli/src/main/scala/scala/cli/commands/scalafix/Scalafix.scala index 1e177e913e..f5a87de8bd 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/scalafix/Scalafix.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/scalafix/Scalafix.scala @@ -5,23 +5,23 @@ import caseapp.core.help.HelpFormat import dependency.* import scalafix.interfaces.ScalafixError.* import scalafix.interfaces.{ - Scalafix => ScalafixInterface, ScalafixError, ScalafixException, - ScalafixRule + ScalafixRule, + Scalafix as ScalafixInterface } import java.util.Optional - import scala.build.input.{Inputs, Script, SourceScalaFile} import scala.build.internal.{Constants, ExternalBinaryParams, FetchExternalBinary, Runner} import scala.build.options.{BuildOptions, Scope} -import scala.build.{Build, BuildThreads, Logger, Sources} +import scala.build.{Build, BuildThreads, FixArtifacts, Logger, Sources} import scala.cli.CurrentParams +import coursier.cache.FileCache import scala.cli.commands.compile.Compile.buildOptionsOrExit import scala.cli.commands.fmt.FmtUtil.* import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions} -import scala.cli.commands.{ScalaCommand, SpecificationLevel, compile} +import scala.cli.commands.{compile, ScalaCommand, SpecificationLevel} import scala.cli.config.Keys import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils @@ -29,6 +29,7 @@ import scala.collection.mutable import scala.collection.mutable.Buffer import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* +import scala.build.EitherCps.{either, value} object Scalafix extends ScalaCommand[ScalafixOptions] { override def group: String = HelpCommandGroup.Main.toString @@ -94,88 +95,163 @@ object Scalafix extends ScalaCommand[ScalafixOptions] { val configFilePathOpt = options.scalafixConf.map(os.Path(_, os.pwd)) val relPaths = sourcePaths.map(_.toNIO.getFileName) - val scalafix = ScalafixInterface - .fetchAndClassloadInstance(scalaBinaryVersion) - .newArguments() - .withWorkingDirectory(workspace.toNIO) - .withPaths(relPaths.asJava) - .withRules(options.rules.asJava) - .withConfig(configFilePathOpt.map(_.toNIO).toJava) - .withScalaVersion(scalaVersion) - - logger.debug( - s"Processing ${sourcePaths.size} Scala sources" + // --config | -c <.scalafix.conf> + // File path to a .scalafix.conf configuration file. + // --sourceroot + // Absolute path passed to semanticdb with + // -P:semanticdb:sourceroot:. Relative filenames persisted in the + // Semantic DB are absolutized by the sourceroot. Defaults to current + // working directory if not provided. + // --classpath + // java.io.File.pathSeparator separated list of directories or jars + // containing '.semanticdb' files. The 'semanticdb' files are emitted by + // the semanticdb-scalac compiler plugin and are necessary for semantic + // rules like ExplicitResultTypes to function. + // --classpath-auto-roots + // Automatically infer --classpath starting from these directories. + // Ignored if --classpath is provided. + // --no-strict-semanticdb + // Disable validation when loading semanticdb files. + // --rules | -r + // Scalafix rules to run. + // --test + // Exit non-zero code if files have not been fixed. Won't write to files. + + val res = Build.build( + inputs, + buildOptionsWithSemanticDb, + compilerMaker, + None, + logger, + crossBuilds = false, + buildTests = false, + partial = None, + actionableDiagnostics = actionableDiagnostics ) + val builds = res.orExit(logger) - val rulesThatWillRun: Either[ScalafixException, mutable.Buffer[ScalafixRule]] = - try - Right(scalafix.rulesThatWillRun().asScala) - catch - case e: ScalafixException => Left(e) - val needToBuild: Boolean = rulesThatWillRun match - case Right(rules) => rules.exists(_.kind().isSemantic) - case Left(_) => true - - val preparedScalafixInstance = if (needToBuild) { - val res = Build.build( - inputs, - buildOptionsWithSemanticDb, - compilerMaker, - None, + val successfulBuildOpt = for { + build <- builds.get(Scope.Main) + sOpt <- build.successfulOpt + } yield sOpt + + val classPaths = successfulBuildOpt.map(_.fullClassPath).getOrElse(Seq.empty) + val externalDeps = + options.shared.dependencies.compileOnlyDependency ++ successfulBuildOpt.map( + _.options.classPathOptions.extraCompileOnlyDependencies.values.flatten.map(_.value.render) + ).getOrElse(Seq.empty) + val scalacOptions = options.shared.scalac.scalacOption ++ successfulBuildOpt.map( + _.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) + ).getOrElse(Seq.empty) + + val scalafixOptions = + configFilePathOpt.map(file => Seq("-c", file.toString)).getOrElse(Nil) ++ + Seq("--sourceroot", workspace.toString) ++ + Seq("--classpath", classPaths.mkString(java.io.File.pathSeparator)) + //Seq("--scalac-options") ++ scalacOptions + + either { + val artifacts = FixArtifacts.artifacts( + value(buildOptions.finalRepositories), logger, - crossBuilds = false, - buildTests = false, - partial = None, - actionableDiagnostics = actionableDiagnostics + buildOptions.internal.cache.getOrElse(FileCache()) ) - val builds = res.orExit(logger) - - val successfulBuildOpt = for { - build <- builds.get(Scope.Main) - sOpt <- build.successfulOpt - } yield sOpt - - val classPaths = successfulBuildOpt.map(_.fullClassPath).getOrElse(Seq.empty) - val externalDeps = - options.shared.dependencies.compileOnlyDependency ++ successfulBuildOpt.map( - _.options.classPathOptions.extraCompileOnlyDependencies.values.flatten.map(_.value.render) - ).getOrElse(Seq.empty) - val scalacOptions = options.shared.scalac.scalacOption ++ successfulBuildOpt.map( - _.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) - ).getOrElse(Seq.empty) - scalafix - .withScalacOptions(scalacOptions.asJava) - .withClasspath(classPaths.map(_.toNIO).asJava) - .withToolClasspath(Seq.empty.asJava, externalDeps.asJava) - } - else - scalafix - - val customScalafixInstance = preparedScalafixInstance - .withParsedArguments(options.scalafixArg.asJava) - - val errors = if (options.check) { - val evaluation = customScalafixInstance.evaluate() - if (evaluation.isSuccessful) - evaluation.getFileEvaluations.foldLeft(List.empty[String]) { - case (errors, fileEvaluation) => - val problemMessage = fileEvaluation.getErrorMessage.toScala.orElse( - fileEvaluation.previewPatchesAsUnifiedDiff.toScala - ) - errors ++ problemMessage - } - else - evaluation.getErrorMessage.toScala.toList - } - else - customScalafixInstance.run().map(prepareErrorMessage).toList + val proc = Runner.runJvm( + buildOptions.javaHome().value.javaCommand, + buildOptions.javaOptions.javaOpts.toSeq.map(_.value.value), + value(artifacts.map(_.artifacts.map(_._2))), + "scalafix.cli.Cli", + scalafixOptions, + logger, + cwd = Some(workspace) + ) - if (errors.isEmpty) sys.exit(0) - else { - errors.tapEach(logger.error) - sys.exit(1) + sys.exit(proc.waitFor()) } + +// val scalafix = ScalafixInterface +// .fetchAndClassloadInstance(scalaBinaryVersion) +// .newArguments() +// .withWorkingDirectory(workspace.toNIO) +// .withPaths(relPaths.asJava) +// .withRules(options.rules.asJava) +// .withConfig(configFilePathOpt.map(_.toNIO).toJava) +// .withScalaVersion(scalaVersion) +// +// logger.debug( +// s"Processing ${sourcePaths.size} Scala sources" +// ) +// +// val rulesThatWillRun: Either[ScalafixException, mutable.Buffer[ScalafixRule]] = +// try +// Right(scalafix.rulesThatWillRun().asScala) +// catch +// case e: ScalafixException => Left(e) +// val needToBuild: Boolean = rulesThatWillRun match +// case Right(rules) => rules.exists(_.kind().isSemantic) +// case Left(_) => true +// +// val preparedScalafixInstance = if (needToBuild) { +// val res = Build.build( +// inputs, +// buildOptionsWithSemanticDb, +// compilerMaker, +// None, +// logger, +// crossBuilds = false, +// buildTests = false, +// partial = None, +// actionableDiagnostics = actionableDiagnostics +// ) +// val builds = res.orExit(logger) +// +// val successfulBuildOpt = for { +// build <- builds.get(Scope.Main) +// sOpt <- build.successfulOpt +// } yield sOpt +// +// val classPaths = successfulBuildOpt.map(_.fullClassPath).getOrElse(Seq.empty) +// val externalDeps = +// options.shared.dependencies.compileOnlyDependency ++ successfulBuildOpt.map( +// _.options.classPathOptions.extraCompileOnlyDependencies.values.flatten.map(_.value.render) +// ).getOrElse(Seq.empty) +// val scalacOptions = options.shared.scalac.scalacOption ++ successfulBuildOpt.map( +// _.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) +// ).getOrElse(Seq.empty) +// +// scalafix +// .withScalacOptions(scalacOptions.asJava) +// .withClasspath(classPaths.map(_.toNIO).asJava) +// .withToolClasspath(Seq.empty.asJava, externalDeps.asJava) +// } +// else +// scalafix +// +// val customScalafixInstance = preparedScalafixInstance +// .withParsedArguments(options.scalafixArg.asJava) +// +// val errors = if (options.check) { +// val evaluation = customScalafixInstance.evaluate() +// if (evaluation.isSuccessful) +// evaluation.getFileEvaluations.foldLeft(List.empty[String]) { +// case (errors, fileEvaluation) => +// val problemMessage = fileEvaluation.getErrorMessage.toScala.orElse( +// fileEvaluation.previewPatchesAsUnifiedDiff.toScala +// ) +// errors ++ problemMessage +// } +// else +// evaluation.getErrorMessage.toScala.toList +// } +// else +// customScalafixInstance.run().map(prepareErrorMessage).toList +// +// if (errors.isEmpty) sys.exit(0) +// else { +// errors.tapEach(logger.error) +// sys.exit(1) +// } } private def prepareErrorMessage(error: ScalafixError): String = error match diff --git a/modules/integration/src/main/scala/scala/cli/integration/TestInputs.scala b/modules/integration/src/main/scala/scala/cli/integration/TestInputs.scala index d79d80fc3e..fd90d91d87 100644 --- a/modules/integration/src/main/scala/scala/cli/integration/TestInputs.scala +++ b/modules/integration/src/main/scala/scala/cli/integration/TestInputs.scala @@ -92,8 +92,8 @@ object TestInputs { case ex: IOException => System.err.println(s"Ignoring $ex while removing $tmpDir0") } - try f(tmpDir0) - finally removeAll() + f(tmpDir0) + //finally removeAll() } private def tmpDir: os.Path = { From 8ce554fd9399447232745fd131bbbe756a199531 Mon Sep 17 00:00:00 2001 From: Vadim Chelyshov Date: Wed, 16 Oct 2024 16:34:15 +0300 Subject: [PATCH 2/2] scalafix: fix native image support --- build.sc | 2 + .../main/scala/scala/build/FixArtifacts.scala | 34 --- .../scala/scala/build/ScalafixArtifacts.scala | 151 +++++++++++++ .../scala/scala/build/internal/Runner.scala | 2 - .../cli/commands/scalafix/Scalafix.scala | 208 +++++------------- .../scala/cli/integration/TestInputs.scala | 4 +- .../integration/ScalafixTestDefinitions.scala | 51 ++++- .../cli/integration/ScalafixTests213.scala | 4 +- project/deps.sc | 2 +- 9 files changed, 265 insertions(+), 193 deletions(-) delete mode 100644 modules/build/src/main/scala/scala/build/FixArtifacts.scala create mode 100644 modules/build/src/main/scala/scala/build/ScalafixArtifacts.scala diff --git a/build.sc b/build.sc index 140c6f8e47..87cd6692d0 100644 --- a/build.sc +++ b/build.sc @@ -505,6 +505,8 @@ trait Core extends ScalaCliCrossSbtModule | def mavenAppArtifactId = "${Deps.Versions.mavenAppArtifactId}" | def mavenAppGroupId = "${Deps.Versions.mavenAppGroupId}" | def mavenAppVersion = "${Deps.Versions.mavenAppVersion}" + | + | def scalafixVersion = "${Deps.Versions.scalafix}" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) diff --git a/modules/build/src/main/scala/scala/build/FixArtifacts.scala b/modules/build/src/main/scala/scala/build/FixArtifacts.scala deleted file mode 100644 index 5827ba619a..0000000000 --- a/modules/build/src/main/scala/scala/build/FixArtifacts.scala +++ /dev/null @@ -1,34 +0,0 @@ -package scala.build - -import coursier.cache.FileCache -import coursier.core.{Repository, Version} -import coursier.util.Task -import dependency.* - -import scala.build.EitherCps.{either, value} -import scala.build.errors.BuildException -import scala.build.internal.CsLoggerUtil.* - -final case class FixArtifacts( - artifacts: Seq[(String, os.Path)] -) - -object FixArtifacts { - def artifacts( - extraRepositories: Seq[Repository], - logger: Logger, - cache: FileCache[Task] - ): Either[BuildException, FixArtifacts] = either { - val scalafixDeps = Seq(dep"ch.epfl.scala:scalafix-cli_2.13.14:0.12.1") - val fixArtifacts = Artifacts.artifacts( - scalafixDeps.map(Positioned.none), - extraRepositories, - None, - logger, - cache.withMessage(s"Downloading 0.12.1") - ) - FixArtifacts( - artifacts = value(fixArtifacts) - ) - } -} diff --git a/modules/build/src/main/scala/scala/build/ScalafixArtifacts.scala b/modules/build/src/main/scala/scala/build/ScalafixArtifacts.scala new file mode 100644 index 0000000000..f3386325cc --- /dev/null +++ b/modules/build/src/main/scala/scala/build/ScalafixArtifacts.scala @@ -0,0 +1,151 @@ +package scala.build + +import coursier.cache.FileCache +import coursier.core.{Repository, Version} +import coursier.util.Task +import dependency.* + +import scala.build.EitherCps.{either, value} +import scala.build.errors.BuildException +import scala.build.internal.CsLoggerUtil.* +import scala.build.internal.Constants +import java.util.Properties +import coursier.error.CoursierError +import scala.build.errors.FetchingDependenciesError +import coursier.error.ResolutionError +import org.apache.commons.compress.archivers.zip.ZipFile +import os.Path +import java.io.ByteArrayInputStream + +final case class ScalafixArtifacts( + scalafixJars: Seq[os.Path], + toolsJars: Seq[os.Path] +) + +object ScalafixArtifacts { + + def artifacts( + scalaVersion: String, + compileOnlyDeps: Seq[Dependency], + extraRepositories: Seq[Repository], + logger: Logger, + cache: FileCache[Task] + ): Either[BuildException, ScalafixArtifacts] = + either { + val scalafixProperties = + value(fetchOrLoadScalafixProperties(extraRepositories, logger, cache)) + val key = + value(scalafixPropsKey(scalaVersion)) + val fetchScalaVersion = scalafixProperties.getProperty(key) + + val scalafixDeps = + Seq(dep"ch.epfl.scala:scalafix-cli_$fetchScalaVersion:${Constants.scalafixVersion}") + val scalafix = + value( + Artifacts.artifacts( + scalafixDeps.map(Positioned.none), + extraRepositories, + None, + logger, + cache.withMessage(s"Downloading scalafix-cli ${Constants.scalafixVersion}") + ) + ) + + val tools = + value( + Artifacts.artifacts( + compileOnlyDeps.map(Positioned.none), + extraRepositories, + None, + logger, + cache.withMessage(s"Downloading tools classpath for scalafix") + ) + ) + ScalafixArtifacts(scalafix.map(_._2), tools.map(_._2)) + } + + private def fetchOrLoadScalafixProperties( + extraRepositories: Seq[Repository], + logger: Logger, + cache: FileCache[Task] + ): Either[BuildException, Properties] = + either { + val cacheDir = Directories.directories.cacheDir / "scalafix-props-cache" + val cachePath = cacheDir / s"scalafix-interfaces-${Constants.scalafixVersion}.properties" + + val content = + if (!os.exists(cachePath)) { + val interfacesJar = value(fetchScalafixInterfaces(extraRepositories, logger, cache)) + val propsData = value(readScalafixProperties(interfacesJar)) + if (!os.exists(cacheDir)) os.makeDir(cacheDir) + os.write(cachePath, propsData) + propsData + } + else os.read(cachePath) + + val props = new Properties() + val stream = new ByteArrayInputStream(content.getBytes()) + props.load(stream) + props + } + + private def fetchScalafixInterfaces( + extraRepositories: Seq[Repository], + logger: Logger, + cache: FileCache[Task] + ): Either[BuildException, Path] = + either { + val scalafixInterfaces = dep"ch.epfl.scala:scalafix-interfaces:${Constants.scalafixVersion}" + + val fetchResult = + value( + Artifacts.artifacts( + List(scalafixInterfaces).map(Positioned.none), + extraRepositories, + None, + logger, + cache.withMessage(s"Downloading scalafix-interfaces ${scalafixInterfaces.version}") + ) + ) + + val expectedJarName = s"scalafix-interfaces-${Constants.scalafixVersion}.jar" + val interfacesJar = fetchResult.collectFirst { + case (_, path) if path.last == expectedJarName => path + } + + value( + interfacesJar.toRight(new BuildException("Failed to found scalafix-interfaces jar") {}) + ) + } + + private def readScalafixProperties(jar: Path): Either[BuildException, String] = { + import scala.jdk.CollectionConverters.* + val zipFile = new ZipFile(jar.toNIO) + val entry = zipFile.getEntries().asScala.find(entry => + entry.getName() == "scalafix-interfaces.properties" + ) + val out = + entry.toRight(new BuildException("Failed to found scalafix properties") {}) + .map { entry => + val stream = zipFile.getInputStream(entry) + val bytes = stream.readAllBytes() + new String(bytes) + } + zipFile.close() + out + } + + private def scalafixPropsKey(scalaVersion: String): Either[BuildException, String] = { + val regex = "(\\d)\\.(\\d+).+".r + scalaVersion match { + case regex("2", "12") => Right("scala212") + case regex("2", "13") => Right("scala213") + case regex("3", x) if x.toInt <= 3 => Right("scala3LTS") + case regex("3", _) => Right("scala3Next") + case _ => + Left(new BuildException(s"Scalafix is not supported for Scala version: $scalaVersion") {}) + } + + } + +} diff --git a/modules/build/src/main/scala/scala/build/internal/Runner.scala b/modules/build/src/main/scala/scala/build/internal/Runner.scala index 3b3e40137a..f1a6f7a780 100644 --- a/modules/build/src/main/scala/scala/build/internal/Runner.scala +++ b/modules/build/src/main/scala/scala/build/internal/Runner.scala @@ -176,8 +176,6 @@ object Runner { useManifest, scratchDirOpt ) - - println(command.mkString(" ")) if (allowExecve) maybeExec("java", command, logger, cwd = cwd, extraEnv = extraEnv) diff --git a/modules/cli/src/main/scala/scala/cli/commands/scalafix/Scalafix.scala b/modules/cli/src/main/scala/scala/cli/commands/scalafix/Scalafix.scala index f5a87de8bd..11f4e8df79 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/scalafix/Scalafix.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/scalafix/Scalafix.scala @@ -15,7 +15,7 @@ import java.util.Optional import scala.build.input.{Inputs, Script, SourceScalaFile} import scala.build.internal.{Constants, ExternalBinaryParams, FetchExternalBinary, Runner} import scala.build.options.{BuildOptions, Scope} -import scala.build.{Build, BuildThreads, FixArtifacts, Logger, Sources} +import scala.build.{Build, BuildThreads, ScalafixArtifacts, Logger, Sources} import scala.cli.CurrentParams import coursier.cache.FileCache import scala.cli.commands.compile.Compile.buildOptionsOrExit @@ -30,6 +30,8 @@ import scala.collection.mutable.Buffer import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* import scala.build.EitherCps.{either, value} +import java.io.File +import scala.build.Artifacts object Scalafix extends ScalaCommand[ScalafixOptions] { override def group: String = HelpCommandGroup.Main.toString @@ -84,38 +86,10 @@ object Scalafix extends ScalaCommand[ScalafixOptions] { val scalaVersion = options.buildOptions.orExit(logger).scalaParams.orExit(logger).map(_.scalaVersion) .getOrElse(Constants.defaultScalaVersion) + val scalaBinVersion = + options.buildOptions.orExit(logger).scalaParams.orExit(logger).map(_.scalaBinaryVersion) - val scalaBinaryVersion = scalaVersion match - case v if v.startsWith("2.12.") => "2.12" - case v if v.startsWith("2.13.") => "2.13" - case v if v.startsWith("3.") => "2.13" - case _ => - logger.error("Unsupported scala version " + scalaVersion) - sys.exit(1) val configFilePathOpt = options.scalafixConf.map(os.Path(_, os.pwd)) - val relPaths = sourcePaths.map(_.toNIO.getFileName) - - // --config | -c <.scalafix.conf> - // File path to a .scalafix.conf configuration file. - // --sourceroot - // Absolute path passed to semanticdb with - // -P:semanticdb:sourceroot:. Relative filenames persisted in the - // Semantic DB are absolutized by the sourceroot. Defaults to current - // working directory if not provided. - // --classpath - // java.io.File.pathSeparator separated list of directories or jars - // containing '.semanticdb' files. The 'semanticdb' files are emitted by - // the semanticdb-scalac compiler plugin and are necessary for semantic - // rules like ExplicitResultTypes to function. - // --classpath-auto-roots - // Automatically infer --classpath starting from these directories. - // Ignored if --classpath is provided. - // --no-strict-semanticdb - // Disable validation when loading semanticdb files. - // --rules | -r - // Scalafix rules to run. - // --test - // Exit non-zero code if files have not been fixed. Won't write to files. val res = Build.build( inputs, @@ -130,128 +104,60 @@ object Scalafix extends ScalaCommand[ScalafixOptions] { ) val builds = res.orExit(logger) - val successfulBuildOpt = for { - build <- builds.get(Scope.Main) - sOpt <- build.successfulOpt - } yield sOpt - - val classPaths = successfulBuildOpt.map(_.fullClassPath).getOrElse(Seq.empty) - val externalDeps = - options.shared.dependencies.compileOnlyDependency ++ successfulBuildOpt.map( - _.options.classPathOptions.extraCompileOnlyDependencies.values.flatten.map(_.value.render) - ).getOrElse(Seq.empty) - val scalacOptions = options.shared.scalac.scalacOption ++ successfulBuildOpt.map( - _.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) - ).getOrElse(Seq.empty) - - val scalafixOptions = - configFilePathOpt.map(file => Seq("-c", file.toString)).getOrElse(Nil) ++ - Seq("--sourceroot", workspace.toString) ++ - Seq("--classpath", classPaths.mkString(java.io.File.pathSeparator)) - //Seq("--scalac-options") ++ scalacOptions - - either { - val artifacts = FixArtifacts.artifacts( - value(buildOptions.finalRepositories), - logger, - buildOptions.internal.cache.getOrElse(FileCache()) - ) - - val proc = Runner.runJvm( - buildOptions.javaHome().value.javaCommand, - buildOptions.javaOptions.javaOpts.toSeq.map(_.value.value), - value(artifacts.map(_.artifacts.map(_._2))), - "scalafix.cli.Cli", - scalafixOptions, - logger, - cwd = Some(workspace) - ) + builds.get(Scope.Main).flatMap(_.successfulOpt) match + case None => sys.exit(1) + case Some(build) => + val classPaths = build.fullClassPath + val compileOnlyDeps = { + val params = ScalaParameters(scalaVersion) + build.options.classPathOptions.extraCompileOnlyDependencies.values.flatten.map( + _.value.applyParams(params) + ) + } - sys.exit(proc.waitFor()) - } + val scalacOptions = options.shared.scalac.scalacOption ++ + build.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) + + either { + val artifacts = + value( + ScalafixArtifacts.artifacts( + scalaVersion, + compileOnlyDeps, + value(buildOptions.finalRepositories), + logger, + buildOptions.internal.cache.getOrElse(FileCache()) + ) + ) + + val scalafixOptions = + configFilePathOpt.map(file => Seq("-c", file.toString)).getOrElse(Nil) ++ + Seq("--sourceroot", workspace.toString) ++ + Seq("--classpath", classPaths.mkString(java.io.File.pathSeparator)) ++ + options.scalafixConf.toList.flatMap(scalafixConf => List("--config", scalafixConf)) ++ + (if (options.check) Seq("--test") else Nil) ++ + (if (scalacOptions.nonEmpty) scalacOptions.flatMap(Seq("--scalac-options", _)) + else Nil) ++ + (if (artifacts.toolsJars.nonEmpty) + Seq("--tool-classpath", artifacts.toolsJars.mkString(java.io.File.pathSeparator)) + else Nil) ++ + options.rules.flatMap(Seq("-r", _)) + ++ options.scalafixArg + + val proc = Runner.runJvm( + buildOptions.javaHome().value.javaCommand, + buildOptions.javaOptions.javaOpts.toSeq.map(_.value.value), + artifacts.scalafixJars, + "scalafix.cli.Cli", + scalafixOptions, + logger, + cwd = Some(workspace), + allowExecve = true + ) + + sys.exit(proc.waitFor()) + } -// val scalafix = ScalafixInterface -// .fetchAndClassloadInstance(scalaBinaryVersion) -// .newArguments() -// .withWorkingDirectory(workspace.toNIO) -// .withPaths(relPaths.asJava) -// .withRules(options.rules.asJava) -// .withConfig(configFilePathOpt.map(_.toNIO).toJava) -// .withScalaVersion(scalaVersion) -// -// logger.debug( -// s"Processing ${sourcePaths.size} Scala sources" -// ) -// -// val rulesThatWillRun: Either[ScalafixException, mutable.Buffer[ScalafixRule]] = -// try -// Right(scalafix.rulesThatWillRun().asScala) -// catch -// case e: ScalafixException => Left(e) -// val needToBuild: Boolean = rulesThatWillRun match -// case Right(rules) => rules.exists(_.kind().isSemantic) -// case Left(_) => true -// -// val preparedScalafixInstance = if (needToBuild) { -// val res = Build.build( -// inputs, -// buildOptionsWithSemanticDb, -// compilerMaker, -// None, -// logger, -// crossBuilds = false, -// buildTests = false, -// partial = None, -// actionableDiagnostics = actionableDiagnostics -// ) -// val builds = res.orExit(logger) -// -// val successfulBuildOpt = for { -// build <- builds.get(Scope.Main) -// sOpt <- build.successfulOpt -// } yield sOpt -// -// val classPaths = successfulBuildOpt.map(_.fullClassPath).getOrElse(Seq.empty) -// val externalDeps = -// options.shared.dependencies.compileOnlyDependency ++ successfulBuildOpt.map( -// _.options.classPathOptions.extraCompileOnlyDependencies.values.flatten.map(_.value.render) -// ).getOrElse(Seq.empty) -// val scalacOptions = options.shared.scalac.scalacOption ++ successfulBuildOpt.map( -// _.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) -// ).getOrElse(Seq.empty) -// -// scalafix -// .withScalacOptions(scalacOptions.asJava) -// .withClasspath(classPaths.map(_.toNIO).asJava) -// .withToolClasspath(Seq.empty.asJava, externalDeps.asJava) -// } -// else -// scalafix -// -// val customScalafixInstance = preparedScalafixInstance -// .withParsedArguments(options.scalafixArg.asJava) -// -// val errors = if (options.check) { -// val evaluation = customScalafixInstance.evaluate() -// if (evaluation.isSuccessful) -// evaluation.getFileEvaluations.foldLeft(List.empty[String]) { -// case (errors, fileEvaluation) => -// val problemMessage = fileEvaluation.getErrorMessage.toScala.orElse( -// fileEvaluation.previewPatchesAsUnifiedDiff.toScala -// ) -// errors ++ problemMessage -// } -// else -// evaluation.getErrorMessage.toScala.toList -// } -// else -// customScalafixInstance.run().map(prepareErrorMessage).toList -// -// if (errors.isEmpty) sys.exit(0) -// else { -// errors.tapEach(logger.error) -// sys.exit(1) -// } } private def prepareErrorMessage(error: ScalafixError): String = error match diff --git a/modules/integration/src/main/scala/scala/cli/integration/TestInputs.scala b/modules/integration/src/main/scala/scala/cli/integration/TestInputs.scala index fd90d91d87..d79d80fc3e 100644 --- a/modules/integration/src/main/scala/scala/cli/integration/TestInputs.scala +++ b/modules/integration/src/main/scala/scala/cli/integration/TestInputs.scala @@ -92,8 +92,8 @@ object TestInputs { case ex: IOException => System.err.println(s"Ignoring $ex while removing $tmpDir0") } - f(tmpDir0) - //finally removeAll() + try f(tmpDir0) + finally removeAll() } private def tmpDir: os.Path = { diff --git a/modules/integration/src/test/scala/scala/cli/integration/ScalafixTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/ScalafixTestDefinitions.scala index 2c48be8728..f43a0dc6ec 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ScalafixTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ScalafixTestDefinitions.scala @@ -55,7 +55,7 @@ abstract class ScalafixTestDefinitions extends ScalaCliSuite with TestScalaVersi cwd = root, check = false ) - expect(res.exitCode == 1) + expect(res.exitCode != 0) val updatedContent = noCrLf(os.read(root / "Hello.scala")) expect(updatedContent == noCrLf(simpleInputsOriginalContent)) } @@ -100,4 +100,53 @@ abstract class ScalafixTestDefinitions extends ScalaCliSuite with TestScalaVersi expect(updatedContent == expectedContent) } } + + test("rule args") { + val input = TestInputs( + os.rel / confFileName -> + s"""|rules = [ + | RemoveUnused, + | ExplicitResultTypes + |] + |""".stripMargin, + os.rel / "Hello.scala" -> + s"""|//> using options $unusedRuleOption + |package hello + | + |object Hello { + | def a = { + | val x = 1 // keep unused - exec only ExplicitResultTypes + | 42 + | } + |} + |""".stripMargin + ) + + input.fromRoot { root => + os.proc( + TestUtil.cli, + "scalafix", + ".", + "--rules", + "ExplicitResultTypes", + "--power", + scalaVersionArgs + ).call(cwd = root) + val updatedContent = noCrLf(os.read(root / "Hello.scala")) + val expected = + s"""|//> using options $unusedRuleOption + |package hello + | + |object Hello { + | def a: Int = { + | val x = 1 // keep unused - exec only ExplicitResultTypes + | 42 + | } + |} + |""".stripMargin + + expect(updatedContent == expected) + + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/ScalafixTests213.scala b/modules/integration/src/test/scala/scala/cli/integration/ScalafixTests213.scala index c0e4338662..213991c72e 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ScalafixTests213.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ScalafixTests213.scala @@ -8,7 +8,7 @@ class ScalafixTests213 extends ScalafixTestDefinitions with Test213 { test("external rule") { val unnamedParamsInputsContent: String = """//> using options -P:semanticdb:synthetics:on - |//> using compileOnly.dep "com.github.jatcwang::scalafix-named-params:0.2.4" + |//> using compileOnly.dep "com.github.jatcwang::scalafix-named-params:0.2.5" | |package foo | @@ -35,7 +35,7 @@ class ScalafixTests213 extends ScalafixTestDefinitions with Test213 { ) val expectedContent: String = noCrLf { """//> using options -P:semanticdb:synthetics:on - |//> using compileOnly.dep "com.github.jatcwang::scalafix-named-params:0.2.4" + |//> using compileOnly.dep "com.github.jatcwang::scalafix-named-params:0.2.5" | |package foo | diff --git a/project/deps.sc b/project/deps.sc index 236677e747..409b57e978 100644 --- a/project/deps.sc +++ b/project/deps.sc @@ -118,7 +118,7 @@ object Deps { def mavenAppArtifactId = "maven-app" def mavenAppGroupId = "com.example" def mavenAppVersion = "0.1-SNAPSHOT" - def scalafix = "0.12.1" + def scalafix = "0.13.0" } // DO NOT hardcode a Scala version in this dependency string // This dependency is used to ensure that Ammonite is available for Scala versions