diff --git a/build.sbt b/build.sbt index 193de309e..fde047e09 100644 --- a/build.sbt +++ b/build.sbt @@ -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 diff --git a/internal/util-position/src/main/scala-3/sbt/internal/util/SourcePositionMacro.scala b/internal/util-position/src/main/scala-3/sbt/internal/util/SourcePositionMacro.scala index 2eca9fca8..130c7d753 100644 --- a/internal/util-position/src/main/scala-3/sbt/internal/util/SourcePositionMacro.scala +++ b/internal/util-position/src/main/scala-3/sbt/internal/util/SourcePositionMacro.scala @@ -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 } } } diff --git a/internal/util-position/src/main/scala/sbt/internal/util/Positions.scala b/internal/util-position/src/main/scala/sbt/internal/util/Positions.scala index a1d6f7123..ad4e611a6 100644 --- a/internal/util-position/src/main/scala/sbt/internal/util/Positions.scala +++ b/internal/util-position/src/main/scala/sbt/internal/util/Positions.scala @@ -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 diff --git a/internal/util-position/src/test/scala/sbt/internal/util/SourcePositionSpec.scala b/internal/util-position/src/test/scala/sbt/internal/util/SourcePositionSpec.scala index f41cef10b..8d4ecabea 100644 --- a/internal/util-position/src/test/scala/sbt/internal/util/SourcePositionSpec.scala +++ b/internal/util-position/src/test/scala/sbt/internal/util/SourcePositionSpec.scala @@ -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 diff --git a/main-settings/src/main/scala/sbt/std/TaskMacro.scala b/main-settings/src/main/scala/sbt/std/TaskMacro.scala index 893dcea99..ee10da330 100644 --- a/main-settings/src/main/scala/sbt/std/TaskMacro.scala +++ b/main-settings/src/main/scala/sbt/std/TaskMacro.scala @@ -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 diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 5faa19a60..807ffe838 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -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 => diff --git a/main/src/main/scala/sbt/internal/LintUnused.scala b/main/src/main/scala/sbt/internal/LintUnused.scala index 215830fd6..844bbf04e 100644 --- a/main/src/main/scala/sbt/internal/LintUnused.scala +++ b/main/src/main/scala/sbt/internal/LintUnused.scala @@ -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], diff --git a/sbt-app/src/sbt-test/project/scala-version/build.sbt b/sbt-app/src/sbt-test/project/scala-version/build.sbt new file mode 100644 index 000000000..8e0298392 --- /dev/null +++ b/sbt-app/src/sbt-test/project/scala-version/build.sbt @@ -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, + ) diff --git a/sbt-app/src/sbt-test/project/scala-version/test b/sbt-app/src/sbt-test/project/scala-version/test new file mode 100644 index 000000000..bc33e68f0 --- /dev/null +++ b/sbt-app/src/sbt-test/project/scala-version/test @@ -0,0 +1 @@ +> checkScalaVersionWarning