っても難しいことはなにもない。
WASM向けにビルドされたoxc-parser
というライブラリが用意されてるので、それを使うだけ。
oxc-parser
oxc-parser - npm https://www.npmjs.com/package/oxc-parser
今のところ公開されてるAPIは4つ。
parseSync(source, options)
parseAsync(source, options)
基本的にはこのどっちかを使うはず。どちらもJSON文字列が返ってくる。この時点ではJavaScriptのオブジェクトにはなってないことに注意。
parseWithoutReturn(source, options)
parseSyncBuffer(source, options)
parseWithoutReturn()
はベンチマーク目的らしいので、まあ使うことはないはず。
parseSyncBuffer()
は、Flexbuffers(Flatbuffersのスキーマレスverらしい)形式のBuffer
が返ってくる。これはJSONって変換やっぱ遅いよな・・ってことで、実験的にバイナリASTを返してるそうな。
feat(parser/napi): add flexbuffer to AST transfer (2x speedup) by HerringtonDarkholme · Pull Request #1680 · oxc-project/oxc https://github.com/oxc-project/oxc/pull/1680
中身にアクセスするには、flatbuffers/js/flexbuffers
が必要。
内部的には
リポジトリとしてはこのあたり。
oxc/napi/parser at main · oxc-project/oxc https://github.com/oxc-project/oxc/tree/d7ecd21801ec75fbfbceedfbf92c29de20640e7f/napi/parser
napi-rs
を使って変換してるので、Node.jsを前提としたコードが生成される。(さっきのBuffer
然り、node:fs
やnode:path
も使ってる)
napi-rs/napi-rs: A framework for building compiled Node.js add-ons in Rust via Node-API https://github.com/napi-rs/napi-rs
ASTを走査する
書くまでもないけど一応。
import { parseAsync } from "oxc-parser";
const SOURCE = `
const x = {
a: 1,
b: [true, false],
c: () => {},
d: new Date(),
};
console.log(x);
`;
const { program } = await parseAsync(SOURCE);
const ast = JSON.parse(program);
console.log(ast);
まあそのまんま。これを実行すると、次のASTが手に入る。
{
"type": "Program",
"start": 0,
"end": 106,
"sourceType": {
"language": "javaScript",
"moduleKind": "script",
"variant": "standard",
"alwaysStrict": false
},
"directives": [],
"hashbang": null,
"body": [
{
"type": "VariableDeclaration",
"start": 3,
"end": 87,
"kind": "const",
"declarations": [
{
"type": "VariableDeclarator",
"start": 9,
"end": 86,
"id": {
"type": "BindingPattern",
"kind": {
"type": "BindingIdentifier",
"start": 9,
"end": 10,
"name": "x"
},
"typeAnnotation": null,
"optional": false
},
"init": {
"type": "ObjectExpression",
"start": 13,
"end": 86,
"properties": [
{
"type": "ObjectProperty",
"start": 19,
"end": 23,
"kind": "init",
"key": {
"type": "IdentifierName",
"start": 19,
"end": 20,
"name": "a"
},
"value": {
"type": "NumberLiteral",
"start": 22,
"end": 23,
"value": 1
},
"init": null,
"method": false,
"shorthand": false,
"computed": false
},
{
"type": "ObjectProperty",
"start": 29,
"end": 45,
"kind": "init",
"key": {
"type": "IdentifierName",
"start": 29,
"end": 30,
"name": "b"
},
"value": {
"type": "ArrayExpression",
"start": 32,
"end": 45,
"elements": [
{
"type": "BooleanLiteral",
"start": 33,
"end": 37,
"value": true
},
{
"type": "BooleanLiteral",
"start": 39,
"end": 44,
"value": false
}
],
"trailing_comma": null
},
"init": null,
"method": false,
"shorthand": false,
"computed": false
},
{
"type": "ObjectProperty",
"start": 51,
"end": 62,
"kind": "init",
"key": {
"type": "IdentifierName",
"start": 51,
"end": 52,
"name": "c"
},
"value": {
"type": "ArrowExpression",
"span": {
"start": 54,
"end": 62
},
"expression": false,
"generator": false,
"async": false,
"params": {
"type": "FormalParameters",
"start": 54,
"end": 56,
"kind": "ArrowFormalParameters",
"items": [],
"rest": null
},
"body": {
"type": "FunctionBody",
"start": 60,
"end": 62,
"directives": [],
"statements": []
},
"typeParameters": null,
"returnType": null
},
"init": null,
"method": false,
"shorthand": false,
"computed": false
},
{
"type": "ObjectProperty",
"start": 68,
"end": 81,
"kind": "init",
"key": {
"type": "IdentifierName",
"start": 68,
"end": 69,
"name": "d"
},
"value": {
"type": "NewExpression",
"start": 71,
"end": 81,
"callee": {
"type": "IdentifierReference",
"start": 75,
"end": 79,
"name": "Date"
},
"arguments": [],
"type_parameters": null
},
"init": null,
"method": false,
"shorthand": false,
"computed": false
}
],
"trailing_comma": {
"start": 81,
"end": 81
}
},
"definite": false
}
],
"modifiers": null
},
{
"type": "ExpressionStatement",
"start": 90,
"end": 105,
"expression": {
"type": "CallExpression",
"start": 90,
"end": 104,
"callee": {
"type": "StaticMemberExpression",
"start": 90,
"end": 101,
"object": {
"type": "IdentifierReference",
"start": 90,
"end": 97,
"name": "console"
},
"property": {
"type": "IdentifierName",
"start": 98,
"end": 101,
"name": "log"
},
"optional": false
},
"arguments": [
{
"type": "IdentifierReference",
"start": 102,
"end": 103,
"name": "x"
}
],
"optional": false,
"typeParameters": null
}
}
]
}
ちなみに、OxcのASTはESTreeというSpecに完全には準拠してない。
The Oxc AST differs slightly from the estree AST by removing ambiguous nodes and introducing distinct types. https://github.com/oxc-project/oxc/?tab=readme-ov-file#-ast-and-parser
ので、世にある走査用のライブラリを使う場合は注意が必要。
というわけで、estree-walker
を使う場合はこのように。
import { asyncWalk } from "estree-walker";
// ...
let depth = 0;
await asyncWalk(ast, {
enter(node) {
console.log("ENTER", " ".repeat(depth), node.type);
depth++;
},
leave(node) {
console.log("LEAVE", " ".repeat(depth), node.type);
depth--;
},
});
続く
napi-rs
を使っててNode.js用ってことは、ブラウザで動かすのは無理か〜- って思ったけど、そもそもDocsにASTビューワーついてるやん?
- あれどうやってんのかな・・
- 調べたら
oxc-parser
使ってないやん!
というわけで、次回はその真相を探ります。