Skip to content

Commit

Permalink
Add support for the duplicate named capturing groups proposal (#14805)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Sep 2, 2022
1 parent eec9574 commit 80c6889
Show file tree
Hide file tree
Showing 52 changed files with 286 additions and 23 deletions.
19 changes: 10 additions & 9 deletions Makefile
Expand Up @@ -185,15 +185,16 @@ test-test262-update-allowlist:


new-version-checklist:
# @echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
# @echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
# @echo "!!!!!! !!!!!!"
# @echo "!!!!!! Write any message that should !!!!!!"
# @echo "!!!!!! block the release here !!!!!!"
# @echo "!!!!!! !!!!!!"
# @echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
# @echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
# @exit 1
@echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
@echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
@echo "!!!!!! !!!!!!"
@echo "!!!!!! Update minVersion in the wrapRegExp helper, !!!!!!"
@echo "!!!!!! and the requried core version in the duplicate !!!!!!"
@echo "!!!!!! named groups plugin. !!!!!!"
@echo "!!!!!! !!!!!!"
@echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
@echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
@exit 1

new-version:
$(MAKE) new-version-checklist
Expand Down
Expand Up @@ -5,6 +5,7 @@ export const FEATURES = Object.freeze({
namedCaptureGroups: 1 << 3,
unicodeSetsFlag_syntax: 1 << 4,
unicodeSetsFlag: 1 << 5,
duplicateNamedCaptureGroups: 1 << 6,
});

// We can't use a symbol because this needs to always be the same, even if
Expand Down
54 changes: 46 additions & 8 deletions packages/babel-helper-create-regexp-features-plugin/src/index.ts
@@ -1,5 +1,11 @@
import rewritePattern from "regexpu-core";
import { featuresKey, FEATURES, enableFeature, runtimeKey } from "./features";
import {
featuresKey,
FEATURES,
enableFeature,
runtimeKey,
hasFeature,
} from "./features";
import { generateRegexpuOptions, canSkipRegexpu, transformFlags } from "./util";
import type { NodePath } from "@babel/traverse";

Expand Down Expand Up @@ -44,16 +50,37 @@ export function createRegExpFeaturePlugin({
const features = file.get(featuresKey) ?? 0;
let newFeatures = enableFeature(features, FEATURES[feature]);

const { useUnicodeFlag, runtime = true } = options;
const { useUnicodeFlag, runtime } = options;
if (useUnicodeFlag === false) {
newFeatures = enableFeature(newFeatures, FEATURES.unicodeFlag);
}
if (newFeatures !== features) {
file.set(featuresKey, newFeatures);
}

if (!runtime) {
file.set(runtimeKey, false);
if (runtime !== undefined) {
if (
file.has(runtimeKey) &&
file.get(runtimeKey) !== runtime &&
// TODO(Babel 8): Remove this check. It's necessary because in Babel 7
// we allow multiple copies of transform-named-capturing-groups-regex
// with conflicting 'runtime' options.
hasFeature(newFeatures, FEATURES.duplicateNamedCaptureGroups)
) {
throw new Error(
`The 'runtime' option must be the same for ` +
`'@babel/plugin-transform-named-capturing-groups-regex' and ` +
`'@babel/plugin-proposal-duplicate-named-capturing-groups-regex'.`,
);
}
// TODO(Babel 8): Remove this check and always set it.
// It's necessary because in Babel 7 we allow multiple copies of
// transform-named-capturing-groups-regex with conflicting 'runtime' options.
if (feature === "namedCaptureGroups") {
if (!runtime || !file.has(runtimeKey)) file.set(runtimeKey, runtime);
} else {
file.set(runtimeKey, runtime);
}
}

if (!file.has(versionKey) || file.get(versionKey) < version) {
Expand All @@ -68,13 +95,24 @@ export function createRegExpFeaturePlugin({
const features = file.get(featuresKey);
const runtime = file.get(runtimeKey) ?? true;

const regexpuOptions = generateRegexpuOptions(features);
if (canSkipRegexpu(node, regexpuOptions)) return;
const regexpuOptions = generateRegexpuOptions(node.pattern, features);
if (canSkipRegexpu(node, regexpuOptions)) {
return;
}

const namedCaptureGroups: Record<string, number> = {};
const namedCaptureGroups: Record<string, number | number[]> = {
__proto__: null,
};
if (regexpuOptions.namedGroups === "transform") {
regexpuOptions.onNamedGroup = (name, index) => {
namedCaptureGroups[name] = index;
const prev = namedCaptureGroups[name];
if (typeof prev === "number") {
namedCaptureGroups[name] = [prev, index];
} else if (Array.isArray(prev)) {
prev.push(index);
} else {
namedCaptureGroups[name] = index;
}
};
}

Expand Down
22 changes: 20 additions & 2 deletions packages/babel-helper-create-regexp-features-plugin/src/util.ts
Expand Up @@ -3,7 +3,10 @@ import { FEATURES, hasFeature } from "./features";

import type { RegexpuOptions } from "regexpu-core";

export function generateRegexpuOptions(toTransform: number): RegexpuOptions {
export function generateRegexpuOptions(
pattern: string,
toTransform: number,
): RegexpuOptions {
type Experimental = 1;

const feat = <Stability extends 0 | 1 = 0>(
Expand All @@ -13,14 +16,29 @@ export function generateRegexpuOptions(toTransform: number): RegexpuOptions {
return hasFeature(toTransform, FEATURES[name]) ? ok : false;
};

const featDuplicateNamedGroups = (): "transform" | false => {
if (!feat("duplicateNamedCaptureGroups")) return false;

// This can return false positive, for example for /\(?<a>\)/.
// However, it's such a rare occurrence that it's ok to compile
// the regexp even if we only need to compile regexps with
// duplicate named capturing groups.
const regex = /\(\?<([^>]+)>/g;
const seen = new Set();
for (let match; (match = regex.exec(pattern)); seen.add(match[1])) {
if (seen.has(match[1])) return "transform";
}
return false;
};

return {
unicodeFlag: feat("unicodeFlag"),
unicodeSetsFlag:
feat<Experimental>("unicodeSetsFlag") ||
feat<Experimental>("unicodeSetsFlag_syntax", "parse"),
dotAllFlag: feat("dotAllFlag"),
unicodePropertyEscapes: feat("unicodePropertyEscape"),
namedGroups: feat("namedCaptureGroups"),
namedGroups: feat("namedCaptureGroups") || featDuplicateNamedGroups(),
onNamedGroup: () => {},
};
}
Expand Down
@@ -0,0 +1,7 @@
{
"plugins": [
["transform-named-capturing-groups-regex", { "runtime": true }],
["proposal-duplicate-named-capturing-groups-regex", { "runtime": false }]
],
"throws": "The 'runtime' option must be the same for '@babel/plugin-transform-named-capturing-groups-regex' and '@babel/plugin-proposal-duplicate-named-capturing-groups-regex'."
}
@@ -0,0 +1 @@
/(?<year>\d{4})/
@@ -0,0 +1,7 @@
{
"plugins": [
["transform-named-capturing-groups-regex", { "runtime": false }],
["proposal-duplicate-named-capturing-groups-regex", { "runtime": true }]
],
"throws": "The 'runtime' option must be the same for '@babel/plugin-transform-named-capturing-groups-regex' and '@babel/plugin-proposal-duplicate-named-capturing-groups-regex'."
}
@@ -0,0 +1 @@
/(?<year>\d{4})/
2 changes: 1 addition & 1 deletion packages/babel-helpers/src/helpers-generated.ts
Expand Up @@ -55,6 +55,6 @@ export default Object.freeze({
),
wrapRegExp: helper(
"7.2.6",
'import setPrototypeOf from"setPrototypeOf";import inherits from"inherits";export default function _wrapRegExp(){_wrapRegExp=function(re,groups){return new BabelRegExp(re,void 0,groups)};var _super=RegExp.prototype,_groups=new WeakMap;function BabelRegExp(re,flags,groups){var _this=new RegExp(re,flags);return _groups.set(_this,groups||_groups.get(re)),setPrototypeOf(_this,BabelRegExp.prototype)}function buildGroups(result,re){var g=_groups.get(re);return Object.keys(g).reduce((function(groups,name){return groups[name]=result[g[name]],groups}),Object.create(null))}return inherits(BabelRegExp,RegExp),BabelRegExp.prototype.exec=function(str){var result=_super.exec.call(this,str);return result&&(result.groups=buildGroups(result,this)),result},BabelRegExp.prototype[Symbol.replace]=function(str,substitution){if("string"==typeof substitution){var groups=_groups.get(this);return _super[Symbol.replace].call(this,str,substitution.replace(/\\$<([^>]+)>/g,(function(_,name){return"$"+groups[name]})))}if("function"==typeof substitution){var _this=this;return _super[Symbol.replace].call(this,str,(function(){var args=arguments;return"object"!=typeof args[args.length-1]&&(args=[].slice.call(args)).push(buildGroups(args,_this)),substitution.apply(this,args)}))}return _super[Symbol.replace].call(this,str,substitution)},_wrapRegExp.apply(this,arguments)}',
'import setPrototypeOf from"setPrototypeOf";import inherits from"inherits";export default function _wrapRegExp(){_wrapRegExp=function(re,groups){return new BabelRegExp(re,void 0,groups)};var _super=RegExp.prototype,_groups=new WeakMap;function BabelRegExp(re,flags,groups){var _this=new RegExp(re,flags);return _groups.set(_this,groups||_groups.get(re)),setPrototypeOf(_this,BabelRegExp.prototype)}function buildGroups(result,re){var g=_groups.get(re);return Object.keys(g).reduce((function(groups,name){var i=g[name];if("number"==typeof i)groups[name]=result[i];else{for(var k=0;void 0===result[i[k]]&&k+1<i.length;)k++;groups[name]=result[i[k]]}return groups}),Object.create(null))}return inherits(BabelRegExp,RegExp),BabelRegExp.prototype.exec=function(str){var result=_super.exec.call(this,str);return result&&(result.groups=buildGroups(result,this)),result},BabelRegExp.prototype[Symbol.replace]=function(str,substitution){if("string"==typeof substitution){var groups=_groups.get(this);return _super[Symbol.replace].call(this,str,substitution.replace(/\\$<([^>]+)>/g,(function(_,name){return"$"+groups[name]})))}if("function"==typeof substitution){var _this=this;return _super[Symbol.replace].call(this,str,(function(){var args=arguments;return"object"!=typeof args[args.length-1]&&(args=[].slice.call(args)).push(buildGroups(args,_this)),substitution.apply(this,args)}))}return _super[Symbol.replace].call(this,str,substitution)},_wrapRegExp.apply(this,arguments)}',
),
});
11 changes: 10 additions & 1 deletion packages/babel-helpers/src/helpers/wrapRegExp.js
Expand Up @@ -56,7 +56,16 @@ export default function _wrapRegExp() {

var g = _groups.get(re);
return Object.keys(g).reduce(function (groups, name) {
groups[name] = result[g[name]];
var i = g[name];
if (typeof i === "number") groups[name] = result[i];
else {
// i is an array of indexes
var k = 0;
// if no group matched, we stop at k = i.length - 1 and then
// we store result[i[i.length - 1]] which is undefined.
while (result[i[k]] === undefined && k + 1 < i.length) k++;
groups[name] = result[i[k]];
}
return groups;
}, Object.create(null));
}
Expand Down
@@ -0,0 +1,3 @@
src
test
*.log
@@ -0,0 +1,19 @@
# @babel/plugin-proposal-duplicate-named-capturing-groups-regex

> Compile regular expressions using duplicate named groups to index-based groups.
See our website [@babel/plugin-proposal-duplicate-named-capturing-groups-regex](https://babeljs.io/docs/en/babel-plugin-proposal-duplicate-named-capturing-groups-regex) for more information.

## Install

Using npm:

```sh
npm install --save-dev @babel/plugin-proposal-duplicate-named-capturing-groups-regex
```

or using yarn:

```sh
yarn add @babel/plugin-proposal-duplicate-named-capturing-groups-regex --dev
```
@@ -0,0 +1,52 @@
{
"name": "@babel/plugin-proposal-duplicate-named-capturing-groups-regex",
"version": "7.18.6",
"description": "Compile regular expressions using duplicate named groups to index-based groups.",
"homepage": "https://babel.dev/docs/en/next/babel-plugin-proposal-duplicate-named-capturing-groups-regex",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"keywords": [
"babel-plugin",
"regex",
"regexp",
"regular expressions"
],
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-plugin-proposal-duplicate-named-capturing-groups-regex"
},
"bugs": "https://github.com/babel/babel/issues",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "workspace:^",
"@babel/helper-plugin-utils": "workspace:^"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
},
"devDependencies": {
"@babel/core": "workspace:^",
"@babel/helper-plugin-test-runner": "workspace:^",
"core-js": "^3.22.1"
},
"engines": {
"node": ">=6.9.0"
},
"author": "The Babel Team (https://babel.dev/team)",
"conditions": {
"USE_ESM": [
{
"type": "module"
},
null
]
},
"exports": {
".": "./lib/index.js",
"./package.json": "./package.json"
},
"type": "commonjs"
}
@@ -0,0 +1,20 @@
/* eslint-disable @babel/development/plugin-name */
import { createRegExpFeaturePlugin } from "@babel/helper-create-regexp-features-plugin";
import { declare } from "@babel/helper-plugin-utils";

export interface Options {
runtime?: boolean;
}

export default declare((api, options: Options) => {
const { runtime } = options;
if (runtime !== undefined && typeof runtime !== "boolean") {
throw new Error("The 'runtime' option must be boolean");
}

return createRegExpFeaturePlugin({
name: "proposal-duplicate-named-capturing-groups-regex",
feature: "duplicateNamedCaptureGroups",
options: { runtime },
});
});
@@ -0,0 +1 @@
/(?:(?<year>\d\d\d\d)|(?<year>\d\d))-(?<month>\d\d)/;
@@ -0,0 +1,5 @@
/*#__PURE__*/
babelHelpers.wrapRegExp(/(?:(\d\d\d\d)|(\d\d))\x2D(\d\d)/, {
year: [1, 2],
month: 3
});
@@ -0,0 +1,5 @@
let regexp = /(?<named>a)|(?<named>b)|c/;

expect("a".match(regexp).groups).toEqual({ named: "a" });
expect("b".match(regexp).groups).toEqual({ named: "b" });
expect("c".match(regexp).groups).toEqual({ named: undefined });
@@ -0,0 +1,3 @@
{
"plugins": ["proposal-duplicate-named-capturing-groups-regex"]
}
@@ -0,0 +1 @@
/(?:(?<year>\d\d\d\d)|(?<year>\d\d)) is \k<year>/;
@@ -0,0 +1 @@
/(?:(\d\d\d\d)|(\d\d)) is \1\2/;
@@ -0,0 +1,5 @@
{
"plugins": [
["proposal-duplicate-named-capturing-groups-regex", { "runtime": false }]
]
}
@@ -0,0 +1 @@
/(?<year>\d\d\d\d) is \k<year>/;
@@ -0,0 +1 @@
/(?<year>\d\d\d\d) is \k<year>/;
@@ -0,0 +1 @@
/(?<year>\d\d\d\d) is \k<year>/;
@@ -0,0 +1,6 @@
{
"plugins": [
["proposal-duplicate-named-capturing-groups-regex", { "runtime": false }],
["transform-named-capturing-groups-regex", { "runtime": false }]
]
}
@@ -0,0 +1 @@
/(\d\d\d\d) is \1/;
@@ -0,0 +1 @@
"foo".match(/(?<double>.)\k<double>/);
@@ -0,0 +1,3 @@
"foo".match( /*#__PURE__*/babelHelpers.wrapRegExp(/(.)\1/, {
double: 1
}));
@@ -0,0 +1 @@
/no-groups-\(?<looks-like-a-group>looks\)\u{10000}/u;
@@ -0,0 +1,6 @@
{
"plugins": [
"transform-named-capturing-groups-regex",
"transform-unicode-regex"
]
}
@@ -0,0 +1 @@
/no\x2Dgroups\x2D\(?<looks\x2Dlike\x2Da\x2Dgroup>looks\)(?:\uD800\uDC00)/;
@@ -0,0 +1 @@
/no-groups-\(?<looks-like-a-group>looks\)/;
@@ -0,0 +1 @@
/no\x2Dgroups\x2D\(?<looks\x2Dlike\x2Da\x2Dgroup>looks\)/;
@@ -0,0 +1 @@
"foo".match(/(.)\1/);
@@ -0,0 +1 @@
"foo".match(/(.)\1/);
@@ -0,0 +1,3 @@
{
"plugins": ["transform-named-capturing-groups-regex"]
}
@@ -0,0 +1 @@
"abba".match(/(.)(?<n>.)\k<n>\1/);
@@ -0,0 +1,3 @@
"abba".match( /*#__PURE__*/babelHelpers.wrapRegExp(/(.)(.)\2\1/, {
n: 2
}));
@@ -0,0 +1 @@
/^(?<x>.)\k<x>$/.test("aa");
@@ -0,0 +1 @@
/^(.)\1$/.test("aa");
@@ -0,0 +1,3 @@
import runner from "@babel/helper-plugin-test-runner";

runner(import.meta.url);
@@ -0,0 +1 @@
{ "type": "module" }

0 comments on commit 80c6889

Please sign in to comment.