[2.x] 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 55aa1b52ff
commit 6cb786d010
9 changed files with 140 additions and 32 deletions

View File

@ -290,8 +290,11 @@ lazy val utilPosition = (project in file("internal") / "util-position")
utilCommonSettings,
name := "Util Position",
scalacOptions += "-language:experimental.macros",
libraryDependencies ++= Seq(scalatest % "test"),
libraryDependencies ++= Seq(hedgehog % Test),
mimaSettings,
mimaBinaryIssueFilters ++= Seq(
exclude[ReversedMissingMethodProblem]("sbt.internal.util.FilePosition.sourceCode"),
),
)
lazy val utilCore = project

View File

@ -23,13 +23,14 @@ abstract class SourcePositionImpl {
object SourcePositionImpl {
def fromEnclosingImpl(using Quotes): Expr[SourcePosition] = {
val x = quotes.reflect.Position.ofMacroExpansion
'{
LinePosition(
path = ${ Expr(x.sourceFile.name) },
startLine = ${ Expr(x.startLine + 1) }
)
}
val pos = quotes.reflect.Position.ofMacroExpansion
if pos.startLine >= 0 then
'{
LinePosition(
path = ${ Expr(pos.sourceFile.name) },
startLine = ${ Expr(pos.startLine) }
).withSourceCode(${ Expr(pos.sourceCode) })
}
else '{ NoPosition }
}
}

View File

@ -13,18 +13,40 @@ sealed trait SourcePosition
sealed trait FilePosition extends SourcePosition {
def path: String
def startLine: Int
def sourceCode: Option[String]
}
case object NoPosition extends SourcePosition
final case class LinePosition(path: String, startLine: Int) extends FilePosition
final case class LinePosition(path: String, startLine: Int) extends FilePosition {
private var _sourceCode: Option[String] = None
def sourceCode: Option[String] = _sourceCode
def withSourceCode(code: String): LinePosition =
val o = copy()
o._sourceCode = Some(code)
o
def withSourceCode(c: Option[String]): LinePosition =
c match
case Some(code) => this.withSourceCode(code)
case None => this
}
final case class LineRange(start: Int, end: Int) {
def shift(n: Int) = new LineRange(start + n, end + n)
}
final case class RangePosition(path: String, range: LineRange) extends FilePosition {
private var _sourceCode: Option[String] = None
def startLine = range.start
def sourceCode: Option[String] = _sourceCode
def withSourceCode(code: String): RangePosition =
val o = copy()
o._sourceCode = Some(code)
o
def withSourceCode(c: Option[String]): RangePosition =
c match
case Some(code) => this.withSourceCode(code)
case None => this
}
object SourcePosition extends SourcePositionImpl

View File

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

View File

@ -12,7 +12,7 @@ package std
import Def.{ Initialize, Setting }
import sbt.internal.util.Types.Id
import sbt.internal.util.appmacro.{ Cont, ContextUtil, ContextUtil0 }
import sbt.internal.util.{ LinePosition, NoPosition, SourcePosition }
import sbt.internal.util.{ SourcePosition, SourcePositionImpl }
import language.experimental.macros
import scala.quoted.*
@ -184,13 +184,7 @@ object TaskMacro:
}
private[sbt] def sourcePosition(using qctx: Quotes): Expr[SourcePosition] =
import qctx.reflect.*
val pos = Position.ofMacroExpansion
if pos.startLine >= 0 && pos.sourceCode != None then
val name = Expr(pos.sourceCode.get)
val line = Expr(pos.startLine)
'{ LinePosition($name, $line) }
else '{ NoPosition }
SourcePositionImpl.fromEnclosingImpl
end TaskMacro

View File

@ -973,7 +973,11 @@ object BuiltinCommands {
st => setupGlobalFileTreeRepository(Clean.addCacheStoreFactoryFactory(st))
)
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 =>

View File

@ -10,7 +10,7 @@ package sbt
package internal
import Keys.*
import sbt.internal.util.{ FilePosition, NoPosition, SourcePosition }
import sbt.internal.util.{ FilePosition, LinePosition, NoPosition, SourcePosition }
import java.io.File
import ProjectExtra.{ extract, scopedKeyData }
import Scope.Global
@ -165,6 +165,40 @@ object LintUnused {
unusedKeys.map(u => (u.scoped, display.show(u.scoped), u.positions)).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.rescope(p)
key = scalaVersion.rescope(scope)
data = Project.scopedKeyData(structure, key.scopedKey)
sv = extracted.get(key)
isPlugin = extracted.get(sbtPlugin.rescope(scope))
mb = extracted.get(isMetaBuild.rescope(scope))
auto = extracted.get(autoScalaLibrary.rescope(scope))
msi = extracted.get(managedScalaInstance.rescope(scope))
(_, sk) = extracted.runTask(skip.rescope(scope.rescope(publish.key)), state)
display = p match
case ProjectRef(_, id) => id
case _ | null => Reference.display(p)
c <- comp.get(data.map(_.definingKey).getOrElse(key.scopedKey))
setting <- c.settings.headOption
do
if auto && msi && !isPlugin && !mb && !sk then
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 case class UnusedKey(
scoped: ScopedKey[?],
positions: Seq[SourcePosition],

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