TypeScript 5.5 で追加された正規表現構文チェックを理解する
TypeScript 5.5で、@graphemeclusterさんによって正規表現リテラルの構文チェックが導入されました🎉
この構文チェックによって、正規表現に間違いがあった場合、事前にTypeScriptがエラーを出力してくれます。
この機能について、次のことが気になったので調べてみました。
- どんな構文がエラーになるか
- なぜ導入されたか
- どうやってチェックしているか
- JavaScriptで実行できるがTypeScriptでエラーになる構文はあるか
- ESLintとのカバー範囲の違い
本記事に関して、誤り等があれば指摘いただけると嬉しいです。
どんな構文がエラーになるか
TypeScript 5.5では、正規表現に関するエラーメッセージが40個程度追加されています。
例えば、下記のような構文は、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年ごろに立てられています。
同年に、正規表現の構文チェックを実装した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の内容を見てみます。
diffを見ると、正規表現の構文チェックはScannerで実装されていることがわかります。TypeScript Deep DiveのScannerの項によると、ソースコードはScannerを経由してToken Streamになり、その後Parserを通してASTに変換されます。
SourceCode ~~ scanner ~~> Token Stream ~~ parser ~~> AST
つまり、トークン単位でソースコードをスキャンしていき、例えば、/
が出てきたら正規表現としてさらに中身を解析し、特定のパターンをチェックしてエラーを報告するといった形で実装されています。
例えば、不明なUnicodeプロパティの名前や値のチェックは、次に書かれています。
字句解析をしていき、\d
, \D
, \s
, \S
, \w
, \W
の場合は、単純な文字クラスエスケープとして処理、\P
または\p
の場合は、Unicodeプロパティエスケープとして処理します。その次の文字が {
ならプロパティの解析を開始します。その後、nonBinaryUnicodeProperties
やvaluesOfNonBinaryUnicodeProperties
を参照して、プロパティ名や値が有効かどうかをチェックしています。有効なプロパティ名や値はScannerの末尾に自前で定義し管理されています。
この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に記載の正規表現の構文については、下記の記事が参考になります。
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コードポイントエスケープを利用すると、\u
がu
一文字として解釈され"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])。
-
Add
invalid regular expression literal
error #3432 by Schmavery · Pull Request #4387 · microsoft/TypeScript ↩︎ -
Added validation for regex literals via RegExp constructor by JoshuaKGoldberg · Pull Request #35957 · microsoft/TypeScript ↩︎
-
https://github.com/microsoft/TypeScript/blob/867476e57a83dd6d8d6668308a7aa8ff14c422c4/src/compiler/scanner.ts#L2845-L2846 ↩︎
-
https://github.com/microsoft/TypeScript/pull/55600#discussion_r1569073347 ↩︎
Discussion