Skip to content

Commit 18e287f

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 7616b69 commit 18e287f

File tree

9 files changed

+300
-50
lines changed

9 files changed

+300
-50
lines changed

CONTRIBUTING.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,22 @@ 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 rule writing
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 rule execution
15+
1416
- `scalafix-cli/` command-line interface.
17+
- `scalafix-interfaces` Java facade to run rules within an existing JVM instance.
18+
- `scalafix-reflect/` utilities to compile and classload rules from
19+
configuration.
20+
- `scalafix-versions` Java implementation to advertize which Scala versions
21+
`scalafix-cli` is published with.
22+
23+
### Others
24+
1525
- `scalafix-tests/` projects for unit and integration tests.
1626
- `scalafix-docs/` documentation code for the Scalafix website.
1727

build.sbt

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +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("scala3LTS", scala3LTS)
29-
props.put("scala3Next", scala3Next)
3026
val out =
3127
(Compile / managedResourceDirectories).value.head /
3228
"scalafix-interfaces.properties"
3329
IO.write(props, "Scalafix version constants", out)
3430
List(out)
35-
},
36-
(Compile / javacOptions) ++= List(
37-
"-Xlint:all",
38-
"-Werror"
39-
),
40-
(Compile / doc / javacOptions) := List("-Xdoclint:none"),
41-
libraryDependencies += coursierInterfaces,
42-
moduleName := "scalafix-interfaces",
43-
crossPaths := false,
44-
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+
}
4550
)
4651
.disablePlugins(ScalafixPlugin)
52+
.dependsOn(interfaces)
4753

4854
// Scala 3 macros vendored separately (i.e. without runtime classes), to
4955
// shadow Scala 2.13 macros in the Scala 3 compiler classpath, while producing
@@ -340,22 +346,37 @@ lazy val integration = projectMatrix
340346
"inputSourceroot" ->
341347
resolve(input, Compile / sourceDirectory).value,
342348
"outputSourceroot" ->
343-
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+
)
344355
),
345356
Test / test := (Test / test)
346357
.dependsOn(
347-
(resolve(cli, publishLocalTransitive) +: cli.projectRefs
358+
interfaces / Compile / packageBin,
359+
versions / Compile / packageBin
360+
)
361+
.dependsOn(resolve(cli, publishLocalTransitive))
362+
.dependsOn(
363+
cli.projectRefs
348364
// always publish Scala 3 artifacts to test Scala 3 minor version fallbacks
349365
.collect { case p @ LocalProject(n) if n.startsWith("cli3") => p }
350-
.map(_ / publishLocalTransitive)): _*
366+
.map(_ / publishLocalTransitive): _*
351367
)
352368
.value,
353369
Test / testWindows := (Test / testWindows)
354370
.dependsOn(
355-
(resolve(cli, publishLocalTransitive) +: cli.projectRefs
371+
interfaces / Compile / packageBin,
372+
versions / Compile / packageBin
373+
)
374+
.dependsOn(resolve(cli, publishLocalTransitive))
375+
.dependsOn(
376+
cli.projectRefs
356377
// always publish Scala 3 artifacts to test Scala 3 minor version fallbacks
357378
.collect { case p @ LocalProject(n) if n.startsWith("cli3") => p }
358-
.map(_ / publishLocalTransitive)): _*
379+
.map(_ / publishLocalTransitive): _*
359380
)
360381
.value
361382
)

project/ScalafixBuild.scala

Lines changed: 23 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 = {
@@ -156,6 +166,19 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
156166
buildInfoObject := "RulesBuildInfo"
157167
)
158168

169+
// must match ScalafixProperties logic
170+
def cliVersionsProperties() = {
171+
val props = new java.util.Properties()
172+
props.put("scala212", scala212)
173+
props.put("scala213", scala213)
174+
props.put("scala33", scala33)
175+
props.put("scala35", scala35)
176+
props.put("scala36", scala36)
177+
props.put("scala3LTS", scala3LTS)
178+
props.put("scala3Next", scala3Next)
179+
props
180+
}
181+
159182
lazy val testWindows =
160183
taskKey[Unit]("run tests, excluding those incompatible with Windows")
161184

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

Lines changed: 2 additions & 24 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;
@@ -137,30 +138,6 @@ static Scalafix fetchAndClassloadInstance(String requestedScalaVersion) throws S
137138
static Scalafix fetchAndClassloadInstance(String requestedScalaVersion, List<Repository> repositories)
138139
throws ScalafixException {
139140

140-
String requestedScalaMajorMinorOrMajorVersion =
141-
requestedScalaVersion.replaceAll("^(\\d+\\.\\d+).*", "$1");
142-
143-
String scalaVersionKey;
144-
if (requestedScalaMajorMinorOrMajorVersion.equals("2.12")) {
145-
scalaVersionKey = "scala212";
146-
} else if (requestedScalaMajorMinorOrMajorVersion.equals("2.13") ||
147-
requestedScalaMajorMinorOrMajorVersion.equals("2")) {
148-
scalaVersionKey = "scala213";
149-
} else if (requestedScalaMajorMinorOrMajorVersion.equals("3.0") ||
150-
requestedScalaMajorMinorOrMajorVersion.equals("3.1") ||
151-
requestedScalaMajorMinorOrMajorVersion.equals("3.2") ||
152-
requestedScalaMajorMinorOrMajorVersion.equals("3.3")) {
153-
scalaVersionKey = "scala33";
154-
} else if (requestedScalaMajorMinorOrMajorVersion.equals("3.5")) {
155-
scalaVersionKey = "scala35";
156-
} else if (requestedScalaMajorMinorOrMajorVersion.equals("3.6")) {
157-
scalaVersionKey = "scala36";
158-
} else if (requestedScalaMajorMinorOrMajorVersion.startsWith("3")) {
159-
scalaVersionKey = "scala3Next";
160-
} else {
161-
throw new IllegalArgumentException("Unsupported scala version " + requestedScalaVersion);
162-
}
163-
164141
Properties properties = new Properties();
165142
String propertiesPath = "scalafix-interfaces.properties";
166143
InputStream stream = Scalafix.class.getClassLoader().getResourceAsStream(propertiesPath);
@@ -171,6 +148,7 @@ static Scalafix fetchAndClassloadInstance(String requestedScalaVersion, List<Rep
171148
}
172149

173150
String scalafixVersion = properties.getProperty("scalafixVersion");
151+
String scalaVersionKey = ScalafixProperties.getScalaVersionKey(requestedScalaVersion);
174152
String scalaVersion = properties.getProperty(scalaVersionKey);
175153
if (scalafixVersion == null || scalaVersion == null)
176154
throw new ScalafixException("Failed to lookup versions from '" + propertiesPath + "'");
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 of ScalafixVersions, as declared
48+
* via ServiceLoader
49+
*/
50+
static ScalafixVersions get(List<URL> classpathUrls) {
51+
ClassLoader parent = new ScalafixInterfacesClassloader(ScalafixVersions.class.getClassLoader());
52+
ClassLoader classLoader = new URLClassLoader(classpathUrls.stream().toArray(URL[]::new), parent);
53+
ServiceLoader<ScalafixVersions> loader = ServiceLoader.load(ScalafixVersions.class, classLoader);
54+
Iterator<ScalafixVersions> iterator = loader.iterator();
55+
if (iterator.hasNext()) {
56+
return iterator.next();
57+
} else {
58+
throw new IllegalStateException("No implementation found");
59+
}
60+
}
61+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package scalafix.internal.interfaces;
2+
3+
//TODO: inline in scalafix.internal.versions.ScalafixVersions 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.startsWith("3")) {
25+
return "scala3Next";
26+
} else {
27+
throw new IllegalArgumentException("Unsupported scala version " + requestedScalaVersion);
28+
}
29+
}
30+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package scalafix.tests.versions
2+
3+
import scala.jdk.CollectionConverters._
4+
5+
import org.scalatest.funsuite.AnyFunSuite
6+
import scalafix.Versions
7+
import scalafix.interfaces.ScalafixVersions
8+
import scalafix.tests.BuildInfo
9+
10+
/**
11+
* This test require interfaces & versions to be packaged. `packageBin` is done
12+
* automatically as part of `sbt integrationX / test`, so make sure to run that
13+
* once if you want to run the test with testOnly or through BSP.
14+
*/
15+
class ScalafixVersionsImplSuite extends AnyFunSuite {
16+
17+
lazy val versions: ScalafixVersions =
18+
ScalafixVersions.get(BuildInfo.versionsJars.map(_.toURL).asJava)
19+
20+
test("scalafixVersion") {
21+
assert(versions.scalafixVersion() == Versions.version)
22+
}
23+
24+
test("fail to get 2.11 with full version") {
25+
assertThrows[IllegalArgumentException](
26+
versions.cliScalaVersion("2.11.0")
27+
)
28+
}
29+
30+
test("fail to get 2.11 with minor version") {
31+
assertThrows[IllegalArgumentException](
32+
versions.cliScalaVersion("2.11")
33+
)
34+
}
35+
36+
test("get 2.12 with full version") {
37+
assert(versions.cliScalaVersion("2.12.20") == Versions.scala212)
38+
}
39+
40+
test("get 2.12 with major.minor version") {
41+
assert(versions.cliScalaVersion("2.12") == Versions.scala212)
42+
}
43+
44+
test("get 2.13 with full version") {
45+
assert(versions.cliScalaVersion("2.13.15") == Versions.scala213)
46+
}
47+
48+
test("get 2.13 with major.minor version") {
49+
assert(versions.cliScalaVersion("2.13") == Versions.scala213)
50+
}
51+
52+
test("get 2.13 with major version") {
53+
assert(versions.cliScalaVersion("2") == Versions.scala213)
54+
}
55+
56+
test("get 3LTS with full pre-LTS version") {
57+
assert(versions.cliScalaVersion("3.0.0") == Versions.scala3LTS)
58+
}
59+
60+
test("get 3LTS with major.minor pre-LTS version") {
61+
assert(versions.cliScalaVersion("3.2") == Versions.scala3LTS)
62+
}
63+
64+
test("get 3LTS with full LTS version") {
65+
assert(versions.cliScalaVersion("3.3.4") == Versions.scala3LTS)
66+
}
67+
68+
test("get 3LTS with major.minor LTS version") {
69+
assert(versions.cliScalaVersion("3.3") == Versions.scala3LTS)
70+
}
71+
72+
test("get 3.5 with full version") {
73+
assert(versions.cliScalaVersion("3.5.2") == Versions.scala35)
74+
}
75+
76+
test("get 3.5 with major.minor version") {
77+
assert(versions.cliScalaVersion("3.5") == Versions.scala35)
78+
}
79+
80+
test("get 3Next with full version") {
81+
assert(versions.cliScalaVersion("3.6.2") == Versions.scala3Next)
82+
}
83+
84+
test("get 3Next with major.minor version") {
85+
assert(versions.cliScalaVersion("3.6") == Versions.scala3Next)
86+
}
87+
88+
test("get 3Next with major version") {
89+
assert(versions.cliScalaVersion("3") == Versions.scala3Next)
90+
}
91+
92+
}

0 commit comments

Comments
 (0)