mirror of https://github.com/sbt/sbt.git
Support annotated definitions in build.sbt
Previously, sbt would fail to load build.sbt files when they included annotated definitions because the parser would not correctly recognize those definitions as such. In sbt 1.x, this used to be fine, because there was little use for annotations in build.sbt. Starting with sbt 2, whether caching should be enabled for a task key can be controlled via annotations on the task key definition. Because these can appear in build.sbt, support for annotations in build.sbt becomes more important. This patch enhances parsing of build.sbt by keeping the parsed trees around so that the AST can be used to determine whether a given line represents a setting or a definition, rather than relying on string matching.
This commit is contained in:
parent
c565b5a82b
commit
da0fa5388e
|
|
@ -22,6 +22,7 @@ import scala.jdk.CollectionConverters.*
|
|||
import xsbti.PathBasedFile
|
||||
import xsbti.VirtualFile
|
||||
import xsbti.VirtualFileRef
|
||||
import dotty.tools.dotc.ast.untpd.{ Annotated, ValOrDefDef, Tree }
|
||||
|
||||
/**
|
||||
* This file is responsible for compiling the .sbt files used to configure sbt builds.
|
||||
|
|
@ -97,12 +98,13 @@ private[sbt] object EvaluateConfigurations {
|
|||
builtinImports: Seq[String],
|
||||
offset: Int
|
||||
): ParsedFile = {
|
||||
def loseTree(l: (String, Tree, LineRange)): (String, LineRange) = (l._1, l._3)
|
||||
val (importStatements, settingsAndDefinitions) = splitExpressions(file, lines)
|
||||
val allImports = builtinImports.map(s => (s, -1)) ++ addOffset(offset, importStatements)
|
||||
val (definitions, settings) = splitSettingsDefinitions(
|
||||
addOffsetToRange(offset, settingsAndDefinitions)
|
||||
)
|
||||
new ParsedFile(allImports, definitions, settings)
|
||||
new ParsedFile(allImports, definitions.map(loseTree), settings.map(loseTree))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -194,11 +196,14 @@ private[sbt] object EvaluateConfigurations {
|
|||
private def resolveBase(f: File, p: Project) =
|
||||
p.copy(base = IO.resolve(f, p.base))
|
||||
|
||||
def addOffset(offset: Int, lines: Seq[(String, Int)]): Seq[(String, Int)] =
|
||||
private def addOffset(offset: Int, lines: Seq[(String, Int)]): Seq[(String, Int)] =
|
||||
lines.map { (s, i) => (s, i + offset) }
|
||||
|
||||
def addOffsetToRange(offset: Int, ranges: Seq[(String, LineRange)]): Seq[(String, LineRange)] =
|
||||
ranges.map { (s, r) => (s, r.shift(offset)) }
|
||||
private def addOffsetToRange(
|
||||
offset: Int,
|
||||
ranges: Seq[(String, Tree, LineRange)]
|
||||
): Seq[(String, Tree, LineRange)] =
|
||||
ranges.map { (s, t, r) => (s, t, r.shift(offset)) }
|
||||
|
||||
/**
|
||||
* The name of the class we cast DSL "setting" (vs. definition) lines to.
|
||||
|
|
@ -286,20 +291,28 @@ private[sbt] object EvaluateConfigurations {
|
|||
private[sbt] def splitExpressions(
|
||||
file: VirtualFileRef,
|
||||
lines: Seq[String]
|
||||
): (Seq[(String, Int)], Seq[(String, LineRange)]) =
|
||||
): (Seq[(String, Int)], Seq[(String, Tree, LineRange)]) =
|
||||
val split = SbtParser(file, lines)
|
||||
// TODO - Look at pulling the parsed expression trees from the SbtParser and stitch them back into a different
|
||||
// scala compiler rather than re-parsing.
|
||||
(split.imports, split.settings)
|
||||
(
|
||||
split.imports,
|
||||
split.settings.zip(split.settingsTrees).map { case ((s, r), (_, t)) =>
|
||||
(s, t, r)
|
||||
}
|
||||
)
|
||||
|
||||
private def splitSettingsDefinitions(
|
||||
lines: Seq[(String, LineRange)]
|
||||
): (Seq[(String, LineRange)], Seq[(String, LineRange)]) =
|
||||
lines partition { case (line, _) => isDefinition(line) }
|
||||
lines: Seq[(String, Tree, LineRange)]
|
||||
): (Seq[(String, Tree, LineRange)], Seq[(String, Tree, LineRange)]) =
|
||||
lines partition { case (_, tree, _) => isDefinition(tree) }
|
||||
|
||||
private def isDefinition(line: String): Boolean = {
|
||||
val trimmed = line.trim
|
||||
DefinitionKeywords.exists(trimmed.startsWith(_))
|
||||
private def isDefinition(tree: Tree): Boolean = {
|
||||
tree match {
|
||||
case Annotated(arg, annot) => isDefinition(arg)
|
||||
case _: ValOrDefDef => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
private def extractedValTypes: Seq[String] =
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ package parser
|
|||
|
||||
import sbt.internal.util.LineRange
|
||||
import xsbti.VirtualFileRef
|
||||
import dotty.tools.dotc.ast.untpd.Tree
|
||||
|
||||
object SplitExpressions:
|
||||
type SplitExpression =
|
||||
(VirtualFileRef, Seq[String]) => (Seq[(String, Int)], Seq[(String, LineRange)])
|
||||
(VirtualFileRef, Seq[String]) => (Seq[(String, Int)], Seq[(String, Tree, LineRange)])
|
||||
end SplitExpressions
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ package parser
|
|||
|
||||
import sbt.internal.util.LineRange
|
||||
import xsbti.VirtualFileRef
|
||||
import dotty.tools.dotc.ast.untpd.Tree
|
||||
|
||||
trait SplitExpression {
|
||||
extension (splitter: SplitExpressions.SplitExpression)
|
||||
def apply(s: String): (Seq[(String, Int)], Seq[(String, LineRange)]) =
|
||||
def apply(s: String): (Seq[(String, Int)], Seq[(String, Tree, LineRange)]) =
|
||||
splitter(VirtualFileRef.of("noFile"), s.split('\n').toSeq)
|
||||
}
|
||||
|
||||
|
|
@ -53,6 +54,16 @@ trait SplitExpressionsBehavior extends SplitExpression { this: verify.BasicTestS
|
|||
assert(imports.size == 2)
|
||||
assert(settingsAndDefs.size == 1)
|
||||
}
|
||||
|
||||
test("parse a config containing an annotated definition") {
|
||||
val (imports, settingsAndDefs) = splitter(
|
||||
"""|import foo.Bar
|
||||
|@foo
|
||||
|lazy val root = (project in file(".")).enablePlugins(PlayScala)""".stripMargin
|
||||
)
|
||||
assert(imports.size == 1)
|
||||
assert(settingsAndDefs.size == 1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import scala.annotation.{tailrec, nowarn}
|
||||
import sbt.util.cacheLevel
|
||||
|
||||
@tailrec
|
||||
def even(x: Int): Boolean = Math.abs(x) match
|
||||
case 0 => true
|
||||
case 1 => false
|
||||
case n => even(n - 2)
|
||||
|
||||
@transient val foo = 4
|
||||
|
||||
@cacheLevel(include = Array.empty)
|
||||
lazy val myTask = taskKey[Boolean]("...")
|
||||
|
||||
@nowarn
|
||||
lazy val myProject = project.settings(
|
||||
myTask := {
|
||||
assert(!file("ran").exists)
|
||||
println("running")
|
||||
IO.touch(file("ran"))
|
||||
even(2)
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
$ absent ran
|
||||
> myTask
|
||||
$ exists ran
|
||||
|
||||
# verify the task is not cached (i.e. annotation is not lost)
|
||||
-> myTask
|
||||
Loading…
Reference in New Issue