From 33a01f3ceb30e837508a79b3baae2527b15d4a25 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 27 Sep 2017 02:21:56 -0400 Subject: [PATCH] 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 `/config:intask::key`, this adds `//intask/key` where 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 --- build.sbt | 8 +- .../scala/sbt/internal/util/Attributes.scala | 5 + .../src/test/scala/SettingsTest.scala | 7 +- .../sbt/internal/util/complete/Parsers.scala | 9 ++ main-settings/src/main/scala/sbt/Def.scala | 32 +++- .../src/main/scala/sbt/Reference.scala | 2 +- main-settings/src/main/scala/sbt/Scope.scala | 89 +++++++++-- main/src/main/scala/sbt/Defaults.scala | 9 +- main/src/main/scala/sbt/SlashSyntax.scala | 148 ++++++++++++++++++ main/src/main/scala/sbt/internal/Act.scala | 100 ++++++++++-- .../main/scala/sbt/internal/KeyIndex.scala | 72 +++++++-- main/src/main/scala/sbt/internal/Load.scala | 9 +- main/src/test/scala/Delegates.scala | 3 +- main/src/test/scala/ParseKey.scala | 37 +++-- .../test/scala/sbt/internal/TestBuild.scala | 41 +++-- .../internal/server/SettingQueryTest.scala | 2 +- notes/1.1.0/unified-shell.markdown | 49 ++++++ sbt/src/main/scala/package.scala | 1 + .../actions/update-state-fail/build.sbt | 2 +- .../sbt-test/project/source-plugins/build.sbt | 2 +- .../source-plugins/project/plugins.sbt | 7 + sbt/src/sbt-test/project/unified/build.sbt | 70 +++++++++ .../unified/project/Dependencies.scala | 5 + .../src/test/scala/example/HelloTests.scala | 14 ++ sbt/src/sbt-test/project/unified/test | 28 ++++ 25 files changed, 673 insertions(+), 78 deletions(-) create mode 100644 main/src/main/scala/sbt/SlashSyntax.scala create mode 100644 notes/1.1.0/unified-shell.markdown create mode 100644 sbt/src/sbt-test/project/source-plugins/project/plugins.sbt create mode 100644 sbt/src/sbt-test/project/unified/build.sbt create mode 100644 sbt/src/sbt-test/project/unified/project/Dependencies.scala create mode 100644 sbt/src/sbt-test/project/unified/src/test/scala/example/HelloTests.scala create mode 100644 sbt/src/sbt-test/project/unified/test diff --git a/build.sbt b/build.sbt index df0ee03e5..5e695c989 100644 --- a/build.sbt +++ b/build.sbt @@ -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( diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Attributes.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Attributes.scala index 7c24cfd29..80de459b2 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Attributes.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Attributes.scala @@ -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 diff --git a/internal/util-collection/src/test/scala/SettingsTest.scala b/internal/util-collection/src/test/scala/SettingsTest.scala index 4dc8421d6..65878a59c 100644 --- a/internal/util-collection/src/test/scala/SettingsTest.scala +++ b/internal/util-collection/src/test/scala/SettingsTest.scala @@ -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) diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parsers.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parsers.scala index 55dea1033..3e15f3a14 100644 --- a/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parsers.scala +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parsers.scala @@ -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 } diff --git a/main-settings/src/main/scala/sbt/Def.scala b/main-settings/src/main/scala/sbt/Def.scala index 534e674e3..8f3eca2e4 100644 --- a/main-settings/src/main/scala/sbt/Def.scala +++ b/main-settings/src/main/scala/sbt/Def.scala @@ -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 { diff --git a/main-settings/src/main/scala/sbt/Reference.scala b/main-settings/src/main/scala/sbt/Reference.scala index cd7e3c08a..2867bed95 100644 --- a/main-settings/src/main/scala/sbt/Reference.scala +++ b/main-settings/src/main/scala/sbt/Reference.scala @@ -91,7 +91,7 @@ object Reference { case LocalRootProject => "{}" case LocalProject(id) => "{}" + id case RootProject(uri) => "{" + uri + " }" - case ProjectRef(uri, id) => "{" + uri + "}" + id + case ProjectRef(uri, id) => s"""ProjectRef(uri("$uri"),"$id")""" } def buildURI(ref: ResolvedReference): URI = ref match { diff --git a/main-settings/src/main/scala/sbt/Scope.scala b/main-settings/src/main/scala/sbt/Scope.scala index 25dd46c85..2fa44221f 100644 --- a/main-settings/src/main/scala/sbt/Scope.scala +++ b/main-settings/src/main/scala/sbt/Scope.scala @@ -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) + "/" diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 638ccbe21..8a4ceab4a 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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, diff --git a/main/src/main/scala/sbt/SlashSyntax.scala b/main/src/main/scala/sbt/SlashSyntax.scala new file mode 100644 index 000000000..b6a8b560b --- /dev/null +++ b/main/src/main/scala/sbt/SlashSyntax.scala @@ -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) + } +} diff --git a/main/src/main/scala/sbt/internal/Act.scala b/main/src/main/scala/sbt/internal/Act.scala index c34640143..c72e89dda 100644 --- a/main/src/main/scala/sbt/internal/Act.scala +++ b/main/src/main/scala/sbt/internal/Act.scala @@ -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] diff --git a/main/src/main/scala/sbt/internal/KeyIndex.scala b/main/src/main/scala/sbt/internal/KeyIndex.scala index 48ce42d27..573fed713 100644 --- a/main/src/main/scala/sbt/internal/KeyIndex.scala +++ b/main/src/main/scala/sbt/internal/KeyIndex.scala @@ -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], diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index 30e802782..0ed05e692 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -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), diff --git a/main/src/test/scala/Delegates.scala b/main/src/test/scala/Delegates.scala index 7136b943d..df2dec96b 100644 --- a/main/src/test/scala/Delegates.scala +++ b/main/src/test/scala/Delegates.scala @@ -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]) => diff --git a/main/src/test/scala/ParseKey.scala b/main/src/test/scala/ParseKey.scala index 2d2198f10..a0b62d8c3 100644 --- a/main/src/test/scala/ParseKey.scala +++ b/main/src/test/scala/ParseKey.scala @@ -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 = { diff --git a/main/src/test/scala/sbt/internal/TestBuild.scala b/main/src/test/scala/sbt/internal/TestBuild.scala index 5f0a1dda4..96a4b1383 100644 --- a/main/src/test/scala/sbt/internal/TestBuild.scala +++ b/main/src/test/scala/sbt/internal/TestBuild.scala @@ -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)) diff --git a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala index 3ef94c83b..c928e108f 100644 --- a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala +++ b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala @@ -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 ^") } diff --git a/notes/1.1.0/unified-shell.markdown b/notes/1.1.0/unified-shell.markdown new file mode 100644 index 000000000..f10435046 --- /dev/null +++ b/notes/1.1.0/unified-shell.markdown @@ -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 `/config:intask::key`, this adds +`//intask/key` where `` 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 diff --git a/sbt/src/main/scala/package.scala b/sbt/src/main/scala/package.scala index 94603839f..0954f8306 100644 --- a/sbt/src/main/scala/package.scala +++ b/sbt/src/main/scala/package.scala @@ -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) diff --git a/sbt/src/sbt-test/actions/update-state-fail/build.sbt b/sbt/src/sbt-test/actions/update-state-fail/build.sbt index cb3695fd4..6a5c0fc9f 100644 --- a/sbt/src/sbt-test/actions/update-state-fail/build.sbt +++ b/sbt/src/sbt-test/actions/update-state-fail/build.sbt @@ -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]("") diff --git a/sbt/src/sbt-test/project/source-plugins/build.sbt b/sbt/src/sbt-test/project/source-plugins/build.sbt index 1e0d4c099..0b5906837 100644 --- a/sbt/src/sbt-test/project/source-plugins/build.sbt +++ b/sbt/src/sbt-test/project/source-plugins/build.sbt @@ -4,4 +4,4 @@ organization := "org.example" proguardSettings -useJGit \ No newline at end of file +useJGit diff --git a/sbt/src/sbt-test/project/source-plugins/project/plugins.sbt b/sbt/src/sbt-test/project/source-plugins/project/plugins.sbt new file mode 100644 index 000000000..6dc6ae42e --- /dev/null +++ b/sbt/src/sbt-test/project/source-plugins/project/plugins.sbt @@ -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") diff --git a/sbt/src/sbt-test/project/unified/build.sbt b/sbt/src/sbt-test/project/unified/build.sbt new file mode 100644 index 000000000..fdbdaf2c0 --- /dev/null +++ b/sbt/src/sbt-test/project/unified/build.sbt @@ -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 +} diff --git a/sbt/src/sbt-test/project/unified/project/Dependencies.scala b/sbt/src/sbt-test/project/unified/project/Dependencies.scala new file mode 100644 index 000000000..0d84ec7d4 --- /dev/null +++ b/sbt/src/sbt-test/project/unified/project/Dependencies.scala @@ -0,0 +1,5 @@ +import sbt._ + +object Dependencies { + val uTest = "com.lihaoyi" %% "utest" % "0.5.3" +} diff --git a/sbt/src/sbt-test/project/unified/src/test/scala/example/HelloTests.scala b/sbt/src/sbt-test/project/unified/src/test/scala/example/HelloTests.scala new file mode 100644 index 000000000..a9b386667 --- /dev/null +++ b/sbt/src/sbt-test/project/unified/src/test/scala/example/HelloTests.scala @@ -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) + } + } +} diff --git a/sbt/src/sbt-test/project/unified/test b/sbt/src/sbt-test/project/unified/test new file mode 100644 index 000000000..bbfaab29b --- /dev/null +++ b/sbt/src/sbt-test/project/unified/test @@ -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