Skip to content

Commit 7957916

Browse files
committed
scalafix-interfaces must now be provided alongside sbt-scalafix
- scalafixEnable & scalafixAllowDynamicFallback allow to load the latest cli versions if no explicit scalafix-interfaces was provided - scalafixEnable now simply targets the latest semanticdb-scalac version - pin build-time interfaces version to decouple as much as possible, but make sure to dogfood the latest scalafix version for scalafix invocation time, to detect potential regressions
1 parent 12a95b6 commit 7957916

File tree

40 files changed

+185
-232
lines changed

40 files changed

+185
-232
lines changed

.scala-steward.conf

+7
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ updates.pin = [
66
# JGit 6.x requires Java 11, see https://www.eclipse.org/lists/cross-project-issues-dev/msg18654.html
77
{ groupId = "org.eclipse.jgit", artifactId = "org.eclipse.jgit", version = "5." },
88
]
9+
10+
dependencyOverrides = [
11+
{
12+
dependency = { groupId = "ch.epfl.scala", artifactId = "scalafix-interfaces" },
13+
pullRequests = { frequency = "@asap" },
14+
},
15+
]

build.sbt

+1
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,6 @@ scriptedParallelInstances := 2
101101
scriptedLaunchOpts ++= Seq(
102102
"-Xmx2048M",
103103
s"-Dplugin.version=${version.value}",
104+
s"-Dscalafix-interfaces.version=${Dependencies.scalafixInterfacesVersion}",
104105
"-Dsbt-scalafix.uselastmodified=true" // the caching scripted relies on sbt-scalafix only checking file attributes, not content
105106
)

project/Dependencies.scala

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ import sbt._
22

33
object Dependencies {
44
val x = List(1) // scalafix:ok
5-
def scalafixVersion: String = "0.14.2"
5+
6+
// keep this as low as possible, to allow bumping sbt-scalafix without scalafix-interfaces
7+
def scalafixInterfacesVersion: String =
8+
"0.14.2+48-9b6e03ac-SNAPSHOT" // scala-steward:off
69

710
val all = List(
11+
"ch.epfl.scala" % "scalafix-interfaces" % scalafixInterfacesVersion
12+
exclude ("ch.epfl.scala", "scalafix-properties"),
13+
"ch.epfl.scala" % "scalafix-interfaces" % scalafixInterfacesVersion % Test,
814
"org.eclipse.jgit" % "org.eclipse.jgit" % "5.13.3.202401111512-r",
9-
"ch.epfl.scala" % "scalafix-interfaces" % scalafixVersion,
1015
"io.get-coursier" % "interface" % "1.0.28",
1116
"org.scala-lang.modules" %% "scala-collection-compat" % "2.13.0"
1217
)

project/plugins.sbt

+3
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ Compile / unmanagedSourceDirectories ++= {
1010
)
1111
}
1212
libraryDependencies ++= Dependencies.all
13+
14+
// latest version of interfaces to test compatibility & get most recent behavior for scalafix invocations
15+
libraryDependencies += "ch.epfl.scala" % "scalafix-interfaces" % "0.14.2"

readme.md

+9-5
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,18 @@ https://github.com/scalacenter/scalafix/issues
1212

1313
## Nightlies
1414

15-
Our CI publishes a [snapshot release to Sonatype](https://oss.sonatype.org/content/repositories/snapshots/ch/epfl/scala/sbt-scalafix_2.12_1.0/)
16-
on every merge into main. The latest snapshot at the time of the writing can be used via:
15+
Our CI publishes a snapshot release to Sonatype on every merge into main. The latest snapshot(s) of
16+
[the plugin](https://oss.sonatype.org/content/repositories/snapshots/ch/epfl/scala/sbt-scalafix_2.12_1.0/)
17+
and/or [scalafix-interfaces](https://oss.sonatype.org/content/repositories/snapshots/ch/epfl/scala/scalafix-interfaces/)
18+
can be used via:
1719

1820
```diff
1921
// project/plugins.sbt
20-
-addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.34")
21-
+addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.34+5-5dfe5fb6-SNAPSHOT")
22-
+resolvers += Resolver.sonatypeRepo("snapshots")
22+
+resolvers ++= Resolver.sonatypeOssRepos("snapshots")
23+
-addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.2")
24+
+addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.2+17-4ba873d2-SNAPSHOT")
25+
-libraryDependencies += "ch.epfl.scala" % "scalafix-interfaces" % "0.14.2"
26+
+libraryDependencies += "ch.epfl.scala" % "scalafix-interfaces" % "0.14.2+48-9b6e03ac-SNAPSHOT"
2327
```
2428

2529
## Team

src/main/scala/scalafix/sbt/BuildInfo.scala

+5-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ object BuildInfo {
2929
props.load(stream)
3030
case None =>
3131
sys.error(
32-
s"failed to load the resource file '$propertiesPath'. To fix this problem, make sure to enable the sbt-scalafix in 'project/plugins.sbt' and validate that the scalafix-interfaces jar is available on the classpath of the sbt build."
32+
s"failed to load the resource file '$propertiesPath'; " +
33+
"to fix this problem, add \"ch.epfl.scala\" % \"scalafix-interfaces\" " +
34+
"to libraryDependencies in 'project/plugins.sbt'"
3335
)
3436
}
3537
props
@@ -39,7 +41,8 @@ object BuildInfo {
3941
Option(props.getProperty(key)).getOrElse {
4042
sys.error(
4143
s"sbt-scalafix property '$key' missing in $propertiesPath; " +
42-
"to fix this problem, upgrade to the latest version of Scalafix."
44+
"to fix this problem, upgrade to a more recent version of " +
45+
"\"ch.epfl.scala\" % \"scalafix-interfaces\""
4346
)
4447
}
4548
}

src/main/scala/scalafix/sbt/ScalafixEnable.scala

+60-157
Original file line numberDiff line numberDiff line change
@@ -1,188 +1,91 @@
11
package scalafix.sbt
22

3-
import scala.jdk.CollectionConverters.*
4-
import scala.util.*
5-
63
import sbt.*
74
import sbt.Keys.*
8-
import sbt.VersionNumber.SemVer
95

106
import coursierapi.Repository
117

12-
import ScalafixPlugin.autoImport.scalafixResolvers
8+
import ScalafixPlugin.autoImport.*
139

14-
/** Command to automatically enable semanticdb compiler output for shell session
15-
*/
10+
/** Command to automatically prepare the build for scalafix invocations */
1611
object ScalafixEnable {
1712

18-
/** If the provided Scala binary version is supported, return the latest scala
19-
* full version for which the recommended semanticdb-scalac is available
20-
*/
21-
private lazy val recommendedSemanticdbScalacScalaVersion
22-
: PartialFunction[(Long, Long), VersionNumber] = (for {
23-
v <- BuildInfo.supportedScalaVersions
24-
p <- CrossVersion.partialVersion(v).toList
25-
} yield p -> VersionNumber(v)).toMap
26-
27-
/** If the provided Scala binary version is supported, return the latest scala
28-
* full version for which the recommended semanticdb-scalac is available, or
29-
* None if semanticdb support is built-in in the compiler
30-
*/
31-
private lazy val maybeRecommendedSemanticdbScalacScalaVersion
32-
: PartialFunction[(Long, Long), Option[VersionNumber]] =
33-
recommendedSemanticdbScalacScalaVersion.andThen(Some.apply).orElse {
34-
// semanticdb is built-in in the Scala 3 compiler
35-
case (major, _) if major == 3 => None
36-
}
13+
private def latestSemanticdbScalac(
14+
scalaVersion: String,
15+
repositories: Seq[Repository]
16+
): Option[String] = {
17+
val semanticdbScalacModule = coursierapi.Module.parse(
18+
"org.scalameta:::semanticdb-scalac",
19+
coursierapi.ScalaVersion.of(scalaVersion)
20+
)
21+
Option(
22+
coursierapi.Versions.create
23+
.withModule(semanticdbScalacModule)
24+
.withRepositories(repositories*)
25+
.versions()
26+
.getMergedListings
27+
.getRelease
28+
).filter(_.nonEmpty)
29+
}
3730

38-
/** Collect compatible projects across the entire build */
39-
private def collectProjects(extracted: Extracted): Seq[CompatibleProject] =
31+
private def collectProjects(extracted: Extracted): Seq[CompatibleProject] = {
32+
val repositories = (ThisBuild / scalafixResolvers)
33+
.get(extracted.structure.data)
34+
.getOrElse(Seq.empty)
4035
for {
41-
p <- extracted.structure.allProjectRefs
42-
scalaV <- (p / scalaVersion).get(extracted.structure.data).toList
43-
partialVersion <- CrossVersion.partialVersion(scalaV).toList
44-
maybeRecommendedSemanticdbScalacV <-
45-
maybeRecommendedSemanticdbScalacScalaVersion.lift(partialVersion).toList
46-
scalafixResolvers0 <- (p / scalafixResolvers)
47-
.get(extracted.structure.data)
48-
.toList
49-
semanticdbCompilerPlugin0 <- (p / semanticdbCompilerPlugin)
50-
.get(extracted.structure.data)
51-
.toList
52-
} yield CompatibleProject(
53-
p,
54-
VersionNumber(scalaV),
55-
semanticdbCompilerPlugin0,
56-
scalafixResolvers0,
57-
maybeRecommendedSemanticdbScalacV
58-
)
36+
(scalaVersion, projectRefs) <-
37+
extracted.structure.allProjectRefs
38+
.flatMap { projectRef =>
39+
(projectRef / scalaVersion)
40+
.get(extracted.structure.data)
41+
.map(sv => (sv, projectRef))
42+
}
43+
.foldLeft(Map.empty[String, Seq[ProjectRef]]) {
44+
case (acc, (sv, projectRef)) =>
45+
acc.updated(sv, acc.getOrElse(sv, Seq.empty) :+ projectRef)
46+
}
47+
.toSeq
48+
maybeSemanticdbVersion <-
49+
if (scalaVersion.startsWith("2."))
50+
latestSemanticdbScalac(scalaVersion, repositories) match {
51+
case None =>
52+
Seq.empty // don't activate semanticdb
53+
case Some(version) =>
54+
Seq(Some(version)) // activate semanticdb with plugin
55+
}
56+
else Seq(None) // activate semanticdb without plugin
57+
projectRef <- projectRefs
58+
} yield CompatibleProject(projectRef, maybeSemanticdbVersion)
59+
}
5960

6061
private case class CompatibleProject(
6162
ref: ProjectRef,
62-
scalaVersion0: VersionNumber,
63-
semanticdbCompilerPlugin0: ModuleID,
64-
scalafixResolvers0: Seq[Repository],
65-
maybeRecommendedSemanticdbScalacScalaV: Option[VersionNumber]
63+
semanticdbVersion: Option[String]
6664
)
6765

6866
lazy val command: Command = Command.command(
6967
"scalafixEnable",
7068
briefHelp =
7169
"Configure SemanticdbPlugin for scalafix on supported projects.",
72-
detail = """1. set semanticdbEnabled := true
73-
|2. for scala 2.x,
74-
| - set semanticdbCompilerPlugin to the scalameta version tracked by scalafix if available for scalaVersion,
75-
| - otherwise set semanticdbCompilerPlugin to a compatible version available for scalaVersion,
76-
| - otherwise force scalaVersion to the latest version supported by the scalameta version tracked by scalafix.""".stripMargin
70+
detail = """1. set scalafixAllowDynamicFallback := true
71+
|2. set semanticdbEnabled := true
72+
|3. for scala 2.x, set semanticdbVersion to the latest scalameta version""".stripMargin
7773
) { s =>
7874
val extracted = Project.extract(s)
7975
val scalacOptionsSettings = Seq(Compile, Test).flatMap(
8076
inConfig(_)(ScalafixPlugin.relaxScalacOptionsConfigSettings)
8177
)
78+
val dynamicFallbackSetting =
79+
scalafixAllowDynamicFallback := true
8280
val settings = for {
8381
project <- collectProjects(extracted)
84-
enableSemanticdbPlugin <-
85-
project.maybeRecommendedSemanticdbScalacScalaV.toList
86-
.flatMap { recommendedSemanticdbScalacScalaV =>
87-
88-
import scalafix.internal.sbt.Implicits._
89-
val semanticdbScalacModule =
90-
coursierapi.Dependency
91-
.parse(
92-
project.semanticdbCompilerPlugin0.asCoursierCoordinates,
93-
coursierapi.ScalaVersion.of(project.scalaVersion0.toString)
94-
)
95-
.getModule
96-
val recommendedSemanticdbV =
97-
VersionNumber(BuildInfo.scalametaVersion)
98-
val compatibleSemanticdbVs = Try(
99-
coursierapi.Versions.create
100-
.withRepositories(project.scalafixResolvers0*)
101-
.withModule(semanticdbScalacModule)
102-
.versions()
103-
.getMergedListings
104-
.getAvailable
105-
.asScala
106-
.map(VersionNumber.apply)
107-
// don't use snapshots
108-
.filter(_.extras == Nil)
109-
// https://github.com/scalameta/scalameta/blob/main/COMPATIBILITY.md
110-
.filter(SemVer.isCompatible(_, recommendedSemanticdbV))
111-
.toList
112-
)
113-
114-
compatibleSemanticdbVs match {
115-
case Success(Nil) | Failure(_) =>
116-
Seq(
117-
scalaVersion := {
118-
val v = recommendedSemanticdbScalacScalaV.toString
119-
sLog.value.warn(
120-
s"Forcing scalaVersion to $v in project " +
121-
s"${project.ref.project} since no semanticdb-scalac " +
122-
s"version binary-compatible with $recommendedSemanticdbV " +
123-
s"and cross-published for scala " +
124-
s"${project.scalaVersion0.toString} was found - " +
125-
s"consider bumping scala"
126-
)
127-
v
128-
},
129-
semanticdbVersion := recommendedSemanticdbV.toString
130-
)
131-
case Success(available)
132-
if available.contains(recommendedSemanticdbV) =>
133-
Seq(
134-
semanticdbVersion := recommendedSemanticdbV.toString
135-
)
136-
case Success(earliestAvailable :: tail) =>
137-
val safeRecommendedSemanticdbV =
138-
if (recommendedSemanticdbV.toString == "4.13.1.1")
139-
VersionNumber("4.13.1")
140-
else recommendedSemanticdbV
141-
142-
val futureVersion =
143-
SemanticSelector.apply(s">${safeRecommendedSemanticdbV}")
144-
145-
if (earliestAvailable.matchesSemVer(futureVersion)) {
146-
Seq(
147-
semanticdbVersion := {
148-
val v = earliestAvailable.toString
149-
sLog.value.info(
150-
s"Setting semanticdbVersion to $v in project " +
151-
s"${project.ref.project} since the version " +
152-
s"${recommendedSemanticdbV} tracked by scalafix " +
153-
s"${BuildInfo.scalafixVersion} will not be " +
154-
s"published for scala " +
155-
s"${project.scalaVersion0.toString} - " +
156-
s"consider upgrading sbt-scalafix"
157-
)
158-
v
159-
}
160-
)
161-
} else {
162-
val latestAvailable =
163-
tail.lastOption.getOrElse(earliestAvailable)
164-
Seq(
165-
semanticdbVersion := {
166-
val v = latestAvailable.toString
167-
sLog.value.info(
168-
s"Setting semanticdbVersion to $v in project " +
169-
s"${project.ref.project} since the version " +
170-
s"${recommendedSemanticdbV} tracked by scalafix " +
171-
s"${BuildInfo.scalafixVersion} is no longer " +
172-
s"published for scala " +
173-
s"${project.scalaVersion0.toString} - " +
174-
s"consider bumping scala"
175-
)
176-
v
177-
}
178-
)
179-
}
180-
}
181-
} :+ (semanticdbEnabled := true)
182-
settings <-
183-
inScope(ThisScope.copy(project = Select(project.ref)))(
184-
scalacOptionsSettings ++ enableSemanticdbPlugin
185-
)
82+
semanticdbPluginSettings <- (semanticdbEnabled := true) +:
83+
project.semanticdbVersion.map { version =>
84+
semanticdbVersion := version
85+
}.toSeq
86+
settings <- inScope(ThisScope.copy(project = Select(project.ref)))(
87+
scalacOptionsSettings ++ semanticdbPluginSettings :+ dynamicFallbackSetting
88+
)
18689
} yield settings
18790
extracted.appendWithSession(settings, s)
18891
}

src/main/scala/scalafix/sbt/ScalafixPlugin.scala

+16-2
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,13 @@ object ScalafixPlugin extends AutoPlugin {
8989
"Defaults to a wrapper around `sbt.Logger`."
9090
)
9191

92-
val scalafixSemanticdb: ModuleID =
92+
val scalafixAllowDynamicFallback: SettingKey[Boolean] =
93+
settingKey[Boolean](
94+
"Control whether the latest version of scalafix may be downloaded instead of failing when " +
95+
"scalafix-interfaces is not explicitly provided. Off by default because non-deterministic."
96+
)
97+
98+
lazy val scalafixSemanticdb: ModuleID =
9399
scalafixSemanticdb(BuildInfo.scalametaVersion)
94100
def scalafixSemanticdb(scalametaVersion: String): ModuleID =
95101
("org.scalameta" % "semanticdb-scalac" % scalametaVersion)
@@ -263,7 +269,8 @@ object ScalafixPlugin extends AutoPlugin {
263269
"via `semanticdbVersion` does follow the version recommended " +
264270
"for Scalafix, but is not supported for the given Scala " +
265271
s"version ${scalaV}. Please consider upgrading to a more recent version " +
266-
"of sbt-scalafix and/or Scala, or uninstalling sbt-scalafix plugin."
272+
"of \"ch.epfl.scala\" % \"scalafix-interfaces\" and/or Scala, or " +
273+
"uninstalling sbt-scalafix."
267274
throw inc.copy(message = Some(msg))
268275
case _ =>
269276
}
@@ -287,6 +294,7 @@ object ScalafixPlugin extends AutoPlugin {
287294
scalafixCaching := true,
288295
scalafixResolvers := defaultScalafixResolvers,
289296
scalafixDependencies := Nil,
297+
scalafixAllowDynamicFallback := false,
290298
commands += ScalafixEnable.command,
291299
scalafixInterfaceCache := new BlockingCache,
292300
concurrentRestrictions += Tags.exclusiveGroup(Scalafix),
@@ -443,6 +451,12 @@ object ScalafixPlugin extends AutoPlugin {
443451
config: ConfigKey
444452
): Def.Initialize[Task[Unit]] = {
445453
val task = Def.taskDyn {
454+
if (!scalafixAllowDynamicFallback.value) {
455+
// force scalafix properties to be loaded, to trigger an actionnable error instead
456+
// of letting the built-in scalafix-interfaces fallback to the latest scalafix version
457+
BuildInfo.scalafixVersion
458+
}
459+
446460
implicit val conv: FileConverter = fileConverter.value
447461

448462
val errorLogger =
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
resolvers += Resolver.sonatypeRepo("public")
22
libraryDependencies += "com.googlecode.java-diff-utils" % "diffutils" % "1.3.0"
33
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % sys.props("plugin.version"))
4+
libraryDependencies += "ch.epfl.scala" % "scalafix-interfaces" %
5+
sys.props("scalafix-interfaces.version")
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
resolvers += Resolver.sonatypeRepo("public")
22
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % sys.props("plugin.version"))
3+
libraryDependencies += "ch.epfl.scala" % "scalafix-interfaces" %
4+
sys.props("scalafix-interfaces.version")

0 commit comments

Comments
 (0)