Saltar al contenido principal

Índices de coincidencia de RegExp

· 5 min de lectura
Maya Armyanova ([@Zmayski](https://twitter.com/Zmayski)), expresando nuevas características regularmente

JavaScript ahora está equipado con una nueva mejora de expresiones regulares, llamada “índices de coincidencia”. Imagínate que quieres encontrar nombres de variables inválidos en el código JavaScript que coincidan con palabras reservadas, y mostrar un símbolo de intercalación y una “línea de subrayado” debajo del nombre de la variable, como:

const function = foo;
^------- Nombre de variable inválido

En el ejemplo anterior, function es una palabra reservada y no puede usarse como nombre de variable. Para eso podríamos escribir la siguiente función:

function displayError(text, message) {
const re = /\b(continue|function|break|for|if)\b/d;
const match = text.match(re);
// El índice `1` corresponde al primer grupo de captura.
const [start, end] = match.indices[1];
const error = ' '.repeat(start) + // Ajusta la posición del símbolo de intercalación.
'^' +
'-'.repeat(end - start - 1) + // Añadir la línea de subrayado.
' ' + message; // Añadir el mensaje.
console.log(text);
console.log(error);
}

const code = 'const function = foo;'; // código erróneo
displayError(code, 'Nombre de variable inválido');
nota

Nota: Para simplificar, el ejemplo anterior contiene solo algunas palabras reservadas de JavaScript.

En resumen, el nuevo array indices almacena las posiciones de inicio y fin de cada grupo de captura coincidente. Este nuevo array está disponible cuando la expresión regular de origen utiliza el flag /d para todos los productos internos que generen objetos de coincidencia de expresiones regulares, incluidos RegExp#exec, String#match y String#matchAll.

Sigue leyendo si estás interesado en cómo funciona en más detalle.

Motivación

Pasemos a un ejemplo más complejo y pensemos en cómo resolverías la tarea de analizar un lenguaje de programación (por ejemplo, lo que hace el compilador de TypeScript) — primero divide el código fuente de entrada en tokens, luego da una estructura sintáctica a esos tokens. Si el usuario escribió un código sintácticamente incorrecto, querrás presentarle un error significativo, idealmente señalando la ubicación donde se encontró el código problemático por primera vez. Por ejemplo, dado el siguiente fragmento de código:

let foo = 42;
// algún otro código
let foo = 1337;

Queremos presentar al programador con un error como:

let foo = 1337;
^
SyntaxError: El identificador 'foo' ya ha sido declarado

Para lograr esto, necesitamos algunos bloques de construcción, el primero de los cuales es reconocer los identificadores de TypeScript. Luego nos enfocaremos en ubicar el lugar exacto donde ocurrió el error. Consideremos el siguiente ejemplo, usando una expresión regular para saber si una cadena es un identificador válido:

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

Nota: Un analizador del mundo real podría usar las recién introducidas escapatorias de propiedades en expresiones regulares y usar la siguiente expresión regular para coincidir con todos los nombres de identificadores válidos de ECMAScript:

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

Para simplificar, sigamos con nuestra expresión regular anterior, que coincide solo con caracteres latinos, números y guiones bajos.

Si encontramos un error con una declaración de variable como se indicó anteriormente y queremos imprimir la posición exacta al usuario, podríamos querer extender la expresión regular anterior y usar una función similar:

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;
}

Uno podría usar la propiedad index en el objeto de coincidencia devuelto por RegExp.prototype.exec, que devuelve la posición inicial de toda la coincidencia. Para casos de uso como el descrito anteriormente, sin embargo, a menudo querrás usar (posiblemente múltiples) grupos de captura. Hasta hace poco, JavaScript no exponía los índices donde comienzan y terminan las subcadenas coincidentes por los grupos de captura.

Explicación de los índices de coincidencia de RegExp

Idealmente queremos imprimir un error en la posición del nombre de la variable, no en la palabra clave let/const (como hace el ejemplo anterior). Pero para eso necesitaríamos encontrar la posición del grupo de captura con el índice 2. (El índice 1 se refiere al grupo de captura (let|const|var) y 0 se refiere a toda la coincidencia.)

Como se mencionó anteriormente, la nueva característica de JavaScript agrega una propiedad indices al resultado (el array de subcadenas) de RegExp.prototype.exec(). Vamos a mejorar nuestro ejemplo de arriba para utilizar esta nueva propiedad:

function getVariablePosition(source) {
// Note la bandera `d`, que habilita `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]

Este ejemplo devuelve el array [4, 7], que es la posición [inicio, fin) de la subcadena coincidente del grupo con índice 2. Basándonos en esta información, nuestro compilador ahora puede imprimir el error deseado.

Características adicionales

El objeto indices también contiene una propiedad groups, que puede ser indexada por los nombres de los grupos de captura nombrados. Usando esto, la función anterior puede reescribirse de la siguiente manera:

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

Soporte para índices de coincidencia de RegExp