Skip to content

Commit

Permalink
Merge pull request #1 from dos65/scalafix-command-native
Browse files Browse the repository at this point in the history
Scalafix command native
  • Loading branch information
Vigorge authored Oct 16, 2024
2 parents 3e08eb2 + 8ce554f commit a409057
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 96 deletions.
2 changes: 2 additions & 0 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
151 changes: 151 additions & 0 deletions modules/build/src/main/scala/scala/build/ScalafixArtifacts.scala
Original file line number Diff line number Diff line change
@@ -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") {})
}

}

}
166 changes: 74 additions & 92 deletions modules/cli/src/main/scala/scala/cli/commands/scalafix/Scalafix.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,33 @@ 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, ScalafixArtifacts, 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
import scala.collection.mutable
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
Expand Down Expand Up @@ -83,99 +86,78 @@ 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)

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 res = Build.build(
inputs,
buildOptionsWithSemanticDb,
compilerMaker,
None,
logger,
crossBuilds = false,
buildTests = false,
partial = None,
actionableDiagnostics = actionableDiagnostics
)
val builds = res.orExit(logger)

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)
)
}

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
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())
)
)
errors ++ problemMessage

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())
}
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
Expand Down
Loading

0 comments on commit a409057

Please sign in to comment.