Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,26 @@ hesitate to ask in the [Discord channel](https://discord.gg/8AHaqGx3Qj).

## Modules

- `scalafix-interfaces` Java facade to run rules within an existing JVM instance.
### For writing rules

- `scalafix-core/` data structures for rewriting and linting Scala source code.
- `scalafix-reflect/` utilities to compile and classload rules from
configuration.
- `scalafix-rules/` built-in rules such as `RemoveUnused`.

### For executing rules

- `scalafix-cli/` command-line interface.
- `scalafix-reflect/` utilities to compile and classload rules from
configuration.

### For tool integration
- `scalafix-interfaces/` Java facade to run rules within an existing JVM instance.
- `scalafix-loader/` Java implementation to dynamically fetch and load
implementations to run rules.
- `scalafix-versions/` Java implementation to advertize which Scala versions
`scalafix-cli` is published with.

### Others

- `scalafix-tests/` projects for unit and integration tests.
- `scalafix-docs/` documentation code for the Scalafix website.

Expand Down
78 changes: 46 additions & 32 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -12,51 +12,63 @@ inThisBuild(
Global / cancelable := true
noPublishAndNoMima

// force javac to fork by setting javaHome to get error messages during compilation,
// see https://github.com/sbt/zinc/issues/520
def inferJavaHome() = {
val home = file(sys.props("java.home"))
val actualHome =
if (System.getProperty("java.version").startsWith("1.8")) home.getParentFile
else home
Some(actualHome)
}

lazy val interfaces = project
.in(file("scalafix-interfaces"))
.settings(
moduleName := "scalafix-interfaces",
javaSettings,
libraryDependencies += coursierInterfaces,
Compile / resourceGenerators += Def.task {
val props = new java.util.Properties()
val props = cliVersionsProperties()
props.put("scalafixVersion", version.value)
props.put("scalafixStableVersion", stableVersion.value)
props.put("scalametaVersion", scalametaV)
props.put("scala212", scala212)
props.put("scala213", scala213)
props.put("scala33", scala33)
props.put("scala35", scala35)
props.put("scala36", scala36)
props.put("scala37", scala37)
props.put("scala3LTS", scala3LTS)
props.put("scala3Next", scala3Next)
val out =
(Compile / managedResourceDirectories).value.head /
"scalafix-interfaces.properties"
IO.write(props, "Scalafix version constants", out)
List(out)
},
(Compile / javacOptions) ++= List(
"-Xlint:all",
"-Werror"
}
)
.disablePlugins(ScalafixPlugin)

lazy val versions = project
.in(file("scalafix-versions"))
.settings(
moduleName := "scalafix-versions",
javaSettings,
mimaPreviousArtifacts := Set.empty, // TODO: remove after 0.14.4
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧

Compile / resourceGenerators += Def.task {
val props = cliVersionsProperties()
props.put("scalafix", version.value)
val out =
(Compile / managedResourceDirectories).value.head /
"scalafix-versions.properties"
IO.write(props, "Scala versions for ch.epfl.scala:::scalafix-cli", out)
List(out)
}
)
.disablePlugins(ScalafixPlugin)
.dependsOn(interfaces)

lazy val loader = project
.in(file("scalafix-loader"))
.settings(
moduleName := "scalafix-loader",
javaSettings,
mimaPreviousArtifacts := Set.empty, // TODO: remove after 0.14.4
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧

libraryDependencies ++= Seq(
typesafeConfig,
lombok % Provided
),
(Compile / doc / javacOptions) := List("-Xdoclint:none"),
(Compile / javaHome) := inferJavaHome(),
(Compile / doc / javaHome) := inferJavaHome(),
libraryDependencies += coursierInterfaces,
moduleName := "scalafix-interfaces",
crossPaths := false,
autoScalaLibrary := false
javacOptions ++= {
// https://inside.java/2024/06/18/quality-heads-up/
if (jdk > 8) Seq("-proc:full")
else Seq() // only backported to Oracle’s 8u release (8u411)
}
)
.disablePlugins(ScalafixPlugin)
.dependsOn(interfaces, versions)

// Scala 3 macros vendored separately (i.e. without runtime classes), to
// shadow Scala 2.13 macros in the Scala 3 compiler classpath, while producing
Expand Down Expand Up @@ -356,11 +368,12 @@ lazy val integration = projectMatrix
resolve(output, Compile / sourceDirectory).value
),
Test / test := (Test / test)
.dependsOn(resolve(cli, publishLocalTransitive))
.dependsOn(
(resolve(cli, publishLocalTransitive) +: cli.projectRefs
cli.projectRefs
// always publish Scala 3 artifacts to test Scala 3 minor version fallbacks
.collect { case p @ LocalProject(n) if n.startsWith("cli3") => p }
.map(_ / publishLocalTransitive)): _*
.map(_ / publishLocalTransitive): _*
)
.value,
Test / testWindows := (Test / testWindows)
Expand All @@ -376,6 +389,7 @@ lazy val integration = projectMatrix
.jvmPlatform(CrossVersion.full, cliScalaVersions)
.enablePlugins(BuildInfoPlugin)
.dependsOn(unit % "compile->test")
.dependsOn(loader % "compile->test")

lazy val expect = projectMatrix
.in(file("scalafix-tests/expect"))
Expand Down
4 changes: 4 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ object Dependencies {
val commontTextV = "1.13.1"
val googleDiffV = "1.3.0"
val jgitV = "5.13.3.202401111512-r"
val lombokV = "1.18.38"
val metaconfigV = "0.15.0"
val nailgunV = "0.9.1"
val scalaXmlV = "2.2.0"
val scalametaV = "4.13.5"
val scalatagsV = "0.13.1"
val scalatestV = "3.2.19"
val munitV = "1.1.0"
val typesafeConfigV = "1.4.3"

val bijectionCore = "com.twitter" %% "bijection-core" % bijectionCoreV
val collectionCompat = "org.scala-lang.modules" %% "scala-collection-compat" % collectionCompatV
Expand All @@ -36,6 +38,7 @@ object Dependencies {
val coursierInterfaces = "io.get-coursier" % "interface" % coursierInterfaceV
val googleDiff = "com.googlecode.java-diff-utils" % "diffutils" % googleDiffV
val jgit = "org.eclipse.jgit" % "org.eclipse.jgit" % jgitV
val lombok = "org.projectlombok" % "lombok" % lombokV
val metaconfig = "org.scalameta" %% "metaconfig-typesafe-config" % metaconfigV
val metacp = "org.scalameta" %% "metacp" % scalametaV
val nailgunServer = "com.martiansoftware" % "nailgun-server" % nailgunV
Expand All @@ -47,6 +50,7 @@ object Dependencies {
val munit = "org.scalameta" %% "munit" % munitV
val semanticdbScalacCore = "org.scalameta" % "semanticdb-scalac-core" % scalametaV cross CrossVersion.full
val semanticdbSharedFor3Use2_13 = "org.scalameta" % "semanticdb-shared" % scalametaV cross CrossVersion.for3Use2_13
val typesafeConfig = "com.typesafe" % "config" % typesafeConfigV

// scala-steward:off

Expand Down
5 changes: 4 additions & 1 deletion project/Mima.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ object Mima {
// See https://github.com/lightbend/mima
Seq(
ProblemFilters.exclude[Problem]("scalafix.internal.*"),
ProblemFilters.exclude[Problem]("scala.meta.internal.*")
ProblemFilters.exclude[Problem]("scala.meta.internal.*"),
// Exceptions
ProblemFilters.exclude[ReversedMissingMethodProblem]("scalafix.interfaces.ScalafixArguments.withRepositories"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scalafix.interfaces.ScalafixArguments.withToolDependencyCoordinates"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scalafix.interfaces.ScalafixArguments.withToolDependencyURLs")
)
}
}
27 changes: 26 additions & 1 deletion project/ScalafixBuild.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,21 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
publish / skip := true
)

lazy val javaSettings = Seq(
(Compile / javacOptions) ++= List(
"-Xlint:all",
"-Werror"
),
(Compile / doc / javacOptions) := List("-Xdoclint:none"),
crossPaths := false,
autoScalaLibrary := false
)

lazy val jdk = System.getProperty("java.specification.version").toDouble

// https://github.com/scalameta/scalameta/issues/2485
lazy val coreScalaVersions = Seq(scala212, scala213)
lazy val cliScalaVersions = {
val jdk = System.getProperty("java.specification.version").toDouble
val scala3Versions =
// Scala 3.5 will never support JDK 23
if (jdk >= 23) Seq(scala33, scala36, scala37)
Expand Down Expand Up @@ -161,6 +172,20 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
buildInfoObject := "RulesBuildInfo"
)

// must match ScalafixProperties logic
def cliVersionsProperties() = {
val props = new java.util.Properties()
props.put("scala212", scala212)
props.put("scala213", scala213)
props.put("scala33", scala33)
props.put("scala35", scala35)
props.put("scala36", scala36)
props.put("scala37", scala37)
props.put("scala3LTS", scala3LTS)
props.put("scala3Next", scala3Next)
props
}

lazy val testWindows =
taskKey[Unit]("run tests, excluding those incompatible with Windows")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
scalafix.internal.interfaces.ScalafixArgumentsImpl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import scalafix.v1.RuleDecoder
final case class ScalafixArgumentsImpl(args: Args = Args.default)
extends ScalafixArguments {

def this() = this(Args.default)

override def run(): Array[ScalafixError] = {
val exit = MainOps.run(Array(), args)
ScalafixErrorImpl.fromScala(exit)
Expand All @@ -48,39 +50,44 @@ final case class ScalafixArgumentsImpl(args: Args = Args.default)
}
}

override def withRepositories(
repositories: util.List[Repository]
): ScalafixArguments =
copy(args = args.copy(repositories = repositories.asScala.toList))

override def withRules(rules: util.List[String]): ScalafixArguments =
copy(args = args.copy(rules = rules.asScala.toList))

override def withToolClasspath(
customURLs: util.List[URL]
): ScalafixArguments =
withToolClasspath(
customURLs,
Nil.asJava,
Repository.defaults()
)
withToolDependencyURLs(customURLs)

override def withToolClasspath(
customURLs: util.List[URL],
customDependenciesCoordinates: util.List[String]
): ScalafixArguments =
withToolClasspath(
customURLs,
customDependenciesCoordinates,
Repository.defaults()
)
withToolDependencyCoordinates(customDependenciesCoordinates)
.withToolDependencyURLs(customURLs)

override def withToolClasspath(
customURLs: util.List[URL],
customDependenciesCoordinates: util.List[String],
repositories: util.List[Repository]
): ScalafixArguments = {
withRepositories(repositories)
.withToolDependencyCoordinates(customDependenciesCoordinates)
.withToolDependencyURLs(customURLs)
}

override def withToolDependencyCoordinates(
withToolDependencyCoordinates: java.util.List[String]
): ScalafixArguments = {
val OrganizeImportsCoordinates =
"""com\.github\.liancheng.*:organize-imports:.*""".r

val keptDependencies: Seq[String] =
customDependenciesCoordinates.asScala
withToolDependencyCoordinates.asScala
.collect {
case dep @ OrganizeImportsCoordinates() =>
args.out.println(
Expand All @@ -107,7 +114,7 @@ final case class ScalafixArgumentsImpl(args: Args = Args.default)
else Versions.scalaVersion

val customDependenciesJARs = ScalafixCoursier.toolClasspath(
repositories,
args.repositories.asJava,
keptDependencies.asJava,
scalaVersionForDependencies
)
Expand Down Expand Up @@ -158,13 +165,23 @@ final case class ScalafixArgumentsImpl(args: Args = Args.default)
}
}

val extraURLs = customURLs.asScala ++ customDependenciesJARs
val extraURLs = customDependenciesJARs
.getFiles()
.asScala
.map(_.toURI().toURL())
val classLoader = new URLClassLoader(
extraURLs.toArray,
getClass.getClassLoader
args.toolClasspath
)
withToolClasspath(classLoader)
}

override def withToolDependencyURLs(
withToolDependencyURLs: java.util.List[java.net.URL]
): ScalafixArguments = {
val classLoader = new URLClassLoader(
withToolDependencyURLs.asScala.toArray,
args.toolClasspath
)
withToolClasspath(classLoader)
}
Expand Down
9 changes: 9 additions & 0 deletions scalafix-cli/src/main/scala/scalafix/internal/v1/Args.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import java.util.regex.Pattern
import java.util.regex.PatternSyntaxException

import scala.annotation.StaticAnnotation
import scala.jdk.CollectionConverters._
import scala.util.Failure
import scala.util.Success
import scala.util.Try
Expand All @@ -22,6 +23,8 @@ import scala.meta.internal.symtab.SymbolTable
import scala.meta.io.AbsolutePath
import scala.meta.io.Classpath

import coursierapi.MavenRepository
import coursierapi.Repository
import metaconfig.Configured._
import metaconfig._
import metaconfig.annotation._
Expand Down Expand Up @@ -158,6 +161,8 @@ case class Args(
"The glob syntax is defined by `nio.FileSystem.getPathMatcher`."
)
exclude: List[PathMatcher] = Nil,
@Description("Maven repositories to fetch the artifacts from")
repositories: List[Repository] = Repository.defaults.asScala.toList,
@Description(
"Additional classpath for compiling and classloading custom rules, as a set of filesystem paths, separated by ':' on Unix or ';' on Windows."
)
Expand Down Expand Up @@ -483,6 +488,8 @@ object Args extends TPrintImplicits {
ConfDecoder.stringConfDecoder.map(glob =>
FileSystems.getDefault.getPathMatcher("glob:" + glob)
)
implicit val repositoryDecoder: ConfDecoder[Repository] =
ConfDecoder.stringConfDecoder.map(base => MavenRepository.of(base))
implicit val scalaVersionDecoder: ConfDecoder[ScalaVersion] =
ScalafixConfig.scalaVersionDecoder

Expand All @@ -505,6 +512,8 @@ object Args extends TPrintImplicits {
ConfEncoder.StringEncoder.contramap(_ => "<stdout>")
implicit val pathMatcherEncoder: ConfEncoder[PathMatcher] =
ConfEncoder.StringEncoder.contramap(_.toString)
implicit val repositoriesEncoder: ConfEncoder[Repository] =
ConfEncoder.StringEncoder.contramap(_.toString)
implicit val callbackEncoder: ConfEncoder[ScalafixMainCallback] =
ConfEncoder.StringEncoder.contramap(_.toString)
implicit val argsEncoder: ConfEncoder[Args] = generic.deriveEncoder
Expand Down
Loading