Перейти к основному содержимому

Индексы совпадений RegExp

· 4 мин. чтения
Майя Армянова ([@Zmayski](https://twitter.com/Zmayski)), регулярно выражая новые возможности

JavaScript теперь оснащён новым улучшением для регулярных выражений, называемым «индексы совпадений». Представьте, что вы хотите найти недопустимые имена переменных в JavaScript коде, совпадающие с зарезервированными словами, и вывести каретку и «подчеркивание» под именем переменной, например:

const function = foo;
^------- Недопустимое имя переменной

В приведённом выше примере function — зарезервированное слово и не может быть использовано как имя переменной. Для этого мы можем написать следующую функцию:

function displayError(text, message) {
const re = /\b(continue|function|break|for|if)\b/d;
const match = text.match(re);
// Индекс `1` соответствует первой группе захвата.
const [start, end] = match.indices[1];
const error = ' '.repeat(start) + // Настройка позиции каретки.
'^' +
'-'.repeat(end - start - 1) + // Добавление подчеркивания.
' ' + message; // Добавление сообщения.
console.log(text);
console.log(error);
}

const code = 'const function = foo;'; // ошибочный код
displayError(code, 'Недопустимое имя переменной');
примечание

Примечание: Для простоты приведённый выше пример содержит только несколько JavaScript зарезервированных слов.

Коротко говоря, новый массив indices хранит начальные и конечные позиции каждой совпадающей группы захвата. Этот новый массив доступен, если исходное регулярное выражение использует флаг /d для всех встроенных методов, создающих объекты совпадений регулярного выражения, включая RegExp#exec, String#match и String#matchAll.

Читайте дальше, если вам интересно, как это работает более подробно.

Мотивация

Перейдём к более сложному примеру и подумаем, как решить задачу разбора языка программирования (например, то, что делает компилятор TypeScript) — сначала разделить исходный код на токены, затем придать этим токенам синтаксическую структуру. Если пользователь написал какой-либо синтаксически некорректный код, вы захотите показать ему полезную ошибку, желательно указав место, где впервые был обнаружен проблемный код. Например, учитывая следующий фрагмент кода:

let foo = 42;
// какой-то другой код
let foo = 1337;

Мы хотели бы показать программисту ошибку следующего вида:

let foo = 1337;
^
SyntaxError: Идентификатор 'foo' уже был объявлен

Чтобы достичь этого, нам потребуется несколько строительных блоков, первый из которых — распознавание идентификаторов 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;
}

Можно использовать свойство index объекта совпадения, возвращаемого RegExp.prototype.exec, который возвращает начальную позицию всего совпадения. Однако для случаев использования, описанных выше, часто требуется использовать (возможно, несколько) группы захвата. До недавнего времени JavaScript не предоставлял индексы, где начинаются и заканчиваются подстроки, соответствующие группам захвата.

Объяснение индексов совпадений RegExp

Идеально, если мы хотим указать ошибку в месте имени переменной, а не ключевого слова let/const (как в приведённом выше примере). Но для этого нам нужно найти позицию группы захвата с индексом 2. (Индекс 1 относится к группе захвата (let|const|var), а 0 относится ко всему совпадению.)

Как уже упоминалось выше, новая функция JavaScript добавляет свойство indices к результату (массиву подстрок) метода RegExp.prototype.exec(). Давайте улучшим наш пример выше, чтобы использовать это новое свойство:

function getVariablePosition(source) {
// Обратите внимание на флаг `d`, который активирует `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], который представляет [start, end) позицию совпавшей подстроки из группы с индексом 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