Индексы совпадений RegExp
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');