0. 背景
TypeScript 在跨文件查找符号定义时,是借助 symbolLinks
进行定位的。
当前文件 import
的符号,会通过 symbolLinks
与其他文件 export
的符号建立关联。
下文我们来探索一下 symbolLinks
的建立和使用过程。
1. 查找定义
1.1 VSCode 示例
我们新建了两个文件 index.ts 还有它的依赖 lib.ts。
index.ts 的内容如下,
import x from './lib';
x
lib.ts
的内容如下,
const a = 1;
export default a;
在 VSCode 中查看 index.ts 文件中第二行 x
的定义,
就会跳转到 lib.ts 第一行 a
的位置。
1.2 Mock GoToDefinition
我们来模拟一下上述 VSCode 查找定义的过程。
以下示例完整的代码在这里:github: debug-symbol-links
克隆、安装依赖、并执行构建之后,选择 Mock GoToDefinition
进行 Debug,
我们成功为 index.ts 中的 x
,找到了它的定义 lib.ts 中的 a
。
与 VSCode 内部实现一致,
我们传给 goToDefinition
的是 x
的位置 23
(文件中从左到右的字符数,从 0
开始),
返回的也是一个位置,fileName
是 lib.ts 的绝对地址,textSpan
表示了 a
的起始位置和宽度。
1.3 Symbol Links
那么 TypeScript 到底是怎样跨文件查找定义的呢?
这就涉及到了 TypeScript 实现中的 symbolLinks
对象。
我们在 node_modules/_typescript@3.7.3@typescript/lib/typescript.js#L35183
,
resolveAlias 函数中,打个条件断点,
来看看 TypeScript 是怎么给 x
这个符号建立 symbolLinks 的。
symbol.name === 'x'
function resolveAlias(symbol: Symbol): Symbol {
...
const links = getSymbolLinks(symbol); // 获取符号 x 的 symbolLinks
if (!links.target) {
links.target = resolvingSymbol; // 先设置一个正在解析的标志
...
const target = getTargetOfAliasDeclaration(node);
if (links.target === resolvingSymbol) {
// 解析到了就在 symbolLinks 中建立关联,否则就关联到 unknown 符号上
links.target = target || unknownSymbol;
}
...
}
...
return links.target;
}
resolveAlias
对于 symbolLinks 来说,是一个很重要的函数。
符号 x
一开始的 symbolLinks
之 target
字段是空的,并未指向其他符号,
resolveAlias
做的事情,就是找到 lib.ts 中的符号 a
。
找 lib.ts 中符号 a
的过程,是通过 getTargetOfAliasDeclaration
来做的,
它会根据 lib.ts 文件语义分析的结果,找到所有它导出的符号,
然后在这些符号上递归调用 resolveAlias
,找到它们 symbolLinks
的 target
。
这样才能从 x
找到 export
,然后再找到 a
。
我们可以取消上述 resolveAlias
断点处的条件判断,看看递归调用过程,
可以看到
resolveAlias
在获取符号 x
的 target
时,又递归了自己,继续获取符号
default
(模块 default
导出的符号)的 target
。
这样就可以将 x
symbolLinks
的 target
直接指向 a
了。
知道了这些之后,我们来 hack 一下,看能不能让 TypeScript 去我们指定的文件中查找定义。
2. Hack ResolveAlias
2.1 示例
为此,我们新建一个 hack.ts 文件作为示例,
import x from './hack_lib';
x
它依赖了 hack_lib.ts,但是这个文件并不存在。
我们要做的事情是,在 resolveAlias
解析不到 x
symbolLinks
之 target
时,
手动给它指定一个 “target”。
完整的代码在这里:github: debug-symbol-links
克隆、安装依赖、并执行构建之后,选择 Hack GoToDefinition
进行 Debug,
居然可以找到 x
的定义了!
我们来看下这是怎么实现的。
2.2 手动建立关联
在 resolveAlias
中,我们嵌入了一些代码,
每次给符号的 symbolLinks
查找完 target
都会调用它。
ts._hackResolveAlias && ts._hackResolveAlias(symbol, links, target, resolveAlias);
在这个函数中,我们进行判断,如果没有找到 target
,就手动给它指定一个。
具体步骤如下:
(1)手动加载一个外部文件,并进行语义分析
(2)找到这个模块导出的符号,并递归调用 resolveAlias
找到符号的源头
(3)设置 symbolLinks
的 target
字段,建立关联
const hackResolveAlias = (symbol: ts.Symbol, links, target: ts.Symbol | undefined, tsResolveAlias) => {
if (
symbol.flags & ts.SymbolFlags.Alias // 是一个符号别名
&& target == null // 且没找到别名
) {
// 认为这是在对导入的符号建立 symbolLinks 时没有成功
// 手动加载一个外部文件,并进行语义分析
const libFilePath = path.join(__dirname, '../../debug/lib.ts');
// 设置它是一个外部模块,语义分析时才会计算 sourceFile.symbol
const isExternalModule = true;
const sourceFile = createSourceFile(libFilePath, isExternalModule);
// 语义分析之后,sourceFile.symbol 才有值
bindSourceFile(sourceFile, compilerOptions);
// 找到这个模块 default 导出的符号
const { symbol: moduleSymbol } = sourceFile as any;
const exportSymbol = moduleSymbol.exports.get(ts.InternalSymbolName.Default);
// 递归解析,找到 default 导出的符号之源头在哪里
const target = tsResolveAlias(exportSymbol);
// 手动建立 symbolLinks
links.target = target;
}
};
2.3 嵌入代码
嵌入代码时,首先锁定了 package.json TypeScript 的版本,
然后使用了配置方式,在 postinstall
时修改文件,
const config = [
// 在 node_modules/typescript/lib/typescript.js#L35185 之前插入 hack 代码
{
file: path.join(__dirname, '../node_modules/typescript/lib/typescript.js'),
embeds: [
{
insert: 35185,
code: `ts._hackResolveAlias && ts._hackResolveAlias(symbol, links, target, resolveAlias);`,
},
],
},
];
源码在这里:github: debug-symbol-links/script/config.js
网友评论