diff --git a/compiler/src/dotty/tools/repl/ParseResult.scala b/compiler/src/dotty/tools/repl/ParseResult.scala index 11f9cd0d72e7..40f76d77ca32 100644 --- a/compiler/src/dotty/tools/repl/ParseResult.scala +++ b/compiler/src/dotty/tools/repl/ParseResult.scala @@ -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 */ @@ -89,6 +98,7 @@ case object Help extends Command { |:load interpret lines in a file |:quit exit the interpreter |:type evaluate the type of the given expression + |:doc print the documentation for the given expresssion |:imports show import history |:reset reset the repl to its initial state, forgetting all session entries """.stripMargin @@ -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 _ => diff --git a/compiler/src/dotty/tools/repl/ReplCompiler.scala b/compiler/src/dotty/tools/repl/ReplCompiler.scala index 825799429e4b..567b3ad2daea 100644 --- a/compiler/src/dotty/tools/repl/ReplCompiler.scala +++ b/compiler/src/dotty/tools/repl/ReplCompiler.scala @@ -3,6 +3,8 @@ 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._ @@ -10,6 +12,7 @@ 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._ @@ -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] = { diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 278a1203d5bb..3d9b9bb6184d 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -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 @@ -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 diff --git a/compiler/test/dotty/tools/repl/DocTests.scala b/compiler/test/dotty/tools/repl/DocTests.scala new file mode 100644 index 000000000000..4cf8e03bb31e --- /dev/null +++ b/compiler/test/dotty/tools/repl/DocTests.scala @@ -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 + } + +}