eslint-community/regexpp: The regular expression parser for ECMAScript. https://github.com/eslint-community/regexpp
OXCでこれをRustにポートする必要があり、そのために。
正規表現それ自体についても別に詳しくないけど、仕様があるだけJSDocよりマシやと信じてる・・・。
直近でES2025をサポートをしようとしてるPRがあったので、せっかくなのでそこ起点で読んでみることにする。
https://github.com/eslint-community/regexpp/commit/d7f0d662424b6cd6a7fcad5f628ca20f25cf6813
(現時点では未レビュー・未マージなので、もしかしたら変わることがあるかも・・・?)
exportされてるAPI
大まかにに3つ。
RegExpParserクラスparseRegExpLiteral(source, options)
RegExpValidatorクラスvalidateRegExpLiteral(source, options)
visitRegExpAST(node, handlers)
あとはASTの各ノード定義と、RegExpSyntaxErrorの構造体など。
シンプルだわ。
RegExpParser
- 内部的な状態である
RegExpParserStateと、RegExpValidatorのインスタンスを抱えてるRegExpValidatorには、RegExpParserStateを渡してある- https://github.com/eslint-community/regexpp/blob/d7f0d662424b6cd6a7fcad5f628ca20f25cf6813/src/parser.ts#L783
- 公開メソッドは3つ
parseLiteral()parsePattern()parseFlags()
- これらが呼ばれるとき、対応する
RegExpValidatorのvalidate(Literal|Flags|Pattern)()が呼ばれる - その結果、
RegExpParserStateが更新され、そのプロパティの一部を使って値をそれぞれ返す- 後述する
patternとかflags
- 後述する
というわけで、このRegExpParserクラスはほとんどガワであり、実体はRegExpValidatorと、RegExpParserStateにある。
ちなみに、Literalとは/foobar/iみたいなフルの正規表現文字列で、Patternはfoobarの部分、Flagsはiの部分のこと。
RegExpParserState
- 後述する
RegExpValidator.Optionsと同じ型 onPatternEnter()、onPatternLeave()やonCharacter()のような、ASTに対応したフックを持つRegExpValidatorがこれを呼び出すことで、RegExpParserState.patternとRegExpParserState.flagsのプロパティが育っていく- ここはいわゆるパーサーっぽい仕事してる
RegExpValidator
- 名前はこうなってるけど、いわゆる構文解析してるのはココ
Readerのインスタンスを抱えてて、これがadvance()とかeat()とかやってるuフラグの有無で、内部実装が変わるRegExpValidator側でも同名のメソッドがマップされてて、そこから呼ばれる
Lexerというわけではなさそう。トークン化せずにそのままRegExpParserStateにつながっていく感じ?
RegExpValidator#validateLiteral()
最もよくあるユースケースの流れを見ておく。
Readerの初期化onLiteralEnter()のフックを呼び- 正規表現パターンに問題がないなら:
if (this.eat(SOLIDUS) && this.eatRegExpBody() && this.eat(SOLIDUS)) {validateFlagsInternal()でフラグを検証し、onRegExpFlags()のフックを呼ぶvalidatePatternInternal()でパターンの検証
onLiteralLeave()のフックを呼ぶ
validatePatternInternal()が解析を進めていく入口になってて、
consumePattern()からはじまりconsumeDisjunction()consumeAlternative()>consumeTerm()>consumeAssertion()>eat()…consumeQuantifier()>eatBracedQuantifier()>eat()…
というように、ASTを掘り進めていき、その都度onXxx()フックが呼ばれる。
consumeXxx()とeatXxx()の数だけ、ASTの種類があるというわけで、それを俯瞰するにはGitHubのSymbolsサイドバーが便利だった。
ASTの種類は、ASTとして固めてexportされてる。
https://github.com/eslint-community/regexpp/blob/d7f0d662424b6cd6a7fcad5f628ca20f25cf6813/src/ast.ts
visitRegExpAST(node, handlers)
実体はこれだけ。
new RegExpVisitor(handlers).visit(node);
visit()の中身も、与えられたASTのnode.typeに応じて、handlersで待ち受けてるそれぞれのフックを呼び出すだけ。
ASTもexportされてるので、これを使わず自分でイテレートしてもよいはず。
ASTの構造
ChatGPT 4oにTSの定義を食わせて聞いたらこれが出てきた。
graph TD
RegExpLiteral --> Pattern
Pattern --> Alternative
Alternative --> Element
Element --> Assertion
Element --> QuantifiableElement
Element --> Quantifier
QuantifiableElement --> Backreference
QuantifiableElement --> CapturingGroup
QuantifiableElement --> Character
QuantifiableElement --> CharacterClass
QuantifiableElement --> CharacterSet
QuantifiableElement --> ExpressionCharacterClass
QuantifiableElement --> Group
QuantifiableElement --> LookaheadAssertion
Assertion --> BoundaryAssertion
Assertion --> LookaroundAssertion
LookaroundAssertion --> LookaheadAssertion
LookaroundAssertion --> LookbehindAssertion
BoundaryAssertion --> EdgeAssertion
BoundaryAssertion --> WordBoundaryAssertion
CharacterClass --> ClassRangesCharacterClass
CharacterClass --> UnicodeSetsCharacterClass
ClassRangesCharacterClass --> Character
ClassRangesCharacterClass --> CharacterClassRange
ClassRangesCharacterClass --> CharacterUnicodePropertyCharacterSet
ClassRangesCharacterClass --> EscapeCharacterSet
UnicodeSetsCharacterClass --> Character
UnicodeSetsCharacterClass --> CharacterClassRange
UnicodeSetsCharacterClass --> ClassStringDisjunction
UnicodeSetsCharacterClass --> EscapeCharacterSet
UnicodeSetsCharacterClass --> ExpressionCharacterClass
UnicodeSetsCharacterClass --> UnicodePropertyCharacterSet
UnicodeSetsCharacterClass --> UnicodeSetsCharacterClass
CharacterSet --> AnyCharacterSet
CharacterSet --> EscapeCharacterSet
CharacterSet --> UnicodePropertyCharacterSet
UnicodePropertyCharacterSet --> CharacterUnicodePropertyCharacterSet
UnicodePropertyCharacterSet --> StringsUnicodePropertyCharacterSet
ExpressionCharacterClass --> ClassIntersection
ExpressionCharacterClass --> ClassSubtraction
ClassStringDisjunction --> StringAlternative
StringAlternative --> Character
CapturingGroup --> Alternative
CapturingGroup --> Backreference
Group --> Alternative
LookaheadAssertion --> Alternative
LookbehindAssertion --> Alternative
Quantifier --> QuantifiableElement
ClassIntersection --> ClassSetOperand
ClassSubtraction --> ClassSetOperand
ClassSetOperand --> Character
ClassSetOperand --> ClassStringDisjunction
ClassSetOperand --> EscapeCharacterSet
ClassSetOperand --> ExpressionCharacterClass
ClassSetOperand --> UnicodePropertyCharacterSet
ClassSetOperand --> UnicodeSetsCharacterClass
Backreference --> CapturingGroup
Backreference --> AmbiguousBackreference
Backreference --> UnambiguousBackreference
AmbiguousBackreference --> CapturingGroup
UnambiguousBackreference --> CapturingGroup
RegExpLiteral --> Flags
あってるかわからんけど、まあ雰囲気をつかむには便利?
他のコード
ソースコード全体をツリー化するとこうなってる。
├── ast.ts
├── ecma-versions.ts
├── group-specifiers.ts
├── index.ts
├── parser.ts
├── reader.ts
├── regexp-syntax-error.ts
├── unicode
│ ├── ids.ts
│ ├── index.ts
│ └── properties.ts
├── validator.ts
└── visitor.ts
ほかの目ぼしいものを拾い読みすると・・・、
- ecma-versions
- ES2025とか特定のバージョンからのみフラグが追加されたりするので、その判別のために
- group-specifiers
- ES2025からグループ名が重複できるようになるので、その対応のために
- https://github.com/tc39/proposal-duplicate-named-capturing-groups
- unicode/
- 文字通り、パースしている最中の文字のコードポイントやらを判別するために
おわりに
コンテキストはさておき、外部依存がなくて、仕様があるコードはやはり読みやすい。
おかげでだいたいの雰囲気はわかった(気がする)。ASTの詳細は追って把握しないといけなそうやけど。
とはいえ、OXCで実装する場合、そのままリライトするのではダメで、ハイパフォーマンスなoxc_parserの流儀に従う必要がある。
なのでそっちの雰囲気も理解しておかないと、コードに落とし込むのは難しそう。やはりOXCの本丸に攻め込む必要がありそうで、こっちのほうが難易度が高そう・・・!