Skip to content

Add :doc command in REPL to show documentation #4669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions compiler/src/dotty/tools/repl/ParseResult.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ object TypeOf {
val command = ":type"
}

/**
* A command that is used to display the documentation associated with
* the given expression.
*/
case class DocOf(expr: String) extends Command
object DocOf {
val command = ":doc"
}

/** `:imports` lists the imports that have been explicitly imported during the
* session
*/
Expand Down Expand Up @@ -89,6 +98,7 @@ case object Help extends Command {
|:load <path> interpret lines in a file
|:quit exit the interpreter
|:type <expression> evaluate the type of the given expression
|:doc <expression> print the documentation for the given expresssion
|:imports show import history
|:reset reset the repl to its initial state, forgetting all session entries
""".stripMargin
Expand Down Expand Up @@ -117,6 +127,7 @@ object ParseResult {
case Imports.command => Imports
case Load.command => Load(arg)
case TypeOf.command => TypeOf(arg)
case DocOf.command => DocOf(arg)
case _ => UnknownCommand(cmd)
}
case _ =>
Expand Down
51 changes: 51 additions & 0 deletions compiler/src/dotty/tools/repl/ReplCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package dotty.tools.repl
import dotty.tools.backend.jvm.GenBCode
import dotty.tools.dotc.ast.Trees._
import dotty.tools.dotc.ast.{tpd, untpd}
import dotty.tools.dotc.ast.tpd.TreeOps
import dotty.tools.dotc.core.Comments.CommentsContext
import dotty.tools.dotc.core.Contexts._
import dotty.tools.dotc.core.Decorators._
import dotty.tools.dotc.core.Flags._
import dotty.tools.dotc.core.Names._
import dotty.tools.dotc.core.Phases
import dotty.tools.dotc.core.Phases.Phase
import dotty.tools.dotc.core.StdNames._
import dotty.tools.dotc.core.Symbols._
import dotty.tools.dotc.reporting.diagnostic.messages
import dotty.tools.dotc.typer.{FrontEnd, ImportInfo}
import dotty.tools.dotc.util.Positions._
Expand Down Expand Up @@ -164,6 +167,54 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler {
}
}

def docOf(expr: String)(implicit state: State): Result[String] = {
implicit val ctx: Context = state.context

/**
* Extract the "selected" symbol from `tree`.
*
* Because the REPL typechecks an expression, special syntax is needed to get the documentation
* of certain symbols:
*
* - To select the documentation of classes, the user needs to pass a call to the class' constructor
* (e.g. `new Foo` to select `class Foo`)
* - When methods are overloaded, the user needs to enter a lambda to specify which functions he wants
* (e.g. `foo(_: Int)` to select `def foo(x: Int)` instead of `def foo(x: String)`
*
* This function returns the right symbol for the received expression, and all the symbols that are
* overridden.
*/
def extractSymbols(tree: tpd.Tree): Iterator[Symbol] = {
val sym = tree match {
case tree if tree.isInstantiation => tree.symbol.owner
case tpd.closureDef(defdef) => defdef.rhs.symbol
case _ => tree.symbol
}
Iterator(sym) ++ sym.allOverriddenSymbols
}

typeCheck(expr).map {
case ValDef(_, _, Block(stats, _)) if stats.nonEmpty =>
val stat = stats.last.asInstanceOf[tpd.Tree]
if (stat.tpe.isError) stat.tpe.show
else {
val docCtx = ctx.docCtx.get
val symbols = extractSymbols(stat)
val doc = symbols.collectFirst {
case sym if docCtx.docstrings.contains(sym) =>
docCtx.docstrings(sym).raw
}
doc.getOrElse(s"// No doc for `${expr}`")
}

case _ =>
"""Couldn't display the documentation for your expression, so sorry :(
|
|Please report this to my masters at github.com/lampepfl/dotty
""".stripMargin
}
}

final def typeCheck(expr: String, errorsAllowed: Boolean = false)(implicit state: State): Result[tpd.ValDef] = {

def wrapped(expr: String, sourceFile: SourceFile, state: State)(implicit ctx: Context): Result[untpd.PackageDef] = {
Expand Down
9 changes: 8 additions & 1 deletion compiler/src/dotty/tools/repl/ReplDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class ReplDriver(settings: Array[String],

/** Create a fresh and initialized context with IDE mode enabled */
private[this] def initialCtx = {
val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions).addMode(Mode.Interactive)
val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions).addMode(Mode.Interactive).addMode(Mode.ReadComments)
val ictx = setup(settings, rootCtx)._2
ictx.base.initialize()(ictx)
ictx
Expand Down Expand Up @@ -338,6 +338,13 @@ class ReplDriver(settings: Array[String],
)
state

case DocOf(expr) =>
compiler.docOf(expr)(newRun(state)).fold(
displayErrors,
res => out.println(SyntaxHighlighting(res))
)
state

case Quit =>
// end of the world!
state
Expand Down
158 changes: 158 additions & 0 deletions compiler/test/dotty/tools/repl/DocTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package dotty.tools
package repl

import org.junit.Test
import org.junit.Assert.assertEquals

class DocTests extends ReplTest {

@Test def docOfDef =
eval("/** doc */ def foo = 0").andThen { implicit s =>
assertEquals("/** doc */", doc("foo"))
}

@Test def docOfVal =
eval("/** doc */ val foo = 0").andThen { implicit s =>
assertEquals("/** doc */", doc("foo"))
}

@Test def docOfObject =
eval("/** doc */ object Foo").andThen { implicit s =>
assertEquals("/** doc */", doc("Foo"))
}

@Test def docOfClass =
eval("/** doc */ class Foo").andThen { implicit s =>
assertEquals("/** doc */", doc("new Foo"))
}

@Test def docOfTrait =
eval("/** doc */ trait Foo").andThen { implicit s =>
assertEquals("/** doc */", doc("new Foo"))
}

@Test def docOfDefInObject =
eval("object O { /** doc */ def foo = 0 }").andThen { implicit s =>
assertEquals("/** doc */", doc("O.foo"))
}

@Test def docOfValInObject =
eval("object O { /** doc */ val foo = 0 }").andThen { implicit s =>
assertEquals("/** doc */", doc("O.foo"))
}

@Test def docOfObjectInObject =
eval("object O { /** doc */ object Foo }").andThen { implicit s =>
assertEquals("/** doc */", doc("O.Foo"))
}

@Test def docOfClassInObject =
eval("object O { /** doc */ class Foo }").andThen { implicit s =>
assertEquals("/** doc */", doc("new O.Foo"))
}

@Test def docOfTraitInObject =
eval("object O { /** doc */ trait Foo }").andThen { implicit s =>
assertEquals("/** doc */", doc("new O.Foo"))
}

@Test def docOfDefInClass =
eval(
"""class C { /** doc */ def foo = 0 }
|val c = new C
""".stripMargin).andThen { implicit s =>
assertEquals("/** doc */", doc("c.foo"))
}

@Test def docOfValInClass =
eval(
"""class C { /** doc */ val foo = 0 }
|val c = new C
""".stripMargin).andThen { implicit s =>
assertEquals("/** doc */", doc("c.foo"))
}

@Test def docOfObjectInClass =
eval(
"""class C { /** doc */ object Foo }
|val c = new C
""".stripMargin).andThen { implicit s =>
assertEquals("/** doc */", doc("c.Foo"))
}

@Test def docOfClassInClass =
eval(
"""class C { /** doc */ class Foo }
|val c = new C
""".stripMargin).andThen { implicit s =>
assertEquals("/** doc */", doc("new c.Foo"))
}

@Test def docOfTraitInClass =
eval(
"""class C { /** doc */ trait Foo }
|val c = new C
""".stripMargin).andThen { implicit s =>
assertEquals("/** doc */", doc("new c.Foo"))
}

@Test def docOfOverloadedDef =
eval(
"""object O {
| /** doc0 */ def foo(x: Int) = x
| /** doc1 */ def foo(x: String) = x
|}
""".stripMargin).andThen { implicit s =>
assertEquals("/** doc0 */", doc("O.foo(_: Int)"))
assertEquals("/** doc1 */", doc("O.foo(_: String)"))
}

@Test def docOfInherited =
eval(
"""class C { /** doc */ def foo = 0 }
|object O extends C
""".stripMargin).andThen { implicit s =>
assertEquals("/** doc */", doc("O.foo"))
}

@Test def docOfOverride =
eval(
"""abstract class A {
| /** doc0 */ def foo(x: Int): Int = x + 1
| /** doc1 */ def foo(x: String): String = x + "foo"
|}
|object O extends A {
| override def foo(x: Int): Int = x
| /** overridden doc */ override def foo(x: String): String = x
|}
""".stripMargin).andThen { implicit s =>
assertEquals("/** doc0 */", doc("O.foo(_: Int)"))
assertEquals("/** overridden doc */", doc("O.foo(_: String)"))
}

@Test def docOfOverrideObject =
eval(
"""abstract class A {
| abstract class Companion { /** doc0 */ def bar: Int }
| /** companion */ def foo: Companion
|}
|object O extends A {
| override object foo extends Companion {
| override def bar: Int = 0
| }
|}
""".stripMargin).andThen { implicit s =>
assertEquals("/** companion */", doc("O.foo"))
assertEquals("/** doc0 */", doc("O.foo.bar"))
}

private def eval(code: String): State =
fromInitialState { implicit s => run(code) }

private def doc(expr: String)(implicit s: State): String = {
storedOutput()
run(s":doc $expr")
storedOutput().trim
}

}