Node.js

[Node] TypeScript 中的 symbolLinks

2020-09-25  本文已影响0人  何幻

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

上一篇下一篇

猜你喜欢

热点阅读