正则表达式匹配索引

发布于 · 标签: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');

对正则表达式匹配索引的支持 #