背景
上文我们从 VSCode Go to Definition
出发,介绍了 TypeScript 处理多文件的过程,
总共分为两个关键步骤,入口文件的处理,以及内置的库文件的处理。
这两个处理过程,都会递归的查找 import 的文件,从而将整个项目都加载进来。
此外 Go to Definition
是通过向 tsserver 发送 definitionAndBoundSpan
消息来实现的。
tsserver 首先根据光标位置拿到最近邻的 ast 节点,
然后读取 TypeChecker 中保存的,该节点对应的符号信息,就可以找到符号定义的位置了。
Go to Definition
会比较简单一些,因为 TypeChecker 已经将节点与符号之间的关联关系处理好了,
它会在定义符号的 ast 节点上,添加 symbol
属性,用到时直接取就行。
(不出意外的话,应该是 addDeclarationToSymbol
函数 src/compiler/binder.ts#L299)
除了 Go to Definition
之外,VSCode 还提供了 Go to References
功能,
这个功能实现起来就比较麻烦了。
const a = 1;
a
a
在 a
定义位置右键,选择 Go to References
,VSCode 就会在当前界面打开一个快捷窗口,
展示了包括 a
定义在内的 3
个引用。
1. 启动调试
与上文的调试方式相同,我们启动了两个 VSCode 实例,
一个打开 vscode v1.45.1 源码,
另一个打开 typescript v3.7.3 源码。
具体的调试配置,需参考第七篇,然后按上一篇指出的那样,再修改其中一个配置。
这里就不再赘述了。
我们分别在 vscode/extensions/typescript-language-features/src/features/references.ts#L26,
以及 typescript/src/server/session.ts#L2251 打下断点,启动调试。
TypeScript 插件(typescript-language-features)发送了名为 references
的消息,
2. 文本搜索
下文我们来专心看 tsserver 查找所有引用的过程。
经过一路跟踪,我们发现业务逻辑主要在 typescript/src/services/findAllReferences.ts 这个文件中,
export function findReferencedSymbols(...): ... {
const node = getTouchingPropertyName(sourceFile, position);
const referencedSymbols = Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken);
...
}
它先找到光标位置的 ast 节点,然后调用了 Core.getReferencedSymbolsForNode
找出所有引用,
位于 src/services/findAllReferences.ts#L535。
后面主要的逻辑我整理了一下,
Core.getReferencedSymbolsForNode # 封装了查找引用的所有逻辑
checker.getSymbolAtLocation # 获取当前 ast 节点对应的符号
getReferencedSymbolsForSymbol
new State # 用了一个 State 对象存储查找引用过程中的信息
getReferencesInContainerOrFiles
for # 在所有的 sourceFiles 中找
searchForName
getReferencesInSourceFile
getReferencesInContainer # 在任何可以定义符号的容器中找
getPossibleSymbolReferencePositions # 通过文本搜索的方式,找出所有可能的位置
for
getReferencesAtLocation # 查看每个位置,是否是给定符号的引用
getTouchingPropertyName # 根据位置计算 ast 节点
state.checker.getSymbolAtLocation # 根据节点,查 TypeChecker 看它引用了谁
addReference
最值得一提的是 getPossibleSymbolReferencePositions
,src/services/findAllReferences.ts#L1188,
它处理所有的 sourceFile
,竟然用文本搜索的方式查找同名变量。
还好这个函数并不算太长,
function getPossibleSymbolReferencePositions(sourceFile: SourceFile, symbolName: string, container: Node = sourceFile): readonly number[] {
const positions: number[] = [];
/// TODO: Cache symbol existence for files to save text search
// Also, need to make this work for unicode escapes.
// Be resilient in the face of a symbol with no name or zero length name
if (!symbolName || !symbolName.length) {
return positions;
}
const text = sourceFile.text;
const sourceLength = text.length;
const symbolNameLength = symbolName.length;
let position = text.indexOf(symbolName, container.pos);
while (position >= 0) {
// If we are past the end, stop looking
if (position > container.end) break;
// We found a match. Make sure it's not part of a larger word (i.e. the char
// before and after it have to be a non-identifier char).
const endPosition = position + symbolNameLength;
if ((position === 0 || !isIdentifierPart(text.charCodeAt(position - 1), ScriptTarget.Latest)) &&
(endPosition === sourceLength || !isIdentifierPart(text.charCodeAt(endPosition), ScriptTarget.Latest))) {
// Found a real match. Keep searching.
positions.push(position);
}
position = text.indexOf(symbolName, position + symbolNameLength + 1);
}
return positions;
}
可以看到,为了查找 symbolName
,
它从 sourceFile.text
(代码文本) 的第一个出现位置,开始往后搜索,
每次移动 symbolNameLength
长度的偏移量。
这样甚至会将注释中的文本也捞出来。
捞出来之后,再来判断是否真的是所查找的引用。
3. 查找符号
文本是否所查找的引用,是通过 getReferencesAtLocation
来判断的,
位于 src/services/findAllReferences.ts#L1291。
它总共做了两件事,
- 根据文本得到 ast 节点
- 根据 ast 节点,用 TypeChecker 判断引用关系
getReferencesAtLocation # 查看每个位置,是否是给定符号的引用
getTouchingPropertyName # 1. 根据位置计算 ast 节点
getTouchingToken
getTokenAtPositionWorker
findPrecedingToken
state.checker.getSymbolAtLocation # 2. 根据节点,查 TypeChecker 看它引用了谁
getSymbolAtLocation
getSymbolAtLocation
getSymbolOfNode
getLateBoundSymbol
getMergedSymbol
addReference
3.1 从文本到 ast 节点
getReferencesAtLocation # 查看每个位置,是否是给定符号的引用
getTouchingPropertyName # 1. 根据位置计算 ast 节点
getTouchingToken
getTokenAtPositionWorker
findPrecedingToken
getTouchingPropertyName
调用了 getTokenAtPositionWorker
,
位于 src/services/utilities.ts#L705 找到了与文本位置最匹配的 ast 节点。
/** Get the token whose text contains the position */
function getTokenAtPositionWorker(sourceFile: SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includePrecedingTokenAtEndPosition: ((n: Node) => boolean) | undefined, includeEndPosition: boolean): Node {
let current: Node = sourceFile;
outer: while (true) {
// find the child that contains 'position'
for (const child of current.getChildren(sourceFile)) {
const start = allowPositionInLeadingTrivia ? child.getFullStart() : child.getStart(sourceFile, /*includeJsDoc*/ true);
if (start > position) {
// If this child begins after position, then all subsequent children will as well.
break;
}
const end = child.getEnd();
if (position < end || (position === end && (child.kind === SyntaxKind.EndOfFileToken || includeEndPosition))) {
current = child;
continue outer;
}
else if (includePrecedingTokenAtEndPosition && end === position) {
const previousToken = findPrecedingToken(position, sourceFile, child);
if (previousToken && includePrecedingTokenAtEndPosition(previousToken)) {
return previousToken;
}
}
}
return current;
}
}
大致看来,它采用了广度优先搜索,从根节点开始调用 current.getChildren
逐个判断各子节点,
如果子节点的起始位置,已经在可能的引用位置之后了,那么所有后代节点就都不用判断了,
否则,就继续沿着 ast 往叶子方向搜索。
总而言之,思路是不断的缩小 ast 子树的范围。
找到位置后,再计算这个位置之前的 token(一般前面如果不是空白字符的话,直接就返回了)。
相关的逻辑在 findPrecedingToken
src/services/utilities.ts#L775 ,就不仔细研究了。
3.2 从 ast 节点中取出符号信息
getReferencesAtLocation # 查看每个位置,是否是给定符号的引用
getTouchingPropertyName # 1. 根据位置计算 ast 节点
...
state.checker.getSymbolAtLocation # 2. 根据节点,查 TypeChecker 看它引用了谁
getSymbolAtLocation
getSymbolAtLocation
getSymbolOfNode
node.symbol
有了 ast 节点之后,就可以借助 TypeChecker 获取节点的符号信息了。
所用到的方法是 checker.getSymbolAtLocation
,src/compiler/checker.ts#L33461。
最后直接是从 node.symbol
属性中取的符号信息。
总结
本文介绍了 VSCode Go to References
的具体实现,它比 Go to Definition
会更复杂一些。
tsserver 先是对 sourceFile
源代码进行全文文本搜索,
然后查找 ast,找与上一步搜到文本位置最匹配的 ast 节点,
最后从 ast 节点中拿到 TypeChecker 之前已挂载好的符号信息。
值得一提的是,ast 定义符号的位置,也算是符号的一处引用,
因此,引用是建立了从 ast 节点到符号的一种映射关系。
参考
vscode v1.45.1
typescript v3.7.3