🧊

OxcのParserを、JavaScriptで使う

っても難しいことはなにもない。

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:fsnode: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使ってないやん!

というわけで、次回はその真相を探ります。