Skip to content

Commit eec8bde

Browse files
Vigorgedos65
andauthored
Scalafix command for scala-cli with basic options and tests (#2968)
* Scalafix command from scala-cli with basic optionns and tests * Refactor documentation and minor inconveniences * Make scalac options and scala version configurable * Remove fixed scalac options * Correct ScalafixOptions tags to experimental * Add more tests and fix external rule dependencies forwarding to scalafix * Differentiate tests for 2.12, 2.13 and 3+ scala versions * Fix undeleted printlns * Fmt and fix * Native image support * scalafix: fix native image support * scalafix: run fix + fix tests * scalafix: add scalafix.dep, more tests * scalafix: gen doc * scalafix: support ExplicitResultTypes for scala3 + minor fixes * scalafix: fix docs tests --------- Co-authored-by: Vadim Chelyshov <[email protected]>
1 parent 095cc09 commit eec8bde

File tree

15 files changed

+840
-36
lines changed

15 files changed

+840
-36
lines changed

build.sc

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,13 @@ object dummy extends Module {
274274
Deps.scalaPy
275275
)
276276
}
277+
object scalafix extends ScalaModule with Bloop.Module {
278+
def skipBloop = true
279+
def scalaVersion = Scala.defaultInternal
280+
def ivyDeps = Agg(
281+
Deps.scalafixInterfaces
282+
)
283+
}
277284
}
278285

279286
trait BuildMacros extends ScalaCliCrossSbtModule
@@ -528,6 +535,8 @@ trait Core extends ScalaCliCrossSbtModule
528535
| def mavenAppArtifactId = "${Deps.Versions.mavenAppArtifactId}"
529536
| def mavenAppGroupId = "${Deps.Versions.mavenAppGroupId}"
530537
| def mavenAppVersion = "${Deps.Versions.mavenAppVersion}"
538+
|
539+
| def scalafixVersion = "${Deps.Versions.scalafix}"
531540
|}
532541
|""".stripMargin
533542
if (!os.isFile(dest) || os.read(dest) != code)
@@ -919,7 +928,8 @@ trait Cli extends CrossSbtModule with ProtoBuildModule with CliLaunchers
919928
Deps.scalaPackager.exclude("com.lihaoyi" -> "os-lib_2.13"),
920929
Deps.signingCli.exclude((organization, "config_2.13")),
921930
Deps.slf4jNop, // to silence jgit
922-
Deps.sttp
931+
Deps.sttp,
932+
Deps.scalafixInterfaces
923933
)
924934
def compileIvyDeps = super.compileIvyDeps() ++ Agg(
925935
Deps.jsoniterMacros,
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package scala.build
2+
3+
import coursier.cache.FileCache
4+
import coursier.core.{Repository, Version}
5+
import coursier.error.{CoursierError, ResolutionError}
6+
import coursier.util.Task
7+
import dependency.*
8+
import org.apache.commons.compress.archivers.zip.ZipFile
9+
import os.Path
10+
11+
import java.io.ByteArrayInputStream
12+
import java.util.Properties
13+
14+
import scala.build.EitherCps.{either, value}
15+
import scala.build.errors.{BuildException, FetchingDependenciesError}
16+
import scala.build.internal.Constants
17+
import scala.build.internal.CsLoggerUtil.*
18+
19+
final case class ScalafixArtifacts(
20+
scalafixJars: Seq[os.Path],
21+
toolsJars: Seq[os.Path]
22+
)
23+
24+
object ScalafixArtifacts {
25+
26+
def artifacts(
27+
scalaVersion: String,
28+
externalRulesDeps: Seq[Positioned[AnyDependency]],
29+
extraRepositories: Seq[Repository],
30+
logger: Logger,
31+
cache: FileCache[Task]
32+
): Either[BuildException, ScalafixArtifacts] =
33+
either {
34+
val scalafixProperties =
35+
value(fetchOrLoadScalafixProperties(extraRepositories, logger, cache))
36+
val key =
37+
value(scalafixPropsKey(scalaVersion))
38+
val fetchScalaVersion = scalafixProperties.getProperty(key)
39+
40+
val scalafixDeps =
41+
Seq(dep"ch.epfl.scala:scalafix-cli_$fetchScalaVersion:${Constants.scalafixVersion}")
42+
43+
val scalafix =
44+
value(
45+
Artifacts.artifacts(
46+
scalafixDeps.map(Positioned.none),
47+
extraRepositories,
48+
None,
49+
logger,
50+
cache.withMessage(s"Downloading scalafix-cli ${Constants.scalafixVersion}")
51+
)
52+
)
53+
54+
val scalaParameters =
55+
// Scalafix for scala 3 uses 2.13-published community rules
56+
// https://github.com/scalacenter/scalafix/issues/2041
57+
if (scalaVersion.startsWith("3")) ScalaParameters(Constants.defaultScala213Version)
58+
else ScalaParameters(scalaVersion)
59+
60+
val tools =
61+
value(
62+
Artifacts.artifacts(
63+
externalRulesDeps,
64+
extraRepositories,
65+
Some(scalaParameters),
66+
logger,
67+
cache.withMessage(s"Downloading scalafix.deps")
68+
)
69+
)
70+
71+
ScalafixArtifacts(scalafix.map(_._2), tools.map(_._2))
72+
}
73+
74+
private def fetchOrLoadScalafixProperties(
75+
extraRepositories: Seq[Repository],
76+
logger: Logger,
77+
cache: FileCache[Task]
78+
): Either[BuildException, Properties] =
79+
either {
80+
val cacheDir = Directories.directories.cacheDir / "scalafix-props-cache"
81+
val cachePath = cacheDir / s"scalafix-interfaces-${Constants.scalafixVersion}.properties"
82+
83+
val content =
84+
if (!os.exists(cachePath)) {
85+
val interfacesJar = value(fetchScalafixInterfaces(extraRepositories, logger, cache))
86+
val propsData = value(readScalafixProperties(interfacesJar))
87+
if (!os.exists(cacheDir)) os.makeDir(cacheDir)
88+
os.write(cachePath, propsData)
89+
propsData
90+
}
91+
else os.read(cachePath)
92+
val props = new Properties()
93+
val stream = new ByteArrayInputStream(content.getBytes())
94+
props.load(stream)
95+
props
96+
}
97+
98+
private def fetchScalafixInterfaces(
99+
extraRepositories: Seq[Repository],
100+
logger: Logger,
101+
cache: FileCache[Task]
102+
): Either[BuildException, Path] =
103+
either {
104+
val scalafixInterfaces = dep"ch.epfl.scala:scalafix-interfaces:${Constants.scalafixVersion}"
105+
106+
val fetchResult =
107+
value(
108+
Artifacts.artifacts(
109+
List(scalafixInterfaces).map(Positioned.none),
110+
extraRepositories,
111+
None,
112+
logger,
113+
cache.withMessage(s"Downloading scalafix-interfaces ${scalafixInterfaces.version}")
114+
)
115+
)
116+
117+
val expectedJarName = s"scalafix-interfaces-${Constants.scalafixVersion}.jar"
118+
val interfacesJar = fetchResult.collectFirst {
119+
case (_, path) if path.last == expectedJarName => path
120+
}
121+
122+
value(
123+
interfacesJar.toRight(new BuildException("Failed to found scalafix-interfaces jar") {})
124+
)
125+
}
126+
127+
private def readScalafixProperties(jar: Path): Either[BuildException, String] = {
128+
import scala.jdk.CollectionConverters.*
129+
val zipFile = new ZipFile(jar.toNIO)
130+
val entry = zipFile.getEntries().asScala.find(entry =>
131+
entry.getName() == "scalafix-interfaces.properties"
132+
)
133+
val out =
134+
entry.toRight(new BuildException("Failed to found scalafix properties") {})
135+
.map { entry =>
136+
val stream = zipFile.getInputStream(entry)
137+
val bytes = stream.readAllBytes()
138+
new String(bytes)
139+
}
140+
zipFile.close()
141+
out
142+
}
143+
144+
private def scalafixPropsKey(scalaVersion: String): Either[BuildException, String] = {
145+
val regex = "(\\d)\\.(\\d+).+".r
146+
scalaVersion match {
147+
case regex("2", "12") => Right("scala212")
148+
case regex("2", "13") => Right("scala213")
149+
case regex("3", x) if x.toInt <= 3 => Right("scala3LTS")
150+
case regex("3", _) => Right("scala3Next")
151+
case _ =>
152+
Left(new BuildException(s"Scalafix is not supported for Scala version: $scalaVersion") {})
153+
}
154+
155+
}
156+
157+
}

modules/cli/src/main/scala/scala/cli/ScalaCliCommands.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class ScalaCliCommands(
3939
export0.Export,
4040
fix.Fix,
4141
fmt.Fmt,
42+
scalafix.Scalafix,
4243
new HelpCmd(help),
4344
installcompletions.InstallCompletions,
4445
installhome.InstallHome,
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package scala.cli.commands.scalafix
2+
3+
import caseapp.*
4+
import caseapp.core.help.HelpFormat
5+
import coursier.cache.FileCache
6+
import dependency.*
7+
import scalafix.interfaces.ScalafixError.*
8+
import scalafix.interfaces.{
9+
Scalafix => ScalafixInterface,
10+
ScalafixError,
11+
ScalafixException,
12+
ScalafixRule
13+
}
14+
15+
import java.io.File
16+
import java.util.Optional
17+
18+
import scala.build.EitherCps.{either, value}
19+
import scala.build.input.{Inputs, Script, SourceScalaFile}
20+
import scala.build.internal.{Constants, ExternalBinaryParams, FetchExternalBinary, Runner}
21+
import scala.build.options.{BuildOptions, Scope}
22+
import scala.build.{Artifacts, Build, BuildThreads, Logger, ScalafixArtifacts, Sources}
23+
import scala.cli.CurrentParams
24+
import scala.cli.commands.compile.Compile.buildOptionsOrExit
25+
import scala.cli.commands.fmt.FmtUtil.*
26+
import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions}
27+
import scala.cli.commands.{ScalaCommand, SpecificationLevel, compile}
28+
import scala.cli.config.Keys
29+
import scala.cli.util.ArgHelpers.*
30+
import scala.cli.util.ConfigDbUtils
31+
import scala.collection.mutable
32+
import scala.collection.mutable.Buffer
33+
import scala.jdk.CollectionConverters.*
34+
import scala.jdk.OptionConverters.*
35+
36+
object Scalafix extends ScalaCommand[ScalafixOptions] {
37+
override def group: String = HelpCommandGroup.Main.toString
38+
override def sharedOptions(options: ScalafixOptions): Option[SharedOptions] = Some(options.shared)
39+
override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.EXPERIMENTAL
40+
41+
val hiddenHelpGroups: Seq[HelpGroup] =
42+
Seq(
43+
HelpGroup.Scala,
44+
HelpGroup.Java,
45+
HelpGroup.Dependency,
46+
HelpGroup.ScalaJs,
47+
HelpGroup.ScalaNative,
48+
HelpGroup.CompilationServer,
49+
HelpGroup.Debug
50+
)
51+
override def helpFormat: HelpFormat = super.helpFormat
52+
.withHiddenGroups(hiddenHelpGroups)
53+
.withHiddenGroupsWhenShowHidden(hiddenHelpGroups)
54+
.withPrimaryGroup(HelpGroup.Format)
55+
override def names: List[List[String]] = List(
56+
List("scalafix")
57+
)
58+
59+
override def runCommand(options: ScalafixOptions, args: RemainingArgs, logger: Logger): Unit = {
60+
val buildOptions = buildOptionsOrExit(options)
61+
val buildOptionsWithSemanticDb = buildOptions.copy(scalaOptions =
62+
buildOptions.scalaOptions.copy(semanticDbOptions =
63+
buildOptions.scalaOptions.semanticDbOptions.copy(generateSemanticDbs = Some(true))
64+
)
65+
)
66+
val inputs = options.shared.inputs(args.all).orExit(logger)
67+
val threads = BuildThreads.create()
68+
val compilerMaker = options.shared.compilerMaker(threads)
69+
val configDb = ConfigDbUtils.configDb.orExit(logger)
70+
val actionableDiagnostics =
71+
options.shared.logging.verbosityOptions.actions.orElse(
72+
configDb.get(Keys.actions).getOrElse(None)
73+
)
74+
75+
val workspace =
76+
if (args.all.isEmpty) os.pwd
77+
else inputs.workspace
78+
79+
val scalaVersion =
80+
options.buildOptions.orExit(logger).scalaParams.orExit(logger).map(_.scalaVersion)
81+
.getOrElse(Constants.defaultScalaVersion)
82+
val scalaBinVersion =
83+
options.buildOptions.orExit(logger).scalaParams.orExit(logger).map(_.scalaBinaryVersion)
84+
85+
val configFilePathOpt = options.scalafixConf.map(os.Path(_, os.pwd))
86+
87+
val res = Build.build(
88+
inputs,
89+
buildOptionsWithSemanticDb,
90+
compilerMaker,
91+
None,
92+
logger,
93+
crossBuilds = false,
94+
buildTests = false,
95+
partial = None,
96+
actionableDiagnostics = actionableDiagnostics
97+
)
98+
val builds = res.orExit(logger)
99+
100+
builds.get(Scope.Main).flatMap(_.successfulOpt) match
101+
case None => sys.exit(1)
102+
case Some(build) =>
103+
val classPaths = build.fullClassPath
104+
105+
val scalacOptions = options.shared.scalac.scalacOption ++
106+
build.options.scalaOptions.scalacOptions.toSeq.map(_.value.value)
107+
108+
either {
109+
val artifacts =
110+
value(
111+
ScalafixArtifacts.artifacts(
112+
scalaVersion,
113+
build.options.classPathOptions.scalafixDependencies.values.flatten,
114+
value(buildOptions.finalRepositories),
115+
logger,
116+
buildOptions.internal.cache.getOrElse(FileCache())
117+
)
118+
)
119+
120+
val scalafixOptions =
121+
options.scalafixConf.toList.flatMap(scalafixConf => List("--config", scalafixConf)) ++
122+
Seq("--sourceroot", workspace.toString) ++
123+
Seq("--classpath", classPaths.mkString(java.io.File.pathSeparator)) ++
124+
Seq("--scala-version", scalaVersion) ++
125+
(if (options.check) Seq("--test") else Nil) ++
126+
(if (scalacOptions.nonEmpty) scalacOptions.flatMap(Seq("--scalac-options", _))
127+
else Nil) ++
128+
(if (artifacts.toolsJars.nonEmpty)
129+
Seq("--tool-classpath", artifacts.toolsJars.mkString(java.io.File.pathSeparator))
130+
else Nil) ++
131+
options.rules.flatMap(Seq("-r", _))
132+
++ options.scalafixArg
133+
134+
val proc = Runner.runJvm(
135+
buildOptions.javaHome().value.javaCommand,
136+
buildOptions.javaOptions.javaOpts.toSeq.map(_.value.value),
137+
artifacts.scalafixJars,
138+
"scalafix.cli.Cli",
139+
scalafixOptions,
140+
logger,
141+
cwd = Some(workspace),
142+
allowExecve = true
143+
)
144+
145+
sys.exit(proc.waitFor())
146+
}
147+
148+
}
149+
150+
}

0 commit comments

Comments
 (0)