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 transform support for the "regexp unicode sets" proposal #14125

Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 10 additions & 0 deletions packages/babel-core/src/parser/util/missing-plugin-helper.ts
Expand Up @@ -201,6 +201,16 @@ const pluginNameMap = {
url: "https://git.io/JvKp3",
},
},
regexpUnicodeSets: {
syntax: {
name: "@babel/plugin-syntax-unicode-sets-regex",
url: "https://git.io/J9GTd",
},
transform: {
name: "@babel/plugin-proposal-unicode-sets-regex",
url: "https://git.io/J9GTQ",
},
},
throwExpressions: {
syntax: {
name: "@babel/plugin-syntax-throw-expressions",
Expand Down
Expand Up @@ -19,7 +19,7 @@
],
"dependencies": {
"@babel/helper-annotate-as-pure": "workspace:^",
"regexpu-core": "^4.7.1"
"regexpu-core": "^5.0.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
Expand Down
Expand Up @@ -3,6 +3,8 @@ export const FEATURES = Object.freeze({
dotAllFlag: 1 << 1,
unicodePropertyEscape: 1 << 2,
namedCaptureGroups: 1 << 3,
unicodeSetsFlag_syntax: 1 << 4,
unicodeSetsFlag: 1 << 5,
});

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

import { types as t } from "@babel/core";
import annotateAsPure from "@babel/helper-annotate-as-pure";

type RegExpFlags = "i" | "g" | "m" | "s" | "u" | "y";

/**
* Remove given flag from given RegExpLiteral node
*
* @param {RegExpLiteral} node
* @param {RegExpFlags} flag
* @returns {void}
*/
function pullFlag(node, flag: RegExpFlags): void {
node.flags = node.flags.replace(flag, "");
}

declare const PACKAGE_JSON: { name: string; version: string };

// Note: Versions are represented as an integer. e.g. 7.1.5 is represented
Expand All @@ -39,9 +20,13 @@ export function createRegExpFeaturePlugin({
name,
feature,
options = {} as any,
manipulateOptions = (() => {}) as (opts: any, parserOpts: any) => void,
}) {
return {
name,

manipulateOptions,

pre() {
const { file } = this;
const features = file.get(featuresKey) ?? 0;
Expand Down Expand Up @@ -70,20 +55,21 @@ export function createRegExpFeaturePlugin({
const { file } = this;
const features = file.get(featuresKey);
const runtime = file.get(runtimeKey) ?? true;
const regexpuOptions = generateRegexpuOptions(node, features);
if (regexpuOptions === null) {
return;
}

const regexpuOptions = generateRegexpuOptions(features);
if (canSkipRegexpu(node, regexpuOptions)) return;

const namedCaptureGroups = {};
if (regexpuOptions.namedGroup) {
if (regexpuOptions.namedGroups === "transform") {
regexpuOptions.onNamedGroup = (name, index) => {
namedCaptureGroups[name] = index;
};
}

node.pattern = rewritePattern(node.pattern, node.flags, regexpuOptions);

if (
regexpuOptions.namedGroup &&
regexpuOptions.namedGroups === "transform" &&
Object.keys(namedCaptureGroups).length > 0 &&
runtime &&
!isRegExpTest(path)
Expand All @@ -96,12 +82,8 @@ export function createRegExpFeaturePlugin({

path.replaceWith(call);
}
if (hasFeature(features, FEATURES.unicodeFlag)) {
pullFlag(node, "u");
}
if (hasFeature(features, FEATURES.dotAllFlag)) {
pullFlag(node, "s");
}

node.flags = transformFlags(regexpuOptions, node.flags);
},
},
};
Expand Down
103 changes: 57 additions & 46 deletions packages/babel-helper-create-regexp-features-plugin/src/util.ts
@@ -1,65 +1,76 @@
import type { types as t } from "@babel/core";
import { FEATURES, hasFeature } from "./features";

type RegexpuOptions = {
useUnicodeFlag: boolean;
unicodeFlag: "transform" | false;
unicodeSetsFlag: "transform" | "parse" | false;
dotAllFlag: "transform" | false;
unicodePropertyEscapes: "transform" | false;
namedGroups: "transform" | false;
onNamedGroup: (name: string, index: number) => void;
namedGroup: boolean;
unicodePropertyEscape: boolean;
dotAllFlag: boolean;
lookbehind: boolean;
};

export function generateRegexpuOptions(node, features): RegexpuOptions | null {
let useUnicodeFlag = false,
dotAllFlag = false,
unicodePropertyEscape = false,
namedGroup = false;
type Stable = 0;

export function generateRegexpuOptions(toTransform: number): RegexpuOptions {
const feat = <IsStable extends 0 | 1>(
name: keyof typeof FEATURES,
ok: "transform" | (IsStable extends Stable ? never : "parse") = "transform",
) => {
return hasFeature(toTransform, FEATURES[name]) ? ok : false;
};

return {
unicodeFlag: feat<Stable>("unicodeFlag"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Since most flags are stable, we can use Experimental = 1 in type notations.

unicodeSetsFlag:
feat("unicodeSetsFlag") || feat("unicodeSetsFlag_syntax", "parse"),
dotAllFlag: feat<Stable>("dotAllFlag"),
unicodePropertyEscapes: feat<Stable>("unicodePropertyEscape"),
namedGroups: feat<Stable>("namedCaptureGroups"),
onNamedGroup: () => {},
};
}

export function canSkipRegexpu(
node: t.RegExpLiteral,
options: RegexpuOptions,
): boolean {
const { flags, pattern } = node;
const flagsIncludesU = flags.includes("u");

if (flagsIncludesU) {
if (!hasFeature(features, FEATURES.unicodeFlag)) {
useUnicodeFlag = true;
}
if (flags.includes("v")) {
if (options.unicodeSetsFlag === "transform") return false;
}

if (flags.includes("u")) {
if (options.unicodeFlag === "transform") return false;
if (
hasFeature(features, FEATURES.unicodePropertyEscape) &&
options.unicodePropertyEscapes === "transform" &&
/\\[pP]{/.test(pattern)
) {
unicodePropertyEscape = true;
return false;
}
}

if (hasFeature(features, FEATURES.dotAllFlag) && flags.indexOf("s") >= 0) {
dotAllFlag = true;
if (flags.includes("s")) {
if (options.dotAllFlag === "transform") return false;
}
if (
hasFeature(features, FEATURES.namedCaptureGroups) &&
/\(\?<(?![=!])/.test(pattern)
) {
namedGroup = true;

if (options.namedGroups === "transform" && /\(\?<(?![=!])/.test(pattern)) {
return false;
}
if (
!namedGroup &&
!unicodePropertyEscape &&
!dotAllFlag &&
(!flagsIncludesU || useUnicodeFlag)
) {
return null;

return true;
}

export function transformFlags(regexpuOptions: RegexpuOptions, flags: string) {
if (regexpuOptions.unicodeSetsFlag === "transform") {
flags = flags.replace("v", "u");
}
// Now we have to feed regexpu-core the regex
if (flagsIncludesU && flags.indexOf("s") >= 0) {
// When flags includes u, `config.unicode` will be enabled even if `u` is supported natively.
// In this case we have to enable dotAllFlag, otherwise `rewritePattern(/./su)` will return
// incorrect result
// https://github.com/mathiasbynens/regexpu-core/blob/v4.6.0/rewrite-pattern.js#L191
dotAllFlag = true;
if (regexpuOptions.unicodeFlag === "transform") {
flags = flags.replace("u", "");
}
return {
useUnicodeFlag,
onNamedGroup: () => {},
namedGroup,
unicodePropertyEscape,
dotAllFlag,
lookbehind: true,
};
if (regexpuOptions.dotAllFlag === "transform") {
flags = flags.replace("s", "");
}
return flags;
}
@@ -1 +1 @@
/([0-9]{4})/;
/(\d{4})/;
3 changes: 3 additions & 0 deletions packages/babel-plugin-proposal-unicode-sets-regex/.npmignore
@@ -0,0 +1,3 @@
src
test
*.log
19 changes: 19 additions & 0 deletions packages/babel-plugin-proposal-unicode-sets-regex/README.md
@@ -0,0 +1,19 @@
# @babel/plugin-proposal-unicode-sets-regex

> Compile regular expressions' unicodeSets (v) flag.
See our website [@babel/plugin-proposal-unicode-sets-regex](https://babeljs.io/docs/en/babel-plugin-proposal-unicode-sets-regex) for more information.

## Install

Using npm:

```sh
npm install --save-dev @babel/plugin-proposal-unicode-sets-regex
```

or using yarn:

```sh
yarn add @babel/plugin-proposal-unicode-sets-regex --dev
```
48 changes: 48 additions & 0 deletions packages/babel-plugin-proposal-unicode-sets-regex/package.json
@@ -0,0 +1,48 @@
{
"name": "@babel/plugin-proposal-unicode-sets-regex",
"version": "7.16.7",
"description": "Compile regular expressions' unicodeSets (v) flag.",
"homepage": "https://babel.dev/docs/en/next/babel-plugin-proposal-unicode-sets-regex",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"keywords": [
"babel-plugin",
"regex",
"regexp",
"unicode",
"sets",
"properties",
"property",
"string",
"strings",
"regular expressions"
],
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-plugin-proposal-unicode-sets-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:^"
},
"author": "The Babel Team (https://babel.dev/team)",
"exports": {
".": "./lib/index.js",
"./package.json": "./package.json"
},
"engines": {
"node": ">=6.9.0"
}
}
15 changes: 15 additions & 0 deletions packages/babel-plugin-proposal-unicode-sets-regex/src/index.ts
@@ -0,0 +1,15 @@
/* eslint-disable @babel/development/plugin-name */
import { createRegExpFeaturePlugin } from "@babel/helper-create-regexp-features-plugin";
import { declare } from "@babel/helper-plugin-utils";

export default declare(api => {
api.assertVersion(7);

return createRegExpFeaturePlugin({
name: "transform-unicode-sets-regex",
feature: "unicodeSetsFlag",
manipulateOptions(opts, parserOpts) {
parserOpts.plugins.push("regexpUnicodeSets");
},
});
});
@@ -0,0 +1 @@
/[[0-7]&&[5-9]]\u{10FFFF}/v;
@@ -0,0 +1,3 @@
{
"plugins": ["proposal-unicode-sets-regex"]
}
@@ -0,0 +1 @@
/[5-7]\u{10FFFF}/u;
@@ -0,0 +1 @@
/[[0-7]&&[5-9]]\u{10FFFF}/v;
@@ -0,0 +1,3 @@
{
"plugins": ["proposal-unicode-sets-regex", "transform-unicode-regex"]
}
@@ -0,0 +1 @@
/[5-7](?:\uDBFF\uDFFF)/;
@@ -0,0 +1,3 @@
import runner from "@babel/helper-plugin-test-runner";

runner(import.meta.url);
@@ -0,0 +1 @@
{ "type": "module" }
3 changes: 3 additions & 0 deletions packages/babel-plugin-syntax-unicode-sets-regex/.npmignore
@@ -0,0 +1,3 @@
src
test
*.log
19 changes: 19 additions & 0 deletions packages/babel-plugin-syntax-unicode-sets-regex/README.md
@@ -0,0 +1,19 @@
# @babel/plugin-syntax-unicode-sets-regex

> Parse regular expressions' unicodeSets (v) flag.
See our website [@babel/plugin-syntax-unicode-sets-regex](https://babeljs.io/docs/en/babel-plugin-syntax-unicode-sets-regex) for more information.

## Install

Using npm:

```sh
npm install --save-dev @babel/plugin-syntax-unicode-sets-regex
```

or using yarn:

```sh
yarn add @babel/plugin-syntax-unicode-sets-regex --dev
```