用Typescript编译器解析获取代码里的类型

1,294 阅读3分钟

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

最近试验了一下用Typescript编译器的接口替换原先用Babel做解析的一段代码,用于尽可能找出ES模块中export的函数。

用Babel的源代码(简化版)如下:

import { transformSync, Visitor } from "@babel/core";
import { Identifier } from "@babel/types";

const funcs = [] as string[];

transformSync(content, {
  parserOpts: {
    plugins: [
      "jsx",
      "typescript",
      "classProperties",
      "optionalChaining",
    ],
  },
  plugins: [
    () => ({
      visitor: {
        ExportDefaultDeclaration({ node: { declaration } }) {
          switch (declaration.type) {
            case "FunctionDeclaration":
            case "ArrowFuntionExpression":
              funcs.push("default");
              break;
          }
        },
        ExportNamedDeclaration({ node }) {
          const { declaration } = node;
          if (declaration) {
            switch (declaration.type) {
              case "FunctionDeclaration":
                if (declaration.id) {
                  funcs.push(declaration.id.name);
                }
                break;
              case "VariableDeclaration":
                declaration.declarations.forEach((d) => {
                  if (!(d.id as Identifier).name) return;
                  switch (d.init?.type) {
                          case "ArrowFunctionExpression":
                          funcs.push((d.id as Identifier).name);
                      break;
                  }
                });
                break;
            }
          }
        },
      } as Visitor,
    }),
  ],
});
console.log(funcs);

写Vistor的时候我大量使用了VSCode Babel AST Explorer插件。UI还不够给力,但很方便。

以上代两个Visitor可以识别的范围非常有限。包括:

export default function f() {}
export default () => {}
export function f() {}
export const f = () => {}

遇到换了个马甲的函数就gg了:

function f() {}
export { f }
export const f2 = f
export default f

当然硬是要找还是可以通过复杂的计算找到的。不过不划算。

这时我想到了在VSCode里可以几乎对随便哪个变量计算出类型的Typescript编译器(当然前提是分析的文件用的是.ts)。千辛万苦找到它的文档:github.com/Microsoft/T…

好在社区有先驱试验可以参考,我找到了react-docgen-typescript。它可以分析React应用文件,找出组件、组件的参数以及参数说明等,非常强大,是著名文档工具storybook的依赖包。

经过不断调试,把原Babel代码修改如下:

import ts from 'typescript';

export default function getFuncs(file: string): string[] {
  const funcs = [] as string[];
  /*
  v4 以前API应该是ts.createProgram([file], {})
  */
  const program = ts.createProgram({
    rootNames: [file],
    options: {},
  });
  const source = program.getSourceFile(file);
  if (!source) return funcs;
  const checker = program.getTypeChecker();
  const moduleSymbol = checker.getSymbolAtLocation(source);
  if (!moduleSymbol) return funcs;
  // 不用自己分析直接帮你找出所有export
  const exports = checker.getExportsOfModule(moduleSymbol);
  exports.forEach((ex) => {
    ex.declarations.forEach((declaration) => {
      // 替换原来的Babel Visitor
      if (
        ts.isFunctionDeclaration(declaration) ||
        ts.isArrowFunction(declaration)
      ) {
        const name = declaration.name?.text;
        funcs.push(ex.name === 'default' ? 'default' : name)
      }
      // 增加识别穿马甲的函数
      else if (
        ts.isVariableDeclaration(declaration) ||
        ts.isIdentifier(declaration)
      ) {
        const type = checker.getTypeOfSymbolAtLocation(ex, declaration);
        if (type.getCallSignatures().length) {
          funcs.push(
            ts.isVariableDeclaration(declaration)
              ? declaration.name.getText()
              : declaration.getText()
          );
        }
      }
    });
    return funcs;
  }

现在我也可以识别穿马甲的函数啦!虽说整个过程秉承了微软一贯的啰嗦难用,初始化后整个识别的过程比原先精简不少。

容我解释一下。首先使用Typescript编译器必须创建一个程序实例:

/*
  v4 以前API应该是ts.createProgram([file], {})
  */
const program = ts.createProgram({
  rootNames: [file],
  options: {},
});

接着因为我们需要大量使用编译器识别类型,获取编译器的type checker:

const checker = program.getTypeChecker();

为了方便让编译器直接帮我找出所有的export,则需要用checker.getSymbolAtLocation对象获得文件的一个ts.SymbolObject才可以继续调用它的接口:

const source = program.getSourceFile(file);
if (!source) return funcs;
const moduleSymbol = checker.getSymbolAtLocation(source);
if (!moduleSymbol) return funcs;
const exports = checker.getExportsOfModule(moduleSymbol);
exports.forEach((ex) => {
	// 处理export的对象,它也是ts.SymbolObject类型
});

最后就用到了魔法一般的接口checker.getTypeOfSymbolAtLocation获得对应ts.SymbolObject的类型属性:

exports.forEach((ex) => {
  ex.declarations.forEach(declaration => {
		const type = checker.getTypeOfSymbolAtLocation(ex, declaration);
    // 处理type,注意type还是一个ts自己的对象
  });
});

好了到这里,接下来的类型判断就比较混乱很难摸清楚套路了。

目前我找到比较容易用来判断是否为函数的方法是看这个type对象有没有Call Signature:

if (type.getCallSignatures().length) return true;

其他试验可行的方法还有检查对象SymbolObject的flag

if (type.symbol.flags & ts.SymbolFlags.Function) return true;

由于没有很好的文档,只能靠编辑器自动完成列表或超链接回源文件寻找合适的接口对象。令人混淆的是除了ts.SymbolFlags之外,还有个ts.TypeFlags,似乎只包含原始的类型,没有函数一类更复杂的类型。