[Node] TypeScript 中的 symbolLinks

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 一开始的 symbolLinkstarget 字段是空的,并未指向其他符号,
resolveAlias 做的事情,就是找到 lib.ts 中的符号 a

找 lib.ts 中符号 a 的过程,是通过 getTargetOfAliasDeclaration 来做的,
它会根据 lib.ts 文件语义分析的结果,找到所有它导出的符号,
然后在这些符号上递归调用 resolveAlias,找到它们 symbolLinkstarget

这样才能从 x 找到 export,然后再找到 a
我们可以取消上述 resolveAlias 断点处的条件判断,看看递归调用过程,


可以看到 resolveAlias 在获取符号 xtarget 时,又递归了自己,
继续获取符号 default(模块 default 导出的符号)的 target

这样就可以将 x symbolLinkstarget 直接指向 a 了。
知道了这些之后,我们来 hack 一下,看能不能让 TypeScript 去我们指定的文件中查找定义。

2. Hack ResolveAlias

2.1 示例

为此,我们新建一个 hack.ts 文件作为示例,

import x from './hack_lib';
x

它依赖了 hack_lib.ts,但是这个文件并不存在

我们要做的事情是,在 resolveAlias 解析不到 x symbolLinkstarget 时,
手动给它指定一个 “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)设置 symbolLinkstarget 字段,建立关联

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


参考

github: debug-symbol-links
TypeScript v3.7.3

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。