fix: prevent Scala stdlib filtering in transitive plugin scenarios#441
fix: prevent Scala stdlib filtering in transitive plugin scenarios#441kitbellew merged 2 commits intoscalameta:mainfrom
Conversation
### Problem
When sbt-scalafmt 2.6.0 is consumed **transitively** through another sbt plugin (e.g., an org-wide auto-plugin that bundles `addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.0")`), the `scalafmtAll` task fails with:
```
[error] (Compile / scalafmtAll) corrupted class path
```
This works correctly when sbt-scalafmt is added directly in the downstream project's `plugins.sbt`.
### Root Cause
`ScalafmtSbtDependencyDownloader.download()` creates a temp module with `ScalaModuleInfo(filterImplicit = true)`. This tells sbt's dependency resolution to filter out "implicit" Scala library dependencies from the resolved artifacts.
The resolved jars are then loaded into `new URLClassLoader(urls, null)` — a **fully isolated classloader with no parent**. `ScalafmtReflect` immediately tries to load `scala.collection.immutable.Set` and `scala.Option` from this classloader.
When sbt-scalafmt is consumed transitively, sbt's resolution context differs: the Scala 2.13 standard library jars (needed by `scalafmt-core_2.13`) may be classified as "implicit" and filtered from `UpdateReport.allFiles`. Since the classloader has no parent (no fallback to the plugin classloader), the reflection fails with `ReflectiveOperationException`, wrapped as `CorruptedClassPath`.
Key chain:
1. `filterImplicit = true` → sbt filters `scala-library` from resolved files
2. `URLClassLoader(urls, null)` → no parent classloader fallback
3. `ScalafmtReflect` loads `scala.Option` → `ClassNotFoundException`
4. Caught as `ReflectiveOperationException` → `CorruptedClassPath`
### Fix
Set `filterImplicit = false` so that Scala standard library jars are always included in the resolution. This is safe because:
- The isolated classloader (`parent = null`) **requires** all jars to be present — there is no parent to fall back to
- The temp module needs Scala 2.13.x libs regardless of the build's Scala version (2.12 for sbt)
- The artifacts are already fully qualified (e.g., `scalafmt-core_2.13`), so cross-versioning behavior from `ScalaModuleInfo` is not relied upon
- `overrideScalaVersion = false` already disables `autoScalaLibrary`, so this change only affects filtering of transitively resolved jars
Fixes #440.
tgodzik
left a comment
There was a problem hiding this comment.
Aside from formatting this looks like a simple enough fix. LGTM from me
Could you run formatting on the PR? Thanks!
@tgodzik done! |
|
@Rossbro2 I am assuming you built this locally and then created a transitive plugin, to verify it works? |
@kitbellew Yes — verified locally with a transitive plugin setup:
Previously with sbt-scalafmt 2.6.0 in the same transitive setup, this would fail with: [error] (Compile / scalafmtAll) corrupted class pathWith the ❓ @kitbellew / @tgodzik What's the process for getting this released? |
Thank you for confirming.
A bit of patience. We have a couple of things to release, this will be last. |
Problem
When sbt-scalafmt 2.6.0 is consumed transitively through another sbt plugin (e.g., an org-wide auto-plugin that bundles
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.0")), thescalafmtAlltask fails with:This works correctly when sbt-scalafmt is added directly in the downstream project's
plugins.sbt.Root Cause
ScalafmtSbtDependencyDownloader.download()creates a temp module withScalaModuleInfo(filterImplicit = true). This tells sbt's dependency resolution to filter out "implicit" Scala library dependencies from the resolved artifacts.The resolved jars are then loaded into
new URLClassLoader(urls, null)— a fully isolated classloader with no parent.ScalafmtReflectimmediately tries to loadscala.collection.immutable.Setandscala.Optionfrom this classloader.When sbt-scalafmt is consumed transitively, sbt's resolution context differs: the Scala 2.13 standard library jars (needed by
scalafmt-core_2.13) may be classified as "implicit" and filtered fromUpdateReport.allFiles. Since the classloader has no parent (no fallback to the plugin classloader), the reflection fails withReflectiveOperationException, wrapped asCorruptedClassPath.Key chain:
filterImplicit = true→ sbt filtersscala-libraryfrom resolved filesURLClassLoader(urls, null)→ no parent classloader fallbackScalafmtReflectloadsscala.Option→ClassNotFoundExceptionReflectiveOperationException→CorruptedClassPathFix
Set
filterImplicit = falseso that Scala standard library jars are always included in the resolution. This is safe because:parent = null) requires all jars to be present — there is no parent to fall back toscalafmt-core_2.13), so cross-versioning behavior fromScalaModuleInfois not relied uponoverrideScalaVersion = falsealready disablesautoScalaLibrary, so this change only affects filtering of transitively resolved jarsFixes #440.