Skip to content

fix: prevent Scala stdlib filtering in transitive plugin scenarios#441

Merged
kitbellew merged 2 commits intoscalameta:mainfrom
Rossbro2:main
May 6, 2026
Merged

fix: prevent Scala stdlib filtering in transitive plugin scenarios#441
kitbellew merged 2 commits intoscalameta:mainfrom
Rossbro2:main

Conversation

@Rossbro2
Copy link
Copy Markdown
Contributor

@Rossbro2 Rossbro2 commented May 5, 2026

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.OptionClassNotFoundException
  4. Caught as ReflectiveOperationExceptionCorruptedClassPath

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.

### 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.
Copy link
Copy Markdown
Contributor

@tgodzik tgodzik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside from formatting this looks like a simple enough fix. LGTM from me

Could you run formatting on the PR? Thanks!

Comment thread plugin/src/main/scala/org/scalafmt/sbt/ScalafmtSbtDependencyDownloader.scala Outdated
@Rossbro2
Copy link
Copy Markdown
Contributor Author

Rossbro2 commented May 6, 2026

Aside from formatting this looks like a simple enough fix. LGTM from me

Could you run formatting on the PR? Thanks!

@tgodzik done!

@kitbellew kitbellew merged commit 6ab70ec into scalameta:main May 6, 2026
4 checks passed
@kitbellew
Copy link
Copy Markdown
Contributor

@Rossbro2 I am assuming you built this locally and then created a transitive plugin, to verify it works?

@Rossbro2
Copy link
Copy Markdown
Contributor Author

Rossbro2 commented May 6, 2026

@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:

  1. Built sbt-scalafmt from main (2.6.0-next-SNAPSHOT) via sbt publishLocal
  2. Published a local snapshot of our org-wide sbt plugin (sbt-core) that bundles addSbtPlugin("org.scalameta" %% "sbt-scalafmt" % "2.6.0-next-SNAPSHOT") as a dependency
  3. Tested on a downstream project that consumes sbt-core transitively — with no direct addSbtPlugin for sbt-scalafmt in the project's plugins.sbt
  4. Ran sbt scalafmtCheckAll — it correctly resolved scalafmt-core, loaded the classloader, and ran formatting checks across all subprojects

Previously with sbt-scalafmt 2.6.0 in the same transitive setup, this would fail with:

 [error] (Compile / scalafmtAll) corrupted class path

With the filterImplicit = false fix, the Scala stdlib jars are properly included in the isolated classloader and formatting works as expected.


@kitbellew / @tgodzik What's the process for getting this released?

@kitbellew
Copy link
Copy Markdown
Contributor

@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:

Thank you for confirming.

@kitbellew / @tgodzik What's the process for getting this released?

A bit of patience. We have a couple of things to release, this will be last.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sbt-scalafmt 2.6.0 "corrupted class path" when consumed transitively through another sbt plugin

3 participants