Unified slash syntax

Fixes sbt/sbt#1812

This adds unified slash syntax for both sbt shell and the build.sbt DSL.
Instead of the current `<project-id>/config:intask::key`,
this adds `<project-id>/<config-ident>/intask/key` where <config-ident> is the Scala identifier notation for the configurations like `Compile` and `Test`.

This also adds a series of implicits called `SlashSyntax` that adds `/` operators to project refererences, configuration, and keys such that the same syntax works in build.sbt.

These examples work for both from the shell and in build.sbt.

    Global / cancelable
    ThisBuild / scalaVersion
    Test / test
    root / Compile / compile / scalacOptions
    ProjectRef(uri("file:/xxx/helloworld/"),"root")/Compile/scalacOptions
    Zero / Zero / name

The inspect command now outputs something that can be copy-pasted:

    > inspect compile
    [info] Task: sbt.inc.Analysis
    [info] Description:
    [info] 	Compiles sources.
    [info] Provided by:
    [info] 	ProjectRef(uri("file:/xxx/helloworld/"),"root")/Compile/compile
    [info] Defined at:
    [info] 	(sbt.Defaults) Defaults.scala:326
    [info] Dependencies:
    [info] 	Compile/manipulateBytecode
    [info] 	Compile/incCompileSetup
    [info] Reverse dependencies:
    [info] 	Compile/printWarnings
    [info] 	Compile/products
    [info] 	Compile/discoveredSbtPlugins
    [info] 	Compile/discoveredMainClasses
    [info] Delegates:
    [info] 	Compile/compile
    [info] 	compile
    [info] 	ThisBuild/Compile/compile
    [info] 	ThisBuild/compile
    [info] 	Zero/Compile/compile
    [info] 	Global/compile
    [info] Related:
    [info] 	Test/compile
This commit is contained in:
Eugene Yokota 2017-09-27 02:21:56 -04:00
parent 67d1da48f1
commit 33a01f3ceb
25 changed files with 673 additions and 78 deletions

View File

@ -45,7 +45,8 @@ def commonSettings: Seq[Setting[_]] =
resolvers += Resolver.sonatypeRepo("snapshots"),
resolvers += "bintray-sbt-maven-releases" at "https://dl.bintray.com/sbt/maven-releases/",
concurrentRestrictions in Global += Util.testExclusiveRestriction,
testOptions += Tests.Argument(TestFrameworks.ScalaCheck, "-w", "1"),
testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-w", "1"),
testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "2"),
javacOptions in compile ++= Seq("-target", "6", "-source", "6", "-Xlint", "-Xlint:-serial"),
crossScalaVersions := Seq(baseScalaVersion),
bintrayPackage := (bintrayPackage in ThisBuild).value,
@ -374,6 +375,11 @@ lazy val mainProj = (project in file("main"))
mimaBinaryIssueFilters ++= Vector(
// Changed the signature of NetworkChannel ctor. internal.
exclude[DirectMissingMethodProblem]("sbt.internal.server.NetworkChannel.*"),
// ctor for ConfigIndex. internal.
exclude[DirectMissingMethodProblem]("sbt.internal.ConfigIndex.*"),
// New and changed methods on KeyIndex. internal.
exclude[ReversedMissingMethodProblem]("sbt.internal.KeyIndex.*"),
exclude[DirectMissingMethodProblem]("sbt.internal.KeyIndex.*"),
)
)
.configure(

View File

@ -92,6 +92,11 @@ object AttributeKey {
rank0: Int
)(implicit mf: Manifest[T], ojw: OptJsonWriter[T]): AttributeKey[T] =
new SharedAttributeKey[T] {
require(name.headOption match {
case Some(c) => c.isLower
case None => false
}, s"A named attribute key must start with a lowercase letter: $name")
def manifest = mf
val label = Util.hyphenToCamel(name)
def description = description0

View File

@ -80,7 +80,12 @@ object SettingsTest extends Properties("settings") {
private def mkAttrKeys[T](nr: Int)(implicit mf: Manifest[T]): Gen[List[AttributeKey[T]]] = {
import Gen._
val nonEmptyAlphaStr =
nonEmptyListOf(alphaChar).map(_.mkString).suchThat(_.forall(_.isLetter))
nonEmptyListOf(alphaChar)
.map({ xs: List[Char] =>
val s = xs.mkString
s.take(1).toLowerCase + s.drop(1)
})
.suchThat(_.forall(_.isLetter))
(for {
list <- Gen.listOfN(nr, nonEmptyAlphaStr) suchThat (l => l.size == l.distinct.size)

View File

@ -43,6 +43,12 @@ trait Parsers {
/** Parses a single letter, according to Char.isLetter, into a Char. */
lazy val Letter = charClass(_.isLetter, "letter")
/** Parses a single letter, according to Char.isUpper, into a Char. */
lazy val Upper = charClass(_.isUpper, "upper")
/** Parses a single letter, according to Char.isLower, into a Char. */
lazy val Lower = charClass(_.isLower, "lower")
/** Parses the first Char in an sbt identifier, which must be a [[Letter]].*/
def IDStart = Letter
@ -67,6 +73,9 @@ trait Parsers {
/** Parses a non-symbolic Scala-like identifier. The identifier must start with [[IDStart]] and contain zero or more [[ScalaIDChar]]s after that.*/
lazy val ScalaID = identifier(IDStart, ScalaIDChar)
/** Parses a non-symbolic Scala-like identifier. The identifier must start with [[Upper]] and contain zero or more [[ScalaIDChar]]s after that.*/
lazy val CapitalizedID = identifier(Upper, ScalaIDChar)
/** Parses a String that starts with `start` and is followed by zero or more characters parsed by `rep`.*/
def identifier(start: Parser[Char], rep: Parser[Char]): Parser[String] =
start ~ rep.* map { case x ~ xs => (x +: xs).mkString }

View File

@ -57,17 +57,36 @@ object Def extends Init[Scope] with TaskMacroExtra {
ref => displayBuildRelative(currentBuild, multi, ref)
))
/**
* Returns a String expression for the given [[Reference]] (BuildRef, [[ProjectRef]], etc)
* relative to the current project.
*/
def displayRelativeReference(current: ProjectRef, project: Reference): String =
displayRelative(current, project, false)
@deprecated("Use displayRelativeReference", "1.1.0")
def displayRelative(current: ProjectRef, multi: Boolean, project: Reference): String =
displayRelative(current, project, true)
/**
* Constructs the String of a given [[Reference]] relative to current.
* Note that this no longer takes "multi" parameter, and omits the subproject id at all times.
*/
private[sbt] def displayRelative(current: ProjectRef,
project: Reference,
trailingSlash: Boolean): String = {
val trailing = if (trailingSlash) "/" else ""
project match {
case BuildRef(current.build) => "{.}/"
case `current` => if (multi) current.project + "/" else ""
case ProjectRef(current.build, x) => x + "/"
case _ => Reference.display(project) + "/"
case BuildRef(current.build) => "ThisBuild" + trailing
case `current` => ""
case ProjectRef(current.build, x) => x + trailing
case _ => Reference.display(project) + trailing
}
}
def displayBuildRelative(currentBuild: URI, multi: Boolean, project: Reference): String =
project match {
case BuildRef(`currentBuild`) => "{.}/"
case BuildRef(`currentBuild`) => "ThisBuild/"
case ProjectRef(`currentBuild`, x) => x + "/"
case _ => Reference.display(project) + "/"
}
@ -80,6 +99,9 @@ object Def extends Init[Scope] with TaskMacroExtra {
def displayMasked(scoped: ScopedKey[_], mask: ScopeMask): String =
Scope.displayMasked(scoped.scope, scoped.key.label, mask)
def displayMasked(scoped: ScopedKey[_], mask: ScopeMask, showZeroConfig: Boolean): String =
Scope.displayMasked(scoped.scope, scoped.key.label, mask, showZeroConfig)
def withColor(s: String, color: Option[String]): String = {
val useColor = ConsoleAppender.formatEnabledInEnv
color match {

View File

@ -91,7 +91,7 @@ object Reference {
case LocalRootProject => "{<this>}<root>"
case LocalProject(id) => "{<this>}" + id
case RootProject(uri) => "{" + uri + " }<root>"
case ProjectRef(uri, id) => "{" + uri + "}" + id
case ProjectRef(uri, id) => s"""ProjectRef(uri("$uri"),"$id")"""
}
def buildURI(ref: ResolvedReference): URI = ref match {

View File

@ -123,29 +123,84 @@ object Scope {
case BuildRef(uri) => BuildRef(resolveBuild(current, uri))
}
def display(config: ConfigKey): String = config.name + ":"
def display(config: ConfigKey): String = guessConfigIdent(config.name) + "/"
private[sbt] val configIdents: Map[String, String] =
Map(
"it" -> "IntegrationTest",
"scala-tool" -> "ScalaTool",
"plugin" -> "CompilerPlugin"
)
private[sbt] val configIdentsInverse: Map[String, String] =
configIdents map { _.swap }
private[sbt] def guessConfigIdent(conf: String): String =
configIdents.applyOrElse(conf, (x: String) => x.capitalize)
private[sbt] def unguessConfigIdent(conf: String): String =
configIdentsInverse.applyOrElse(conf, (x: String) => x.take(1).toLowerCase + x.drop(1))
def displayConfigKey012Style(config: ConfigKey): String = config.name + ":"
def display(scope: Scope, sep: String): String =
displayMasked(scope, sep, showProject, ScopeMask())
def displayMasked(scope: Scope, sep: String, mask: ScopeMask): String =
displayMasked(scope, sep, showProject, mask)
def display(scope: Scope, sep: String, showProject: Reference => String): String =
displayMasked(scope, sep, showProject, ScopeMask())
def displayMasked(
scope: Scope,
sep: String,
showProject: Reference => String,
mask: ScopeMask
): String = {
private[sbt] def displayPedantic(scope: Scope, sep: String): String =
displayMasked(scope, sep, showProject, ScopeMask(), true)
def displayMasked(scope: Scope, sep: String, mask: ScopeMask): String =
displayMasked(scope, sep, showProject, mask)
def displayMasked(scope: Scope, sep: String, mask: ScopeMask, showZeroConfig: Boolean): String =
displayMasked(scope, sep, showProject, mask, showZeroConfig)
def displayMasked(scope: Scope,
sep: String,
showProject: Reference => String,
mask: ScopeMask): String =
displayMasked(scope, sep, showProject, mask, false)
/**
* unified slash style introduced in sbt 1.1.0.
* By default, sbt will no longer display the Zero-config,
* so `name` will render as `name` as opposed to `{uri}proj/Zero/name`.
* Technically speaking an unspecified configuration axis defaults to
* the scope delegation (first configuration defining the key, then Zero).
*/
def displayMasked(scope: Scope,
sep: String,
showProject: Reference => String,
mask: ScopeMask,
showZeroConfig: Boolean): String = {
import scope.{ project, config, task, extra }
val configPrefix = config.foldStrict(display, "*:", ".:")
val zeroConfig = if (showZeroConfig) "Zero/" else ""
val configPrefix = config.foldStrict(display, zeroConfig, "./")
val taskPrefix = task.foldStrict(_.label + "/", "", "./")
val extras = extra.foldStrict(_.entries.map(_.toString).toList, Nil, Nil)
val postfix = if (extras.isEmpty) "" else extras.mkString("(", ", ", ")")
if (scope == GlobalScope) "Global/" + sep + postfix
else
mask.concatShow(projectPrefix(project, showProject), configPrefix, taskPrefix, sep, postfix)
}
// sbt 0.12 style
def display012StyleMasked(scope: Scope,
sep: String,
showProject: Reference => String,
mask: ScopeMask): String = {
import scope.{ project, config, task, extra }
val configPrefix = config.foldStrict(displayConfigKey012Style, "*:", ".:")
val taskPrefix = task.foldStrict(_.label + "::", "", ".::")
val extras = extra.foldStrict(_.entries.map(_.toString).toList, Nil, Nil)
val postfix = if (extras.isEmpty) "" else extras.mkString("(", ", ", ")")
mask.concatShow(projectPrefix(project, showProject), configPrefix, taskPrefix, sep, postfix)
mask.concatShow(projectPrefix012Style(project, showProject),
configPrefix,
taskPrefix,
sep,
postfix)
}
def equal(a: Scope, b: Scope, mask: ScopeMask): Boolean =
@ -154,10 +209,12 @@ object Scope {
(!mask.task || a.task == b.task) &&
(!mask.extra || a.extra == b.extra)
def projectPrefix(
project: ScopeAxis[Reference],
show: Reference => String = showProject
): String =
def projectPrefix(project: ScopeAxis[Reference],
show: Reference => String = showProject): String =
project.foldStrict(show, "Zero/", "./")
def projectPrefix012Style(project: ScopeAxis[Reference],
show: Reference => String = showProject): String =
project.foldStrict(show, "*/", "./")
def showProject = (ref: Reference) => Reference.display(ref) + "/"

View File

@ -2336,13 +2336,20 @@ object Classpaths {
else Def.task((evictionWarningOptions in update).value)
}.value
val extracted = (Project extract state0)
val isPlugin = sbtPlugin.value
val thisRef = thisProjectRef.value
val label =
if (isPlugin) Reference.display(thisRef)
else Def.displayRelativeReference(extracted.currentRef, thisRef)
LibraryManagement.cachedUpdate(
// LM API
lm = dependencyResolution.value,
// Ivy-free ModuleDescriptor
module = ivyModule.value,
s.cacheStoreFactory.sub(updateCacheName.value),
Reference.display(thisProjectRef.value),
label = label,
updateConf,
substituteScalaFiles(scalaOrganization.value, _)(providedScalaJars),
skip = (skip in update).value,

View File

@ -0,0 +1,148 @@
package sbt
import java.io.File
import sbt.librarymanagement.Configuration
/**
* SlashSyntax implements the slash syntax to scope keys for build.sbt DSL.
* The implicits are set up such that the order that the scope components
* must appear in the order of the project axis, the configuration axis, and
* the task axis. This ordering is the same as the shell syntax.
*
* @example
* {{{
* Global / cancelable := true
* ThisBuild / scalaVersion := "2.12.2"
* Test / test := ()
* console / scalacOptions += "-deprecation"
* Compile / console / scalacOptions += "-Ywarn-numeric-widen"
* projA / Compile / console / scalacOptions += "-feature"
* Zero / Zero / name := "foo"
* }}}
*/
trait SlashSyntax {
import SlashSyntax._
implicit def sbtScopeSyntaxRichReference(r: Reference): RichReference =
new RichReference(Scope(Select(r), This, This, This))
implicit def sbtScopeSyntaxRichProject(p: Project): RichReference =
new RichReference(Scope(Select(p), This, This, This))
implicit def sbtScopeSyntaxRichConfiguration(c: Configuration): RichConfiguration =
new RichConfiguration(Scope(This, Select(c), This, This))
implicit def sbtScopeSyntaxRichScope(s: Scope): RichScope =
new RichScope(s)
implicit def sbtScopeSyntaxRichScopeFromScoped(t: Scoped): RichScope =
new RichScope(Scope(This, This, Select(t.key), This))
implicit def sbtScopeSyntaxRichScopeAxis(a: ScopeAxis[Reference]): RichScopeAxis =
new RichScopeAxis(a)
// Materialize the setting key thunk
implicit def sbtScopeSyntaxSettingKeyThunkMaterialize[A](
thunk: SettingKeyThunk[A]): SettingKey[A] =
thunk.materialize
implicit def sbtScopeSyntaxSettingKeyThunkKeyRescope[A](thunk: SettingKeyThunk[A]): RichScope =
thunk.rescope
// Materialize the task key thunk
implicit def sbtScopeSyntaxTaskKeyThunkMaterialize[A](thunk: TaskKeyThunk[A]): TaskKey[A] =
thunk.materialize
implicit def sbtScopeSyntaxTaskKeyThunkRescope[A](thunk: TaskKeyThunk[A]): RichScope =
thunk.rescope
// Materialize the input key thunk
implicit def sbtScopeSyntaxInputKeyThunkMaterialize[A](thunk: InputKeyThunk[A]): InputKey[A] =
thunk.materialize
implicit def sbtScopeSyntaxInputKeyThunkRescope[A](thunk: InputKeyThunk[A]): RichScope =
thunk.rescope
}
object SlashSyntax {
/** RichReference wraps a project to provide the `/` operator for scoping. */
final class RichReference(s: Scope) {
def /(c: Configuration): RichConfiguration = new RichConfiguration(s in c)
// We don't know what the key is for yet, so just capture in a thunk.
def /[A](key: SettingKey[A]): SettingKeyThunk[A] = new SettingKeyThunk(s, key)
// We don't know what the key is for yet, so just capture in a thunk.
def /[A](key: TaskKey[A]): TaskKeyThunk[A] = new TaskKeyThunk(s, key)
// We don't know what the key is for yet, so just capture in a thunk.
def /[A](key: InputKey[A]): InputKeyThunk[A] = new InputKeyThunk(s, key)
}
/** RichConfiguration wraps a configuration to provide the `/` operator for scoping. */
final class RichConfiguration(s: Scope) {
// We don't know what the key is for yet, so just capture in a thunk.
def /[A](key: SettingKey[A]): SettingKeyThunk[A] = new SettingKeyThunk(s, key)
// We don't know what the key is for yet, so just capture in a thunk.
def /[A](key: TaskKey[A]): TaskKeyThunk[A] = new TaskKeyThunk(s, key)
// We don't know what the key is for yet, so just capture in a thunk.
def /[A](key: InputKey[A]): InputKeyThunk[A] = new InputKeyThunk(s, key)
}
/** RichScope wraps a general scope to provide the `/` operator for scoping. */
final class RichScope(scope: Scope) {
def /[A](key: SettingKey[A]): SettingKey[A] = key in scope
def /[A](key: TaskKey[A]): TaskKey[A] = key in scope
def /[A](key: InputKey[A]): InputKey[A] = key in scope
}
/** RichScopeAxis wraps a project axis to provide the `/` operator to `Zero` for scoping. */
final class RichScopeAxis(a: ScopeAxis[Reference]) {
private[this] def toScope: Scope = Scope(a, This, This, This)
def /(c: Configuration): RichConfiguration = new RichConfiguration(toScope in c)
// This is for handling `Zero / Zero / name`.
def /(configAxis: ScopeAxis[ConfigKey]): RichConfiguration =
new RichConfiguration(toScope.copy(config = configAxis))
// We don't know what the key is for yet, so just capture in a thunk.
def /[A](key: SettingKey[A]): SettingKeyThunk[A] = new SettingKeyThunk(toScope, key)
// We don't know what the key is for yet, so just capture in a thunk.
def /[A](key: TaskKey[A]): TaskKeyThunk[A] = new TaskKeyThunk(toScope, key)
// We don't know what the key is for yet, so just capture in a thunk.
def /[A](key: InputKey[A]): InputKeyThunk[A] = new InputKeyThunk(toScope, key)
}
/**
* SettingKeyThunk is a thunk used to hold a scope and a key
* while we're not sure if the key is terminal or task-scoping.
*/
final class SettingKeyThunk[A](base: Scope, key: SettingKey[A]) {
private[sbt] def materialize: SettingKey[A] = key in base
private[sbt] def rescope: RichScope = new RichScope(base in key.key)
}
/**
* TaskKeyThunk is a thunk used to hold a scope and a key
* while we're not sure if the key is terminal or task-scoping.
*/
final class TaskKeyThunk[A](base: Scope, key: TaskKey[A]) {
private[sbt] def materialize: TaskKey[A] = key in base
private[sbt] def rescope: RichScope = new RichScope(base in key.key)
}
/**
* InputKeyThunk is a thunk used to hold a scope and a key
* while we're not sure if the key is terminal or task-scoping.
*/
final class InputKeyThunk[A](base: Scope, key: InputKey[A]) {
private[sbt] def materialize: InputKey[A] = key in base
private[sbt] def rescope: RichScope = new RichScope(base in key.key)
}
}

View File

@ -19,6 +19,13 @@ final class ParsedKey(val key: ScopedKey[_], val mask: ScopeMask)
object Act {
val ZeroString = "*"
private[sbt] val GlobalIdent = "Global"
private[sbt] val ZeroIdent = "Zero"
private[sbt] val ThisBuildIdent = "ThisBuild"
// new separator for unified shell syntax. this allows optional whitespace around /.
private[sbt] val spacedSlash: Parser[Unit] =
token(OptSpace ~> '/' <~ OptSpace).examples("/").map(_ => ())
// this does not take aggregation into account
def scopedKey(index: KeyIndex,
@ -52,12 +59,29 @@ object Act {
current: ProjectRef,
defaultConfigs: Option[ResolvedReference] => Seq[String],
keyMap: Map[String, AttributeKey[_]]): Parser[Seq[Parser[ParsedKey]]] = {
for {
rawProject <- optProjectRef(index, current)
proj = resolveProject(rawProject, current)
confAmb <- config(index configs proj)
partialMask = ScopeMask(rawProject.isExplicit, confAmb.isExplicit, false, false)
} yield taskKeyExtra(index, defaultConfigs, keyMap, proj, confAmb, partialMask)
def fullKey =
for {
rawProject <- optProjectRef(index, current)
proj = resolveProject(rawProject, current)
confAmb <- configIdent(index configs proj,
index configIdents proj,
index.fromConfigIdent(proj))
partialMask = ScopeMask(rawProject.isExplicit, confAmb.isExplicit, false, false)
} yield taskKeyExtra(index, defaultConfigs, keyMap, proj, confAmb, partialMask)
val globalIdent = token(GlobalIdent ~ spacedSlash) ^^^ ParsedGlobal
def globalKey =
for {
g <- globalIdent
} yield
taskKeyExtra(index,
defaultConfigs,
keyMap,
None,
ParsedZero,
ScopeMask(true, true, false, false))
globalKey | fullKey
}
def taskKeyExtra(
@ -148,6 +172,23 @@ object Act {
token((ZeroString ^^^ ParsedZero | value(examples(ID, confs, "configuration"))) <~ sep) ?? Omitted
}
// New configuration parser that's able to parse configuration ident trailed by slash.
private[sbt] def configIdent(confs: Set[String],
idents: Set[String],
fromIdent: String => String): Parser[ParsedAxis[String]] = {
val oldSep: Parser[Char] = ':'
val sep: Parser[Unit] = spacedSlash !!! "Expected '/'"
token(
((ZeroString ^^^ ParsedZero) <~ oldSep)
| ((ZeroString ^^^ ParsedZero) <~ sep)
| ((ZeroIdent ^^^ ParsedZero) <~ sep)
| (value(examples(ID, confs, "configuration")) <~ oldSep)
| (value(examples(CapitalizedID, idents, "configuration ident") map {
fromIdent(_)
}) <~ sep)
) ?? Omitted
}
def configs(explicit: ParsedAxis[String],
defaultConfigs: Option[ResolvedReference] => Seq[String],
proj: Option[ResolvedReference],
@ -156,8 +197,8 @@ object Act {
case Omitted =>
None +: defaultConfigurations(proj, index, defaultConfigs).flatMap(
nonEmptyConfig(index, proj))
case ParsedZero => None :: Nil
case pv: ParsedValue[x] => Some(pv.value) :: Nil
case ParsedZero | ParsedGlobal => None :: Nil
case pv: ParsedValue[x] => Some(pv.value) :: Nil
}
def defaultConfigurations(
@ -220,11 +261,15 @@ object Act {
val valid = allKnown ++ normKeys
val suggested = normKeys.map(_._1).toSet
val keyP = filterStrings(examples(ID, suggested, "key"), valid.keySet, "key") map valid
(token(value(keyP) | ZeroString ^^^ ParsedZero) <~ token("::".id)) ?? Omitted
(token(
value(keyP)
| ZeroString ^^^ ParsedZero
| ZeroIdent ^^^ ParsedZero) <~ (token("::".id) | spacedSlash)) ?? Omitted
}
def resolveTask(task: ParsedAxis[AttributeKey[_]]): Option[AttributeKey[_]] =
task match {
case ParsedZero | Omitted => None
case ParsedZero | ParsedGlobal | Omitted => None
case t: ParsedValue[AttributeKey[_]] @unchecked => Some(t.value)
}
@ -260,10 +305,36 @@ object Act {
}
def projectRef(index: KeyIndex, currentBuild: URI): Parser[ParsedAxis[ResolvedReference]] = {
val zero = token(ZeroString ~ '/') ^^^ ParsedZero
val trailing = '/' !!! "Expected '/' (if selecting a project)"
zero | value(resolvedReference(index, currentBuild, trailing))
val global = token(ZeroString ~ spacedSlash) ^^^ ParsedZero
val zeroIdent = token(ZeroIdent ~ spacedSlash) ^^^ ParsedZero
val thisBuildIdent = value(token(ThisBuildIdent ~ spacedSlash) ^^^ BuildRef(currentBuild))
val trailing = spacedSlash !!! "Expected '/' (if selecting a project)"
global | zeroIdent | thisBuildIdent |
value(resolvedReferenceIdent(index, currentBuild, trailing)) |
value(resolvedReference(index, currentBuild, trailing))
}
private[sbt] def resolvedReferenceIdent(index: KeyIndex,
currentBuild: URI,
trailing: Parser[_]): Parser[ResolvedReference] = {
def projectID(uri: URI) =
token(
DQuoteChar ~> examplesStrict(ID, index projects uri, "project ID") <~ DQuoteChar <~ OptSpace <~ ")" <~ trailing)
def projectRef(uri: URI) = projectID(uri) map { id =>
ProjectRef(uri, id)
}
val uris = index.buildURIs
val resolvedURI = Uri(uris).map(uri => Scope.resolveBuild(currentBuild, uri))
val buildRef = token(
"ProjectRef(" ~> OptSpace ~> "uri(" ~> OptSpace ~> DQuoteChar ~>
resolvedURI <~ DQuoteChar <~ OptSpace <~ ")" <~ spacedComma)
buildRef flatMap { uri =>
projectRef(uri)
}
}
def resolvedReference(index: KeyIndex,
currentBuild: URI,
trailing: Parser[_]): Parser[ResolvedReference] = {
@ -284,11 +355,13 @@ object Act {
}
def optProjectRef(index: KeyIndex, current: ProjectRef): Parser[ParsedAxis[ResolvedReference]] =
projectRef(index, current.build) ?? Omitted
def resolveProject(parsed: ParsedAxis[ResolvedReference],
current: ProjectRef): Option[ResolvedReference] =
parsed match {
case Omitted => Some(current)
case ParsedZero => None
case ParsedGlobal => None
case pv: ParsedValue[rr] => Some(pv.value)
}
@ -375,6 +448,7 @@ object Act {
sealed trait ParsedAxis[+T] {
final def isExplicit = this != Omitted
}
final object ParsedGlobal extends ParsedAxis[Nothing]
final object ParsedZero extends ParsedAxis[Nothing]
final object Omitted extends ParsedAxis[Nothing]
final class ParsedValue[T](val value: T) extends ParsedAxis[T]

View File

@ -9,20 +9,32 @@ import Def.ScopedKey
import sbt.internal.util.complete.DefaultParsers.validID
import sbt.internal.util.Types.some
import sbt.internal.util.{ AttributeKey, Relation }
import sbt.librarymanagement.Configuration
object KeyIndex {
def empty: ExtendableKeyIndex = new KeyIndex0(emptyBuildIndex)
def apply(known: Iterable[ScopedKey[_]], projects: Map[URI, Set[String]]): ExtendableKeyIndex =
(base(projects) /: known) { _ add _ }
def apply(known: Iterable[ScopedKey[_]],
projects: Map[URI, Set[String]],
configurations: Map[String, Seq[Configuration]]): ExtendableKeyIndex =
(base(projects, configurations) /: known) { _ add _ }
def aggregate(known: Iterable[ScopedKey[_]],
extra: BuildUtil[_],
projects: Map[URI, Set[String]]): ExtendableKeyIndex =
(base(projects) /: known) { (index, key) =>
projects: Map[URI, Set[String]],
configurations: Map[String, Seq[Configuration]]): ExtendableKeyIndex =
(base(projects, configurations) /: known) { (index, key) =>
index.addAggregated(key, extra)
}
private[this] def base(projects: Map[URI, Set[String]]): ExtendableKeyIndex = {
val data = for ((uri, ids) <- projects) yield {
val data = ids.map(id => Option(id) -> new ConfigIndex(Map.empty))
private[this] def base(projects: Map[URI, Set[String]],
configurations: Map[String, Seq[Configuration]]): ExtendableKeyIndex = {
val data = for {
(uri, ids) <- projects
} yield {
val data = ids map { id =>
val configs = configurations.getOrElse(id, Seq())
Option(id) -> new ConfigIndex(Map.empty, Map(configs map { c =>
(c.name, c.id)
}: _*))
}
Option(uri) -> new ProjectIndex(data.toMap)
}
new KeyIndex0(new BuildIndex(data.toMap))
@ -33,6 +45,14 @@ object KeyIndex {
def projects(uri: URI) = concat(_.projects(uri))
def exists(project: Option[ResolvedReference]): Boolean = indices.exists(_ exists project)
def configs(proj: Option[ResolvedReference]) = concat(_.configs(proj))
private[sbt] def configIdents(proj: Option[ResolvedReference]) = concat(_.configIdents(proj))
private[sbt] def fromConfigIdent(proj: Option[ResolvedReference])(configIdent: String): String =
(indices find { idx =>
idx.exists(proj)
}) match {
case Some(idx) => idx.fromConfigIdent(proj)(configIdent)
case _ => Scope.unguessConfigIdent(configIdent)
}
def tasks(proj: Option[ResolvedReference], conf: Option[String]) = concat(_.tasks(proj, conf))
def tasks(proj: Option[ResolvedReference], conf: Option[String], key: String) =
concat(_.tasks(proj, conf, key))
@ -46,7 +66,7 @@ object KeyIndex {
private[sbt] def getOr[A, B](m: Map[A, B], key: A, or: B): B = m.getOrElse(key, or)
private[sbt] def keySet[A, B](m: Map[Option[A], B]): Set[A] = m.keys.flatten.toSet
private[sbt] val emptyAKeyIndex = new AKeyIndex(Relation.empty)
private[sbt] val emptyConfigIndex = new ConfigIndex(Map.empty)
private[sbt] val emptyConfigIndex = new ConfigIndex(Map.empty, Map.empty)
private[sbt] val emptyProjectIndex = new ProjectIndex(Map.empty)
private[sbt] val emptyBuildIndex = new BuildIndex(Map.empty)
}
@ -73,6 +93,8 @@ trait KeyIndex {
def keys(proj: Option[ResolvedReference],
conf: Option[String],
task: Option[AttributeKey[_]]): Set[String]
private[sbt] def configIdents(project: Option[ResolvedReference]): Set[String]
private[sbt] def fromConfigIdent(proj: Option[ResolvedReference])(configIdent: String): String
}
trait ExtendableKeyIndex extends KeyIndex {
def add(scoped: ScopedKey[_]): ExtendableKeyIndex
@ -87,13 +109,34 @@ private[sbt] final class AKeyIndex(val data: Relation[Option[AttributeKey[_]], S
def tasks: Set[AttributeKey[_]] = data._1s.flatten.toSet
def tasks(key: String): Set[AttributeKey[_]] = data.reverse(key).flatten.toSet
}
private[sbt] final class ConfigIndex(val data: Map[Option[String], AKeyIndex]) {
/*
* data contains the mapping between a configuration and keys.
* identData contains the mapping between a configuration and its identifier.
*/
private[sbt] final class ConfigIndex(val data: Map[Option[String], AKeyIndex],
val identData: Map[String, String]) {
def add(config: Option[String],
task: Option[AttributeKey[_]],
key: AttributeKey[_]): ConfigIndex =
new ConfigIndex(data updated (config, keyIndex(config).add(task, key)))
key: AttributeKey[_]): ConfigIndex = {
new ConfigIndex(data updated (config, keyIndex(config).add(task, key)), this.identData)
}
def keyIndex(conf: Option[String]): AKeyIndex = getOr(data, conf, emptyAKeyIndex)
def configs: Set[String] = keySet(data)
private[sbt] val configIdentsInverse: Map[String, String] =
identData map { _.swap }
private[sbt] lazy val idents: Set[String] =
configs map { config =>
identData.getOrElse(config, Scope.guessConfigIdent(config))
}
// guess Configuration name from an identifier.
// There's a guessing involved because we could have scoped key that Project is not aware of.
private[sbt] def fromConfigIdent(ident: String): String =
configIdentsInverse.getOrElse(ident, Scope.unguessConfigIdent(ident))
}
private[sbt] final class ProjectIndex(val data: Map[Option[String], ConfigIndex]) {
def add(id: Option[String],
@ -122,6 +165,13 @@ private[sbt] final class KeyIndex0(val data: BuildIndex) extends ExtendableKeyIn
data.data.get(build).flatMap(_.data.get(project)).isDefined
}
def configs(project: Option[ResolvedReference]): Set[String] = confIndex(project).configs
private[sbt] def configIdents(project: Option[ResolvedReference]): Set[String] =
confIndex(project).idents
private[sbt] def fromConfigIdent(proj: Option[ResolvedReference])(configIdent: String): String =
confIndex(proj).fromConfigIdent(configIdent)
def tasks(proj: Option[ResolvedReference], conf: Option[String]): Set[AttributeKey[_]] =
keyIndex(proj, conf).tasks
def tasks(proj: Option[ResolvedReference],

View File

@ -324,8 +324,13 @@ private[sbt] object Load {
val attributeKeys = Index.attributeKeys(data) ++ keys.map(_.key)
val scopedKeys = keys ++ data.allKeys((s, k) => ScopedKey(s, k)).toVector
val projectsMap = projects.mapValues(_.defined.keySet)
val keyIndex = KeyIndex(scopedKeys.toVector, projectsMap)
val aggIndex = KeyIndex.aggregate(scopedKeys.toVector, extra(keyIndex), projectsMap)
val configsMap: Map[String, Seq[Configuration]] = Map(projects.values.toSeq flatMap { bu =>
bu.defined map {
case (k, v) => (k, v.configurations)
}
}: _*)
val keyIndex = KeyIndex(scopedKeys.toVector, projectsMap, configsMap)
val aggIndex = KeyIndex.aggregate(scopedKeys.toVector, extra(keyIndex), projectsMap, configsMap)
new StructureIndex(
Index.stringToKeyMap(attributeKeys),
Index.taskToKeyMap(data),

View File

@ -3,12 +3,13 @@ package sbt
import Project._
import sbt.internal.util.Types.idFun
import sbt.internal.TestBuild._
import sbt.librarymanagement.Configuration
import org.scalacheck._
import Prop._
import Gen._
object Delegates extends Properties("delegates") {
property("generate non-empty configs") = forAll { (c: Seq[Config]) =>
property("generate non-empty configs") = forAll { (c: Seq[Configuration]) =>
c.nonEmpty
}
property("generate non-empty tasks") = forAll { (t: Seq[Taskk]) =>

View File

@ -22,13 +22,17 @@ object ParseKey extends Properties("Key parser test") {
property("An explicitly specified axis is always parsed to that explicit value") =
forAllNoShrink(structureDefinedKey) { (skm: StructureKeyMask) =>
import skm.{ structure, key, mask }
import skm.{ structure, key, mask => mask0 }
val hasZeroConfig = key.scope.config == Zero
val mask = if (hasZeroConfig) mask0.copy(project = true) else mask0
val expected = resolve(structure, key, mask)
val string = displayMasked(key, mask)
("Key: " + displayFull(key)) |:
parseExpected(structure, string, expected, mask)
// Note that this explicitly displays the configuration axis set to Zero.
// This is to disambiguate `proj/Zero/name`, which could render potentially
// as `Zero/name`, but could be interpretted as `Zero/Zero/name`.
val s = displayMasked(key, mask, hasZeroConfig)
("Key: " + displayPedantic(key)) |:
parseExpected(structure, s, expected, mask)
}
property("An unspecified project axis resolves to the current project") =
@ -37,13 +41,16 @@ object ParseKey extends Properties("Key parser test") {
val mask = skm.mask.copy(project = false)
val string = displayMasked(key, mask)
// skip when config axis is set to Zero
val hasZeroConfig = key.scope.config == Zero
("Key: " + displayFull(key)) |:
("Key: " + displayPedantic(key)) |:
("Mask: " + mask) |:
("Current: " + structure.current) |:
parse(structure, string) {
case Left(err) => false
case Right(sk) => sk.scope.project == Select(structure.current)
case Left(err) => false
case Right(sk) if hasZeroConfig => true
case Right(sk) => sk.scope.project == Select(structure.current)
}
}
@ -53,7 +60,7 @@ object ParseKey extends Properties("Key parser test") {
val mask = skm.mask.copy(task = false)
val string = displayMasked(key, mask)
("Key: " + displayFull(key)) |:
("Key: " + displayPedantic(key)) |:
("Mask: " + mask) |:
parse(structure, string) {
case Left(err) => false
@ -69,15 +76,18 @@ object ParseKey extends Properties("Key parser test") {
val string = displayMasked(key, mask)
val resolvedConfig = Resolve.resolveConfig(structure.extra, key.key, mask)(key.scope).config
("Key: " + displayFull(key)) |:
("Key: " + displayPedantic(key)) |:
("Mask: " + mask) |:
("Expected configuration: " + resolvedConfig.map(_.name)) |:
parse(structure, string) {
case Right(sk) => sk.scope.config == resolvedConfig
case Right(sk) => (sk.scope.config == resolvedConfig) || (sk.scope == Scope.GlobalScope)
case Left(err) => false
}
}
def displayPedantic(scoped: ScopedKey[_]): String =
Scope.displayPedantic(scoped.scope, scoped.key.label)
lazy val structureDefinedKey: Gen[StructureKeyMask] = structureKeyMask { s =>
for (scope <- TestBuild.scope(s.env); key <- oneOf(s.allAttributeKeys.toSeq))
yield ScopedKey(scope, key)
@ -101,7 +111,10 @@ object ParseKey extends Properties("Key parser test") {
("Mask: " + mask) |:
parse(structure, s) {
case Left(err) => false
case Right(sk) => Project.equal(sk, expected, mask)
case Right(sk) =>
(s"${sk}.key == ${expected}.key: ${sk.key == expected.key}") |:
(s"${sk.scope} == ${expected.scope}: ${Scope.equal(sk.scope, expected.scope, mask)}") |:
Project.equal(sk, expected, mask)
}
def parse(structure: Structure, s: String)(f: Either[String, ScopedKey[_]] => Prop): Prop = {

View File

@ -5,6 +5,7 @@ import Def.{ ScopedKey, Setting }
import sbt.internal.util.{ AttributeKey, AttributeMap, Relation, Settings }
import sbt.internal.util.Types.{ const, some }
import sbt.internal.util.complete.Parser
import sbt.librarymanagement.Configuration
import java.net.URI
import org.scalacheck._
@ -119,7 +120,7 @@ abstract class TestBuild {
lazy val allProjects = builds.flatMap(_.allProjects)
def rootProject(uri: URI): String = buildMap(uri).root.id
def inheritConfig(ref: ResolvedReference, config: ConfigKey) =
projectFor(ref).confMap(config.name).extended map toConfigKey
projectFor(ref).confMap(config.name).extendsConfigs map toConfigKey
def inheritTask(task: AttributeKey[_]) = taskMap.get(task) match {
case None => Nil; case Some(t) => t.delegates map getKey
}
@ -144,7 +145,7 @@ abstract class TestBuild {
} yield Scope(project = ref, config = c, task = t, extra = Zero)
}
def getKey: Taskk => AttributeKey[_] = _.key
def toConfigKey: Config => ConfigKey = c => ConfigKey(c.name)
def toConfigKey: Configuration => ConfigKey = c => ConfigKey(c.name)
final class Build(val uri: URI, val projects: Seq[Proj]) {
override def toString = "Build " + uri.toString + " :\n " + projects.mkString("\n ")
val allProjects = projects map { p =>
@ -156,7 +157,7 @@ abstract class TestBuild {
final class Proj(
val id: String,
val delegates: Seq[ProjectRef],
val configurations: Seq[Config]
val configurations: Seq[Configuration]
) {
override def toString =
"Project " + id + "\n Delegates:\n " + delegates.mkString("\n ") +
@ -164,9 +165,6 @@ abstract class TestBuild {
val confMap = mapBy(configurations)(_.name)
}
final class Config(val name: String, val extended: Seq[Config]) {
override def toString = name + " (extends: " + extended.map(_.name).mkString(", ") + ")"
}
final class Taskk(val key: AttributeKey[String], val delegates: Seq[Taskk]) {
override def toString =
key.label + " (delegates: " + delegates.map(_.key.label).mkString(", ") + ")"
@ -222,7 +220,15 @@ abstract class TestBuild {
val keys = data.allKeys((s, key) => ScopedKey(s, key))
val keyMap = keys.map(k => (k.key.label, k.key)).toMap[String, AttributeKey[_]]
val projectsMap = env.builds.map(b => (b.uri, b.projects.map(_.id).toSet)).toMap
new Structure(env, current, data, KeyIndex(keys, projectsMap), keyMap)
val confs = for {
b <- env.builds.toVector
p <- b.projects.toVector
c <- p.configurations.toVector
} yield c
val confMap = Map(confs map { c =>
(c.name, Seq(c))
}: _*)
new Structure(env, current, data, KeyIndex(keys, projectsMap, confMap), keyMap)
}
implicit lazy val mkEnv: Gen[Env] = {
@ -239,7 +245,14 @@ abstract class TestBuild {
}
implicit lazy val idGen: Gen[String] =
for (size <- chooseShrinkable(1, MaxIDSize); cs <- listOfN(size, alphaChar)) yield cs.mkString
for {
size <- chooseShrinkable(1, MaxIDSize)
cs <- listOfN(size, alphaChar)
} yield {
val xs = cs.mkString
xs.take(1).toLowerCase + xs.drop(1)
}
implicit lazy val optIDGen: Gen[Option[String]] = frequency((1, idGen map some.fn), (1, None))
implicit lazy val uriGen: Gen[URI] = for (sch <- idGen; ssp <- idGen; frag <- optIDGen)
yield new URI(sch, ssp, frag.orNull)
@ -256,7 +269,7 @@ abstract class TestBuild {
implicit def genProjects(build: URI)(implicit genID: Gen[String],
maxDeps: Gen[Int],
count: Gen[Int],
confs: Gen[Seq[Config]]): Gen[Seq[Proj]] =
confs: Gen[Seq[Configuration]]): Gen[Seq[Proj]] =
genAcyclic(maxDeps, genID, count) { (id: String) =>
for (cs <- confs) yield { (deps: Seq[Proj]) =>
new Proj(id, deps.map { dep =>
@ -264,10 +277,16 @@ abstract class TestBuild {
}, cs)
}
}
def genConfigs(implicit genName: Gen[String],
maxDeps: Gen[Int],
count: Gen[Int]): Gen[Seq[Config]] =
genAcyclicDirect[Config, String](maxDeps, genName, count)((key, deps) => new Config(key, deps))
count: Gen[Int]): Gen[Seq[Configuration]] =
genAcyclicDirect[Configuration, String](maxDeps, genName, count)(
(key, deps) =>
Configuration
.of(key.capitalize, key)
.withExtendsConfigs(deps.toVector))
def genTasks(implicit genName: Gen[String], maxDeps: Gen[Int], count: Gen[Int]): Gen[Seq[Taskk]] =
genAcyclicDirect[Taskk, String](maxDeps, genName, count)((key, deps) =>
new Taskk(AttributeKey[String](key), deps))

View File

@ -202,7 +202,7 @@ object SettingQueryTest extends org.specs2.mutable.Specification {
"scalaVersion" in qko("Not a valid project ID: scalaVersion\\nscalaVersion\\n ^")
"t/scalacOptions" in qko(
s"Key {$baseUri}t/compile:scalacOptions is a task, can only query settings")
s"""Key ProjectRef(uri(\\"$baseUri\\"),\\"t\\")/Compile/scalacOptions is a task, can only query settings""")
"t/fooo" in qko(
"Expected ':' (if selecting a configuration)\\nNot a valid key: fooo (similar: fork)\\nt/fooo\\n ^")
}

View File

@ -0,0 +1,49 @@
### Fixes with compatibility implications
-
### Improvements
- Unifies sbt shell and build.sbt syntax. See below.
### Bug fixes
-
### Unified slash syntax for sbt shell and build.sbt
This adds unified slash syntax for both sbt shell and the build.sbt DSL.
Instead of the current `<project-id>/config:intask::key`, this adds
`<project-id>/<config-ident>/intask/key` where `<config-ident>` is the Scala identifier
notation for the configurations like `Compile` and `Test`. (The old shell syntax will continue to function)
These examples work both from the shell and in build.sbt.
Global / cancelable
ThisBuild / scalaVersion
Test / test
root / Compile / compile / scalacOptions
ProjectRef(uri("file:/xxx/helloworld/"),"root")/Compile/scalacOptions
Zero / Zero / name
The inspect command now outputs something that can be copy-pasted:
> inspect compile
[info] Task: sbt.inc.Analysis
[info] Description:
[info] Compiles sources.
[info] Provided by:
[info] ProjectRef(uri("file:/xxx/helloworld/"),"root")/Compile/compile
[info] Defined at:
[info] (sbt.Defaults) Defaults.scala:326
[info] Dependencies:
[info] Compile/manipulateBytecode
[info] Compile/incCompileSetup
....
[#3434][3434] by [@eed3si9n][@eed3si9n]
[3434]: https://github.com/sbt/sbt/pull/3434
[@eed3si9n]: https://github.com/eed3si9n
[@dwijnand]: http://github.com/dwijnand

View File

@ -16,6 +16,7 @@ package object sbt
with sbt.ScopeFilter.Make
with sbt.BuildSyntax
with sbt.OptionSyntax
with sbt.SlashSyntax
with sbt.Import {
// IO
def uri(s: String): URI = new URI(s)

View File

@ -1,4 +1,4 @@
lazy val akey = AttributeKey[Int]("TestKey")
lazy val akey = AttributeKey[Int]("testKey")
lazy val testTask = taskKey[String]("")
lazy val check = inputKey[Unit]("")

View File

@ -4,4 +4,4 @@ organization := "org.example"
proguardSettings
useJGit
useJGit

View File

@ -0,0 +1,7 @@
lazy val plugins = (project in file("."))
.dependsOn(proguard, git)
// e7b4732969c137db1b5
// d4974f7362bf55d3f52
lazy val proguard = uri("git://github.com/sbt/sbt-proguard.git#e7b4732969c137db1b5")
lazy val git = uri("git://github.com/sbt/sbt-git.git#2e7c2503850698d60bb")

View File

@ -0,0 +1,70 @@
import Dependencies._
import sbt.internal.CommandStrings.{ inspectBrief, inspectDetailed }
import sbt.internal.Inspect
lazy val root = (project in file("."))
.settings(
Global / cancelable := true,
ThisBuild / scalaVersion := "2.12.3",
console / scalacOptions += "-deprecation",
Compile / console / scalacOptions += "-Ywarn-numeric-widen",
projA / Compile / console / scalacOptions += "-feature",
Zero / Zero / name := "foo",
libraryDependencies += uTest % Test,
testFrameworks += new TestFramework("utest.runner.Framework"),
commands += Command("inspectCheck", inspectBrief, inspectDetailed)(Inspect.parser) {
case (s, (option, sk)) =>
val actual = Inspect.output(s, option, sk)
val expected = s"""Task: Unit
Description:
\tExecutes all tests.
Provided by:
\tProjectRef(uri("${baseDirectory.value.toURI}"),"root")/Test/test
Defined at:
\t(sbt.Defaults.testTasks) Defaults.scala:670
Dependencies:
\tTest/executeTests
\tTest/test/streams
\tTest/state
\tTest/test/testResultLogger
Delegates:
\tTest/test
\tRuntime/test
\tCompile/test
\ttest
\tThisBuild/Test/test
\tThisBuild/Runtime/test
\tThisBuild/Compile/test
\tThisBuild/test
\tZero/Test/test
\tZero/Runtime/test
\tZero/Compile/test
\tGlobal/test
Related:
\tprojA/Test/test"""
if (processText(actual) == processText(expected)) ()
else {
sys.error(s"""actual:
$actual
expected:
$expected
""")
}
s.log.info(actual)
s
}
)
lazy val projA = (project in file("a"))
def processText(s: String): Vector[String] = {
val xs = s.split(IO.Newline).toVector
.map( _.trim )
// declared location of the task is unstable.
.filterNot( _.contains("Defaults.scala") )
xs
}

View File

@ -0,0 +1,5 @@
import sbt._
object Dependencies {
val uTest = "com.lihaoyi" %% "utest" % "0.5.3"
}

View File

@ -0,0 +1,14 @@
package example
import utest._
import java.nio.file.{ Files, Path, Paths }
object HelloTests extends TestSuite {
val tests = Tests {
'test1 - {
val p = Paths.get("target", "foo")
Files.createFile(p)
}
}
}

View File

@ -0,0 +1,28 @@
> root/Compile/compile
# "Global" now works in shell.
> show Global/cancelable
# "Global" now works in shell. optional whitespace around /
> show Global / cancelable
# "ThisBuild" now works in shell.
> show ThisBuild/scalaVersion
# "ThisBuild" now works in shell. optional whitespace around /
> show ThisBuild / scalaVersion
$ mkdir target
> Test/test
# Check the side-effect of executing uTest
$ exists target/foo
# use all axes
> show root/Compile/compile/scalacOptions
# use all axes. optional whitespace around /
> show root / Compile / compile / scalacOptions
> show Zero / Zero / name
> inspectCheck test