🧊

@eslint-community/regexpp のコードを読む

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

https://github.com/eslint-community/regexpp/blob/d7f0d662424b6cd6a7fcad5f628ca20f25cf6813/src/index.ts

大まかにに3つ。

  • RegExpParserクラス
    • parseRegExpLiteral(source, options)
  • RegExpValidatorクラス
    • validateRegExpLiteral(source, options)
  • visitRegExpAST(node, handlers)

あとはASTの各ノード定義と、RegExpSyntaxErrorの構造体など。

シンプルだわ。

RegExpParser

https://github.com/eslint-community/regexpp/blob/d7f0d662424b6cd6a7fcad5f628ca20f25cf6813/src/parser.ts

  • 内部的な状態であるRegExpParserStateと、RegExpValidatorのインスタンスを抱えてる
  • 公開メソッドは3つ
    • parseLiteral()
    • parsePattern()
    • parseFlags()
  • これらが呼ばれるとき、対応するRegExpValidatorvalidate(Literal|Flags|Pattern)()が呼ばれる
  • その結果、RegExpParserStateが更新され、そのプロパティの一部を使って値をそれぞれ返す
    • 後述するpatternとかflags

というわけで、このRegExpParserクラスはほとんどガワであり、実体はRegExpValidatorと、RegExpParserStateにある。

ちなみに、Literalとは/foobar/iみたいなフルの正規表現文字列で、Patternfoobarの部分、Flagsiの部分のこと。

RegExpParserState

https://github.com/eslint-community/regexpp/blob/d7f0d662424b6cd6a7fcad5f628ca20f25cf6813/src/parser.ts#L53

  • 後述するRegExpValidator.Optionsと同じ型
  • onPatternEnter()onPatternLeave()onCharacter()のような、ASTに対応したフックを持つ
  • RegExpValidatorがこれを呼び出すことで、RegExpParserState.patternRegExpParserState.flagsのプロパティが育っていく
    • ここはいわゆるパーサーっぽい仕事してる

RegExpValidator

https://github.com/eslint-community/regexpp/blob/d7f0d662424b6cd6a7fcad5f628ca20f25cf6813/src/validator.ts

Lexerというわけではなさそう。トークン化せずにそのままRegExpParserStateにつながっていく感じ?

RegExpValidator#validateLiteral()

https://github.com/eslint-community/regexpp/blob/d7f0d662424b6cd6a7fcad5f628ca20f25cf6813/src/validator.ts#L664

最もよくあるユースケースの流れを見ておく。

  • 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)

https://github.com/eslint-community/regexpp/blob/d7f0d662424b6cd6a7fcad5f628ca20f25cf6813/src/index.ts#L34

実体はこれだけ。

new RegExpVisitor(handlers).visit(node);

visit()の中身も、与えられたASTのnode.typeに応じて、handlersで待ち受けてるそれぞれのフックを呼び出すだけ。

ASTもexportされてるので、これを使わず自分でイテレートしてもよいはず。

ASTの構造

ChatGPT 4oにTSの定義を食わせて聞いたらこれが出てきた。

https://twitter.com/leaysgur/status/1803063346918248846

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
  • unicode/
    • 文字通り、パースしている最中の文字のコードポイントやらを判別するために

おわりに

コンテキストはさておき、外部依存がなくて、仕様があるコードはやはり読みやすい。

おかげでだいたいの雰囲気はわかった(気がする)。ASTの詳細は追って把握しないといけなそうやけど。

とはいえ、OXCで実装する場合、そのままリライトするのではダメで、ハイパフォーマンスなoxc_parserの流儀に従う必要がある。

なのでそっちの雰囲気も理解しておかないと、コードに落とし込むのは難しそう。やはりOXCの本丸に攻め込む必要がありそうで、こっちのほうが難易度が高そう・・・!