Skip to content

Commit cff3f67

Browse files
committed
add scalafix-loader
1 parent e12f479 commit cff3f67

File tree

16 files changed

+509
-201
lines changed

16 files changed

+509
-201
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ hesitate to ask in the [Discord channel](https://discord.gg/8AHaqGx3Qj).
1919

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

build.sbt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ lazy val versions = project
5151
.disablePlugins(ScalafixPlugin)
5252
.dependsOn(interfaces)
5353

54+
lazy val loader = project
55+
.in(file("scalafix-loader"))
56+
.settings(
57+
moduleName := "scalafix-loader",
58+
javaSettings,
59+
mimaPreviousArtifacts := Set.empty, // TODO: remove after 0.14.3
60+
libraryDependencies += lombok % Provided,
61+
javacOptions ++= {
62+
// https://inside.java/2024/06/18/quality-heads-up/
63+
if (jdk > 8) Seq("-proc:full")
64+
else Seq() // only backported to Oracle’s 8u release (8u411)
65+
}
66+
)
67+
.disablePlugins(ScalafixPlugin)
68+
.dependsOn(interfaces)
69+
5470
// Scala 3 macros vendored separately (i.e. without runtime classes), to
5571
// shadow Scala 2.13 macros in the Scala 3 compiler classpath, while producing
5672
// code valid against Scala 2.13 bytecode
@@ -384,6 +400,7 @@ lazy val integration = projectMatrix
384400
.jvmPlatform(CrossVersion.full, cliScalaVersions)
385401
.enablePlugins(BuildInfoPlugin)
386402
.dependsOn(unit % "compile->test")
403+
.dependsOn(loader % "compile->test")
387404

388405
lazy val expect = projectMatrix
389406
.in(file("scalafix-tests/expect"))

project/Dependencies.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ object Dependencies {
2121
val commontTextV = "1.13.1"
2222
val googleDiffV = "1.3.0"
2323
val jgitV = "5.13.3.202401111512-r"
24+
val lombokV = "1.18.38"
2425
val metaconfigV = "0.15.0"
2526
val nailgunV = "0.9.1"
2627
val scalaXmlV = "2.2.0"
@@ -36,6 +37,7 @@ object Dependencies {
3637
val coursierInterfaces = "io.get-coursier" % "interface" % coursierInterfaceV
3738
val googleDiff = "com.googlecode.java-diff-utils" % "diffutils" % googleDiffV
3839
val jgit = "org.eclipse.jgit" % "org.eclipse.jgit" % jgitV
40+
val lombok = "org.projectlombok" % "lombok" % lombokV
3941
val metaconfig = "org.scalameta" %% "metaconfig-typesafe-config" % metaconfigV
4042
val metacp = "org.scalameta" %% "metacp" % scalametaV
4143
val nailgunServer = "com.martiansoftware" % "nailgun-server" % nailgunV

project/Mima.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ object Mima {
77
// See https://github.com/lightbend/mima
88
Seq(
99
ProblemFilters.exclude[Problem]("scalafix.internal.*"),
10-
ProblemFilters.exclude[Problem]("scala.meta.internal.*")
10+
ProblemFilters.exclude[Problem]("scala.meta.internal.*"),
1111
// Exceptions
12+
ProblemFilters.exclude[ReversedMissingMethodProblem]("scalafix.interfaces.ScalafixArguments.withRepositories"),
13+
ProblemFilters.exclude[ReversedMissingMethodProblem]("scalafix.interfaces.ScalafixArguments.withToolDependencyCoordinates"),
14+
ProblemFilters.exclude[ReversedMissingMethodProblem]("scalafix.interfaces.ScalafixArguments.withToolDependencyURLs")
1215
)
1316
}
1417
}

project/ScalafixBuild.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
4141
autoScalaLibrary := false
4242
)
4343

44+
lazy val jdk = System.getProperty("java.specification.version").toDouble
45+
4446
// https://github.com/scalameta/scalameta/issues/2485
4547
lazy val coreScalaVersions = Seq(scala212, scala213)
4648
lazy val cliScalaVersions = {
47-
val jdk = System.getProperty("java.specification.version").toDouble
4849
val scala3Versions =
4950
// Scala 3.5 will never support JDK 23
5051
if (jdk >= 23) Seq(scala33, scala36, scala37)

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

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,39 +48,44 @@ final case class ScalafixArgumentsImpl(args: Args = Args.default)
4848
}
4949
}
5050

51+
override def withRepositories(
52+
repositories: util.List[Repository]
53+
): ScalafixArguments =
54+
copy(args = args.copy(repositories = repositories.asScala.toList))
55+
5156
override def withRules(rules: util.List[String]): ScalafixArguments =
5257
copy(args = args.copy(rules = rules.asScala.toList))
5358

5459
override def withToolClasspath(
5560
customURLs: util.List[URL]
5661
): ScalafixArguments =
57-
withToolClasspath(
58-
customURLs,
59-
Nil.asJava,
60-
Repository.defaults()
61-
)
62+
withToolDependencyURLs(customURLs)
6263

6364
override def withToolClasspath(
6465
customURLs: util.List[URL],
6566
customDependenciesCoordinates: util.List[String]
6667
): ScalafixArguments =
67-
withToolClasspath(
68-
customURLs,
69-
customDependenciesCoordinates,
70-
Repository.defaults()
71-
)
68+
withToolDependencyCoordinates(customDependenciesCoordinates)
69+
.withToolDependencyURLs(customURLs)
7270

7371
override def withToolClasspath(
7472
customURLs: util.List[URL],
7573
customDependenciesCoordinates: util.List[String],
7674
repositories: util.List[Repository]
7775
): ScalafixArguments = {
76+
withRepositories(repositories)
77+
.withToolDependencyCoordinates(customDependenciesCoordinates)
78+
.withToolDependencyURLs(customURLs)
79+
}
7880

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

8287
val keptDependencies: Seq[String] =
83-
customDependenciesCoordinates.asScala
88+
withToolDependencyCoordinates.asScala
8489
.collect {
8590
case dep @ OrganizeImportsCoordinates() =>
8691
args.out.println(
@@ -107,7 +112,7 @@ final case class ScalafixArgumentsImpl(args: Args = Args.default)
107112
else Versions.scalaVersion
108113

109114
val customDependenciesJARs = ScalafixCoursier.toolClasspath(
110-
repositories,
115+
args.repositories.asJava,
111116
keptDependencies.asJava,
112117
scalaVersionForDependencies
113118
)
@@ -158,13 +163,23 @@ final case class ScalafixArgumentsImpl(args: Args = Args.default)
158163
}
159164
}
160165

161-
val extraURLs = customURLs.asScala ++ customDependenciesJARs
166+
val extraURLs = customDependenciesJARs
162167
.getFiles()
163168
.asScala
164169
.map(_.toURI().toURL())
165170
val classLoader = new URLClassLoader(
166171
extraURLs.toArray,
167-
getClass.getClassLoader
172+
args.toolClasspath
173+
)
174+
withToolClasspath(classLoader)
175+
}
176+
177+
override def withToolDependencyURLs(
178+
withToolDependencyURLs: java.util.List[java.net.URL]
179+
): ScalafixArguments = {
180+
val classLoader = new URLClassLoader(
181+
withToolDependencyURLs.asScala.toArray,
182+
args.toolClasspath
168183
)
169184
withToolClasspath(classLoader)
170185
}

scalafix-cli/src/main/scala/scalafix/internal/v1/Args.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import java.util.regex.Pattern
1313
import java.util.regex.PatternSyntaxException
1414

1515
import scala.annotation.StaticAnnotation
16+
import scala.jdk.CollectionConverters._
1617
import scala.util.Failure
1718
import scala.util.Success
1819
import scala.util.Try
@@ -22,6 +23,8 @@ import scala.meta.internal.symtab.SymbolTable
2223
import scala.meta.io.AbsolutePath
2324
import scala.meta.io.Classpath
2425

26+
import coursierapi.MavenRepository
27+
import coursierapi.Repository
2528
import metaconfig.Configured._
2629
import metaconfig._
2730
import metaconfig.annotation._
@@ -158,6 +161,8 @@ case class Args(
158161
"The glob syntax is defined by `nio.FileSystem.getPathMatcher`."
159162
)
160163
exclude: List[PathMatcher] = Nil,
164+
@Description("Maven repositories to fetch the artifacts from")
165+
repositories: List[Repository] = Repository.defaults.asScala.toList,
161166
@Description(
162167
"Additional classpath for compiling and classloading custom rules, as a set of filesystem paths, separated by ':' on Unix or ';' on Windows."
163168
)
@@ -483,6 +488,8 @@ object Args extends TPrintImplicits {
483488
ConfDecoder.stringConfDecoder.map(glob =>
484489
FileSystems.getDefault.getPathMatcher("glob:" + glob)
485490
)
491+
implicit val repositoryDecoder: ConfDecoder[Repository] =
492+
ConfDecoder.stringConfDecoder.map(base => MavenRepository.of(base))
486493
implicit val scalaVersionDecoder: ConfDecoder[ScalaVersion] =
487494
ScalafixConfig.scalaVersionDecoder
488495

@@ -505,6 +512,8 @@ object Args extends TPrintImplicits {
505512
ConfEncoder.StringEncoder.contramap(_ => "<stdout>")
506513
implicit val pathMatcherEncoder: ConfEncoder[PathMatcher] =
507514
ConfEncoder.StringEncoder.contramap(_.toString)
515+
implicit val repositoriesEncoder: ConfEncoder[Repository] =
516+
ConfEncoder.StringEncoder.contramap(_.toString)
508517
implicit val callbackEncoder: ConfEncoder[ScalafixMainCallback] =
509518
ConfEncoder.StringEncoder.contramap(_.toString)
510519
implicit val argsEncoder: ConfEncoder[Args] = generic.deriveEncoder

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

Lines changed: 33 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package scalafix.interfaces;
22

33
import coursierapi.Repository;
4+
45
import scalafix.internal.interfaces.ScalafixCoursier;
56
import scalafix.internal.interfaces.ScalafixInterfacesClassloader;
67
import scalafix.internal.interfaces.ScalafixProperties;
@@ -11,16 +12,21 @@
1112
import java.lang.reflect.InvocationTargetException;
1213
import java.net.URL;
1314
import java.net.URLClassLoader;
15+
import java.util.Iterator;
1416
import java.util.List;
1517
import java.util.Properties;
18+
import java.util.ServiceLoader;
1619

1720
/**
18-
* Public API for reflectively invoking Scalafix from a build tool or IDE integration.
21+
* Public API for reflectively invoking Scalafix from a build tool or IDE
22+
* integration.
1923
* <p>
20-
* To obtain an instance of Scalafix, use one of the static factory methods.
24+
* To obtain an instance of Scalafix, classload
25+
* <code>ch.epfl.scala:scalafix-loader</code> and use {@link #get()}.
2126
*
22-
* @implNote This interface is not intended to be extended, the only implementation of this interface
23-
* should live in the Scalafix repository.
27+
* @implNote This interface is not intended to be extended, the only
28+
* implementation of this interface should live in the Scalafix
29+
* repository.
2430
*/
2531
public interface Scalafix {
2632

@@ -102,44 +108,17 @@ public interface Scalafix {
102108
String scala3Next();
103109

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

125118
/**
126-
* Fetch JARs containing an implementation of {@link Scalafix} from the provided repositories using Coursier and
127-
* classload an instance of it via runtime reflection.
128-
* <p>
129-
* The custom classloader optionally provided with {@link ScalafixArguments#withToolClasspath} to compile and
130-
* classload external rules must have the classloader of the returned instance as ancestor to share a common
131-
* loaded instance of `scalafix-core`, and therefore have been compiled against the requested Scala version.
132-
*
133-
* @param requestedScalaVersion A full Scala version (i.e. "3.3.4") or a major.minor one (i.e. "3.3") to infer
134-
* the major.minor Scala version that should be available in the classloader of the
135-
* returned instance. To be able to run advanced semantic rules using the Scala
136-
* Presentation Compiler such as ExplicitResultTypes, this must be source-compatible
137-
* with the version that the target classpath is built with, as provided with
138-
* {@link ScalafixArguments#withScalaVersion}.
139-
* @param repositories Maven/Ivy repositories to fetch the JARs from.
140-
* @return An implementation of the {@link Scalafix} interface.
141-
* @throws ScalafixException in case of errors during artifact resolution/fetching.
119+
* @deprecated Use {@link #get()} instead.
142120
*/
121+
@Deprecated
143122
static Scalafix fetchAndClassloadInstance(String requestedScalaVersion, List<Repository> repositories)
144123
throws ScalafixException {
145124

@@ -164,36 +143,34 @@ static Scalafix fetchAndClassloadInstance(String requestedScalaVersion, List<Rep
164143
}
165144

166145
/**
167-
* JVM runtime reflection method helper to classload an instance of {@link Scalafix}.
168-
* <p>
169-
* The custom classloader optionally provided with {@link ScalafixArguments#withToolClasspath} to compile and
170-
* classload external rules must have the provided classloader as ancestor to share a common loaded instance
171-
* of `scalafix-core`, and therefore must have been compiled against the same Scala binary version as
172-
* the one in the classLoader provided here.
173-
* <p>
174-
* Unless you have an advanced use-case, prefer the high-level overloads that cannot cause runtime errors
175-
* due to an invalid classloader hierarchy.
176-
*
177-
* @param classLoader Classloader containing the full Scalafix classpath, including the scalafix-cli module. To be
178-
* able to run advanced semantic rules using the Scala Presentation Compiler such as
179-
* ExplicitResultTypes, this Scala binary version in that classloader should match the one that
180-
* the target classpath was built with, as provided with
181-
* {@link ScalafixArguments#withScalaVersion}.
182-
* @return An implementation of the {@link Scalafix} interface.
183-
* @throws ScalafixException in case of errors during classloading, most likely caused
184-
* by an incorrect classloader argument.
146+
* @deprecated Use {@link #get()} instead.
185147
*/
148+
@Deprecated
186149
static Scalafix classloadInstance(ClassLoader classLoader) throws ScalafixException {
187150
try {
188151
Class<?> cls = classLoader.loadClass("scalafix.internal.interfaces.ScalafixImpl");
189152
Constructor<?> ctor = cls.getDeclaredConstructor();
190153
ctor.setAccessible(true);
191154
return (Scalafix) ctor.newInstance();
192-
} catch (ClassNotFoundException | NoSuchMethodException |
193-
IllegalAccessException | InvocationTargetException |
194-
InstantiationException ex) {
155+
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException
156+
| InstantiationException ex) {
195157
throw new ScalafixException(
196158
"Failed to reflectively load Scalafix with classloader " + classLoader.toString(), ex);
197159
}
198160
}
161+
162+
/**
163+
* Obtains an implementation of Scalafix using the current classpath.
164+
*
165+
* @return the first available implementation advertised as a service provider.
166+
*/
167+
static Scalafix get() {
168+
ServiceLoader<Scalafix> loader = ServiceLoader.load(Scalafix.class);
169+
Iterator<Scalafix> iterator = loader.iterator();
170+
if (iterator.hasNext()) {
171+
return iterator.next();
172+
} else {
173+
throw new IllegalStateException("No implementation found");
174+
}
175+
}
199176
}

0 commit comments

Comments
 (0)