Skip to content

Commit

Permalink
Print out CLI help message when inspecting commands (#2522)
Browse files Browse the repository at this point in the history
Fixes #2198

There's a bit of duplication if a user both adds `@mainargs.arg(doc =
"...")` scala annotation as well as a `@param foo` scaladoc annotation,
but that's an inherent issue with mainargs and not something we aim to
fix here.

We render the mainargs CLI help message the same way it's done in
mainargs. There isn't a nice helper that renders the whole thing at
once, so i have to piece it together from various helpers, but it's not
too complex and works out ok.

Tested via additions to
`integration.feature[docannotations].local.test`, and manually

```bash
$ ./mill -i dev.run example/basic/1-simple-scala -i inspect run # command with 1 arg
run(JavaModule.scala:720)
    Runs this modules code in a subprocess and waits for it to finish

    args <str>...

Inputs:
    finalMainClass
    runClasspath
    forkArgs
    forkEnv
    forkWorkingDir
    runUseArgsFile

$ ./mill -i dev.run example/basic/1-simple-scala -i inspect compile # not a command
compile(ScalaModule.scala:211)
    Compiles the current module to generate compiled classfiles/bytecode.

    When you override this, you probably also want to override [[bspCompileClassesPath]].

Inputs:
    scalaVersion
    upstreamCompileOutput
    allSourceFiles
    compileClasspath
    javacOptions
    scalaOrganization
    allScalacOptions
    scalaCompilerClasspath
    scalacPluginClasspath
    zincReportCachedProblems

$ ./mill -i dev.run example/basic/1-simple-scala -i inspect console # command with no args
console(ScalaModule.scala:369)
    Opens up a Scala console with your module and all dependencies present,
    for you to test and operate your code interactively.

Inputs:
    scalaVersion
    runClasspath
    scalaCompilerClasspath
    forkArgs
    forkEnv
    forkWorkingDir

$ ./mill -i dev.run example/basic/1-simple-scala -i inspect ivyDepsTree # command with lots of args
ivyDepsTree(JavaModule.scala:688)
    Command to print the transitive dependency tree to STDOUT.

    --inverse              Invert the tree representation, so that the root is on the bottom val
                           inverse (will be forced when used with whatDependsOn)
    --whatDependsOn <str>  Possible list of modules (org:artifact) to target in the tree in order to
                           see where a dependency stems from.
    --withCompile          Include the compile-time only dependencies (`compileIvyDeps`, provided
                           scope) into the tree.
    --withRuntime          Include the runtime dependencies (`runIvyDeps`, runtime scope) into the
                           tree.

Inputs:
    transitiveIvyDeps
    scalaVersion
    scalaVersion
    scalaOrganization
    scalaVersion
```

I also removed the `numOverrides` field on `mill.define.Discover`, since
it's unused since #1600 landed a
while back
  • Loading branch information
lihaoyi authored May 16, 2023
1 parent ef45438 commit 156c7a4
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 44 deletions.
4 changes: 2 additions & 2 deletions integration/feature/docannotations/repo/build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ object core extends JavaModule {
object test extends Tests with JUnitTests

/**
* Core Task Docz!
* Core Target Docz!
*/
def task = T {
def target = T {
import collection.JavaConverters._
println(this.getClass.getClassLoader.getResources("scalac-plugin.xml").asScala.toList)
"Hello!"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,109 @@ package mill.integration
import utest._

object DocAnnotationsTests extends IntegrationTestSuite {
def globMatches(glob: String, input: String) = {
StringContext
.glob(
// Normalize the line separator to be `\n` for comparisons
glob.stripMargin.linesIterator.mkString("\n").split("\\.\\.\\."),
input.linesIterator.mkString("\n")
)
.isDefined
}

val tests = Tests {
initWorkspace()
test("test") - {
val res = eval("inspect", "core.test.ivyDeps")
assert(res == true)

val inheritedIvyDeps = ujson.read(meta("inspect"))("value").str
assert(
inheritedIvyDeps.contains("core.test.ivyDeps"),
inheritedIvyDeps.contains("Overriden ivyDeps Docs!!!"),
inheritedIvyDeps.contains("Any ivy dependencies you want to add to this Module")
globMatches(
"""core.test.ivyDeps(build.sc:...)
| Overriden ivyDeps Docs!!!
|
| Any ivy dependencies you want to add to this Module, in the format
| ivy"org::name:version" for Scala dependencies or ivy"org:name:version"
| for Java dependencies
|
|Inputs:
|""".stripMargin,
inheritedIvyDeps
)
)

assert(eval("inspect", "core.task"))
val task = ujson.read(meta("inspect"))("value").str
assert(eval("inspect", "core.target"))
val target = ujson.read(meta("inspect"))("value").str
pprint.log(target)
assert(
task.contains("Core Task Docz!")
globMatches(
"""core.target(build.sc:...)
| Core Target Docz!
|
|Inputs:
|""",
target
)
)

assert(eval("inspect", "inspect"))
val doc = ujson.read(meta("inspect"))("value").str
assert(
doc.contains("Displays metadata about the given task without actually running it.")
globMatches(
"""inspect(MainModule.scala:...)
| Displays metadata about the given task without actually running it.
|
|Inputs:
|""".stripMargin,
doc
)
)

assert(eval("inspect", "core.run"))
val run = ujson.read(meta("inspect"))("value").str

assert(
globMatches(
"""core.run(JavaModule.scala:...)
| Runs this module's code in a subprocess and waits for it to finish
|
| args <str>...
|
|Inputs:
| core.finalMainClass
| core.runClasspath
| core.forkArgs
| core.forkEnv
| core.forkWorkingDir
| core.runUseArgsFile
|""",
run
)
)

assert(eval("inspect", "core.ivyDepsTree"))

val ivyDepsTree = ujson.read(meta("inspect"))("value").str
assert(
globMatches(
"""core.ivyDepsTree(JavaModule.scala:...)
| Command to print the transitive dependency tree to STDOUT.
|
| --inverse Invert the tree representation, so that the root is on the bottom val
| inverse (will be forced when used with whatDependsOn)
| --whatDependsOn <str> Possible list of modules (org:artifact) to target in the tree in order to
| see where a dependency stems from.
| --withCompile Include the compile-time only dependencies (`compileIvyDeps`, provided
| scope) into the tree.
| --withRuntime Include the runtime dependencies (`runIvyDeps`, runtime scope) into the
| tree.
|
|Inputs:
| core.transitiveIvyDeps
|""".stripMargin,
ivyDepsTree
)
)
}
}
Expand Down
27 changes: 11 additions & 16 deletions main/define/src/mill/define/Discover.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@ import scala.reflect.macros.blackbox
* the `T.command` methods we find. This mapping from `Class[_]` to `MainData`
* can then be used later to look up the `MainData` for any module.
*/
case class Discover[T] private (value: Map[Class[_], Seq[(Int, mainargs.MainData[_, _])]]) {
private[mill] def copy(value: Map[Class[_], Seq[(Int, mainargs.MainData[_, _])]] = value)
: Discover[T] =
case class Discover[T] private (value: Map[Class[_], Seq[mainargs.MainData[_, _]]]) {
private[mill] def copy(value: Map[Class[_], Seq[mainargs.MainData[_, _]]] = value): Discover[T] =
new Discover[T](value)
}
object Discover {
def apply[T](value: Map[Class[_], Seq[(Int, mainargs.MainData[_, _])]]): Discover[T] =
def apply[T](value: Map[Class[_], Seq[mainargs.MainData[_, _]]]): Discover[T] =
new Discover[T](value)
def apply[T]: Discover[T] = macro Router.applyImpl[T]

private def unapply[T](discover: Discover[T])
: Option[Map[Class[_], Seq[(Int, mainargs.MainData[_, _])]]] = Some(discover.value)
: Option[Map[Class[_], Seq[mainargs.MainData[_, _]]]] = Some(discover.value)

private class Router(val ctx: blackbox.Context) extends mainargs.Macros(ctx) {
import c.universe._
Expand Down Expand Up @@ -90,18 +89,14 @@ object Discover {
for {
m <- methods.toList
if m.returnType <:< weakTypeOf[mill.define.Command[_]]
} yield (
m.overrides.length,
extractMethod(
m.name,
m.paramLists.flatten,
m.pos,
m.annotations.find(_.tree.tpe =:= typeOf[mainargs.main]),
curCls,
weakTypeOf[Any]
)
} yield extractMethod(
m.name,
m.paramLists.flatten,
m.pos,
m.annotations.find(_.tree.tpe =:= typeOf[mainargs.main]),
curCls,
weakTypeOf[Any]
)

}
if overridesRoutes.nonEmpty
} yield {
Expand Down
9 changes: 4 additions & 5 deletions main/resolve/src/mill/resolve/Resolve.scala
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ object Resolve {
(cls, entryPoints) <- discover.value
if cls.isAssignableFrom(target.getClass)
ep <- entryPoints
if ep._2.name == name
if ep.name == name
} yield {
def withNullDefault(a: mainargs.ArgSig): mainargs.ArgSig = {
if (a.default.nonEmpty) a
Expand All @@ -133,7 +133,6 @@ object Resolve {
}

val flattenedArgSigsWithDefaults = ep
._2
.flattenedArgSigs
.map { case (arg, term) => (withNullDefault(arg), term) }

Expand All @@ -142,9 +141,9 @@ object Resolve {
flattenedArgSigsWithDefaults,
allowPositional = true,
allowRepeats = false,
allowLeftover = ep._2.argSigs0.exists(_.reader.isLeftover)
allowLeftover = ep.argSigs0.exists(_.reader.isLeftover)
).flatMap { (grouped: TokenGrouping[_]) =>
val mainData = ep._2.asInstanceOf[MainData[_, Any]]
val mainData = ep.asInstanceOf[MainData[_, Any]]
val mainDataWithDefaults = mainData
.copy(argSigs0 = mainData.argSigs0.map(withNullDefault))

Expand All @@ -159,7 +158,7 @@ object Resolve {
case f: mainargs.Result.Failure =>
Left(
mainargs.Renderer.renderResult(
ep._2,
ep,
f,
totalWidth = 100,
printHelpOnError = true,
Expand Down
64 changes: 50 additions & 14 deletions main/src/mill/main/MainModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@ package mill.main

import java.util.concurrent.LinkedBlockingQueue
import mill.{BuildInfo, T}
import mill.api.{Ctx, Logger, PathRef, Result, internal}
import mill.define.{Command, NamedTask, Segments, TargetImpl, Task}
import mill.api.{Ctx, Logger, PathRef, Result}
import mill.define.{Command, NamedTask, Segments, Task}
import mill.eval.{Evaluator, EvaluatorPaths}
import mill.resolve.{Resolve, SelectMode}
import mill.resolve.SelectMode.Separated
import mill.util.{PrintLogger, Watchable}
import pprint.{Renderer, Tree, Truncated}
import ujson.Value

import scala.collection.mutable
import scala.util.chaining.scalaUtilChainingOps

object MainModule {

Expand Down Expand Up @@ -234,7 +232,42 @@ trait MainModule extends mill.Module {
for (a <- annots.distinct)
yield mill.util.Util.cleanupScaladoc(a.value).map("\n " + _).mkString

pprint.Tree.Lazy(ctx =>
pprint.Tree.Lazy { ctx =>
val mainMethodSig =
if (t.asCommand.isEmpty) List()
else {
val mainDataOpt = evaluator
.rootModule
.millDiscover
.value
.get(t.ctx.enclosingCls)
.flatMap(_.find(_.name == t.ctx.segments.parts.last))

mainDataOpt match {
case Some(mainData) if mainData.renderedArgSigs.nonEmpty =>
val rendered = mainargs.Renderer.formatMainMethodSignature(
mainDataOpt.get,
leftIndent = 2,
totalWidth = 100,
leftColWidth = mainargs.Renderer.getLeftColWidth(mainData.renderedArgSigs),
docsOnNewLine = false,
customName = None,
customDoc = None
)

// trim first line containing command name, since we already render
// the command name below with the filename and line num
val trimmedRendered = rendered
.linesIterator
.drop(1)
.mkString("\n")

List("\n", trimmedRendered, "\n")

case _ => List()
}
}

Iterator(
ctx.applyPrefixColor(t.toString).toString,
"(",
Expand All @@ -244,12 +277,15 @@ trait MainModule extends mill.Module {
t.ctx.lineNum.toString,
")",
allDocs.mkString("\n"),
"\n",
"\n",
ctx.applyPrefixColor("Inputs").toString,
":"
) ++ t.inputs.distinct.iterator.flatMap(rec).map("\n " + _.render)
)
"\n"
) ++
mainMethodSig.iterator ++
Iterator(
"\n",
ctx.applyPrefixColor("Inputs").toString,
":"
) ++ t.inputs.distinct.iterator.flatMap(rec).map("\n " + _.render)
}
}

MainModule.resolveTasks(evaluator, targets, SelectMode.Multi) { tasks =>
Expand All @@ -266,9 +302,9 @@ trait MainModule extends mill.Module {
rendered = renderer.rec(tree, 0, 0).iter
truncated = new Truncated(rendered, defaults.defaultWidth, defaults.defaultHeight)
} yield {
new StringBuilder().tap { sb =>
for { str <- truncated ++ Iterator("\n") } sb.append(str)
}.toString()
val sb = new StringBuilder()
for { str <- truncated ++ Iterator("\n") } sb.append(str)
sb.toString()
}).mkString("\n")
T.log.outputStream.println(output)
fansi.Str(output).plainText
Expand Down

0 comments on commit 156c7a4

Please sign in to comment.