Skip to content

Commit d55b09b

Browse files
Merge pull request #1152 from bjaglin/interfaces-fetch
scalafix-interfaces: higher-level API for class loading
2 parents 881ee0c + 3694af6 commit d55b09b

File tree

14 files changed

+434
-103
lines changed

14 files changed

+434
-103
lines changed

CONTRIBUTING.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ hesitate to ask in the [gitter channel](https://gitter.im/scalacenter/scalafix).
66

77
## Modules
88

9+
- `scalafix-interfaces` Java facade to run rules within an existing JVM instance.
910
- `scalafix-core/` data structures for rewriting and linting Scala source code.
1011
- `scalafix-reflect/` utilities to compile and classload rules from
1112
configuration.

build.sbt

+7-4
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ lazy val interfaces = project
3939
"scalafix-interfaces.properties"
4040
IO.write(props, "Scalafix version constants", out)
4141
List(out)
42-
4342
},
4443
javacOptions.in(Compile) ++= List(
4544
"-Xlint:all",
@@ -48,9 +47,9 @@ lazy val interfaces = project
4847
javacOptions.in(Compile, doc) := List("-Xdoclint:none"),
4948
javaHome.in(Compile) := inferJavaHome(),
5049
javaHome.in(Compile, doc) := inferJavaHome(),
50+
libraryDependencies += "io.get-coursier" % "interface" % coursierInterfaceV,
5151
moduleName := "scalafix-interfaces",
52-
crossVersion := CrossVersion.disabled,
53-
crossScalaVersions := List(scala213),
52+
crossPaths := false,
5453
autoScalaLibrary := false
5554
)
5655

@@ -243,7 +242,11 @@ lazy val unit = project
243242
"scalafix-tests" / "shared" / "src" / "main",
244243
"sharedClasspath" ->
245244
classDirectory.in(testsShared, Compile).value
246-
)
245+
),
246+
test.in(Test) := test
247+
.in(Test)
248+
.dependsOn(crossPublishLocalBinTransitive.in(cli))
249+
.value
247250
)
248251
.enablePlugins(BuildInfoPlugin)
249252
.dependsOn(

project/Dependencies.scala

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ object Dependencies {
99
def scala212 = "2.12.11"
1010
def scala213 = "2.13.2"
1111
def coursierV = "2.0.0-RC5-6"
12+
def coursierInterfaceV = "0.0.22"
1213
val currentScalaVersion = scala213
1314

1415
val jgit = "org.eclipse.jgit" % "org.eclipse.jgit" % "5.7.0.202003110725-r"

project/ScalafixBuild.scala

+32-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
2929
skip in publish := true
3030
) ++ noMima
3131
lazy val supportedScalaVersions = List(scala213, scala211, scala212)
32+
lazy val publishLocalTransitive =
33+
taskKey[Unit]("Run publishLocal on this project and its dependencies")
34+
lazy val crossPublishLocalBinTransitive = taskKey[Unit](
35+
"Run, for each crossVersion, publishLocal without packageDoc & packageSrc, on this project and its dependencies"
36+
)
3237
lazy val isFullCrossVersion = Seq(
3338
crossVersion := CrossVersion.full
3439
)
@@ -203,6 +208,7 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
203208
},
204209
commands += Command.command("ci-213-windows") { s =>
205210
s"++$scala213" ::
211+
"cli/crossPublishLocalBinTransitive" :: // scalafix.tests.interfaces.ScalafixSuite
206212
s"unit/testOnly -- -l scalafix.internal.tests.utils.SkipWindows" ::
207213
s
208214
},
@@ -311,6 +317,31 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
311317
organization.value % s"${moduleName.value}_$binaryVersion" % previousArtifactVersion
312318
)
313319
},
314-
mimaBinaryIssueFilters ++= Mima.ignoredABIProblems
320+
mimaBinaryIssueFilters ++= Mima.ignoredABIProblems,
321+
publishLocalTransitive := Def.taskDyn {
322+
val ref = thisProjectRef.value
323+
publishLocal.all(ScopeFilter(inDependencies(ref)))
324+
}.value,
325+
crossPublishLocalBinTransitive := {
326+
val currentState = state.value
327+
val ref = thisProjectRef.value
328+
val versions = crossScalaVersions.value
329+
versions.map {
330+
version =>
331+
val withScalaVersion = Project
332+
.extract(currentState)
333+
.appendWithoutSession(
334+
Seq(
335+
scalaVersion.in(ThisBuild) := version,
336+
publishArtifact.in(ThisBuild, packageDoc) := false,
337+
publishArtifact.in(ThisBuild, packageSrc) := false
338+
),
339+
currentState
340+
)
341+
Project
342+
.extract(withScalaVersion)
343+
.runTask(publishLocalTransitive.in(ref), withScalaVersion)
344+
}
345+
}
315346
)
316347
}

scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixArgumentsImpl.scala

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package scalafix.internal.interfaces
22

33
import java.io.PrintStream
4-
import java.net.URLClassLoader
4+
import java.net.{URL, URLClassLoader}
55
import java.nio.charset.Charset
66
import java.nio.file.Path
77
import java.nio.file.PathMatcher
88
import java.util
99
import java.util.Optional
10+
11+
import coursierapi.Repository
1012
import metaconfig.Conf
1113
import metaconfig.Configured
14+
1215
import scala.jdk.CollectionConverters._
1316
import scala.meta.io.AbsolutePath
1417
import scala.meta.io.Classpath
@@ -23,6 +26,7 @@ import scalafix.internal.v1.Args
2326
import scalafix.internal.v1.MainOps
2427
import scalafix.internal.v1.Rules
2528
import scalafix.v1.RuleDecoder
29+
import scalafix.Versions
2630

2731
final case class ScalafixArgumentsImpl(args: Args = Args.default)
2832
extends ScalafixArguments {
@@ -35,6 +39,41 @@ final case class ScalafixArgumentsImpl(args: Args = Args.default)
3539
override def withRules(rules: util.List[String]): ScalafixArguments =
3640
copy(args = args.copy(rules = rules.asScala.toList))
3741

42+
override def withToolClasspath(
43+
customURLs: util.List[URL]
44+
): ScalafixArguments =
45+
withToolClasspath(
46+
new URLClassLoader(customURLs.asScala.toArray, getClass.getClassLoader)
47+
)
48+
49+
override def withToolClasspath(
50+
customURLs: util.List[URL],
51+
customDependenciesCoordinates: util.List[String]
52+
): ScalafixArguments =
53+
withToolClasspath(
54+
customURLs,
55+
customDependenciesCoordinates,
56+
Repository.defaults()
57+
)
58+
59+
override def withToolClasspath(
60+
customURLs: util.List[URL],
61+
customDependenciesCoordinates: util.List[String],
62+
repositories: util.List[Repository]
63+
): ScalafixArguments = {
64+
val customDependenciesJARs = ScalafixCoursier.toolClasspath(
65+
repositories,
66+
customDependenciesCoordinates,
67+
Versions.scalaVersion
68+
)
69+
val extraURLs = customURLs.asScala ++ customDependenciesJARs.asScala
70+
val classLoader = new URLClassLoader(
71+
extraURLs.toArray,
72+
getClass.getClassLoader
73+
)
74+
withToolClasspath(classLoader)
75+
}
76+
3877
override def withToolClasspath(
3978
classLoader: URLClassLoader
4079
): ScalafixArguments =

scalafix-interfaces/src/main/java/scalafix/interfaces/Scalafix.java

+95-5
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
package scalafix.interfaces;
22

3+
import coursierapi.Repository;
4+
import scalafix.internal.interfaces.ScalafixCoursier;
5+
import scalafix.internal.interfaces.ScalafixInterfacesClassloader;
36

7+
import java.io.IOException;
8+
import java.io.InputStream;
49
import java.lang.reflect.Constructor;
510
import java.lang.reflect.InvocationTargetException;
11+
import java.net.URL;
12+
import java.net.URLClassLoader;
13+
import java.util.List;
14+
import java.util.Properties;
615

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

1726
/**
18-
* @return Construct a new instance of {@link ScalafixArguments} that can be later passed to {@link #runMain(ScalafixArguments) }.
27+
* @return Construct a new instance of {@link ScalafixArguments}.
1928
*/
2029
ScalafixArguments newArguments();
2130

@@ -58,15 +67,96 @@ public interface Scalafix {
5867
*/
5968
String scala213();
6069

70+
/**
71+
* Fetch JARs containing an implementation of {@link Scalafix} using Coursier and classload an instance of it via
72+
* runtime reflection.
73+
* <p>
74+
* The custom classloader optionally provided with {@link ScalafixArguments#withToolClasspath} to compile and
75+
* classload external rules must have the classloader of the returned instance as ancestor to share a common
76+
* loaded instance of `scalafix-core`, and therefore have been compiled against the requested Scala binary version.
77+
*
78+
* @param scalaBinaryVersion The Scala binary version ("2.13" for example) available in the classloader of the
79+
* returned instance. To be able to run advanced semantic rules using the Scala
80+
* Presentation Compiler such as ExplicitResultTypes, this must match the binary
81+
* version that the target classpath was built with, as provided with
82+
* {@link ScalafixArguments#withScalaVersion}.
83+
* @return An implementation of the {@link Scalafix} interface.
84+
* @throws ScalafixException in case of errors during artifact resolution/fetching.
85+
*/
86+
static Scalafix fetchAndClassloadInstance(String scalaBinaryVersion) throws ScalafixException {
87+
return fetchAndClassloadInstance(scalaBinaryVersion, Repository.defaults());
88+
}
89+
90+
/**
91+
* Fetch JARs containing an implementation of {@link Scalafix} from the provided repositories using Coursier and
92+
* classload an instance of it via runtime reflection.
93+
* <p>
94+
* The custom classloader optionally provided with {@link ScalafixArguments#withToolClasspath} to compile and
95+
* classload external rules must have the classloader of the returned instance as ancestor to share a common
96+
* loaded instance of `scalafix-core`, and therefore have been compiled against the requested Scala binary version.
97+
*
98+
* @param scalaBinaryVersion The Scala binary version ("2.13" for example) available in the classloader of the
99+
* returned instance. To be able to run advanced semantic rules using the Scala
100+
* Presentation Compiler such as ExplicitResultTypes, this must match the binary
101+
* version that the target classpath was built with, as provided with
102+
* {@link ScalafixArguments#withScalaVersion}.
103+
* @param repositories Maven/Ivy repositories to fetch the JARs from.
104+
* @return An implementation of the {@link Scalafix} interface.
105+
* @throws ScalafixException in case of errors during artifact resolution/fetching.
106+
*/
107+
static Scalafix fetchAndClassloadInstance(String scalaBinaryVersion, List<Repository> repositories)
108+
throws ScalafixException {
109+
110+
String scalaVersionKey;
111+
switch (scalaBinaryVersion) {
112+
case "2.11":
113+
scalaVersionKey = "scala211";
114+
break;
115+
case "2.12":
116+
scalaVersionKey = "scala212";
117+
break;
118+
case "2.13":
119+
scalaVersionKey = "scala213";
120+
break;
121+
default:
122+
throw new IllegalArgumentException("Unsupported scala version " + scalaBinaryVersion);
123+
}
61124

125+
Properties properties = new Properties();
126+
String propertiesPath = "scalafix-interfaces.properties";
127+
InputStream stream = Scalafix.class.getClassLoader().getResourceAsStream(propertiesPath);
128+
try {
129+
properties.load(stream);
130+
} catch (IOException | NullPointerException e) {
131+
throw new ScalafixException("Failed to load '" + propertiesPath + "' to lookup versions", e);
132+
}
133+
134+
String scalafixVersion = properties.getProperty("scalafixVersion");
135+
String scalaVersion = properties.getProperty(scalaVersionKey);
136+
if (scalafixVersion == null || scalaVersion == null)
137+
throw new ScalafixException("Failed to lookup versions from '" + propertiesPath + "'");
138+
139+
List<URL> jars = ScalafixCoursier.scalafixCliJars(repositories, scalafixVersion, scalaVersion);
140+
ClassLoader parent = new ScalafixInterfacesClassloader(Scalafix.class.getClassLoader());
141+
return classloadInstance(new URLClassLoader(jars.stream().toArray(URL[]::new), parent));
142+
}
62143

63144
/**
64145
* JVM runtime reflection method helper to classload an instance of {@link Scalafix}.
146+
* <p>
147+
* The custom classloader optionally provided with {@link ScalafixArguments#withToolClasspath} to compile and
148+
* classload external rules must have the provided classloader as ancestor to share a common loaded instance
149+
* of `scalafix-core`, and therefore must have been compiled against the same Scala binary version as
150+
* the one in the classLoader provided here.
65151
*
66-
* @param classLoader Classloader containing the full Scalafix classpath, including the scalafix-cli module.
152+
* @param classLoader Classloader containing the full Scalafix classpath, including the scalafix-cli module. To be
153+
* able to run advanced semantic rules using the Scala Presentation Compiler such as
154+
* ExplicitResultTypes, this Scala binary version in that classloader should match the one that
155+
* the target classpath was built with, as provided with
156+
* {@link ScalafixArguments#withScalaVersion}.
67157
* @return An implementation of the {@link Scalafix} interface.
68158
* @throws ScalafixException in case of errors during classloading, most likely caused
69-
* by an incorrect classloader argument.
159+
* by an incorrect classloader argument.
70160
*/
71161
static Scalafix classloadInstance(ClassLoader classLoader) throws ScalafixException {
72162
try {

0 commit comments

Comments
 (0)