*️⃣

TypeScript 5.5 で追加された正規表現構文チェックを理解する

2024/06/21に公開

TypeScript 5.5で、@graphemeclusterさんによって正規表現リテラルの構文チェックが導入されました🎉
この構文チェックによって、正規表現に間違いがあった場合、事前にTypeScriptがエラーを出力してくれます。

https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#regular-expression-syntax-checking

この機能について、次のことが気になったので調べてみました。

  • どんな構文がエラーになるか
  • なぜ導入されたか
  • どうやってチェックしているか
  • JavaScriptで実行できるがTypeScriptでエラーになる構文はあるか
  • ESLintとのカバー範囲の違い

本記事に関して、誤り等があれば指摘いただけると嬉しいです。

どんな構文がエラーになるか

TypeScript 5.5では、正規表現に関するエラーメッセージが40個程度追加されています。

https://github.com/microsoft/TypeScript/blob/327bd0990f2ce3a7062f4c9bf0b8027cc44b2f4f/src/compiler/diagnosticMessages.json#L1648-L1803

例えば、下記のような構文は、5.5でエラーになります。

// 存在しないフラグ
var re = /a/b; // エラー: Unknown regular expression flag.

// かっこの閉じ忘れ
var re = /(/; // エラー: ')' expected.

// 不明な Unicode プロパティの名前や値
var re = /\p{a}/u; // エラー: Unknown Unicode property name or value.

よくある間違いについては改善方法まで提示してくれるメッセージになっています。

また、TypeScriptのコンパイラオプションで指定されたECMAScriptバージョンよりも新しい構文を使用している場合もエラーを出してくれるようです。

var re = /(?<ab>ab)/;
//エラー: Named capturing groups are only available when targeting 'ES2018' or later.

なぜ導入されたか

正規表現の構文チェックに関する Issueは、2015年ごろに立てられています。

https://github.com/microsoft/TypeScript/issues/3432

同年に、正規表現の構文チェックを実装した2つのPR[1][2]も出ています。両方とも、RegExpコンストラクタに正規表現リテラルを渡して、エラーになったら構文エラーとして扱うというものです。
ただ、RegExpコンストラクタに依存する方法は、Node.js v8で動かなかったり、実行環境によって動作が保証されないなど色々問題があり両方ともクローズされています。

Now, if you wanted to go more the distance and actually check specific parts of the string against the regex grammar in the ES6 specification, then I would be more ok with that :)

クローズ時のコメントを意訳すると、ECMAScriptの仕様に準拠した構文チェックであれば良いと書かれていそうです。
今回のTypeScript 5.5まで正規表現の構文チェックが実装されなかったのは、特に理由があるわけではなく、単に実装する人がいなかったのかなという印象を受けました。

どうやってチェックしているか

正規表現の構文チェックが実装されたPRの内容を見てみます。

https://github.com/microsoft/TypeScript/pull/55600

diffを見ると、正規表現の構文チェックはScannerで実装されていることがわかります。TypeScript Deep DiveのScannerの項によると、ソースコードはScannerを経由してToken Streamになり、その後Parserを通してASTに変換されます。

SourceCode ~~ scanner ~~> Token Stream ~~ parser ~~> AST

つまり、トークン単位でソースコードをスキャンしていき、例えば、/が出てきたら正規表現としてさらに中身を解析し、特定のパターンをチェックしてエラーを報告するといった形で実装されています。

例えば、不明なUnicodeプロパティの名前や値のチェックは、次に書かれています。

https://github.com/microsoft/TypeScript/blob/327bd0990f2ce3a7062f4c9bf0b8027cc44b2f4f/src/compiler/scanner.ts#L3454-L3533

字句解析をしていき、\d, \D, \s, \S, \w, \Wの場合は、単純な文字クラスエスケープとして処理、\Pまたは\pの場合は、Unicodeプロパティエスケープとして処理します。その次の文字が { ならプロパティの解析を開始します。その後、nonBinaryUnicodePropertiesvaluesOfNonBinaryUnicodePropertiesを参照して、プロパティ名や値が有効かどうかをチェックしています。有効なプロパティ名や値はScannerの末尾に自前で定義し管理されています。

https://github.com/microsoft/TypeScript/blob/327bd0990f2ce3a7062f4c9bf0b8027cc44b2f4f/src/compiler/scanner.ts#L4056-L4081

このPRのレビューでは、ScannerではなくParserで構文チェックのロジックを持った方が、後にASTノードを追加して詳細な解析ができるようになって良さそうというコメントもありましたが、パフォーマンスの懸念が優先されたようです。

マージされた際のコメントをみると、

we still want to do some refactoring here and possibly move the logic to the parser or another component.

ロジックをParserやその他コンポーネントに移動するリファクタリングがしたいと書かれており、今後、実装内容は変わるかも知れません。

JavaScript で実行できるが TypeScript でエラーになる構文はあるか

TypeScriptの正規表現の構文チェックがECMAScript仕様をどこまで網羅しているかは、PRからは読み取れませんでした。PRにはTest262からテストケースが追加されていましたが、ファイル数が多すぎるため削除されたようです。

TypeScriptはStage 3以降から実装するポリシーになっています。TypeScript 5.5には、現在 Stage 3であるRegular Expression Pattern Modifiersという構文の対応も含まれていた[3]ので、ポリシーから外れた実装にはなっていない印象です。

レガシーな構文での挙動の違い

8進エスケープなどレガシーな構文の一部は、主要なブラウザのJavaScriptエンジンでは実行可能ですが、TypeScript 5.5ではエラーになります。

const regex = /\01/;
// エラー:Octal escape sequences are not allowed. Use the syntax '\x01'.

レガシーな構文は本文でLegacyと記載されるかECMAScript Annex Bに記載されています。Annex Bとは、Webブラウザー互換性のためにJavaScriptのレガシーな挙動について定めた仕様のことです。そのため主要ブラウザはAnnex Bに定められた仕様を実装しています。Annex BならびにAnnex Bに記載の正規表現の構文については、下記の記事が参考になります。

https://zenn.dev/qnighy/articles/1d96f2c0c662f6#正規表現のレガシー文法

5.5 Beta時点では、Annex Bに記載の構文もエラーが報告されていたようですが、影響範囲が大きいため5.5ではチェックが緩くなっています。正規表現の構文チェックにおいてAnnex Bをどうするかはデザインミーティングなどで議論されているようです。

その他挙動の違い

以下の構文もTypeScript 5.5ではエラーになります。

var re = /\u{61}/;
// エラー: Hexadecimal digit expected.

\u{xxxx}Unicodeコードポイントエスケープと呼ばれ、uフラグがついたUnicodeモードでのみ動作します。上記のように、uフラグのない正規表現リテラル中でUnicodeコードポイントエスケープを利用すると、\uu一文字として解釈され"u".repeat(61)とマッチします。ユーザが間違って使用している可能性が高いためエラーにしているのかなと予想しますが、エラー文がHexadecimal digit expected.になっているので、意図を理解するのは難しいかもしれません。

挙動差への対応まとめ

レガシーな構文は基本的にはエラーを出さないようにして、ユーザが間違って使っていると考えられる構文に対してはエラーを出すようにしているのかなと思いました。
また、一部エラーになるものについては理解して使っているなら// @ts-ignoreするか、後にコンパイラオプションやフラグによって制御できるようになるかも知れません。

ESLint とのカバー範囲の違い

正規表現の構文チェックには、ESLintを使うことができます。ESLintはデフォルトパーサにEspreeを使用しており、有効でない正規表現についてはその時点でパースエラーになります。ESLintはその上で、各種正規表現のルールに対してregexppというライブラリを使用しASTを使った解析をしています。

ESLintのプラグインの一つであるeslint-plugin-regexpでもregexppは使われており、正規表現のベストプラクティスに関するルールがたくさん定められています。その中でもregexp/strictというルールで、Annex Bに記載のあいまいな構文を禁止できます。

このルールで禁止される構文のうち、TypeScript 5.5でエラーになるのは前述のUnicodeコードポイントエスケープの利用ミスの構文だけでした。

感想

今後の構文チェックのロジックの移行、パフォーマンスチューニングなどは気になるので、追ってみようかなと思います。TypeScriptほど大きいOSSプロジェクトのPRをしっかり見ることは初めてでしたが、パフォーマンス周りに敏感で、その書き方ってパフォーマンスに影響あるんだ、みたいなのをレビューから知れて面白かったです(例えばデフォルト引数はパフォーマンス上よくないらしい[4])。

脚注
  1. Add invalid regular expression literal error #3432 by Schmavery · Pull Request #4387 · microsoft/TypeScript ↩︎

  2. Added validation for regex literals via RegExp constructor by JoshuaKGoldberg · Pull Request #35957 · microsoft/TypeScript ↩︎

  3. https://github.com/microsoft/TypeScript/blob/867476e57a83dd6d8d6668308a7aa8ff14c422c4/src/compiler/scanner.ts#L2845-L2846 ↩︎

  4. https://github.com/microsoft/TypeScript/pull/55600#discussion_r1569073347 ↩︎

GitHubで編集を提案
サイボウズ フロントエンド

Discussion