美文网首页Node.js
[Node] TypeScript 中的 symbolLinks

[Node] TypeScript 中的 symbolLinks

作者: 何幻 | 来源:发表于2020-09-25 13:57 被阅读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

    相关文章

      网友评论

        本文标题:[Node] TypeScript 中的 symbolLinks

        本文链接:https://www.haomeiwen.com/subject/rrcfuktx.html