Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for the duplicate named capturing groups proposal #14805

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -39,6 +39,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" }