Skip to content

Commit

Permalink
Merge pull request #1152 from bjaglin/interfaces-fetch
Browse files Browse the repository at this point in the history
scalafix-interfaces: higher-level API for class loading
  • Loading branch information
github-brice-jaglin authored Jun 8, 2020
2 parents 881ee0c + 3694af6 commit d55b09b
Show file tree
Hide file tree
Showing 14 changed files with 434 additions and 103 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ hesitate to ask in the [gitter channel](https://gitter.im/scalacenter/scalafix).

## Modules

- `scalafix-interfaces` Java facade to run rules within an existing JVM instance.
- `scalafix-core/` data structures for rewriting and linting Scala source code.
- `scalafix-reflect/` utilities to compile and classload rules from
configuration.
Expand Down
11 changes: 7 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ lazy val interfaces = project
"scalafix-interfaces.properties"
IO.write(props, "Scalafix version constants", out)
List(out)

},
javacOptions.in(Compile) ++= List(
"-Xlint:all",
Expand All @@ -48,9 +47,9 @@ lazy val interfaces = project
javacOptions.in(Compile, doc) := List("-Xdoclint:none"),
javaHome.in(Compile) := inferJavaHome(),
javaHome.in(Compile, doc) := inferJavaHome(),
libraryDependencies += "io.get-coursier" % "interface" % coursierInterfaceV,
moduleName := "scalafix-interfaces",
crossVersion := CrossVersion.disabled,
crossScalaVersions := List(scala213),
crossPaths := false,
autoScalaLibrary := false
)

Expand Down Expand Up @@ -243,7 +242,11 @@ lazy val unit = project
"scalafix-tests" / "shared" / "src" / "main",
"sharedClasspath" ->
classDirectory.in(testsShared, Compile).value
)
),
test.in(Test) := test
.in(Test)
.dependsOn(crossPublishLocalBinTransitive.in(cli))
.value
)
.enablePlugins(BuildInfoPlugin)
.dependsOn(
Expand Down
1 change: 1 addition & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ object Dependencies {
def scala212 = "2.12.11"
def scala213 = "2.13.2"
def coursierV = "2.0.0-RC5-6"
def coursierInterfaceV = "0.0.22"
val currentScalaVersion = scala213

val jgit = "org.eclipse.jgit" % "org.eclipse.jgit" % "5.7.0.202003110725-r"
Expand Down
33 changes: 32 additions & 1 deletion project/ScalafixBuild.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
skip in publish := true
) ++ noMima
lazy val supportedScalaVersions = List(scala213, scala211, scala212)
lazy val publishLocalTransitive =
taskKey[Unit]("Run publishLocal on this project and its dependencies")
lazy val crossPublishLocalBinTransitive = taskKey[Unit](
"Run, for each crossVersion, publishLocal without packageDoc & packageSrc, on this project and its dependencies"
)
lazy val isFullCrossVersion = Seq(
crossVersion := CrossVersion.full
)
Expand Down Expand Up @@ -203,6 +208,7 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
},
commands += Command.command("ci-213-windows") { s =>
s"++$scala213" ::
"cli/crossPublishLocalBinTransitive" :: // scalafix.tests.interfaces.ScalafixSuite
s"unit/testOnly -- -l scalafix.internal.tests.utils.SkipWindows" ::
s
},
Expand Down Expand Up @@ -311,6 +317,31 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
organization.value % s"${moduleName.value}_$binaryVersion" % previousArtifactVersion
)
},
mimaBinaryIssueFilters ++= Mima.ignoredABIProblems
mimaBinaryIssueFilters ++= Mima.ignoredABIProblems,
publishLocalTransitive := Def.taskDyn {
val ref = thisProjectRef.value
publishLocal.all(ScopeFilter(inDependencies(ref)))
}.value,
crossPublishLocalBinTransitive := {
val currentState = state.value
val ref = thisProjectRef.value
val versions = crossScalaVersions.value
versions.map {
version =>
val withScalaVersion = Project
.extract(currentState)
.appendWithoutSession(
Seq(
scalaVersion.in(ThisBuild) := version,
publishArtifact.in(ThisBuild, packageDoc) := false,
publishArtifact.in(ThisBuild, packageSrc) := false
),
currentState
)
Project
.extract(withScalaVersion)
.runTask(publishLocalTransitive.in(ref), withScalaVersion)
}
}
)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package scalafix.internal.interfaces

import java.io.PrintStream
import java.net.URLClassLoader
import java.net.{URL, URLClassLoader}
import java.nio.charset.Charset
import java.nio.file.Path
import java.nio.file.PathMatcher
import java.util
import java.util.Optional

import coursierapi.Repository
import metaconfig.Conf
import metaconfig.Configured

import scala.jdk.CollectionConverters._
import scala.meta.io.AbsolutePath
import scala.meta.io.Classpath
Expand All @@ -23,6 +26,7 @@ import scalafix.internal.v1.Args
import scalafix.internal.v1.MainOps
import scalafix.internal.v1.Rules
import scalafix.v1.RuleDecoder
import scalafix.Versions

final case class ScalafixArgumentsImpl(args: Args = Args.default)
extends ScalafixArguments {
Expand All @@ -35,6 +39,41 @@ final case class ScalafixArgumentsImpl(args: Args = Args.default)
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(
new URLClassLoader(customURLs.asScala.toArray, getClass.getClassLoader)
)

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

override def withToolClasspath(
customURLs: util.List[URL],
customDependenciesCoordinates: util.List[String],
repositories: util.List[Repository]
): ScalafixArguments = {
val customDependenciesJARs = ScalafixCoursier.toolClasspath(
repositories,
customDependenciesCoordinates,
Versions.scalaVersion
)
val extraURLs = customURLs.asScala ++ customDependenciesJARs.asScala
val classLoader = new URLClassLoader(
extraURLs.toArray,
getClass.getClassLoader
)
withToolClasspath(classLoader)
}

override def withToolClasspath(
classLoader: URLClassLoader
): ScalafixArguments =
Expand Down
100 changes: 95 additions & 5 deletions scalafix-interfaces/src/main/java/scalafix/interfaces/Scalafix.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
package scalafix.interfaces;

import coursierapi.Repository;
import scalafix.internal.interfaces.ScalafixCoursier;
import scalafix.internal.interfaces.ScalafixInterfacesClassloader;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
import java.util.Properties;

/**
* Public API for reflectively invoking Scalafix from a build tool or IDE integration.
*
* To obtain an instance of Scalafix, use {@link Scalafix#classloadInstance(ClassLoader)}.
* <p>
* To obtain an instance of Scalafix, use one of the static factory methods.
*
* @implNote This interface is not intended to be extended, the only implementation of this interface
* should live in the Scalafix repository.
*/
public interface Scalafix {

/**
* @return Construct a new instance of {@link ScalafixArguments} that can be later passed to {@link #runMain(ScalafixArguments) }.
* @return Construct a new instance of {@link ScalafixArguments}.
*/
ScalafixArguments newArguments();

Expand Down Expand Up @@ -58,15 +67,96 @@ public interface Scalafix {
*/
String scala213();

/**
* Fetch JARs containing an implementation of {@link Scalafix} using Coursier and classload an instance of it via
* runtime reflection.
* <p>
* 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 binary version.
*
* @param scalaBinaryVersion The Scala binary version ("2.13" for example) 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 match the binary
* version 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 artifact resolution/fetching.
*/
static Scalafix fetchAndClassloadInstance(String scalaBinaryVersion) throws ScalafixException {
return fetchAndClassloadInstance(scalaBinaryVersion, 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.
* <p>
* 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 binary version.
*
* @param scalaBinaryVersion The Scala binary version ("2.13" for example) 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 match the binary
* version that the target classpath was 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.
*/
static Scalafix fetchAndClassloadInstance(String scalaBinaryVersion, List<Repository> repositories)
throws ScalafixException {

String scalaVersionKey;
switch (scalaBinaryVersion) {
case "2.11":
scalaVersionKey = "scala211";
break;
case "2.12":
scalaVersionKey = "scala212";
break;
case "2.13":
scalaVersionKey = "scala213";
break;
default:
throw new IllegalArgumentException("Unsupported scala version " + scalaBinaryVersion);
}

Properties properties = new Properties();
String propertiesPath = "scalafix-interfaces.properties";
InputStream stream = Scalafix.class.getClassLoader().getResourceAsStream(propertiesPath);
try {
properties.load(stream);
} catch (IOException | NullPointerException e) {
throw new ScalafixException("Failed to load '" + propertiesPath + "' to lookup versions", e);
}

String scalafixVersion = properties.getProperty("scalafixVersion");
String scalaVersion = properties.getProperty(scalaVersionKey);
if (scalafixVersion == null || scalaVersion == null)
throw new ScalafixException("Failed to lookup versions from '" + propertiesPath + "'");

List<URL> jars = ScalafixCoursier.scalafixCliJars(repositories, scalafixVersion, scalaVersion);
ClassLoader parent = new ScalafixInterfacesClassloader(Scalafix.class.getClassLoader());
return classloadInstance(new URLClassLoader(jars.stream().toArray(URL[]::new), parent));
}

/**
* JVM runtime reflection method helper to classload an instance of {@link Scalafix}.
* <p>
* 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.
*
* @param classLoader Classloader containing the full Scalafix classpath, including the scalafix-cli module.
* @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.
* by an incorrect classloader argument.
*/
static Scalafix classloadInstance(ClassLoader classLoader) throws ScalafixException {
try {
Expand Down
Loading

0 comments on commit d55b09b

Please sign in to comment.