Skip to content

Commit c9a449f

Browse files
committed
Add experimental APIs to query the compiler interactively
This commit adds a set of APIs in dotty.tools.dotc.interactive._ designed for interactive uses of the compiler like in IDEs. This API is just a first draft driven by the requirements of the Dotty Language Server and will evolve based on usage. Usage is roughly as follow: - Create an instance of InteractiveDriver with the compiler flags you need - Call InteractiveDriver#run(uri, code) to typecheck `code` and keep a reference to it via the identifier `uri`. The return value of this method are the errors/warnings/infos generated during the compilation - Use InteractiveDriver#openedTrees(uri) to get all top-level class typechecked trees generated for `uri`, or InteractiveDriver#allTrees to get all top-level trees opened in this driver instance and available on the classpath (unpickled from the TASTY section of classfiles) - Query the trees using one of the many methods in the Interactive object. See the code in language-server/ added in the following commit for a concrete example.
1 parent fc2ca4b commit c9a449f

File tree

5 files changed

+465
-0
lines changed

5 files changed

+465
-0
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package dotty.tools
2+
package dotc
3+
package interactive
4+
5+
import scala.annotation.tailrec
6+
import scala.collection._
7+
8+
import ast.{NavigateAST, Trees, tpd, untpd}
9+
import core._, core.Decorators.{sourcePos => _, _}
10+
import Contexts._, Flags._, Names._, NameOps._, Symbols._, SymDenotations._, Trees._, Types._
11+
import util.Positions._, util.SourcePosition
12+
import NameKinds.SimpleNameKind
13+
14+
/** High-level API to get information out of typed trees, designed to be used by IDEs.
15+
*
16+
* @see `InteractiveDriver` to get typed trees from code.
17+
*/
18+
object Interactive {
19+
import ast.tpd._
20+
21+
/** Does this tree define a symbol ? */
22+
def isDefinition(tree: Tree) =
23+
tree.isInstanceOf[DefTree with NameTree]
24+
25+
/** The type of the closest enclosing tree with a type containing position `pos`. */
26+
def enclosingType(trees: List[SourceTree], pos: SourcePosition)(implicit ctx: Context): Type = {
27+
val path = pathTo(trees, pos)
28+
if (path.isEmpty) NoType
29+
else path.head.tpe
30+
}
31+
32+
/** The source symbol of the closest enclosing tree with a symbol containing position `pos`.
33+
*
34+
* @see sourceSymbol
35+
*/
36+
def enclosingSourceSymbol(trees: List[SourceTree], pos: SourcePosition)(implicit ctx: Context): Symbol = {
37+
pathTo(trees, pos).dropWhile(!_.symbol.exists).headOption match {
38+
case Some(tree) =>
39+
sourceSymbol(tree.symbol)
40+
case None =>
41+
NoSymbol
42+
}
43+
}
44+
45+
/** A symbol related to `sym` that is defined in source code.
46+
*
47+
* @see enclosingSourceSymbol
48+
*/
49+
@tailrec def sourceSymbol(sym: Symbol)(implicit ctx: Context): Symbol =
50+
if (!sym.exists)
51+
sym
52+
else if (sym.is(ModuleVal))
53+
sourceSymbol(sym.moduleClass) // The module val always has a zero-extent position
54+
else if (sym.is(Synthetic)) {
55+
val linked = sym.linkedClass
56+
if (linked.exists && !linked.is(Synthetic))
57+
linked
58+
else
59+
sourceSymbol(sym.owner)
60+
}
61+
else if (sym.isPrimaryConstructor)
62+
sourceSymbol(sym.owner)
63+
else sym
64+
65+
/** Possible completions at position `pos` */
66+
def completions(trees: List[SourceTree], pos: SourcePosition)(implicit ctx: Context): List[Symbol] = {
67+
val path = pathTo(trees, pos)
68+
val boundary = enclosingDefinitionInPath(path).symbol
69+
70+
path.take(1).flatMap {
71+
case Select(qual, _) =>
72+
// When completing "`a.foo`, return the members of `a`
73+
completions(qual.tpe, boundary)
74+
case _ =>
75+
// FIXME: Get all declarations available in the current scope, not just
76+
// those from the enclosing class
77+
boundary.enclosingClass match {
78+
case csym: ClassSymbol =>
79+
val classRef = csym.classInfo.typeRef
80+
completions(classRef, boundary)
81+
case _ =>
82+
Nil
83+
}
84+
}
85+
}
86+
87+
/** Possible completions of members of `prefix` which are accessible when called inside `boundary` */
88+
def completions(prefix: Type, boundary: Symbol)(implicit ctx: Context): List[Symbol] = {
89+
val boundaryCtx = ctx.withOwner(boundary)
90+
prefix.memberDenots(completionsFilter, (name, buf) =>
91+
buf ++= prefix.member(name).altsWith(_.symbol.isAccessibleFrom(prefix)(boundaryCtx))
92+
).map(_.symbol).toList
93+
}
94+
95+
/** Filter for names that should appear when looking for completions. */
96+
private[this] object completionsFilter extends NameFilter {
97+
def apply(pre: Type, name: Name)(implicit ctx: Context): Boolean =
98+
!name.isConstructorName && name.is(SimpleNameKind)
99+
}
100+
101+
/** Find named trees with a non-empty position whose symbol match `sym` in `trees`.
102+
*
103+
* Note that nothing will be found for symbols not defined in source code,
104+
* use `sourceSymbol` to get a symbol related to `sym` that is defined in
105+
* source code.
106+
*
107+
* @param includeReferences If true, include references and not just definitions
108+
* @param includeOverriden If true, include trees whose symbol is overriden by `sym`
109+
*/
110+
def namedTrees(trees: List[SourceTree], includeReferences: Boolean, includeOverriden: Boolean, sym: Symbol)
111+
(implicit ctx: Context): List[SourceTree] =
112+
if (!sym.exists)
113+
Nil
114+
else
115+
namedTrees(trees, includeReferences, matchSymbol(_, sym, includeOverriden))
116+
117+
/** Find named trees with a non-empty position whose name contains `nameSubstring` in `trees`.
118+
*
119+
* @param includeReferences If true, include references and not just definitions
120+
*/
121+
def namedTrees(trees: List[SourceTree], includeReferences: Boolean, nameSubstring: String)
122+
(implicit ctx: Context): List[SourceTree] =
123+
namedTrees(trees, includeReferences, _.show.toString.contains(nameSubstring))
124+
125+
/** Find named trees with a non-empty position satisfying `treePredicate` in `trees`.
126+
*
127+
* @param includeReferences If true, include references and not just definitions
128+
*/
129+
def namedTrees(trees: List[SourceTree], includeReferences: Boolean, treePredicate: NameTree => Boolean)
130+
(implicit ctx: Context): List[SourceTree] = {
131+
val buf = new mutable.ListBuffer[SourceTree]
132+
133+
trees foreach { case SourceTree(topTree, source) =>
134+
(new TreeTraverser {
135+
override def traverse(tree: Tree)(implicit ctx: Context) = {
136+
tree match {
137+
case _: Inlined =>
138+
// Skip inlined trees
139+
case tree: NameTree
140+
if tree.symbol.exists
141+
&& !tree.symbol.is(Synthetic)
142+
&& tree.pos.exists
143+
&& !tree.pos.isZeroExtent
144+
&& (includeReferences || isDefinition(tree))
145+
&& treePredicate(tree) =>
146+
buf += SourceTree(tree, source)
147+
case _ =>
148+
}
149+
traverseChildren(tree)
150+
}
151+
}).traverse(topTree)
152+
}
153+
154+
buf.toList
155+
}
156+
157+
/** Check if `tree` matches `sym`.
158+
* This is the case if `sym` is the symbol of `tree` or, if `includeOverriden`
159+
* is true, if `sym` is overriden by `tree`.
160+
*/
161+
def matchSymbol(tree: Tree, sym: Symbol, includeOverriden: Boolean)(implicit ctx: Context): Boolean =
162+
(sym == tree.symbol) || (includeOverriden && tree.symbol.allOverriddenSymbols.contains(sym))
163+
164+
165+
/** The reverse path to the node that closest encloses position `pos`,
166+
* or `Nil` if no such path exists. If a non-empty path is returned it starts with
167+
* the tree closest enclosing `pos` and ends with an element of `trees`.
168+
*/
169+
def pathTo(trees: List[SourceTree], pos: SourcePosition)(implicit ctx: Context): List[Tree] =
170+
trees.find(_.pos.contains(pos)) match {
171+
case Some(tree) =>
172+
// FIXME: We shouldn't need a cast. Change NavigateAST.pathTo to return a List of Tree?
173+
val path = NavigateAST.pathTo(pos.pos, tree.tree).asInstanceOf[List[untpd.Tree]]
174+
175+
path.dropWhile(!_.hasType).asInstanceOf[List[tpd.Tree]]
176+
case None =>
177+
Nil
178+
}
179+
180+
/** The first tree in the path that is a definition. */
181+
def enclosingDefinitionInPath(path: List[Tree])(implicit ctx: Context): Tree =
182+
path.find(_.isInstanceOf[DefTree]).getOrElse(EmptyTree)
183+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package dotty.tools
2+
package dotc
3+
package interactive
4+
5+
import core._
6+
import Phases._
7+
import typer._
8+
9+
class InteractiveCompiler extends Compiler {
10+
// TODO: Figure out what phases should be run in IDEs
11+
// More phases increase latency but allow us to report more errors.
12+
// This could be improved by reporting errors back to the IDE
13+
// after each phase group instead of waiting for the pipeline to finish.
14+
override def phases: List[List[Phase]] = List(
15+
List(new FrontEnd)
16+
)
17+
}

0 commit comments

Comments
 (0)