Skip to content

Commit 2f3d9d5

Browse files
committed
forward compatibility for cli full Scala version lookups
An old scalafix-interfaces client should be able to get the full Scala version for a recent scalafix-cli version. This is achieved by encapsulating property file lookup behind a Java interface, for which an implementation can easily be retrieved via ServiceLoader, as long as the recent scalafix-versions JAR has been fetched.
1 parent 775f7b5 commit 2f3d9d5

File tree

9 files changed

+303
-53
lines changed

9 files changed

+303
-53
lines changed

CONTRIBUTING.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,24 @@ hesitate to ask in the [Discord channel](https://discord.gg/8AHaqGx3Qj).
66

77
## Modules
88

9-
- `scalafix-interfaces` Java facade to run rules within an existing JVM instance.
9+
### For writing rules
10+
1011
- `scalafix-core/` data structures for rewriting and linting Scala source code.
11-
- `scalafix-reflect/` utilities to compile and classload rules from
12-
configuration.
1312
- `scalafix-rules/` built-in rules such as `RemoveUnused`.
13+
14+
### For executing rules
15+
1416
- `scalafix-cli/` command-line interface.
17+
- `scalafix-reflect/` utilities to compile and classload rules from
18+
configuration.
19+
20+
### For tool integration
21+
- `scalafix-interfaces/` Java facade to run rules within an existing JVM instance.
22+
- `scalafix-versions/` Java implementation to advertize which Scala versions
23+
`scalafix-cli` is published with.
24+
25+
### Others
26+
1527
- `scalafix-tests/` projects for unit and integration tests.
1628
- `scalafix-docs/` documentation code for the Scalafix website.
1729

build.sbt

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,41 @@ noPublishAndNoMima
1515
lazy val interfaces = project
1616
.in(file("scalafix-interfaces"))
1717
.settings(
18+
moduleName := "scalafix-interfaces",
19+
javaSettings,
20+
libraryDependencies += coursierInterfaces,
1821
Compile / resourceGenerators += Def.task {
19-
val props = new java.util.Properties()
22+
val props = cliVersionsProperties()
2023
props.put("scalafixVersion", version.value)
2124
props.put("scalafixStableVersion", stableVersion.value)
2225
props.put("scalametaVersion", scalametaV)
23-
props.put("scala212", scala212)
24-
props.put("scala213", scala213)
25-
props.put("scala33", scala33)
26-
props.put("scala35", scala35)
27-
props.put("scala36", scala36)
28-
props.put("scala37", scala37)
29-
props.put("scala3LTS", scala3LTS)
30-
props.put("scala3Next", scala3Next)
3126
val out =
3227
(Compile / managedResourceDirectories).value.head /
3328
"scalafix-interfaces.properties"
3429
IO.write(props, "Scalafix version constants", out)
3530
List(out)
36-
},
37-
(Compile / javacOptions) ++= List(
38-
"-Xlint:all",
39-
"-Werror"
40-
),
41-
(Compile / doc / javacOptions) := List("-Xdoclint:none"),
42-
libraryDependencies += coursierInterfaces,
43-
moduleName := "scalafix-interfaces",
44-
crossPaths := false,
45-
autoScalaLibrary := false
31+
}
32+
)
33+
.disablePlugins(ScalafixPlugin)
34+
35+
lazy val versions = project
36+
.in(file("scalafix-versions"))
37+
.settings(
38+
moduleName := "scalafix-versions",
39+
javaSettings,
40+
mimaPreviousArtifacts := Set.empty, // TODO: remove after 0.14.3
41+
Compile / resourceGenerators += Def.task {
42+
val props = cliVersionsProperties()
43+
props.put("scalafix", version.value)
44+
val out =
45+
(Compile / managedResourceDirectories).value.head /
46+
"scalafix-versions.properties"
47+
IO.write(props, "Scala versions for ch.epfl.scala:::scalafix-cli", out)
48+
List(out)
49+
}
4650
)
4751
.disablePlugins(ScalafixPlugin)
52+
.dependsOn(interfaces)
4853

4954
// Scala 3 macros vendored separately (i.e. without runtime classes), to
5055
// shadow Scala 2.13 macros in the Scala 3 compiler classpath, while producing
@@ -341,22 +346,37 @@ lazy val integration = projectMatrix
341346
"inputSourceroot" ->
342347
resolve(input, Compile / sourceDirectory).value,
343348
"outputSourceroot" ->
344-
resolve(output, Compile / sourceDirectory).value
349+
resolve(output, Compile / sourceDirectory).value,
350+
"versionsJars" ->
351+
Seq(
352+
(interfaces / Compile / packageBin / artifactPath).value,
353+
(versions / Compile / packageBin / artifactPath).value
354+
)
345355
),
346356
Test / test := (Test / test)
347357
.dependsOn(
348-
(resolve(cli, publishLocalTransitive) +: cli.projectRefs
358+
interfaces / Compile / packageBin,
359+
versions / Compile / packageBin
360+
)
361+
.dependsOn(resolve(cli, publishLocalTransitive))
362+
.dependsOn(
363+
cli.projectRefs
349364
// always publish Scala 3 artifacts to test Scala 3 minor version fallbacks
350365
.collect { case p @ LocalProject(n) if n.startsWith("cli3") => p }
351-
.map(_ / publishLocalTransitive)): _*
366+
.map(_ / publishLocalTransitive): _*
352367
)
353368
.value,
354369
Test / testWindows := (Test / testWindows)
355370
.dependsOn(
356-
(resolve(cli, publishLocalTransitive) +: cli.projectRefs
371+
interfaces / Compile / packageBin,
372+
versions / Compile / packageBin
373+
)
374+
.dependsOn(resolve(cli, publishLocalTransitive))
375+
.dependsOn(
376+
cli.projectRefs
357377
// always publish Scala 3 artifacts to test Scala 3 minor version fallbacks
358378
.collect { case p @ LocalProject(n) if n.startsWith("cli3") => p }
359-
.map(_ / publishLocalTransitive)): _*
379+
.map(_ / publishLocalTransitive): _*
360380
)
361381
.value
362382
)

project/ScalafixBuild.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
3131
publish / skip := true
3232
)
3333

34+
lazy val javaSettings = Seq(
35+
(Compile / javacOptions) ++= List(
36+
"-Xlint:all",
37+
"-Werror"
38+
),
39+
(Compile / doc / javacOptions) := List("-Xdoclint:none"),
40+
crossPaths := false,
41+
autoScalaLibrary := false
42+
)
43+
3444
// https://github.com/scalameta/scalameta/issues/2485
3545
lazy val coreScalaVersions = Seq(scala212, scala213)
3646
lazy val cliScalaVersions = {
@@ -161,6 +171,20 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
161171
buildInfoObject := "RulesBuildInfo"
162172
)
163173

174+
// must match ScalafixProperties logic
175+
def cliVersionsProperties() = {
176+
val props = new java.util.Properties()
177+
props.put("scala212", scala212)
178+
props.put("scala213", scala213)
179+
props.put("scala33", scala33)
180+
props.put("scala35", scala35)
181+
props.put("scala36", scala36)
182+
props.put("scala37", scala37)
183+
props.put("scala3LTS", scala3LTS)
184+
props.put("scala3Next", scala3Next)
185+
props
186+
}
187+
164188
lazy val testWindows =
165189
taskKey[Unit]("run tests, excluding those incompatible with Windows")
166190

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

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import coursierapi.Repository;
44
import scalafix.internal.interfaces.ScalafixCoursier;
55
import scalafix.internal.interfaces.ScalafixInterfacesClassloader;
6+
import scalafix.internal.interfaces.ScalafixProperties;
67

78
import java.io.IOException;
89
import java.io.InputStream;
@@ -142,32 +143,6 @@ static Scalafix fetchAndClassloadInstance(String requestedScalaVersion) throws S
142143
static Scalafix fetchAndClassloadInstance(String requestedScalaVersion, List<Repository> repositories)
143144
throws ScalafixException {
144145

145-
String requestedScalaMajorMinorOrMajorVersion =
146-
requestedScalaVersion.replaceAll("^(\\d+\\.\\d+).*", "$1");
147-
148-
String scalaVersionKey;
149-
if (requestedScalaMajorMinorOrMajorVersion.equals("2.12")) {
150-
scalaVersionKey = "scala212";
151-
} else if (requestedScalaMajorMinorOrMajorVersion.equals("2.13") ||
152-
requestedScalaMajorMinorOrMajorVersion.equals("2")) {
153-
scalaVersionKey = "scala213";
154-
} else if (requestedScalaMajorMinorOrMajorVersion.equals("3.0") ||
155-
requestedScalaMajorMinorOrMajorVersion.equals("3.1") ||
156-
requestedScalaMajorMinorOrMajorVersion.equals("3.2") ||
157-
requestedScalaMajorMinorOrMajorVersion.equals("3.3")) {
158-
scalaVersionKey = "scala33";
159-
} else if (requestedScalaMajorMinorOrMajorVersion.equals("3.5")) {
160-
scalaVersionKey = "scala35";
161-
} else if (requestedScalaMajorMinorOrMajorVersion.equals("3.6")) {
162-
scalaVersionKey = "scala36";
163-
} else if (requestedScalaMajorMinorOrMajorVersion.equals("3.7")) {
164-
scalaVersionKey = "scala37";
165-
} else if (requestedScalaMajorMinorOrMajorVersion.startsWith("3")) {
166-
scalaVersionKey = "scala3Next";
167-
} else {
168-
throw new IllegalArgumentException("Unsupported scala version " + requestedScalaVersion);
169-
}
170-
171146
Properties properties = new Properties();
172147
String propertiesPath = "scalafix-interfaces.properties";
173148
InputStream stream = Scalafix.class.getClassLoader().getResourceAsStream(propertiesPath);
@@ -178,6 +153,7 @@ static Scalafix fetchAndClassloadInstance(String requestedScalaVersion, List<Rep
178153
}
179154

180155
String scalafixVersion = properties.getProperty("scalafixVersion");
156+
String scalaVersionKey = ScalafixProperties.getScalaVersionKey(requestedScalaVersion);
181157
String scalaVersion = properties.getProperty(scalaVersionKey);
182158
if (scalafixVersion == null || scalaVersion == null)
183159
throw new ScalafixException("Failed to lookup versions from '" + propertiesPath + "'");
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package scalafix.interfaces;
2+
3+
import java.net.URL;
4+
import java.net.URLClassLoader;
5+
import java.util.Iterator;
6+
import java.util.List;
7+
import java.util.ServiceLoader;
8+
9+
import scalafix.internal.interfaces.ScalafixInterfacesClassloader;
10+
11+
/**
12+
* Public API for looking up which one of the
13+
* <code>ch.epfl.scala:::scalafix-cli</code> artifacts of a Scalafix release is
14+
* the most appropriate, given the Scala version used to compile Scalafix target
15+
* sources.
16+
* <p>
17+
* To obtain an instance of ScalafixVersions, fetch the corresponding
18+
* <code>ch.epfl.scala:scalafix-versions</code> and use {@link #get}.
19+
*
20+
* @implNote This interface is not intended to be extended, the only
21+
* implementation of this interface should live in the Scalafix
22+
* repository.
23+
*/
24+
public interface ScalafixVersions {
25+
26+
/**
27+
* @return the Scalafix release described.
28+
*/
29+
String scalafixVersion();
30+
31+
/**
32+
* Returns the most appropriate full Scala version to resolve
33+
* <code>ch.epfl.scala:::scalafix-cli:scalafixVersion</code> with.
34+
*
35+
* @param sourcesScalaVersion The Scala version (i.e. "3.3.4") used to compile
36+
* Scalafix target sources.
37+
* @return a full Scala version to resolve the artifact with.
38+
*/
39+
String cliScalaVersion(String sourcesScalaVersion);
40+
41+
/**
42+
* Obtains an implementation of ScalafixVersions using the provided
43+
* classpath URLs.
44+
*
45+
* @param classpathUrls URLs to be used in the classloader for loading
46+
* a ScalafixVersions implementation
47+
* @return the first available implementation advertised as a service provider.
48+
*/
49+
static ScalafixVersions get(List<URL> classpathUrls) {
50+
ClassLoader parent = new ScalafixInterfacesClassloader(ScalafixVersions.class.getClassLoader());
51+
ClassLoader classLoader = new URLClassLoader(classpathUrls.stream().toArray(URL[]::new), parent);
52+
ServiceLoader<ScalafixVersions> loader = ServiceLoader.load(ScalafixVersions.class, classLoader);
53+
Iterator<ScalafixVersions> iterator = loader.iterator();
54+
if (iterator.hasNext()) {
55+
return iterator.next();
56+
} else {
57+
throw new IllegalStateException("No implementation found");
58+
}
59+
}
60+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package scalafix.internal.interfaces;
2+
3+
//TODO: move to scalafix-versions when scalafix.interfaces.Scalafix is removed
4+
public class ScalafixProperties {
5+
6+
public static String getScalaVersionKey(String requestedScalaVersion) {
7+
String requestedScalaMajorMinorOrMajorVersion =
8+
requestedScalaVersion.replaceAll("^(\\d+\\.\\d+).*", "$1");
9+
10+
if (requestedScalaMajorMinorOrMajorVersion.equals("2.12")) {
11+
return "scala212";
12+
} else if (requestedScalaMajorMinorOrMajorVersion.equals("2.13") ||
13+
requestedScalaMajorMinorOrMajorVersion.equals("2")) {
14+
return "scala213";
15+
} else if (requestedScalaMajorMinorOrMajorVersion.equals("3.0") ||
16+
requestedScalaMajorMinorOrMajorVersion.equals("3.1") ||
17+
requestedScalaMajorMinorOrMajorVersion.equals("3.2") ||
18+
requestedScalaMajorMinorOrMajorVersion.equals("3.3")) {
19+
return "scala33";
20+
} else if (requestedScalaMajorMinorOrMajorVersion.equals("3.5")) {
21+
return "scala35";
22+
} else if (requestedScalaMajorMinorOrMajorVersion.equals("3.6")) {
23+
return "scala36";
24+
} else if (requestedScalaMajorMinorOrMajorVersion.equals("3.7")) {
25+
return "scala37";
26+
} else if (requestedScalaMajorMinorOrMajorVersion.startsWith("3")) {
27+
return "scala3Next";
28+
} else {
29+
throw new IllegalArgumentException("Unsupported scala version " + requestedScalaVersion);
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)