RegExpマッチインデックス

公開日 ・タグ:ECMAScript

JavaScriptには、"マッチインデックス"と呼ばれる新しい正規表現拡張機能が追加されました。JavaScriptコード内で予約語と一致する無効な変数名を見つけ、キャレットとアンダーラインをその変数名の下に表示したいとしましょう。

const function = foo;
^------- Invalid variable name

上記の例では、functionは予約語であり、変数名として使用できません。そのため、次の関数を記述することができます。

function displayError(text, message) {
const re = /\b(continue|function|break|for|if)\b/d;
const match = text.match(re);
// Index `1` corresponds to the first capture group.
const [start, end] = match.indices[1];
const error = ' '.repeat(start) + // Adjust the caret position.
'^' +
'-'.repeat(end - start - 1) + // Append the underline.
' ' + message; // Append the message.
console.log(text);
console.log(error);
}

const code = 'const function = foo;'; // faulty code
displayError(code, 'Invalid variable name');

注記:簡略化のため、上記の例にはJavaScriptの予約語の一部のみが含まれています。

簡単に言うと、新しいindices配列は、一致した各キャプチャグループの開始位置と終了位置を格納します。この新しい配列は、ソース正規表現が/dフラグを使用している場合、RegExp#execString#match、およびString#matchAllを含む、正規表現マッチオブジェクトを生成するすべての組み込み関数で使用できます。

詳細な動作に興味がある方は、読み進めてください。

動機 #

より複雑な例に移り、プログラミング言語の解析タスク(例えばTypeScriptコンパイラが行うことなど)をどのように解決するかを考えてみましょう。まず、入力ソースコードをトークンに分割し、次にそれらのトークンに構文構造を与えます。ユーザーが構文的に誤ったコードを書いた場合、意味のあるエラーを提示し、理想的には問題のあるコードが最初に検出された場所にポインタを表示したいでしょう。例えば、次のコードスニペットの場合

let foo = 42;
// some other code
let foo = 1337;

プログラマには次のようなエラーを表示したいでしょう。

let foo = 1337;
^
SyntaxError: Identifier 'foo' has already been declared

これを実現するには、いくつかの構成要素が必要で、まずTypeScript識別子を認識することです。次に、エラーが発生した正確な位置を特定することに焦点を当てます。次の例では、正規表現を使用して文字列が有効な識別子かどうかを判断します。

function isIdentifier(name) {
const re = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
return re.exec(name) !== null;
}

注記:現実世界のパーサーは、新しく導入された正規表現のプロパティエスケープを利用し、有効なすべてのECMAScript識別子名を一致させるために次の正規表現を使用できます。

const re = /^[$_\p{ID_Start}][$_\u200C\u200D\p{ID_Continue}]*$/u;

簡略化のため、ラテン文字、数字、アンダースコアのみを一致させる以前の正規表現を使用することにします。

上記の変数宣言でエラーが発生し、ユーザーに正確な位置を出力したい場合、上記の正規表現を拡張し、同様の関数を使用することができます。

function getDeclarationPosition(source) {
const re = /(let|const|var)\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/;
const match = re.exec(source);
if (!match) return -1;
return match.index;
}

RegExp.prototype.execによって返されるマッチオブジェクトのindexプロパティを使用できます。これは、全体の一致の開始位置を返します。しかし、上記のようなユースケースでは、(複数の)キャプチャグループを使用したいことがよくあります。最近まで、JavaScriptはキャプチャグループによって一致する部分文字列が始まりと終わるインデックスを公開していませんでした。

RegExpマッチインデックスの説明 #

理想的には、let/constキーワードの位置ではなく(上記の例のように)、変数名の位置にエラーを出力したいと考えています。しかし、そのためには、インデックス2のキャプチャグループの位置を見つける必要があります。(インデックス1(let|const|var)キャプチャグループを参照し、0は全体の一致を参照します。)

上記のように、新しいJavaScript機能は、RegExp.prototype.exec()の結果(部分文字列の配列)にindicesプロパティを追加します。この新しいプロパティを利用するために、上記の例を拡張しましょう。

function getVariablePosition(source) {
// Notice the `d` flag, which enables `match.indices`
const re = /(let|const|var)\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/d;
const match = re.exec(source);
if (!match) return undefined;
return match.indices[2];
}
getVariablePosition('let foo');
// → [4, 7]

この例は配列[4, 7]を返します。これは、インデックス2のグループから一致した部分文字列の[開始位置, 終了位置)です。この情報に基づいて、コンパイラは目的のエラーを出力できるようになりました。

追加機能 #

indicesオブジェクトには、名前付きキャプチャグループの名前によりインデックス付けできるgroupsプロパティも含まれています。それを用いると、上記の関数は次のように書き直すことができます。

function getVariablePosition(source) {
const re = /(?<keyword>let|const|var)\s+(?<id>[a-zA-Z_$][0-9a-zA-Z_$]*)/d;
const match = re.exec(source);
if (!match) return -1;
return match.indices.groups.id;
}
getVariablePosition('let foo');

RegExpマッチインデックスのサポート #