Skip to content

Commit 3c3130d

Browse files
authored
Merge pull request #4669 from dotty-staging/topic/tasty-doc-repl
Add `:doc` command in REPL to show documentation
2 parents d28ba06 + 6a9e067 commit 3c3130d

File tree

4 files changed

+228
-1
lines changed

4 files changed

+228
-1
lines changed

compiler/src/dotty/tools/repl/ParseResult.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ object TypeOf {
6060
val command = ":type"
6161
}
6262

63+
/**
64+
* A command that is used to display the documentation associated with
65+
* the given expression.
66+
*/
67+
case class DocOf(expr: String) extends Command
68+
object DocOf {
69+
val command = ":doc"
70+
}
71+
6372
/** `:imports` lists the imports that have been explicitly imported during the
6473
* session
6574
*/
@@ -89,6 +98,7 @@ case object Help extends Command {
8998
|:load <path> interpret lines in a file
9099
|:quit exit the interpreter
91100
|:type <expression> evaluate the type of the given expression
101+
|:doc <expression> print the documentation for the given expresssion
92102
|:imports show import history
93103
|:reset reset the repl to its initial state, forgetting all session entries
94104
""".stripMargin
@@ -117,6 +127,7 @@ object ParseResult {
117127
case Imports.command => Imports
118128
case Load.command => Load(arg)
119129
case TypeOf.command => TypeOf(arg)
130+
case DocOf.command => DocOf(arg)
120131
case _ => UnknownCommand(cmd)
121132
}
122133
case _ =>

compiler/src/dotty/tools/repl/ReplCompiler.scala

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ package dotty.tools.repl
33
import dotty.tools.backend.jvm.GenBCode
44
import dotty.tools.dotc.ast.Trees._
55
import dotty.tools.dotc.ast.{tpd, untpd}
6+
import dotty.tools.dotc.ast.tpd.TreeOps
7+
import dotty.tools.dotc.core.Comments.CommentsContext
68
import dotty.tools.dotc.core.Contexts._
79
import dotty.tools.dotc.core.Decorators._
810
import dotty.tools.dotc.core.Flags._
911
import dotty.tools.dotc.core.Names._
1012
import dotty.tools.dotc.core.Phases
1113
import dotty.tools.dotc.core.Phases.Phase
1214
import dotty.tools.dotc.core.StdNames._
15+
import dotty.tools.dotc.core.Symbols._
1316
import dotty.tools.dotc.reporting.diagnostic.messages
1417
import dotty.tools.dotc.typer.{FrontEnd, ImportInfo}
1518
import dotty.tools.dotc.util.Positions._
@@ -152,6 +155,54 @@ class ReplCompiler extends Compiler {
152155
}
153156
}
154157

158+
def docOf(expr: String)(implicit state: State): Result[String] = {
159+
implicit val ctx: Context = state.context
160+
161+
/**
162+
* Extract the "selected" symbol from `tree`.
163+
*
164+
* Because the REPL typechecks an expression, special syntax is needed to get the documentation
165+
* of certain symbols:
166+
*
167+
* - To select the documentation of classes, the user needs to pass a call to the class' constructor
168+
* (e.g. `new Foo` to select `class Foo`)
169+
* - When methods are overloaded, the user needs to enter a lambda to specify which functions he wants
170+
* (e.g. `foo(_: Int)` to select `def foo(x: Int)` instead of `def foo(x: String)`
171+
*
172+
* This function returns the right symbol for the received expression, and all the symbols that are
173+
* overridden.
174+
*/
175+
def extractSymbols(tree: tpd.Tree): Iterator[Symbol] = {
176+
val sym = tree match {
177+
case tree if tree.isInstantiation => tree.symbol.owner
178+
case tpd.closureDef(defdef) => defdef.rhs.symbol
179+
case _ => tree.symbol
180+
}
181+
Iterator(sym) ++ sym.allOverriddenSymbols
182+
}
183+
184+
typeCheck(expr).map {
185+
case ValDef(_, _, Block(stats, _)) if stats.nonEmpty =>
186+
val stat = stats.last.asInstanceOf[tpd.Tree]
187+
if (stat.tpe.isError) stat.tpe.show
188+
else {
189+
val docCtx = ctx.docCtx.get
190+
val symbols = extractSymbols(stat)
191+
val doc = symbols.collectFirst {
192+
case sym if docCtx.docstrings.contains(sym) =>
193+
docCtx.docstrings(sym).raw
194+
}
195+
doc.getOrElse(s"// No doc for `${expr}`")
196+
}
197+
198+
case _ =>
199+
"""Couldn't display the documentation for your expression, so sorry :(
200+
|
201+
|Please report this to my masters at github.com/lampepfl/dotty
202+
""".stripMargin
203+
}
204+
}
205+
155206
final def typeCheck(expr: String, errorsAllowed: Boolean = false)(implicit state: State): Result[tpd.ValDef] = {
156207

157208
def wrapped(expr: String, sourceFile: SourceFile, state: State)(implicit ctx: Context): Result[untpd.PackageDef] = {

compiler/src/dotty/tools/repl/ReplDriver.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class ReplDriver(settings: Array[String],
6363

6464
/** Create a fresh and initialized context with IDE mode enabled */
6565
private[this] def initialCtx = {
66-
val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions).addMode(Mode.Interactive)
66+
val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions).addMode(Mode.Interactive).addMode(Mode.ReadComments)
6767
val ictx = setup(settings, rootCtx)._2
6868
ictx.base.initialize()(ictx)
6969
ictx
@@ -332,6 +332,13 @@ class ReplDriver(settings: Array[String],
332332
)
333333
state
334334

335+
case DocOf(expr) =>
336+
compiler.docOf(expr)(newRun(state)).fold(
337+
displayErrors,
338+
res => out.println(SyntaxHighlighting(res))
339+
)
340+
state
341+
335342
case Quit =>
336343
// end of the world!
337344
state
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package dotty.tools
2+
package repl
3+
4+
import org.junit.Test
5+
import org.junit.Assert.assertEquals
6+
7+
class DocTests extends ReplTest {
8+
9+
@Test def docOfDef =
10+
eval("/** doc */ def foo = 0").andThen { implicit s =>
11+
assertEquals("/** doc */", doc("foo"))
12+
}
13+
14+
@Test def docOfVal =
15+
eval("/** doc */ val foo = 0").andThen { implicit s =>
16+
assertEquals("/** doc */", doc("foo"))
17+
}
18+
19+
@Test def docOfObject =
20+
eval("/** doc */ object Foo").andThen { implicit s =>
21+
assertEquals("/** doc */", doc("Foo"))
22+
}
23+
24+
@Test def docOfClass =
25+
eval("/** doc */ class Foo").andThen { implicit s =>
26+
assertEquals("/** doc */", doc("new Foo"))
27+
}
28+
29+
@Test def docOfTrait =
30+
eval("/** doc */ trait Foo").andThen { implicit s =>
31+
assertEquals("/** doc */", doc("new Foo"))
32+
}
33+
34+
@Test def docOfDefInObject =
35+
eval("object O { /** doc */ def foo = 0 }").andThen { implicit s =>
36+
assertEquals("/** doc */", doc("O.foo"))
37+
}
38+
39+
@Test def docOfValInObject =
40+
eval("object O { /** doc */ val foo = 0 }").andThen { implicit s =>
41+
assertEquals("/** doc */", doc("O.foo"))
42+
}
43+
44+
@Test def docOfObjectInObject =
45+
eval("object O { /** doc */ object Foo }").andThen { implicit s =>
46+
assertEquals("/** doc */", doc("O.Foo"))
47+
}
48+
49+
@Test def docOfClassInObject =
50+
eval("object O { /** doc */ class Foo }").andThen { implicit s =>
51+
assertEquals("/** doc */", doc("new O.Foo"))
52+
}
53+
54+
@Test def docOfTraitInObject =
55+
eval("object O { /** doc */ trait Foo }").andThen { implicit s =>
56+
assertEquals("/** doc */", doc("new O.Foo"))
57+
}
58+
59+
@Test def docOfDefInClass =
60+
eval(
61+
"""class C { /** doc */ def foo = 0 }
62+
|val c = new C
63+
""".stripMargin).andThen { implicit s =>
64+
assertEquals("/** doc */", doc("c.foo"))
65+
}
66+
67+
@Test def docOfValInClass =
68+
eval(
69+
"""class C { /** doc */ val foo = 0 }
70+
|val c = new C
71+
""".stripMargin).andThen { implicit s =>
72+
assertEquals("/** doc */", doc("c.foo"))
73+
}
74+
75+
@Test def docOfObjectInClass =
76+
eval(
77+
"""class C { /** doc */ object Foo }
78+
|val c = new C
79+
""".stripMargin).andThen { implicit s =>
80+
assertEquals("/** doc */", doc("c.Foo"))
81+
}
82+
83+
@Test def docOfClassInClass =
84+
eval(
85+
"""class C { /** doc */ class Foo }
86+
|val c = new C
87+
""".stripMargin).andThen { implicit s =>
88+
assertEquals("/** doc */", doc("new c.Foo"))
89+
}
90+
91+
@Test def docOfTraitInClass =
92+
eval(
93+
"""class C { /** doc */ trait Foo }
94+
|val c = new C
95+
""".stripMargin).andThen { implicit s =>
96+
assertEquals("/** doc */", doc("new c.Foo"))
97+
}
98+
99+
@Test def docOfOverloadedDef =
100+
eval(
101+
"""object O {
102+
| /** doc0 */ def foo(x: Int) = x
103+
| /** doc1 */ def foo(x: String) = x
104+
|}
105+
""".stripMargin).andThen { implicit s =>
106+
assertEquals("/** doc0 */", doc("O.foo(_: Int)"))
107+
assertEquals("/** doc1 */", doc("O.foo(_: String)"))
108+
}
109+
110+
@Test def docOfInherited =
111+
eval(
112+
"""class C { /** doc */ def foo = 0 }
113+
|object O extends C
114+
""".stripMargin).andThen { implicit s =>
115+
assertEquals("/** doc */", doc("O.foo"))
116+
}
117+
118+
@Test def docOfOverride =
119+
eval(
120+
"""abstract class A {
121+
| /** doc0 */ def foo(x: Int): Int = x + 1
122+
| /** doc1 */ def foo(x: String): String = x + "foo"
123+
|}
124+
|object O extends A {
125+
| override def foo(x: Int): Int = x
126+
| /** overridden doc */ override def foo(x: String): String = x
127+
|}
128+
""".stripMargin).andThen { implicit s =>
129+
assertEquals("/** doc0 */", doc("O.foo(_: Int)"))
130+
assertEquals("/** overridden doc */", doc("O.foo(_: String)"))
131+
}
132+
133+
@Test def docOfOverrideObject =
134+
eval(
135+
"""abstract class A {
136+
| abstract class Companion { /** doc0 */ def bar: Int }
137+
| /** companion */ def foo: Companion
138+
|}
139+
|object O extends A {
140+
| override object foo extends Companion {
141+
| override def bar: Int = 0
142+
| }
143+
|}
144+
""".stripMargin).andThen { implicit s =>
145+
assertEquals("/** companion */", doc("O.foo"))
146+
assertEquals("/** doc0 */", doc("O.foo.bar"))
147+
}
148+
149+
private def eval(code: String): State =
150+
fromInitialState { implicit s => run(code) }
151+
152+
private def doc(expr: String)(implicit s: State): String = {
153+
storedOutput()
154+
run(s":doc $expr")
155+
storedOutput().trim
156+
}
157+
158+
}

0 commit comments

Comments
 (0)