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#matchString#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 尚未公開擷取群組比對到的子字串的開始和結束索引。

正規表示式比對索引說明 #

理想情況下,我們希望在變數名稱的位置列印錯誤,而不是在 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 的群組中比對到的子字串的 [start, end) 位置。根據這些資訊,我們的編譯器現在可以列印所需的錯誤。

其他功能 #

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');

支援正規表示式比對索引 #