diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff3daac27..d68023343 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/build.sbt b/build.sbt index 78aa27149..b82f3554d 100644 --- a/build.sbt +++ b/build.sbt @@ -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 + 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 + 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 @@ -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) @@ -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")) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index b98044f29..362dfdac6 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -21,6 +21,7 @@ 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" @@ -28,6 +29,7 @@ object Dependencies { 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 @@ -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 @@ -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 diff --git a/project/Mima.scala b/project/Mima.scala index be59815dc..58bc9fff1 100644 --- a/project/Mima.scala +++ b/project/Mima.scala @@ -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") ) } } diff --git a/project/ScalafixBuild.scala b/project/ScalafixBuild.scala index 084ef6efb..bc220617a 100644 --- a/project/ScalafixBuild.scala +++ b/project/ScalafixBuild.scala @@ -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) @@ -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") diff --git a/scalafix-cli/src/main/resources/META-INF/services/scalafix.interfaces.ScalafixArguments b/scalafix-cli/src/main/resources/META-INF/services/scalafix.interfaces.ScalafixArguments new file mode 100644 index 000000000..f5a9ee4a7 --- /dev/null +++ b/scalafix-cli/src/main/resources/META-INF/services/scalafix.interfaces.ScalafixArguments @@ -0,0 +1 @@ +scalafix.internal.interfaces.ScalafixArgumentsImpl diff --git a/scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixArgumentsImpl.scala b/scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixArgumentsImpl.scala index fe46d7380..4b25244dd 100644 --- a/scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixArgumentsImpl.scala +++ b/scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixArgumentsImpl.scala @@ -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) @@ -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( @@ -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 ) @@ -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) } diff --git a/scalafix-cli/src/main/scala/scalafix/internal/v1/Args.scala b/scalafix-cli/src/main/scala/scalafix/internal/v1/Args.scala index c2f6f0cf1..6bcc5ac38 100644 --- a/scalafix-cli/src/main/scala/scalafix/internal/v1/Args.scala +++ b/scalafix-cli/src/main/scala/scalafix/internal/v1/Args.scala @@ -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 @@ -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._ @@ -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." ) @@ -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 @@ -505,6 +512,8 @@ object Args extends TPrintImplicits { ConfEncoder.StringEncoder.contramap(_ => "") 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 diff --git a/scalafix-interfaces/src/main/java/scalafix/interfaces/Scalafix.java b/scalafix-interfaces/src/main/java/scalafix/interfaces/Scalafix.java index b2dc9c8e1..080439072 100644 --- a/scalafix-interfaces/src/main/java/scalafix/interfaces/Scalafix.java +++ b/scalafix-interfaces/src/main/java/scalafix/interfaces/Scalafix.java @@ -1,8 +1,10 @@ package scalafix.interfaces; import coursierapi.Repository; + import scalafix.internal.interfaces.ScalafixCoursier; import scalafix.internal.interfaces.ScalafixInterfacesClassloader; +import scalafix.internal.interfaces.ScalafixProperties; import java.io.IOException; import java.io.InputStream; @@ -10,16 +12,21 @@ import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.net.URLClassLoader; +import java.util.Iterator; import java.util.List; import java.util.Properties; +import java.util.ServiceLoader; /** - * Public API for reflectively invoking Scalafix from a build tool or IDE integration. + * Public API for reflectively invoking Scalafix from a build tool or IDE + * integration. *

- * To obtain an instance of Scalafix, use one of the static factory methods. + * To obtain an instance of Scalafix, classload + * ch.epfl.scala:scalafix-loader and use {@link #get()}. * - * @implNote This interface is not intended to be extended, the only implementation of this interface - * should live in the Scalafix repository. + * @implNote This interface is not intended to be extended, the only + * implementation of this interface should live in the Scalafix + * repository. */ public interface Scalafix { @@ -101,73 +108,20 @@ public interface Scalafix { String scala3Next(); /** - * Fetch JARs containing an implementation of {@link Scalafix} using Coursier and classload an instance of it via - * runtime reflection. - *

- * The custom classloader optionally provided with {@link ScalafixArguments#withToolClasspath} to compile and - * classload external rules must have the classloader of the returned instance as ancestor to share a common - * loaded instance of `scalafix-core`, and therefore have been compiled against the requested Scala version. - * - * @param requestedScalaVersion A full Scala version (i.e. "3.3.4") or a major.minor one (i.e. "3.3") to infer - * the major.minor Scala version that should be available in the classloader of the - * returned instance. To be able to run advanced semantic rules using the Scala - * Presentation Compiler such as ExplicitResultTypes, this must be source-compatible - * with the version that the target classpath is built with, as provided with - * {@link ScalafixArguments#withScalaVersion}. - * @return An implementation of the {@link Scalafix} interface. - * @throws ScalafixException in case of errors during artifact resolution/fetching. + * @deprecated Use {@link #get()} instead. */ + @Deprecated static Scalafix fetchAndClassloadInstance(String requestedScalaVersion) throws ScalafixException { return fetchAndClassloadInstance(requestedScalaVersion, Repository.defaults()); } /** - * Fetch JARs containing an implementation of {@link Scalafix} from the provided repositories using Coursier and - * classload an instance of it via runtime reflection. - *

- * The custom classloader optionally provided with {@link ScalafixArguments#withToolClasspath} to compile and - * classload external rules must have the classloader of the returned instance as ancestor to share a common - * loaded instance of `scalafix-core`, and therefore have been compiled against the requested Scala version. - * - * @param requestedScalaVersion A full Scala version (i.e. "3.3.4") or a major.minor one (i.e. "3.3") to infer - * the major.minor Scala version that should be available in the classloader of the - * returned instance. To be able to run advanced semantic rules using the Scala - * Presentation Compiler such as ExplicitResultTypes, this must be source-compatible - * with the version that the target classpath is built with, as provided with - * {@link ScalafixArguments#withScalaVersion}. - * @param repositories Maven/Ivy repositories to fetch the JARs from. - * @return An implementation of the {@link Scalafix} interface. - * @throws ScalafixException in case of errors during artifact resolution/fetching. + * @deprecated Use {@link #get()} instead. */ + @Deprecated static Scalafix fetchAndClassloadInstance(String requestedScalaVersion, List repositories) throws ScalafixException { - String requestedScalaMajorMinorOrMajorVersion = - requestedScalaVersion.replaceAll("^(\\d+\\.\\d+).*", "$1"); - - String scalaVersionKey; - if (requestedScalaMajorMinorOrMajorVersion.equals("2.12")) { - scalaVersionKey = "scala212"; - } else if (requestedScalaMajorMinorOrMajorVersion.equals("2.13") || - requestedScalaMajorMinorOrMajorVersion.equals("2")) { - scalaVersionKey = "scala213"; - } else if (requestedScalaMajorMinorOrMajorVersion.equals("3.0") || - requestedScalaMajorMinorOrMajorVersion.equals("3.1") || - requestedScalaMajorMinorOrMajorVersion.equals("3.2") || - requestedScalaMajorMinorOrMajorVersion.equals("3.3")) { - scalaVersionKey = "scala33"; - } else if (requestedScalaMajorMinorOrMajorVersion.equals("3.5")) { - scalaVersionKey = "scala35"; - } else if (requestedScalaMajorMinorOrMajorVersion.equals("3.6")) { - scalaVersionKey = "scala36"; - } else if (requestedScalaMajorMinorOrMajorVersion.equals("3.7")) { - scalaVersionKey = "scala37"; - } else if (requestedScalaMajorMinorOrMajorVersion.startsWith("3")) { - scalaVersionKey = "scala3Next"; - } else { - throw new IllegalArgumentException("Unsupported scala version " + requestedScalaVersion); - } - Properties properties = new Properties(); String propertiesPath = "scalafix-interfaces.properties"; InputStream stream = Scalafix.class.getClassLoader().getResourceAsStream(propertiesPath); @@ -178,6 +132,7 @@ static Scalafix fetchAndClassloadInstance(String requestedScalaVersion, List - * The custom classloader optionally provided with {@link ScalafixArguments#withToolClasspath} to compile and - * classload external rules must have the provided classloader as ancestor to share a common loaded instance - * of `scalafix-core`, and therefore must have been compiled against the same Scala binary version as - * the one in the classLoader provided here. - *

- * Unless you have an advanced use-case, prefer the high-level overloads that cannot cause runtime errors - * due to an invalid classloader hierarchy. - * - * @param classLoader Classloader containing the full Scalafix classpath, including the scalafix-cli module. To be - * able to run advanced semantic rules using the Scala Presentation Compiler such as - * ExplicitResultTypes, this Scala binary version in that classloader should match the one that - * the target classpath was built with, as provided with - * {@link ScalafixArguments#withScalaVersion}. - * @return An implementation of the {@link Scalafix} interface. - * @throws ScalafixException in case of errors during classloading, most likely caused - * by an incorrect classloader argument. + * @deprecated Use {@link #get()} instead. */ + @Deprecated static Scalafix classloadInstance(ClassLoader classLoader) throws ScalafixException { try { Class cls = classLoader.loadClass("scalafix.internal.interfaces.ScalafixImpl"); Constructor ctor = cls.getDeclaredConstructor(); ctor.setAccessible(true); return (Scalafix) ctor.newInstance(); - } catch (ClassNotFoundException | NoSuchMethodException | - IllegalAccessException | InvocationTargetException | - InstantiationException ex) { + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException + | InstantiationException ex) { throw new ScalafixException( "Failed to reflectively load Scalafix with classloader " + classLoader.toString(), ex); } } + + /** + * Obtains an implementation of Scalafix using the current classpath. + * + * @return the first available implementation advertised as a service provider. + */ + static Scalafix get() { + ServiceLoader loader = ServiceLoader.load(Scalafix.class); + Iterator iterator = loader.iterator(); + if (iterator.hasNext()) { + return iterator.next(); + } else { + throw new IllegalStateException("No implementation found"); + } + } } diff --git a/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixArguments.java b/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixArguments.java index 5715130bd..aea5192b1 100644 --- a/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixArguments.java +++ b/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixArguments.java @@ -12,212 +12,241 @@ import java.util.Optional; /** - * Wrapper around arguments for invoking the Scalafix command-line interface main method. + * Wrapper around arguments for invoking the Scalafix command-line interface + * main method. *

- * To obtain an instance of ScalafixArguments, use {@link scalafix.interfaces.Scalafix#newArguments()}. - * Instances of ScalafixArguments are immutable and thread safe. It is safe to re-use the same - * ScalafixArguments instance for multiple Scalafix invocations. Re-using the same instance is - * particularly encouraged when a custom toolClasspath is provided, in order to amortize the - * cost/time of fetching artifacts, classloading them & warming up the JIT code cache. + * To obtain an instance of ScalafixArguments, use + * {@link scalafix.interfaces.Scalafix#newArguments()}. + * Instances of ScalafixArguments are immutable and thread safe. * - * @implNote This interface is not intended for extension, the only implementation of this interface - * should live in the Scalafix repository. + * @implNote This interface is not intended for extension, the only + * implementation of this interface should live in the Scalafix + * repository. */ public interface ScalafixArguments { /** - * @param rules The rules passed via the --rules flag matching the syntax provided in - * rules = [ "... " ] in .scalafix.conf files. + * The rules that are valid arguments for {@link #withRules(List) }. + *

+ * Takes into account built-in rules as well as the tool classpath provided via + * {@link #withToolClasspath(URLClassLoader) }. + * + * @throws ScalafixException In case of an error loading the runner. */ - ScalafixArguments withRules(List rules); + List availableRules() throws ScalafixException; /** - * @param customURLs Extra URLs for classloading and compiling external rules. + * Similar to {@link #run()}, but without any side effects on the source files. + * Via the returned {@link ScalafixEvaluation}, for each file, diagnostics can + * be inspected, and patches can be previewed and applied. + *

+ * Incompatible with {@link #withMainCallback} and {@link #withMode}. + * + * @throws ScalafixException In case of an error loading the runner. */ - ScalafixArguments withToolClasspath(List customURLs); + ScalafixEvaluation evaluate() throws ScalafixException; /** - * @param customURLs Extra URLs for classloading and compiling external rules. - * @param customDependenciesCoordinates Extra dependencies for classloading and compiling external rules. - * For example "com.nequissimus::sort-imports:0.5.2". - * Artifacts will be resolved against the Scala version in the classloader - * of the parent {@link Scalafix} instance and fetched using Coursier. - * @throws ScalafixException in case of errors during artifact resolution/fetching. + * The rules that would run when calling {@link #run() } + *

+ * Takes into account rules that are configured in .scalafix.conf. + * + * @throws ScalafixException In case of an error loading the runner. */ - ScalafixArguments withToolClasspath( - List customURLs, - List customDependenciesCoordinates - ) throws ScalafixException; + List rulesThatWillRun() throws ScalafixException; /** - * @param customURLs Extra URLs for classloading and compiling external rules. - * @param customDependenciesCoordinates Extra dependencies for classloading and compiling external rules. - * For example "com.nequissimus::sort-imports:0.5.2". - * Artifacts will be resolved against the Scala version in the classloader - * of the parent {@link Scalafix} instance and fetched using Coursier. - * @param repositories Maven/Ivy repositories to fetch the artifacts from. - * @throws ScalafixException in case of errors during artifact resolution/fetching. + * Run the Scalafix commmand-line interface main function. + * + * @throws ScalafixException In case of an error loading the runner. */ - ScalafixArguments withToolClasspath( - List customURLs, - List customDependenciesCoordinates, - List repositories - ) throws ScalafixException; + ScalafixError[] run() throws ScalafixException; /** - * @param toolClasspath Custom classpath for classloading and compiling external rules. - * Must be a URLClassLoader (not regular ClassLoader) to support - * compiling sources. This classloader must have as ancestor the - * classloader of the {@link Scalafix} instance that returned this - * {@link ScalafixArguments} instance. Unless you have an advanced - * use-case, prefer the high-level overloads that cannot cause - * runtime errors due to an invalid classloader hierarchy. + * Validates that the passed arguments are valid. + *

+ * Takes into account provided rules, .scalafix.conf configuration, scala + * version, scalac options and other potential problems. The primary purpose + * of this method is to validate the arguments before starting compilation + * to populate {@link #withClasspath(List)}. + * + * @return Optional.empty in case the arguments are valid. Optional.of + * an exception in case of an error loading the arguments. + * + * @throws ScalafixException In case of an error loading the runner. */ - ScalafixArguments withToolClasspath(URLClassLoader toolClasspath); + Optional validate() throws ScalafixException; /** - * @param paths Files and directories to run Scalafix on. The ability to pass in directories - * is primarily supported to make it ergonomic to invoke the command-line interface. - * It's recommended to only pass in files with this API. Directories are recursively - * expanded for files matching the patterns *.scala and *.sbt - * and files that do not match the path matchers provided in {@link #withExcludedPaths(List)}. + * @param charset Charset for reading source files from disk. Defaults to UTF-8. */ - ScalafixArguments withPaths(List paths); + ScalafixArguments withCharset(Charset charset); /** - * @param matchers Optional list of path matchers to exclude files when expanding directories - * in {@link #withPaths(List)}. + * @param classpath Full Java classpath of the module being fixed. Required for + * running semantic rewrites such as ExpliticResultTypes. + * Source files that are to be fixed must be compiled with the + * semanticdb-scalac compiler plugin and must have + * corresponding + * META-INF/semanticdb/../*.semanticdb + * payloads. The dependency classpath must be included as well + * but dependency sources do not have to be compiled with + * semanticdb-scalac. */ - ScalafixArguments withExcludedPaths(List matchers); + ScalafixArguments withClasspath(List classpath); /** - * @param path The working directory of where to invoke the command-line interface. - * Primarily used to absolutize relative directories passed via - * {@link ScalafixArguments#withPaths(List) } and also to auto-detect the - * location of .scalafix.conf. + * @param config Optional path to a .scalafix.conf. If empty, + * Scalafix will infer such a file from the working directory or + * fallback to the default configuration. */ - ScalafixArguments withWorkingDirectory(Path path); + ScalafixArguments withConfig(Optional config); /** - * @param config Optional path to a .scalafix.conf. If empty, Scalafix - * will infer such a file from the working directory or fallback to the default - * configuration. + * @param matchers Optional list of path matchers to exclude files when + * expanding directories in {@link #withPaths(List)}. */ - ScalafixArguments withConfig(Optional config); + ScalafixArguments withExcludedPaths(List matchers); /** - * @param mode The mode to run via --check or --stdout or --auto-suppress-linter-errors or --triggered + * @param callback Handler for reported linter messages. If not provided, + * defaults to printing linter messages to System.out. */ - ScalafixArguments withMode(ScalafixMainMode mode); + ScalafixArguments withMainCallback(ScalafixMainCallback callback); /** - * @param args Unparsed command-line arguments that are fed directly to main(Array[String]) - * @throws ScalafixException In case of an error parsing the provided arguments. + * @param mode The mode to run via --check or --stdout or + * --auto-suppress-linter-errors or --triggered */ - ScalafixArguments withParsedArguments(List args) throws ScalafixException; + ScalafixArguments withMode(ScalafixMainMode mode); /** - * @param out The output stream to use for reporting diagnostics while running Scalafix. - * Defaults to System.out. + * @param args Unparsed command-line arguments that are fed directly to + * main(Array[String]) */ - ScalafixArguments withPrintStream(PrintStream out); + ScalafixArguments withParsedArguments(List args); /** - * @param classpath Full Java classpath of the module being fixed. Required for running - * semantic rewrites such as ExpliticResultTypes. Source files that - * are to be fixed must be compiled with the semanticdb-scalac compiler - * plugin and must have corresponding META-INF/semanticdb/../*.semanticdb - * payloads. The dependency classpath must be included as well but dependency - * sources do not have to be compiled with semanticdb-scalac. + * @param paths Files and directories to run Scalafix on. The ability to pass in + * directories is primarily supported to make it ergonomic to + * invoke the command-line interface. It's recommended to only pass + * in files with this API. Directories are recursively expanded for + * files matching the patterns *.scala and + * *.sbt and files that do not match the path matchers + * provided in {@link #withExcludedPaths(List)}. */ - ScalafixArguments withClasspath(List classpath); + ScalafixArguments withPaths(List paths); /** - * @param path The SemanticDB sources path passed via --sourceroot. Must match path - * in -Xplugin:semanticdb:sourceroot:{path} if used. Defaults - * to the current working directory. + * @param out The output stream to use for reporting diagnostics while running + * Scalafix. Defaults to System.out. */ - ScalafixArguments withSourceroot(Path path); + ScalafixArguments withPrintStream(PrintStream out); /** - * @param path The SemanticDB targetroot paths passed via --semanticdb-targetroots. Must match - * path in -Xplugin:semanticdb:targetroot:{path} if used. + * @param repositories Maven/Ivy repositories to fetch artifacts from. */ - ScalafixArguments withSemanticdbTargetroots(List path); + ScalafixArguments withRepositories(List repositories); /** - * @param callback Handler for reported linter messages. If not provided, defaults to printing - * linter messages to the Stdout. + * @param rules The rules passed via the --rules flag matching the syntax + * provided in rules = [ "... " ] in .scalafix.conf + * files. */ - ScalafixArguments withMainCallback(ScalafixMainCallback callback); - + ScalafixArguments withRules(List rules); /** - * @param charset Charset for reading source files from disk. Defaults to UTF-8. + * @param options The Scala compiler flags used to compile this classpath. + * For example List(-Ywarn-unused-import). */ - ScalafixArguments withCharset(Charset charset); + ScalafixArguments withScalacOptions(List options); /** - * @param version TThe major or binary Scala version that the provided files are targeting, - * for the full version that was used to compile them when a classpath is provided. - * For example "2.12.8" or "2.12" or "2". To be able to run advanced semantic rules - * using the Scala Presentation Compiler (such as ExplicitResultTypes), - * the fullSscala version must match the binary version available in the classloader of - * this instance, as requested/provided in the static factory methods - * of {@link Scalafix}. + * @param version The major or binary Scala version that the provided files are + * targeting, for the full version that was used to compile them + * when a classpath is provided. For example "2.12.8" or "2.12" + * or "2". To be able to run advanced semantic rules using the + * Scala Presentation Compiler (such as ExplicitResultTypes), the + * fullSscala version must match the binary version available in + * the classloader of this instance, as requested/provided in the + * static factory methods of {@link Scalafix}. * @throws ScalafixException In case of an error parsing the scalaVersion. */ ScalafixArguments withScalaVersion(String version); /** - * @param options The Scala compiler flags used to compile this classpath. - * For example List(-Ywarn-unused-import). + * @param path The SemanticDB targetroot paths passed via + * --semanticdb-targetroots. Must match path in + * -Xplugin:semanticdb:targetroot:{path} if + * used. */ - ScalafixArguments withScalacOptions(List options); + ScalafixArguments withSemanticdbTargetroots(List path); + /** + * @param path The SemanticDB sources path passed via --sourceroot. Must match + * path in + * -Xplugin:semanticdb:sourceroot:{path} if + * used. Defaults to the current working directory. + */ + ScalafixArguments withSourceroot(Path path); /** - * The rules that are valid arguments for {@link #withRules(List) }. - *

- * Takes into account built-in rules as well as the tool classpath provided via - * {@link #withToolClasspath(URLClassLoader) }. + * @deprecated Use {@link #withToolDependencyURLs()} instead. */ - List availableRules(); + @Deprecated + ScalafixArguments withToolClasspath(List customURLs); + /** + * @deprecated Use {@link #withToolDependencyURLs()} & + * {@link #withToolDependencyCoordinates()} instead. + */ + @Deprecated + ScalafixArguments withToolClasspath( + List customURLs, + List customDependenciesCoordinates); /** - * The rules that would run when calling {@link #run() } - *

- * Takes into account rules that are configured in .scalafix.conf. - * - * @throws ScalafixException In case of an error loading the configured rules. + * @deprecated Use {@link #withToolDependencyURLs()}, + * {@link #withToolDependencyCoordinates()} & + * {@link #withRepositories()} instead. */ - List rulesThatWillRun() throws ScalafixException; + @Deprecated + ScalafixArguments withToolClasspath( + List customURLs, + List customDependenciesCoordinates, + List repositories); + /** + * @deprecated Not supported when {@link Scalafix#get()} is used. Use + * higher-level {@link #withToolDependencyURLs()}, + * {@link #withToolDependencyCoordinates()} & + * {@link #withRepositories()} instead. + */ + @Deprecated + ScalafixArguments withToolClasspath(URLClassLoader toolClasspath); /** - * Validates that the passed arguments are valid. - *

- * Takes into account provided rules, .scalafix.conf configuration, scala version, - * scalac options and other potential problems. The primary purpose - * of this method is to validate the arguments before starting compilation - * to populate {@link #withClasspath(List)}. - * - * @return Optional.empty in case the arguments are valid. Optional.of - * an exception in case of an error loading the arguments. + * @param dependencyCoordinates Extra dependencies for classloading and + * compiling external rules. For example + * "com.nequissimus::sort-imports:0.5.2". + * Artifacts will be resolved against the + * Scala version in the classloader of the parent + * {@link Scalafix} instance and fetched using + * Coursier. */ - Optional validate(); + ScalafixArguments withToolDependencyCoordinates(List dependencyCoordinates); /** - * Run the Scalafix commmand-line interface main function. + * @param dependencyURLs Extra URLs for classloading and compiling external + * rules. */ - ScalafixError[] run(); + ScalafixArguments withToolDependencyURLs(List dependencyURLs); /** - * Similar to {@link #run()}, but without any side effects on the source files. Via the returned {@link ScalafixEvaluation}, - * for each file, diagnostics can be inspected, and patches can be previewed and applied. - *

- * Incompatible with {@link #withMainCallback} and {@link #withMode}. + * @param path The working directory of where to invoke the command-line + * interface. Primarily used to absolutize relative directories + * passed via {@link ScalafixArguments#withPaths(List) } and also to + * auto-detect the location of .scalafix.conf. */ - ScalafixEvaluation evaluate(); + ScalafixArguments withWorkingDirectory(Path path); } diff --git a/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixVersions.java b/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixVersions.java new file mode 100644 index 000000000..3da8e9ee9 --- /dev/null +++ b/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixVersions.java @@ -0,0 +1,74 @@ +package scalafix.interfaces; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; + +import scalafix.internal.interfaces.ScalafixInterfacesClassloader; + +/** + * Public API for looking up which one of the + * ch.epfl.scala:::scalafix-cli artifacts of a Scalafix release is + * the most appropriate, given the Scala version used to compile Scalafix target + * sources. + *

+ * To obtain an instance of ScalafixVersions, use one of the static methods. + * + * @implNote This interface is not intended to be extended, the only + * implementation of this interface should live in the Scalafix + * repository. + */ +public interface ScalafixVersions { + + /** + * @return the Scalafix release described. + */ + String scalafixVersion(); + + /** + * Returns the most appropriate full Scala version to resolve + * ch.epfl.scala:::scalafix-cli:scalafixVersion with. + * + * @param sourcesScalaVersion The Scala version (i.e. "3.3.4") used to compile + * Scalafix target sources. + * @return a full Scala version to resolve the artifact with. + */ + String cliScalaVersion(String sourcesScalaVersion); + + /** + * Obtains an implementation of ScalafixVersions using the current classpath. + * + * @return the first available implementation advertised as a service provider. + */ + static ScalafixVersions get() { + ServiceLoader loader = ServiceLoader.load(ScalafixVersions.class); + Iterator iterator = loader.iterator(); + if (iterator.hasNext()) { + return iterator.next(); + } else { + throw new IllegalStateException("No implementation found"); + } + } + + /** + * Obtains an implementation of ScalafixVersions using the provided + * classpath URLs. + * + * @param classpathUrls URLs to be used in the classloader for loading + * a ScalafixVersions implementation + * @return the first available implementation advertised as a service provider. + */ + static ScalafixVersions get(List classpathUrls) { + ClassLoader parent = new ScalafixInterfacesClassloader(ScalafixVersions.class.getClassLoader()); + ClassLoader classLoader = new URLClassLoader(classpathUrls.stream().toArray(URL[]::new), parent); + ServiceLoader loader = ServiceLoader.load(ScalafixVersions.class, classLoader); + Iterator iterator = loader.iterator(); + if (iterator.hasNext()) { + return iterator.next(); + } else { + throw new IllegalStateException("No implementation found"); + } + } +} diff --git a/scalafix-interfaces/src/main/java/scalafix/internal/interfaces/ScalafixCoursier.java b/scalafix-interfaces/src/main/java/scalafix/internal/interfaces/ScalafixCoursier.java index f0ed2f663..b015d4c1d 100644 --- a/scalafix-interfaces/src/main/java/scalafix/internal/interfaces/ScalafixCoursier.java +++ b/scalafix-interfaces/src/main/java/scalafix/internal/interfaces/ScalafixCoursier.java @@ -1,9 +1,8 @@ package scalafix.internal.interfaces; -import coursierapi.Module; import coursierapi.*; import coursierapi.error.CoursierError; -import scalafix.interfaces.ScalafixException; +import coursierapi.Module; import java.io.File; import java.net.MalformedURLException; @@ -13,8 +12,13 @@ import java.util.List; import java.util.stream.Collectors; +import scalafix.interfaces.ScalafixException; + +//TODO: move to scalafix-loader when scalafix.interfaces.Scalafix is removed public class ScalafixCoursier { + private static Module VERSIONS_MODULE = Module.of("ch.epfl.scala", "scalafix-versions"); + private static FetchResult fetch( List repositories, List dependencies, @@ -44,6 +48,14 @@ private static List toURLs(FetchResult result) throws ScalafixException { return urls; } + public static List scalafixVersionsJars( + List repositories, + String scalafixVersion + ) throws ScalafixException { + Dependency scalafixProperties = Dependency.of(VERSIONS_MODULE, scalafixVersion); + return toURLs(fetch(repositories, Collections.singletonList(scalafixProperties), ResolutionParams.create())); + } + public static List scalafixCliJars( List repositories, String scalafixVersion, diff --git a/scalafix-interfaces/src/main/java/scalafix/internal/interfaces/ScalafixProperties.java b/scalafix-interfaces/src/main/java/scalafix/internal/interfaces/ScalafixProperties.java new file mode 100644 index 000000000..e81b6bb6b --- /dev/null +++ b/scalafix-interfaces/src/main/java/scalafix/internal/interfaces/ScalafixProperties.java @@ -0,0 +1,32 @@ +package scalafix.internal.interfaces; + +//TODO: move to scalafix-versions when scalafix.interfaces.Scalafix is removed +public class ScalafixProperties { + + public static String getScalaVersionKey(String requestedScalaVersion) { + String requestedScalaMajorMinorOrMajorVersion = + requestedScalaVersion.replaceAll("^(\\d+\\.\\d+).*", "$1"); + + if (requestedScalaMajorMinorOrMajorVersion.equals("2.12")) { + return "scala212"; + } else if (requestedScalaMajorMinorOrMajorVersion.equals("2.13") || + requestedScalaMajorMinorOrMajorVersion.equals("2")) { + return "scala213"; + } else if (requestedScalaMajorMinorOrMajorVersion.equals("3.0") || + requestedScalaMajorMinorOrMajorVersion.equals("3.1") || + requestedScalaMajorMinorOrMajorVersion.equals("3.2") || + requestedScalaMajorMinorOrMajorVersion.equals("3.3")) { + return "scala33"; + } else if (requestedScalaMajorMinorOrMajorVersion.equals("3.5")) { + return "scala35"; + } else if (requestedScalaMajorMinorOrMajorVersion.equals("3.6")) { + return "scala36"; + } else if (requestedScalaMajorMinorOrMajorVersion.equals("3.7")) { + return "scala37"; + } else if (requestedScalaMajorMinorOrMajorVersion.startsWith("3")) { + return "scala3Next"; + } else { + throw new IllegalArgumentException("Unsupported scala version " + requestedScalaVersion); + } + } +} diff --git a/scalafix-loader/src/main/java/scalafix/loader/internal/ScalafixArgumentsImpl.java b/scalafix-loader/src/main/java/scalafix/loader/internal/ScalafixArgumentsImpl.java new file mode 100644 index 000000000..6ef1d0a88 --- /dev/null +++ b/scalafix-loader/src/main/java/scalafix/loader/internal/ScalafixArgumentsImpl.java @@ -0,0 +1,265 @@ +package scalafix.loader.internal; + +import com.typesafe.config.*; + +import coursierapi.Repository; + +import java.io.File; +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.ServiceLoader; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.NonNull; +import lombok.Value; +import lombok.With; +import scalafix.interfaces.Scalafix; +import scalafix.interfaces.ScalafixArguments; +import scalafix.interfaces.ScalafixError; +import scalafix.interfaces.ScalafixEvaluation; +import scalafix.interfaces.ScalafixException; +import scalafix.interfaces.ScalafixMainCallback; +import scalafix.interfaces.ScalafixMainMode; +import scalafix.interfaces.ScalafixRule; +import scalafix.interfaces.ScalafixVersions; +import scalafix.internal.interfaces.ScalafixCoursier; +import scalafix.internal.interfaces.ScalafixInterfacesClassloader; +import scalafix.internal.interfaces.ScalafixProperties; + +@Value +@With +@AllArgsConstructor +public class ScalafixArgumentsImpl implements ScalafixArguments { + + // Fetch arguments + @NonNull Optional config; + @NonNull List repositories; + @NonNull String scalaVersion; + @NonNull Path workingDirectory; + + // Load arguments + List toolDependencyCoordinates; + List toolDependencyURLs; + + // Run arguments + Charset charset; + List classpath; + List excludedPaths; + ScalafixMainCallback mainCallback; + ScalafixMainMode mode; + List parsedArguments; + List paths; + PrintStream printStream; + List rules; + List scalacOptions; + List semanticdbTargetroots; + Path sourceroot; + + public ScalafixArgumentsImpl() { + // Fetch arguments + this.config = Optional.empty(); + this.repositories = Repository.defaults(); + this.scalaVersion = "3"; + this.workingDirectory = Paths.get(System.getProperty("user.dir")); + + // Load arguments + this.toolDependencyCoordinates = null; + this.toolDependencyURLs = null; + + // Run arguments + this.charset = null; + this.classpath = null; + this.excludedPaths = null; + this.mainCallback = null; + this.mode = null; + this.parsedArguments = null; + this.paths = null; + this.printStream = null; + this.rules = null; + this.scalacOptions = null; + this.semanticdbTargetroots = null; + this.sourceroot = null; + } + + @Override + public List availableRules() throws ScalafixException { + return load().availableRules(); + } + + @Override + public ScalafixEvaluation evaluate() throws ScalafixException { + return load().evaluate(); + } + + @Override + public List rulesThatWillRun() throws ScalafixException { + return load().rulesThatWillRun(); + } + + @Override + public ScalafixError[] run() throws ScalafixException { + return load().run(); + } + + @Override + public Optional validate() throws ScalafixException { + return load().validate(); + } + + @SuppressWarnings("deprecation") + @Override + public ScalafixArguments withToolClasspath( + List customURLs) { + return withToolDependencyURLs(customURLs); + } + + @SuppressWarnings("deprecation") + @Override + public ScalafixArguments withToolClasspath( + List customURLs, + List customDependenciesCoordinates) { + return withToolDependencyURLs(customURLs) + .withToolDependencyCoordinates(customDependenciesCoordinates); + } + + @SuppressWarnings("deprecation") + @Override + public ScalafixArguments withToolClasspath( + List customURLs, + List customDependenciesCoordinates, + List repositories) { + return withToolDependencyURLs(customURLs) + .withToolDependencyCoordinates(customDependenciesCoordinates) + .withRepositories(repositories); + } + + @SuppressWarnings("deprecation") + @Override + public ScalafixArguments withToolClasspath( + URLClassLoader toolClasspath) { + throw new UnsupportedOperationException( + "Unsupported method 'withToolClasspath', use withToolDependency*() instead"); + } + + private ScalafixArguments load() throws ScalafixException { + ScalafixVersions scalafixVersions = versionsForConfigOrBuiltinScalafixVersion(); + + List cliJars = ScalafixCoursier.scalafixCliJars( + repositories, + scalafixVersions.scalafixVersion(), + scalafixVersions.cliScalaVersion(scalaVersion)); + ClassLoader parent = new ScalafixInterfacesClassloader(ScalafixArgumentsImpl.class.getClassLoader()); + ClassLoader classLoader = new URLClassLoader(cliJars.stream().toArray(URL[]::new), parent); + + // TODO: memoize + ScalafixArguments arguments = classLoadScalafixArguments(classLoader) + .withConfig(config) + .withRepositories(repositories) + .withScalaVersion(scalaVersion) + .withWorkingDirectory(workingDirectory); + + if (toolDependencyCoordinates != null) { + arguments = arguments.withToolDependencyCoordinates(toolDependencyCoordinates); + } + + if (toolDependencyURLs != null) { + arguments = arguments.withToolDependencyURLs(toolDependencyURLs); + } + + if (charset != null) { + arguments = arguments.withCharset(charset); + } + + if (classpath != null) { + arguments = arguments.withClasspath(classpath); + } + + if (excludedPaths != null) { + arguments = arguments.withExcludedPaths(excludedPaths); + } + + if (mainCallback != null) { + arguments = arguments.withMainCallback(mainCallback); + } + + if (mode != null) { + arguments = arguments.withMode(mode); + } + + if (paths != null) { + arguments = arguments.withPaths(paths); + } + + if (printStream != null) { + arguments = arguments.withPrintStream(printStream); + } + + if (rules != null) { + arguments = arguments.withRules(rules); + } + if (scalacOptions != null) { + arguments = arguments.withScalacOptions(scalacOptions); + } + + if (semanticdbTargetroots != null) { + arguments = arguments.withSemanticdbTargetroots(semanticdbTargetroots); + } + + if (sourceroot != null) { + arguments = arguments.withSourceroot(sourceroot); + } + + // Apply that one last to allow dynamic/user override of build-tool setup + if (parsedArguments != null) { + arguments = arguments.withParsedArguments(parsedArguments); + } + + return arguments; + } + + private ScalafixVersions versionsForConfigOrBuiltinScalafixVersion() throws ScalafixException { + // default to the built-in scalafix version + ScalafixVersions scalafixVersions = ScalafixVersions.get(); + + final String key = "version"; + File configFile = config.orElse(workingDirectory.resolve(".scalafix.conf")).toFile(); + try { + Config typesafeConfig = ConfigFactory.parseFile(configFile); + if (typesafeConfig.hasPath(key)) { + String requestedVersion = typesafeConfig.getString(key); + // TODO: some validation on requestedVersion + if (!scalafixVersions.scalafixVersion().equals(requestedVersion)) { + List versionsJars = ScalafixCoursier.scalafixVersionsJars( + repositories, + requestedVersion); + scalafixVersions = ScalafixVersions.get(versionsJars); + } + } + } catch (ConfigException e) { + } + + return scalafixVersions; + } + + private ScalafixArguments classLoadScalafixArguments(ClassLoader classLoader) throws ScalafixException { + ServiceLoader loader = ServiceLoader.load(ScalafixArguments.class); + Iterator iterator = loader.iterator(); + if (iterator.hasNext()) { + return iterator.next(); + } else { + throw new ScalafixException("No implementation found"); + } + } +} diff --git a/scalafix-loader/src/main/java/scalafix/loader/internal/ScalafixImpl.java b/scalafix-loader/src/main/java/scalafix/loader/internal/ScalafixImpl.java new file mode 100644 index 000000000..bc3392acc --- /dev/null +++ b/scalafix-loader/src/main/java/scalafix/loader/internal/ScalafixImpl.java @@ -0,0 +1,84 @@ +package scalafix.loader.internal; + +import scalafix.interfaces.Scalafix; +import scalafix.interfaces.ScalafixArguments; +import scalafix.interfaces.ScalafixVersions; + +public class ScalafixImpl implements Scalafix { + + @Override + public ScalafixArguments newArguments() { + return new ScalafixArgumentsImpl(); + } + + @Override + public String mainHelp(int screenWidth) { + throw new UnsupportedOperationException("Unimplemented method 'mainHelp'"); + } + + @Override + public String scalaVersion() { + throw new UnsupportedOperationException("Unimplemented method 'scalaVersion'"); + } + + @Override + public String scalafixVersion() { + return ScalafixVersions.get().scalafixVersion(); + } + + @Override + public String scalametaVersion() { + throw new UnsupportedOperationException("Unimplemented method 'scalametaVersion'"); + } + + @Override + public String[] supportedScalaVersions() { + throw new UnsupportedOperationException("Unimplemented method 'supportedScalaVersions'"); + } + + @SuppressWarnings("deprecation") + @Override + public String scala211() { + throw new UnsupportedOperationException("Unimplemented method 'scala211'"); + } + + @Override + public String scala212() { + throw new UnsupportedOperationException("Unimplemented method 'scala212'"); + } + + @Override + public String scala213() { + throw new UnsupportedOperationException("Unimplemented method 'scala213'"); + } + + @Override + public String scala33() { + throw new UnsupportedOperationException("Unimplemented method 'scala33'"); + } + + @Override + public String scala35() { + throw new UnsupportedOperationException("Unimplemented method 'scala35'"); + } + + @Override + public String scala36() { + throw new UnsupportedOperationException("Unimplemented method 'scala36'"); + } + + @Override + public String scala37() { + throw new UnsupportedOperationException("Unimplemented method 'scala37'"); + } + + @Override + public String scala3LTS() { + throw new UnsupportedOperationException("Unimplemented method 'scala3LTS'"); + } + + @Override + public String scala3Next() { + throw new UnsupportedOperationException("Unimplemented method 'scala3Next'"); + } +} diff --git a/scalafix-loader/src/main/resources/META-INF/services/scalafix.interfaces.Scalafix b/scalafix-loader/src/main/resources/META-INF/services/scalafix.interfaces.Scalafix new file mode 100644 index 000000000..40be6f44b --- /dev/null +++ b/scalafix-loader/src/main/resources/META-INF/services/scalafix.interfaces.Scalafix @@ -0,0 +1 @@ +scalafix.loader.internal.ScalafixImpl diff --git a/scalafix-tests/integration/src/test/scala/scalafix/tests/interfaces/ScalafixArgumentsSuite.scala b/scalafix-tests/integration/src/test/scala/scalafix/tests/cli/ScalafixArgumentsImplSuite.scala similarity index 99% rename from scalafix-tests/integration/src/test/scala/scalafix/tests/interfaces/ScalafixArgumentsSuite.scala rename to scalafix-tests/integration/src/test/scala/scalafix/tests/cli/ScalafixArgumentsImplSuite.scala index 8d308c405..c80a6e080 100644 --- a/scalafix-tests/integration/src/test/scala/scalafix/tests/interfaces/ScalafixArgumentsSuite.scala +++ b/scalafix-tests/integration/src/test/scala/scalafix/tests/cli/ScalafixArgumentsImplSuite.scala @@ -1,4 +1,4 @@ -package scalafix.tests.interfaces +package scalafix.tests.cli import java.io.ByteArrayOutputStream import java.io.File @@ -34,7 +34,7 @@ import scalafix.tests.core.Classpaths import scalafix.tests.util.ScalaVersions import scalafix.tests.util.compat.CompatSemanticdb -class ScalafixArgumentsSuite extends AnyFunSuite with DiffAssertions { +class ScalafixArgumentsImplSuite extends AnyFunSuite with DiffAssertions { val scalaVersion: String = BuildInfo.scalaVersion val scalaLibrary: Seq[AbsolutePath] = Classpaths.scalaLibrary.entries diff --git a/scalafix-tests/integration/src/test/scala/scalafix/tests/loader/ScalafixArgumentsImplSuite.scala b/scalafix-tests/integration/src/test/scala/scalafix/tests/loader/ScalafixArgumentsImplSuite.scala new file mode 100644 index 000000000..6049b6843 --- /dev/null +++ b/scalafix-tests/integration/src/test/scala/scalafix/tests/loader/ScalafixArgumentsImplSuite.scala @@ -0,0 +1,66 @@ +package scalafix.tests.loader + +import java.util.Optional + +import scala.collection.JavaConverters._ + +import scala.meta.io.AbsolutePath +import scala.meta.testkit.StringFS + +import org.scalatest.funsuite.AnyFunSuite +import scalafix.Versions +import scalafix.interfaces.ScalafixArguments +import scalafix.loader.internal.ScalafixArgumentsImpl +import scalafix.tests.BuildInfo + +/** + * Some tests below require scalafix-cli & its dependencies to be published so + * that Coursier can fetch them. `publishLocalTransitive` is done automatically + * as part of `sbt integrationX / test`, so make sure to run that once if you + * want to run the test with testOnly or through BSP. + */ +class ScalafixArgumentsImplSuite extends AnyFunSuite { + + val scalaVersion: String = BuildInfo.scalaVersion + + val workingDirectory: AbsolutePath = StringFS.fromString( + s""" + |/.scalafix.conf + |version = "${Versions.version}" + |rules = ExplicitResultTypes + |ExplicitResultTypes.skipSimpleDefinitions = [] + | + |/src/Main.scala + |object Main { + | def foo = 2 + |} + |""".stripMargin + ) + + val arguments: ScalafixArguments = + new ScalafixArgumentsImpl() + .withWorkingDirectory(workingDirectory.toNIO) + .withScalaVersion(scalaVersion) + + test("availableRules") { + val availableRules = arguments + .availableRules() + .asScala + assert(availableRules.exists(_.name() == "ExplicitResultTypes")) + } + + test("rulesThatWillRun") { + val rulesThatWillRun = arguments + .rulesThatWillRun() + .asScala + assert(rulesThatWillRun.map(_.name()) == Seq("ExplicitResultTypes")) + } + + test("validate") { + assert(arguments.validate() == Optional.empty()) + } + + test("evaluate") {} + + test("run --check") {} +} diff --git a/scalafix-tests/integration/src/test/scala/scalafix/tests/loader/ScalafixImplSuite.scala b/scalafix-tests/integration/src/test/scala/scalafix/tests/loader/ScalafixImplSuite.scala new file mode 100644 index 000000000..f439b603b --- /dev/null +++ b/scalafix-tests/integration/src/test/scala/scalafix/tests/loader/ScalafixImplSuite.scala @@ -0,0 +1,15 @@ +package scalafix.tests.loader + +import org.scalatest.funsuite.AnyFunSuite +import scalafix.Versions +import scalafix.interfaces.Scalafix + +class ScalafixImplSuite extends AnyFunSuite { + + test("the scalafix version is exposed") { + assert(Scalafix.get().scalafixVersion == Versions.version) + } + + // TODO: mainHelp + +} diff --git a/scalafix-tests/integration/src/test/scala/scalafix/tests/versions/ScalafixVersionsImplSuite.scala b/scalafix-tests/integration/src/test/scala/scalafix/tests/versions/ScalafixVersionsImplSuite.scala new file mode 100644 index 000000000..2c728dd5c --- /dev/null +++ b/scalafix-tests/integration/src/test/scala/scalafix/tests/versions/ScalafixVersionsImplSuite.scala @@ -0,0 +1,91 @@ +package scalafix.tests.versions + +import org.scalatest.funsuite.AnyFunSuite +import scalafix.Versions +import scalafix.interfaces.ScalafixVersions + +class ScalafixVersionsImplSuite extends AnyFunSuite { + + lazy val versions: ScalafixVersions = ScalafixVersions.get() + + test("scalafixVersion") { + assert(versions.scalafixVersion() == Versions.version) + } + + test("fail to get 2.11 with full version") { + assertThrows[IllegalArgumentException]( + versions.cliScalaVersion("2.11.0") + ) + } + + test("fail to get 2.11 with minor version") { + assertThrows[IllegalArgumentException]( + versions.cliScalaVersion("2.11") + ) + } + + test("get 2.12 with full version") { + assert(versions.cliScalaVersion("2.12.20") == Versions.scala212) + } + + test("get 2.12 with major.minor version") { + assert(versions.cliScalaVersion("2.12") == Versions.scala212) + } + + test("get 2.13 with full version") { + assert(versions.cliScalaVersion("2.13.15") == Versions.scala213) + } + + test("get 2.13 with major.minor version") { + assert(versions.cliScalaVersion("2.13") == Versions.scala213) + } + + test("get 2.13 with major version") { + assert(versions.cliScalaVersion("2") == Versions.scala213) + } + + test("get 3LTS with full pre-LTS version") { + assert(versions.cliScalaVersion("3.0.0") == Versions.scala3LTS) + } + + test("get 3LTS with major.minor pre-LTS version") { + assert(versions.cliScalaVersion("3.2") == Versions.scala3LTS) + } + + test("get 3LTS with full LTS version") { + assert(versions.cliScalaVersion("3.3.4") == Versions.scala3LTS) + } + + test("get 3LTS with major.minor LTS version") { + assert(versions.cliScalaVersion("3.3") == Versions.scala3LTS) + } + + test("get 3.5 with full version") { + assert(versions.cliScalaVersion("3.5.2") == Versions.scala35) + } + + test("get 3.5 with major.minor version") { + assert(versions.cliScalaVersion("3.5") == Versions.scala35) + } + + test("get 3.6 with full version") { + assert(versions.cliScalaVersion("3.6.3") == Versions.scala36) + } + + test("get 3.6 with major.minor version") { + assert(versions.cliScalaVersion("3.6") == Versions.scala36) + } + + test("get 3Next with full version") { + assert(versions.cliScalaVersion("3.7.0") == Versions.scala3Next) + } + + test("get 3Next with major.minor version") { + assert(versions.cliScalaVersion("3.7") == Versions.scala3Next) + } + + test("get 3Next with major version") { + assert(versions.cliScalaVersion("3") == Versions.scala3Next) + } + +} diff --git a/scalafix-versions/src/main/java/scalafix/versions/internal/ScalafixVersionsImpl.java b/scalafix-versions/src/main/java/scalafix/versions/internal/ScalafixVersionsImpl.java new file mode 100644 index 000000000..4ad7a8dfd --- /dev/null +++ b/scalafix-versions/src/main/java/scalafix/versions/internal/ScalafixVersionsImpl.java @@ -0,0 +1,33 @@ +package scalafix.versions.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import scalafix.interfaces.ScalafixVersions; +import scalafix.internal.interfaces.ScalafixProperties; + +public class ScalafixVersionsImpl implements ScalafixVersions { + + private static final String PROPERTIES_PATH = "scalafix-versions.properties"; + + private final Properties properties; + + public ScalafixVersionsImpl() throws IOException { + InputStream stream = getClass().getClassLoader().getResourceAsStream(PROPERTIES_PATH); + Properties properties = new Properties(); + properties.load(stream); + this.properties = properties; + } + + @Override + public String scalafixVersion() { + return properties.getProperty("scalafix"); + } + + @Override + public String cliScalaVersion(String sourcesScalaVersion) { + String scalaVersionKey = ScalafixProperties.getScalaVersionKey(sourcesScalaVersion); + return properties.getProperty(scalaVersionKey); + } +} diff --git a/scalafix-versions/src/main/resources/META-INF/services/scalafix.interfaces.ScalafixVersions b/scalafix-versions/src/main/resources/META-INF/services/scalafix.interfaces.ScalafixVersions new file mode 100644 index 000000000..dd5b4e04f --- /dev/null +++ b/scalafix-versions/src/main/resources/META-INF/services/scalafix.interfaces.ScalafixVersions @@ -0,0 +1 @@ +scalafix.versions.internal.ScalafixVersionsImpl