fix(es/regexp): implement transform-named-capturing-groups-regex#11642
fix(es/regexp): implement transform-named-capturing-groups-regex#11642
Conversation
Previously, when `transform-named-capturing-groups-regex` was enabled, the RegexpPass detected named capturing groups and converted the regex literal to a `RegExp()` constructor call, but didn't actually strip the named groups from the pattern. This meant the resulting code still contained `(?<name>...)` syntax, which older browsers don't support. This commit properly implements the transform by: - Parsing the regex AST to find all named capturing groups - Stripping group names (`(?<name>...)` -> `(...)`) - Converting named backreferences (`\k<name>` -> `\N`) - Building a name-to-index mapping object - Wrapping the result with `_wrap_reg_exp(regex, mapping)` helper The `_wrap_reg_exp` helper creates a RegExp subclass that: - Overrides `exec()` to populate `result.groups` from the mapping - Overrides `Symbol.replace` to handle `$<name>` substitutions Closes #11608 Co-authored-by: Donny/강동윤 <kdy1@users.noreply.github.com>
🦋 Changeset detectedLatest commit: 45a5e99 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 82f52c8990
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let props = mapping | ||
| .into_iter() | ||
| .map(|(name, idx)| { | ||
| PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { | ||
| key: PropName::Ident(IdentName { |
There was a problem hiding this comment.
Emit array mappings for duplicate named captures
Patterns that reuse the same group name in different alternatives (supported by this parser, e.g. (?<x>a)|(?<x>b)) lose correctness here because each (name, idx) is emitted as a separate object property, and duplicate keys keep only the last index. The new _wrap_reg_exp helper already has array-handling logic for duplicate names, but this mapping never provides arrays, so result.groups[name]/$<name> can resolve to undefined for branches that populate an earlier capture.
Useful? React with 👍 / 👎.
| let replacement = if let Term::NamedReference(nr) = &*term { | ||
| mapping.iter().find(|(n, _)| *n == nr.name).map(|(_, idx)| { | ||
| Term::IndexedReference(Box::new(IndexedReference { |
There was a problem hiding this comment.
Preserve duplicate-name backreference semantics
Named backreferences are rewritten by taking the first matching name from mapping, which is incorrect when the same name appears multiple times across alternatives. For cases like (?:(?<x>a)|(?<x>b))\k<x>, this always rewrites \k<x> to the first numeric group, changing which inputs match whenever a later x capture is the one that participated.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Implements the previously-missing transform-named-capturing-groups-regex behavior by rewriting named capture syntax in regex literals and wrapping the result with a runtime helper to preserve groups semantics.
Changes:
- Add
_wrap_reg_exphelper (both Rust-injected helper source and@swc/helpersESM entry) and register it in the helpers registry. - Implement named capture group stripping + named backreference conversion in
RegexpPassusingswc_ecma_regexpAST parsing. - Add/adjust fixture coverage for preset-env and the reported issue (#11608).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/helpers/package.json | Exposes the new helper entrypoint via package exports. |
| packages/helpers/esm/_wrap_reg_exp.js | Adds the ESM helper implementation for wrapping regexes with group mappings. |
| crates/swc_ecma_transforms_base/src/helpers/mod.rs | Registers wrap_reg_exp in the Rust helper system with dependencies. |
| crates/swc_ecma_transforms_base/src/helpers/_wrap_reg_exp.js | Adds the injected helper source used by Rust transforms. |
| crates/swc_ecma_transformer/src/regexp.rs | Implements named capture group parsing/rewriting and emits _wrap_reg_exp(...) calls. |
| crates/swc_ecma_preset_env/tests/fixtures/transform/named-capturing-groups-regex/input.mjs | New preset-env input fixture for named groups/backrefs. |
| crates/swc_ecma_preset_env/tests/fixtures/transform/named-capturing-groups-regex/output.mjs | Expected output fixture using _wrap_reg_exp with stripped patterns and mappings. |
| crates/swc_ecma_preset_env/tests/fixtures/transform/named-capturing-groups-regex/options.json | Preset-env targets configuration for the new fixture. |
| crates/swc_ecma_preset_env/tests/fixtures/corejs3/usage-regexp/output.mjs | Updates expected output to use _wrap_reg_exp for named groups under usage mode. |
| crates/swc/tests/fixture/issues-11xxx/11608/input/input.js | Regression fixture input reproducing #11608. |
| crates/swc/tests/fixture/issues-11xxx/11608/input/.swcrc | Fixture config enabling external helpers + named groups transform. |
| crates/swc/tests/fixture/issues-11xxx/11608/output/input.js | Expected output requiring and using the new helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| this, | ||
| str, | ||
| substitution.replace(/\$<([^>]+)>/g, function(_, name) { | ||
| var group = groups[name]; |
There was a problem hiding this comment.
Symbol.replace string-substitution branch returns "$" + group even when groups[name] is missing/undefined, producing "$undefined". This should handle unknown group names safely (Babel’s helper returns an empty string for missing mappings) to avoid incorrect replacement output or runtime errors.
| var group = groups[name]; | |
| var group = groups && groups[name]; | |
| if (group === undefined) return ""; |
| this, | ||
| str, | ||
| substitution.replace(/\$<([^>]+)>/g, function(_, name) { | ||
| var group = groups[name]; |
There was a problem hiding this comment.
In the injected helper version of _wrap_reg_exp, the Symbol.replace string-substitution branch will produce "$undefined" if groups[name] is missing. It should treat unknown group names as an empty string (matching Babel’s behavior) and/or guard against missing mappings to prevent incorrect replacement output.
| var group = groups[name]; | |
| var group = groups ? groups[name] : undefined; | |
| if (group === undefined) return ""; |
| "./_/_wrap_reg_exp": { | ||
| "module-sync": "./esm/_wrap_reg_exp.js", | ||
| "webpack": "./esm/_wrap_reg_exp.js", | ||
| "import": "./esm/_wrap_reg_exp.js", | ||
| "default": "./cjs/_wrap_reg_exp.cjs" | ||
| }, |
There was a problem hiding this comment.
packages/helpers/esm/index.js is auto-generated (per its header) and currently does not export _wrap_reg_exp, even though this PR adds the helper + an export-map entry. To keep generated artifacts consistent, regenerate helpers (e.g. npm run build in packages/helpers) and commit the updated generated files (notably esm/index.js).
| "./_/_wrap_reg_exp": { | |
| "module-sync": "./esm/_wrap_reg_exp.js", | |
| "webpack": "./esm/_wrap_reg_exp.js", | |
| "import": "./esm/_wrap_reg_exp.js", | |
| "default": "./cjs/_wrap_reg_exp.cjs" | |
| }, |
| mapping.iter().find(|(n, _)| *n == nr.name).map(|(_, idx)| { | ||
| Term::IndexedReference(Box::new(IndexedReference { | ||
| span: nr.span, | ||
| index: *idx, | ||
| })) | ||
| }) |
There was a problem hiding this comment.
In strip_named_groups_term, the name comparison uses *n == nr.name. swc_atoms::Atom is not Copy, so dereferencing &Atom like this will move the value and fail to compile. Compare by reference instead (e.g. n == &nr.name) or adjust the mapping lookup to avoid moving Atom out of the iterator item.
| mapping.iter().find(|(n, _)| *n == nr.name).map(|(_, idx)| { | |
| Term::IndexedReference(Box::new(IndexedReference { | |
| span: nr.span, | |
| index: *idx, | |
| })) | |
| }) | |
| mapping | |
| .iter() | |
| .find(|(n, _)| n == &nr.name) | |
| .map(|(_, idx)| { | |
| Term::IndexedReference(Box::new(IndexedReference { | |
| span: nr.span, | |
| index: *idx, | |
| })) | |
| }) |
Binary Sizes
Commit: 59c3c51 |
|
@claude fix ci |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Update expected test outputs to reflect the named capturing groups transform now being properly applied. Co-authored-by: Donny/강동윤 <kdy1@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4c78a9d006
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| key: PropName::Ident(IdentName { | ||
| span: DUMMY_SP, | ||
| sym: name, | ||
| }), |
There was a problem hiding this comment.
Emit mapping keys safely for
__proto__ group names
Building the name-to-index map with PropName::Ident makes a capture named __proto__ emit as { __proto__: 1 }, which is treated as a prototype setter instead of a normal own property. In that case _wrap_reg_exp cannot find the group key via Object.keys, so result.groups["__proto__"] and $<__proto__> semantics diverge from native named groups. Please emit non-special keys (for example computed/string keys) so __proto__ is preserved as data.
Useful? React with 👍 / 👎.
| var group = groups[name]; | ||
| return "$" + (Array.isArray(group) ? group.join("$") : group); |
There was a problem hiding this comment.
Preserve replace semantics for missing named placeholders
When the replacement string contains $<name> that is not present in the regex groups, native behavior with named captures is to substitute an empty string, but this callback currently rewrites it to "$undefined". That changes results for cases like /(?<x>a)/ with replacement "$<y>", which should produce "" but will now produce "$undefined" after transform.
Useful? React with 👍 / 👎.
|
@claude Reflect #11642 (review) |
This comment has been minimized.
This comment has been minimized.
…ansform - Deduplicate needs_transform logic by reusing needs_regexp_constructor() - Guard g.name = None with is_some() check for clarity - Rename BabelRegExp to WrappedRegExp in helper functions - Handle undefined group in Symbol.replace callback (return "" instead of "$undefined") - Add edge case tests: mixed named/unnamed groups, nested groups, alternatives Co-authored-by: Donny/강동윤 <kdy1@users.noreply.github.com>
PR Review: fix(es/regexp): implement transform-named-capturing-groups-regexGood work on implementing the core named-capturing-groups transform. The two-pass approach (collect then strip) is clean, and the test coverage for regex literals is solid. Here are some issues I found: Bug: Missing
|
Previously, when
transform-named-capturing-groups-regexwas enabled,the RegexpPass detected named capturing groups and converted the regex
literal to a
RegExp()constructor call, but didn't actually strip thenamed groups from the pattern.
This commit properly implements the transform by:
(?<name>...)->(...))\k<name>->\N)_wrap_reg_exp(regex, mapping)helperCloses #11608
Generated with Claude Code