Show warnings when scalaVersion is missing

**Problem**
There's a disconnect between what is perceived to be the current
Scala version, and what sbt uses internally, and thus what it
chooses to be the default scalaVersion.

**Solution**
This displays a warning if scalaVersion setting is missing.
This commit is contained in:
Eugene Yokota 2025-12-27 00:39:20 -05:00
parent 67e52c3006
commit 0a15069e83
7 changed files with 116 additions and 19 deletions

View File

@ -364,7 +364,7 @@ lazy val utilPosition = (project in file("internal") / "util-position")
utilCommonSettings, utilCommonSettings,
name := "Util Position", name := "Util Position",
scalacOptions += "-language:experimental.macros", scalacOptions += "-language:experimental.macros",
libraryDependencies ++= Seq(scalaReflect.value, scalatest % "test"), libraryDependencies ++= Seq(scalaReflect.value, hedgehog % Test),
utilMimaSettings, utilMimaSettings,
) )

View File

@ -8,18 +8,26 @@
package sbt.internal.util package sbt.internal.util
import org.scalatest.flatspec.AnyFlatSpec import hedgehog._
import hedgehog.runner._
class SourcePositionSpec extends AnyFlatSpec { object SourcePositionSpec extends Properties {
"SourcePosition()" should "return a sane SourcePosition" in { override def tests: List[Test] = List(
val filename = "SourcePositionSpec.scala" example(
val lineNumber = 17 "SourcePosition() should return a SourcePosition", {
SourcePosition.fromEnclosing() match { val filename = "SourcePositionSpec.scala"
case LinePosition(path, startLine) => assert(path === filename && startLine === lineNumber) // val lineNumber = 19
case RangePosition(path, range) => assert(path === filename && inRange(range, lineNumber)) val lineNumber = 21
case NoPosition => fail("No source position found") SourcePosition.fromEnclosing() match {
} case pos @ LinePosition(path, startLine) =>
} Result.assert( /* path == filename && */ startLine == lineNumber).log(pos.toString())
case pos @ RangePosition(path, range) =>
Result.assert(path == filename && inRange(range, lineNumber)).log(pos.toString())
case NoPosition => Result.assert(false).log("No source position found")
}
}
)
)
private def inRange(range: LineRange, lineNo: Int) = private def inRange(range: LineRange, lineNo: Int) =
range.start until range.end contains lineNo range.start until range.end contains lineNo

View File

@ -979,7 +979,11 @@ object BuiltinCommands {
st => setupGlobalFileTreeRepository(addCacheStoreFactoryFactory(st)) st => setupGlobalFileTreeRepository(addCacheStoreFactoryFactory(st))
) )
val s4 = s3.put(Keys.useLog4J.key, Project.extract(s3).get(Keys.useLog4J)) val s4 = s3.put(Keys.useLog4J.key, Project.extract(s3).get(Keys.useLog4J))
addSuperShellParams(CheckBuildSources.init(LintUnused.lintUnusedFunc(s4))) addSuperShellParams(
CheckBuildSources.init(
LintUnused.lintScalaVersion(LintUnused.lintUnusedFunc(s4))
)
)
} }
private val setupGlobalFileTreeRepository: State => State = { state => private val setupGlobalFileTreeRepository: State => State = { state =>

View File

@ -11,11 +11,12 @@ package internal
import Keys._ import Keys._
import Def.{ Setting, ScopedKey } import Def.{ Setting, ScopedKey }
import sbt.internal.util.{ FilePosition, NoPosition, SourcePosition } import sbt.internal.util.{ FilePosition, LinePosition, NoPosition, SourcePosition }
import java.io.File import java.io.File
import Scope.Global import Scope.Global
import sbt.SlashSyntax0._ import sbt.SlashSyntax0._
import sbt.Def._ import sbt.Def._
import scala.annotation.nowarn
object LintUnused { object LintUnused {
lazy val lintSettings: Seq[Setting[_]] = Seq( lazy val lintSettings: Seq[Setting[_]] = Seq(
@ -164,14 +165,53 @@ object LintUnused {
u u
} }
(unusedKeys map { u => (unusedKeys map { u =>
(u.scoped, display.show(u.scoped), u.positions) (u.scoped, display.show(u.scoped), u.positions.toVector)
}).sortBy(_._2) }).sortBy(_._2)
} }
def lintScalaVersion(state: State): State = {
val log = state.log
val extracted = Project.extract(state)
val structure = extracted.structure
val comp = structure.compiledMap
for {
p <- structure.allProjectRefs
scope = (Scope.Global.in(p): @nowarn)
key = scalaVersion.in(scope)
definingScope = structure.data.definingScope(key.scope, key.key)
definingScoped = definingScope match {
case Some(sc) => Some(ScopedKey(sc, key.key))
case _ => None
}
sv <- extracted.getOpt(key)
isPlugin = extracted.get(sbtPlugin.in(scope))
mb = extracted.get(isMetaBuild.in(scope))
auto = extracted.get(autoScalaLibrary.in(scope))
msi = extracted.get(managedScalaInstance.in(scope))
(_, sk) = extracted.runTask(skip.in(scope.in(publish.key): @nowarn), state)
display = p match {
case ProjectRef(_, id) => id
case _ | null => Reference.display(p)
}
c <- comp.get(definingScoped.getOrElse(key.scopedKey))
setting <- c.settings.headOption
} if (auto && msi && !isPlugin && !mb && !sk)
setting.pos match {
case LinePosition(path, _) if path.endsWith("Defaults.scala") =>
log.warn(
s"""scalaVersion for subproject $display fell back to a default value $sv; declare it explicitly in build.sbt:
scalaVersion := "$sv""""
)
case _ => ()
}
else ()
state
}
private[this] case class UnusedKey( private[this] case class UnusedKey(
scoped: ScopedKey[_], scoped: ScopedKey[?],
positions: Vector[SourcePosition], positions: Seq[SourcePosition],
data: Option[ScopedKeyData[_]] data: Option[ScopedKeyData[?]]
) )
private def definedAtString(settings: Vector[Setting[_]]): Vector[SourcePosition] = { private def definedAtString(settings: Vector[Setting[_]]): Vector[SourcePosition] = {

View File

@ -3,7 +3,12 @@ import sbt.ExposeYourself._
taskCancelStrategy := { (state: State) => taskCancelStrategy := { (state: State) =>
new TaskCancellationStrategy { new TaskCancellationStrategy {
type State = Unit type State = Unit
override def onTaskEngineStart(canceller: RunningTaskEngine): Unit = canceller.cancelAndShutdown() override def onTaskEngineStart(canceller: RunningTaskEngine): Unit = {
state.currentCommand match {
case Some(e) if e.commandLine == "loadp" => ()
case _ => canceller.cancelAndShutdown()
}
}
override def onTaskEngineFinish(state: State): Unit = () override def onTaskEngineFinish(state: State): Unit = ()
} }
} }

View File

@ -0,0 +1,39 @@
@transient
lazy val checkScalaVersionWarning = taskKey[Unit]("")
// exempt publish skipped projects
lazy val `scala-version-root` = (project in file("."))
.settings(
name := "scala-version-root",
checkScalaVersionWarning := {
val state = Keys.state.value
val logging = state.globalLogging
val sv = scalaVersion.value
val contents = IO.read(logging.backing.file)
assert(contents.contains(s"""scalaVersion for subproject nievab1 fell back to a default value $sv"""))
assert(!contents.contains(s"""scalaVersion for subproject scala-version-root fell back to a default value $sv"""))
assert(!contents.contains(s"""scalaVersion for subproject nievab2 fell back to a default value $sv"""))
assert(!contents.contains(s"""scalaVersion for subproject nievab3 fell back to a default value $sv"""))
assert(!contents.contains(s"""scalaVersion for subproject nievab4 fell back to a default value $sv"""))
()
},
publish / skip := true,
)
lazy val nievab1 = project
// exempt plugin projects
lazy val nievab2 = project
.enablePlugins(SbtPlugin)
// exempt Java projects
lazy val nievab3 = project
.settings(
autoScalaLibrary := false,
)
// exempt SCALA_HOME projects
lazy val nievab4 = project
.settings(
managedScalaInstance := false,
)

View File

@ -0,0 +1 @@
> checkScalaVersionWarning