OXCで正規表現パーサーを実装してるときに全部一通り読んでみて、みんな違ってみんな良いってなったので。
候補はこちらの3つ。
- https://github.com/jviereck/regjsparser
- https://github.com/DmitrySoshnikov/regexp-tree
- https://github.com/eslint-community/regexpp
かのAST Explorerでも、RegExp部門ではこの3つがリストにある。
AST explorer https://astexplorer.net/
前提
ECMAScript本体だと、ESTreeというデファクトがあるけど、残念ながらRegExp
にはない。
ESTreeでのRegExp
の扱いはこんな感じで、ただの文字列でしかない。
interface RegExpLiteral <: Literal {
regex: {
pattern: string;
flags: string;
};
}
https://github.com/estree/estree/blob/master/es5.md#regexpliteral
そういうわけで、この正規表現パターンの中身を、ASTで扱いたいモチベーションがあった人々が、それぞれパーサーを書いたのであろう。
というわけで、npmへの公開が早かった順にご紹介。
regjsparser
jviereck/regjsparser: Parsing the JavaScript’s RegExp in JavaScript. https://github.com/jviereck/regjsparser/tree/gh-pages
先鋒はregjsparser
で、0.0.1
が公開されたのはなんと2013年!にも関わらず、今もメンテが続いている様子。
- ES2024の
v
フラグ - Stage 3のModifiers
といった最新の仕様にも対応してる。
特徴は、なんといっても1ファイルでコードが完結してるところ。
https://github.com/jviereck/regjsparser/blob/38b304d6c38d79d80a0f2417c93a4db1a18e0ab1/parser.js
トータル1654行しかないし、コメントとユーティリティ的なコードを除けば、ほんと1000行ちょっとしかない。サイズも他の実装の1/4くらいしかない。
アーキテクチャも素直で、ほとんどがparseXxx(): Xxx
という感じのシグネチャになっており、とても読みやすい。
ただ効率重視な実装になってる場所もあって、意味的には異なる複数のパターンを、1つの関数でまとめて処理してたりすることもある。 (たとえば https://github.com/jviereck/regjsparser/blob/38b304d6c38d79d80a0f2417c93a4db1a18e0ab1/parser.js#L929 )
基本はUTF-16のコードユニットで処理しつつも、u
フラグ時は、サロゲートペアを2つくっつけて単一文字として扱ってるところが印象的だった。
https://github.com/jviereck/regjsparser/blob/38b304d6c38d79d80a0f2417c93a4db1a18e0ab1/parser.js#L313
循環参照もないシンプルな浅めのASTを出力するので、簡単にJSON化したりできるのも扱いやすいところか。
コスパのいい実装だとは思いつつ、不満も書いておくと・・・。
TypeScriptではなく、一緒に置いてある.d.ts
ファイルが微妙にズレてたり足りてない定義があったりするところは気になる。
あとは、バリデーション系の実装も結構抜けてる印象で、たとえば以下がエラーにならない。
/\p{Invalid}/v
: 不正なユニコードプロパティ/\k<invalid>/u
: 存在しない名前付きキャプチャグループ名/(?<dup>)(?<dup>)/
: グループ名の重複
パース処理自体にもJSのRegExp
を多用してるので、実行パフォーマンスもあまり良くないとは思う。
コードが少ない分、機能も少なめといった感じか。
regexp-tree
DmitrySoshnikov/regexp-tree: Regular expressions processor in JavaScript https://github.com/DmitrySoshnikov/regexp-tree
続いては2017年が初出であるregexp-tree
を。
このパーサーの特徴は、なんといってもSpecテキストであるBNF(風のテキスト)を元に、CLIでコードを生成してるところ。
実際にパーサーを書いてて、Specから生成できんもんかね〜とは思ってたけど、やってる実例があったのは驚いた。(なおコードは読み解くことはできそうにない)
DmitrySoshnikov/syntax: Syntactic analysis toolkit, language-agnostic parser generator. https://github.com/DmitrySoshnikov/syntax
しかし手動でコードを書いてる部分もやはりあるようで、そう全部うまくはいかんのやなーという感想。
不満としては、
v
フラグがエラーになるd
フラグもなぜかエラーになる(パーサーとしての出力には影響ないけど)u
フラグがあっても、サロゲートペアは展開されたまま
というあたりか。
特にv
フラグがエラーになるのは痛い。かと思えば、まだStage 1のx
フラグは実装されてたりして、さらに謎が深まる。
出力されるASTもverboseというか、画一的と言うか、なんというか特徴的な感じ。
そんな感じではあるものの、パーサーの他にもコンポーネントがいろいろあって全方位的なおかげか、この3つの中では一番スターが多い。
regexpp
eslint-community/regexpp: The regular expression parser for ECMAScript. https://github.com/eslint-community/regexpp
最後は、元はmysticatea氏の個人プロジェクトだったが、最近はESLintコミュニティで管理されてる@eslint-community/regexpp
を。
最後発であり、npmに出たのは2022年のことらしい。(個人プロジェクト時は2018年から)
ESLintのコアルールでも使われてるだけあってか、もっとも重厚な仕上がりだと感じた。コードも全部TSで書かれてて、行数もこの中では一番多い。
コードの流れは、Specのパターン通りになっており、素直で読みやすい。
けど、アーキテクチャとしてはJavaScriptらしく(?)ダイナミックな処理が多用されており、コードを追うのは大変だった。
(たとえば https://github.com/eslint-community/regexpp/blob/608145afd010502ef9b0e1c0002a46889a11cb0f/src/parser.ts#L586 )
(他にもparseXxx(): boolean
な関数を呼ぶと、this._xxx
が更新されるとかそういう)
後から知ったことではあるけど、acorn
もパースの過程で正規表現リテラルをちゃんと(内部的にnew RegExp()
するだけではなく)パースしてるようで、regexpp
のコードはacorn
のが元になってると思う。(似てるところが多すぎるので)
そして自分が知る限り、ES2025までのSpecを全部実装していて流石の貫禄!って感じ。
u
フラグとそうでないときで、Reader
クラスの実装を使い分けてた。
出力されるASTも割と重厚な印象なのと、パーサーと言いつつbackreferenceとCapturingグループの紐づけまでやってくれるので、循環参照が発生してて愚直にJSON.stringify()
できないところは気になった。(あくまでJSで書かれたツールで利用するなら問題ではないけど)
Annex Bで定義されてる内容を、あえて許可しないというstrictモードを使い分けられるようになってるのも特徴かも。ただ別にコードが軽くなるわけではないし、いつ使いたくなるのかは知らんけど。
まとめ
(もともとのSpecがあるだけJSDocよりはマシではあるけど、)正規表現をASTで扱いたい人がそんなにいないからか、機能やらASTのデザインやらいろんな線引が難しい分野やなと思った。
そんな3者3様をサクッと眺められるツールも用意したので、正規表現パーサーを実装するときのお供にどうぞ。
JS RegExp AST Viewer https://leaysgur.github.io/js-regexp-ast-viewer/
ちなみに、OXCでの正規表現パーサーの実装は、regjsparser
とregexpp
を足して2で割った感じをイメージして実装してる。