[2.x] feat: Add Best Effort compilation support for Scala 3.5+

This commit is contained in:
statxc 2026-04-09 23:33:32 +02:00
parent 7218b2a1ac
commit 69e958bbc6
9 changed files with 155 additions and 0 deletions

View File

@ -694,6 +694,7 @@ object Defaults extends BuildCommon with DefExtra {
case vf: VirtualFile => vf
},
semanticdbTargetRoot := target.value / (prefix(configuration.value.name) + "meta"),
bestEffortTargetRoot := target.value / (prefix(configuration.value.name) + "betasty"),
compileAnalysisTargetRoot := target.value / (prefix(configuration.value.name) + "zinc"),
earlyCompileAnalysisTargetRoot := target.value / (prefix(
configuration.value.name

View File

@ -257,6 +257,9 @@ object Keys {
val semanticdbIncludeInJar = settingKey[Boolean]("Include *.semanticdb files in published artifacts").withRank(CSetting)
val semanticdbTargetRoot = settingKey[File]("The output directory to produce META-INF/semanticdb/**/*.semanticdb files").withRank(CSetting)
val semanticdbOptions = settingKey[Seq[String]]("The Scalac options introduced for SemanticDB").withRank(CSetting)
val bestEffortEnabled = settingKey[Boolean]("Enables Scala 3 Best Effort compilation to produce .betasty files").withRank(CSetting)
val bestEffortTargetRoot = settingKey[File]("The output directory for Best Effort TASTy (.betasty) files").withRank(CSetting)
val bestEffortOptions = settingKey[Seq[String]]("The Scalac options introduced for Best Effort compilation").withRank(CSetting)
val clean = taskKey[Unit]("Deletes files produced by the build, such as generated sources, compiled classes, and task caches.").withRank(APlusTask)
val console = taskKey[Unit]("Starts the Scala interpreter with the project classes on the classpath.").withRank(APlusTask)

View File

@ -99,6 +99,7 @@ object SysProp:
def allowRootDir: Boolean = getOrFalse("sbt.rootdir")
def legacyTestReport: Boolean = getOrFalse("sbt.testing.legacyreport")
def semanticdb: Boolean = getOrFalse("sbt.semanticdb")
def bestEffort: Boolean = getOrFalse("sbt.bestEffort")
def forceServerStart: Boolean = getOrFalse("sbt.server.forcestart")
def serverAutoStart: Boolean = getOrTrue("sbt.server.autostart")
def remoteCache: Option[URI] = sys.props

View File

@ -0,0 +1,107 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package plugins
import java.io.File
import Keys.*
import sbt.internal.SysProp
import sbt.librarymanagement.syntax.*
import sbt.librarymanagement.Configuration
import ProjectExtra.inConfig
import sbt.internal.inc.ScalaInstance
import sbt.ScopeFilter.Make.*
import sbt.util.CacheImplicits.given
/**
* An AutoPlugin that wires Scala 3's Best Effort compilation into sbt.
*
* When `bestEffortEnabled` is true and the project uses Scala 3.5+,
* the plugin appends `-Ybest-effort` and `-Ybest-effort-dir` to scalacOptions
* so that the compiler produces .betasty files even when compilation fails.
* These files are consumed by IDEs such as Metals for improved code intelligence.
*/
object BestEffortPlugin extends AutoPlugin:
override def requires = JvmPlugin
override def trigger = allRequirements
override lazy val globalSettings: Seq[Def.Setting[?]] = Seq(
bestEffortEnabled := SysProp.bestEffort,
bestEffortOptions := List(),
)
override lazy val projectSettings: Seq[Def.Setting[?]] = Seq(
bestEffortOptions ++= {
val enabled = bestEffortEnabled.value
val sv = scalaVersion.value
if enabled && isScala35Plus(sv) then Seq("-Ybest-effort")
else Nil
},
) ++
inConfig(Compile)(configurationSettings) ++
inConfig(Test)(configurationSettings)
lazy val configurationSettings: Seq[Def.Setting[?]] = List(
compileIncremental := Def.taskIf {
if !bestEffortEnabled.value then compileIncremental.value
else compileIncAndCacheBestEffortTargetRootTask.value
}.value,
bestEffortOptions --= Def.settingDyn {
val scalaV = scalaVersion.value
val config = configuration.value
Def.setting {
bestEffortTargetRoot.?.all(ancestorConfigs(config)).value.flatten
.flatMap(targetRootOptions(scalaV, _))
}
}.value,
bestEffortOptions ++=
targetRootOptions(scalaVersion.value, bestEffortTargetRoot.value),
scalacOptions := (Def.taskDyn {
val orig = scalacOptions.value
val config = configuration.value
if bestEffortEnabled.value then
Def.task {
(orig diff bestEffortOptions.?.all(ancestorConfigs(config)).value.flatten.flatten) ++
bestEffortOptions.value
}
else
Def.task {
orig
}
}).value,
)
private[sbt] def isScala35Plus(scalaVersion: String): Boolean =
ScalaInstance.isDotty(scalaVersion) && {
val versionPart = scalaVersion.stripPrefix("3.")
val minor = versionPart.takeWhile(_.isDigit)
minor.nonEmpty && minor.toInt >= 5
}
def targetRootOptions(scalaVersion: String, targetRoot: File): Seq[String] =
if isScala35Plus(scalaVersion) then
Seq("-Ybest-effort-dir", targetRoot.toString)
else Nil
private val compileIncAndCacheBestEffortTargetRootTask = Def.cachedTask {
val prev = compileIncremental.value
val converter = fileConverter.value
val targetRoot = bestEffortTargetRoot.value
val vfTargetRoot = converter.toVirtualFile(targetRoot.toPath)
Def.declareOutputDirectory(vfTargetRoot)
prev
}
private def ancestorConfigs(config: Configuration) =
def ancestors(configs: Vector[Configuration]): Vector[Configuration] =
configs ++ configs.flatMap(conf => ancestors(conf.extendsConfigs))
ScopeFilter(configurations = inConfigurations(ancestors(config.extendsConfigs)*))
end BestEffortPlugin

View File

@ -0,0 +1,2 @@
scalaVersion := "3.6.4"
bestEffortEnabled := true

View File

@ -0,0 +1,8 @@
object A {
val x: Int = 1
}
//object B
val error: Int = "not an int"
object C {
val z: Boolean = true
}

View File

@ -0,0 +1,8 @@
//object A
object B {
val y: String = "hello"
}
//error
object C {
val z: Boolean = true
}

View File

@ -0,0 +1,6 @@
object A {
val x: Int = 1
}
object B {
val y: String = "hello"
}

View File

@ -0,0 +1,19 @@
# Step 1: Verify best effort flags appear in scalacOptions for Scala 3.5+
> show Compile / scalacOptions
> compile
# Step 2: Introduce a type error — compile should fail but produce .betasty
$ copy-file changes/ErrorFile.scala src/main/scala/A.scala
-> compile
# Step 3: Fix the error — compile should succeed again
$ copy-file changes/FixedFile.scala src/main/scala/A.scala
> compile
# Step 4: Verify best effort flags are absent for Scala 2
> set scalaVersion := "2.13.16"
> show Compile / scalacOptions
# Step 5: Switch back to Scala 3 and verify flags are restored
> set scalaVersion := "3.6.4"
> show Compile / scalacOptions