这是我参与更文挑战的第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,似乎只包含原始的类型,没有函数一类更复杂的类型。