Skip to content

Commit

Permalink
feat: queriable slash syntax (sbt query)
Browse files Browse the repository at this point in the history
**Problem**
We want a more flexible way of aggregating subprojects.

**Solution**
This implements a subproject filtering as a replacement of
the subproject axis in the act command.
  • Loading branch information
eed3si9n committed Sep 26, 2024
1 parent b5d3a6d commit e61ae80
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 78 deletions.
163 changes: 109 additions & 54 deletions main/src/main/scala/sbt/internal/Act.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ import sbt.internal.util.Types.idFun
import sbt.ProjectExtra.{ failure => _, * }
import java.net.URI
import sbt.internal.CommandStrings.{ MultiTaskCommand, ShowCommand, PrintCommand }
import sbt.internal.util.{ AttributeEntry, AttributeKey, AttributeMap, IMap, Settings, Util }
import sbt.internal.util.{
AttributeEntry,
AttributeKey,
AttributeMap,
IMap,
MessageOnlyException,
Settings,
Util,
}
import sbt.util.Show
import scala.collection.mutable

Expand Down Expand Up @@ -49,6 +57,10 @@ object Act {
private[sbt] val colonSeq: Seq[String] = Seq(":")
private[sbt] val colonColonSeq: Seq[String] = Seq("::")

type KeysParser = Parser[Seq[ScopedKey[Any]]]
type KeysParserSep = Parser[Seq[(ScopedKey[Any], Seq[String])]]
type KeysParserFilter = Parser[Seq[(ScopedKey[Any], Option[ProjectQuery])]]

// this does not take aggregation into account
def scopedKey(
index: KeyIndex,
Expand All @@ -57,7 +69,7 @@ object Act {
keyMap: Map[String, AttributeKey[_]],
data: Settings[Scope]
): Parser[ScopedKey[Any]] =
scopedKeySelected(index, current, defaultConfigs, keyMap, data)
scopedKeySelected(index, current, defaultConfigs, keyMap, data, askProject = true)
.map(_.key.asInstanceOf[ScopedKey[Any]])

// the index should be an aggregated index for proper tab completion
Expand All @@ -72,7 +84,8 @@ object Act {
current,
defaultConfigs,
structure.index.keyMap,
structure.data
structure.data,
askProject = true,
)
)
yield Aggregation.aggregate(
Expand All @@ -81,6 +94,28 @@ object Act {
structure.extra
)

def scopedKeyAggregatedFilter(
current: ProjectRef,
defaultConfigs: Option[ResolvedReference] => Seq[String],
structure: BuildStructure
): KeysParserFilter =
for
optQuery <- queryOption.?
selected <- scopedKeySelected(
structure.index.aggregateKeyIndex,
current,
defaultConfigs,
structure.index.keyMap,
structure.data,
askProject = optQuery.isEmpty,
)
yield Aggregation
.aggregate(selected.key, selected.mask, structure.extra)
.map(k => k.asInstanceOf[ScopedKey[Any]] -> optQuery)

private def queryOption: Parser[ProjectQuery] =
ProjectQuery.parser <~ spacedSlash

def scopedKeyAggregatedSep(
current: ProjectRef,
defaultConfigs: Option[ResolvedReference] => Seq[String],
Expand All @@ -91,7 +126,8 @@ object Act {
current,
defaultConfigs,
structure.index.keyMap,
structure.data
structure.data,
askProject = true,
)
yield Aggregation
.aggregate(selected.key, selected.mask, structure.extra)
Expand All @@ -102,24 +138,29 @@ object Act {
current: ProjectRef,
defaultConfigs: Option[ResolvedReference] => Seq[String],
keyMap: Map[String, AttributeKey[_]],
data: Settings[Scope]
data: Settings[Scope],
askProject: Boolean,
): Parser[ParsedKey] =
scopedKeyFull(index, current, defaultConfigs, keyMap).flatMap { choices =>
select(choices, data)(showRelativeKey2(current))
scopedKeyFull(index, current, defaultConfigs, keyMap, askProject = askProject).flatMap {
choices =>
select(choices, data)(showRelativeKey2(current))
}

def scopedKeyFull(
index: KeyIndex,
current: ProjectRef,
defaultConfigs: Option[ResolvedReference] => Seq[String],
keyMap: Map[String, AttributeKey[_]]
keyMap: Map[String, AttributeKey[_]],
askProject: Boolean,
): Parser[Seq[Parser[ParsedKey]]] = {
val confParserCache
: mutable.Map[Option[sbt.ResolvedReference], Parser[(ParsedAxis[String], Seq[String])]] =
mutable.Map.empty
def fullKey =
for {
rawProject <- optProjectRef(index, current)
for
rawProject <-
if askProject then optProjectRef(index, current)
else success(Omitted)
proj = resolveProject(rawProject, current)
confPair <- confParserCache.getOrElseUpdate(
proj,
Expand All @@ -131,7 +172,7 @@ object Act {
)
(confAmb, seps) = confPair
partialMask = ScopeMask(rawProject.isExplicit, confAmb.isExplicit, false, false)
} yield taskKeyExtra(index, defaultConfigs, keyMap, proj, confAmb, partialMask, seps)
yield taskKeyExtra(index, defaultConfigs, keyMap, proj, confAmb, partialMask, seps)

val globalIdent = token(GlobalIdent ~ spacedSlash) ^^^ ParsedGlobal
def globalKey =
Expand Down Expand Up @@ -485,49 +526,59 @@ object Act {

def actParser(s: State): Parser[() => State] = requireSession(s, actParser0(s))

private[this] def actParser0(state: State): Parser[() => State] = {
val extracted = Project extract state
private[this] def actParser0(state: State): Parser[() => State] =
val extracted = Project.extract(state)
import extracted.{ showKey, structure }
import Aggregation.evaluatingParser
actionParser.flatMap { action =>
val akp = aggregatedKeyParserSep(extracted)
def warnOldShellSyntax(seps: Seq[String], keyStrings: String): Unit =
if (seps.contains(":") || seps.contains("::")) {
state.log.warn(
s"sbt 0.13 shell syntax is deprecated; use slash syntax instead: $keyStrings"
)
} else ()
def evaluate(pairs: Seq[(ScopedKey[_], Seq[String])]): Parser[() => State] = {
val kvs = pairs.map(_._1)
val seps = pairs.headOption.map(_._2).getOrElse(Nil)
actionParser.flatMap: action =>
val akp = aggregatedKeyParserFilter(extracted)
// If the task name matches, but the query is empty, we should succeed the parser,
// but fail the task. Otherwise, the composed parser would think we made a typo.
def emptyResult: Parser[() => State] =
Parser.success(() => throw MessageOnlyException("query result is empty"))
def evaluate(kvs: Seq[ScopedKey[_]]): Parser[() => State] =
val preparedPairs = anyKeyValues(structure, kvs)
val showConfig = if (action == PrintAction) {
Aggregation.ShowConfig(true, true, println, false)
} else {
Aggregation.defaultShow(state, showTasks = action == ShowAction)
}
evaluatingParser(state, showConfig)(preparedPairs) map { evaluate => () =>
{
val keyStrings = preparedPairs.map(pp => showKey.show(pp.key)).mkString(", ")
state.log.debug("Evaluating tasks: " + keyStrings)
warnOldShellSyntax(seps, keyStrings)
evaluate()
}
}
}
action match {
case SingleAction => akp.flatMap(evaluate)
case ShowAction | PrintAction | MultiAction =>
rep1sep(akp, token(Space)) flatMap { pairs =>
val flat: mutable.ListBuffer[(ScopedKey[_], Seq[String])] = mutable.ListBuffer.empty
pairs foreach { xs =>
flat ++= xs
}
evaluate(flat.toList)
}
}
val showConfig = action match
case PrintAction =>
Aggregation.ShowConfig(
settingValues = true,
taskValues = true,
print = println,
success = false
)
case _ => Aggregation.defaultShow(state, showTasks = action == ShowAction)
Aggregation
.evaluatingParser(state, showConfig)(preparedPairs)
.map: evaluate =>
() =>
val keyStrings = preparedPairs.map(pp => showKey.show(pp.key)).mkString(", ")
state.log.debug("Evaluating tasks: " + keyStrings)
evaluate()
for
keys <-
action match
case SingleAction => akp
case ShowAction | PrintAction | MultiAction =>
for pairs <- rep1sep(akp, token(Space))
yield pairs.flatten
keys1 = applyQuery(keys, structure)
p <-
if keys.nonEmpty && keys1.isEmpty then emptyResult
else evaluate(keys1.map(_._1))
yield p
end actParser0

private def applyQuery(
pairs: Seq[(ScopedKey[_], Option[ProjectQuery])],
structure: BuildStructure,
): Seq[(ScopedKey[_], Option[ProjectQuery])] =
pairs.filter {
case (_, None) => true
case (keys, Some(query)) =>
val f = query.buildQuery(structure)
keys.scope.project.toOption match
case Some(ref: ProjectRef) => f(ref)
case _ => true
}
}

private[this] final class ActAction
private[this] final val ShowAction, MultiAction, SingleAction, PrintAction = new ActAction
Expand All @@ -551,9 +602,6 @@ object Act {
structure.data
)

type KeysParser = Parser[Seq[ScopedKey[Any]]]
type KeysParserSep = Parser[Seq[(ScopedKey[Any], Seq[String])]]

def aggregatedKeyParser(state: State): KeysParser = aggregatedKeyParser(Project extract state)
def aggregatedKeyParser(extracted: Extracted): KeysParser =
aggregatedKeyParser(extracted.structure, extracted.currentRef)
Expand All @@ -567,6 +615,13 @@ object Act {
currentRef: ProjectRef
): KeysParserSep =
scopedKeyAggregatedSep(currentRef, structure.extra.configurationsForAxis, structure)
private[sbt] def aggregatedKeyParserFilter(extracted: Extracted): KeysParserFilter =
aggregatedKeyParserFilter(extracted.structure, extracted.currentRef)
private[sbt] def aggregatedKeyParserFilter(
structure: BuildStructure,
currentRef: ProjectRef
): KeysParserFilter =
scopedKeyAggregatedFilter(currentRef, structure.extra.configurationsForAxis, structure)

def keyValues[T](state: State)(keys: Seq[ScopedKey[T]]): Values[T] =
keyValues(Project extract state)(keys)
Expand Down
20 changes: 8 additions & 12 deletions main/src/main/scala/sbt/internal/Aggregation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -254,22 +254,18 @@ object Aggregation {
else extra.aggregates.forward(ref)
}

def aggregate[T, Proj](
key: ScopedKey[T],
def aggregate[A1, Proj](
key: ScopedKey[A1],
rawMask: ScopeMask,
extra: BuildUtil[Proj],
reverse: Boolean = false
): Seq[ScopedKey[T]] = {
): Seq[ScopedKey[A1]] =
val mask = rawMask.copy(project = true)
Dag.topologicalSort(key) { k =>
if (reverse)
reverseAggregatedKeys(k, extra, mask)
else if (aggregationEnabled(k, extra.data))
aggregatedKeys(k, extra, mask)
else
Nil
}
}
Dag.topologicalSort(key): (k) =>
if reverse then reverseAggregatedKeys(k, extra, mask)
else if aggregationEnabled(k, extra.data) then aggregatedKeys(k, extra, mask)
else Nil

def reverseAggregatedKeys[T](
key: ScopedKey[T],
extra: BuildUtil[_],
Expand Down
55 changes: 55 additions & 0 deletions main/src/main/scala/sbt/internal/ProjectQuery.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package sbt
package internal

import sbt.internal.util.complete.{ DefaultParsers, Parser }
import sbt.Keys.scalaBinaryVersion
import DefaultParsers.*
import scala.annotation.nowarn
import scala.util.matching.Regex
import sbt.internal.util.AttributeKey

private[sbt] case class ProjectQuery(
projectName: String,
params: Map[AttributeKey[?], String],
):
import ProjectQuery.*
private lazy val pattern: Regex = Regex("^" + projectName.replace(wildcard, ".*") + "$")

@nowarn
def buildQuery(structure: BuildStructure): ProjectRef => Boolean =
(p: ProjectRef) =>
val projectMatches =
if projectName == wildcard then true
else pattern.matches(p.project)
val scalaMatches =
params.get(Keys.scalaBinaryVersion.key) match
case Some(expected) =>
val actualSbv = structure.data.get(Scope.ThisScope.in(p), scalaBinaryVersion.key)
actualSbv match
case Some(sbv) => sbv == expected
case None => true
case None => true
projectMatches && scalaMatches
end ProjectQuery

object ProjectQuery:
private val wildcard = "..."

// make sure @ doesn't match on this one
def projectName: Parser[String] =
charClass(c => c.isLetter || c.isDigit || c == '_' || c == '.').+.string
.examples(wildcard)

def parser: Parser[ProjectQuery] =
(projectName ~
token("@scalaBinaryVersion=" ~> StringBasic.map((scalaBinaryVersion.key, _)))
.examples("@scalaBinaryVersion=3")
.?)
.map { case (proj, params) =>
ProjectQuery(proj, Map(params.toSeq: _*))
}
.filter(
(q) => q.projectName.contains("...") || q.params.nonEmpty,
(msg) => s"$msg isn't a query"
)
end ProjectQuery
4 changes: 2 additions & 2 deletions sbt-app/src/sbt-test/actions/aggregate/project/Marker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ object Marker extends AutoPlugin:
override def trigger = allRequirements
override def requires = sbt.plugins.JvmPlugin
object autoImport {
final lazy val Mark = TaskKey[Unit]("mark")
final def mark: Initialize[Task[Unit]] = mark(baseDirectory)
final lazy val mark = taskKey[Unit]("mark")
final def markTask: Initialize[Task[Unit]] = mark(baseDirectory)
final def mark(project: Reference): Initialize[Task[Unit]] = mark(project / baseDirectory)
final def mark(baseKey: SettingKey[File]): Initialize[Task[Unit]] = baseKey.toTaskable mapN {
base =>
Expand Down
Loading

0 comments on commit e61ae80

Please sign in to comment.