substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub","description":"root tab size preference pre, code tab size var tab size preference locale featureFlags actions enable background steps activity diff file tree activity repo...","inLanguage":"en-US","isPartOf":{"@id":"https://kelaenderkoe.pages.dev/#website"},"datePublished":"2026-08-31T00:30:20.022Z","dateModified":"2026-08-31T00:30:20.022Z","breadcrumb":{"@id":"https://kelaenderkoe.pages.dev/post/java-https-github.com/Rich-Harris/magic-string/issues/341#breadcrumb"},"potentialAction":[{"@type":"ReadAction","target":["https://kelaenderkoe.pages.dev/post/java-https-github.com/Rich-Harris/magic-string/issues/341"]}]},{"@type":"Article","@id":"https://kelaenderkoe.pages.dev/post/java-https-github.com/Rich-Harris/magic-string/issues/341#article","headline":"`replace`/`replaceAll` mishandle ` `replace`/`replaceAll` mishandle `$` substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub `replace`/`replaceAll` mishandle `$` substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub `replace`/`replaceAll` mishandle `$` substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub `replace`/`replaceAll` mishandle `$` substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub `replace`/`replaceAll` mishandle `$` substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub","description":"root tab size preference pre, code tab size var tab size preference locale featureFlags actions enable background steps activity diff file tree activity repo...","url":"https://kelaenderkoe.pages.dev/post/java-https-github.com/Rich-Harris/magic-string/issues/341","datePublished":"2026-08-31T00:30:20.022Z","dateModified":"2026-08-31T00:30:20.022Z","author":{"@type":"Person","name":"Admin","url":"https://kelaenderkoe.pages.dev"},"publisher":{"@type":"Organization","name":"BERJAYA","url":"https://kelaenderkoe.pages.dev","logo":{"@type":"ImageObject","url":"https://kelaenderkoe.pages.dev/logo.png","width":600,"height":60}},"image":{"@type":"ImageObject","url":"https://avatars.githubusercontent.com/u/70063440?u=9a73a4337e08ab660cbb4d33d5a07559d970e51b&v=4&size=48","width":1200,"height":630},"mainEntityOfPage":{"@type":"WebPage","@id":"https://kelaenderkoe.pages.dev/post/java-https-github.com/Rich-Harris/magic-string/issues/341"},"inLanguage":"en-US","keywords":"u003e, u003c, class, u003cspan, span, replace, copilot, false, code, abcabc, string, group, magicstring, match, notranslate"},{"@type":"WebSite","@id":"https://kelaenderkoe.pages.dev/#website","url":"https://kelaenderkoe.pages.dev","name":"BERJAYA","description":"Latest news, articles and resources from BERJAYA","publisher":{"@type":"Organization","name":"BERJAYA","logo":{"@type":"ImageObject","url":"https://kelaenderkoe.pages.dev/logo.png","width":600,"height":60}},"inLanguage":"en-US","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://kelaenderkoe.pages.dev/?s={search_term_string}"},"query-input":"required name=search_term_string"}]},{"@type":"BreadcrumbList","@id":"https://kelaenderkoe.pages.dev/post/java-https-github.com/Rich-Harris/magic-string/issues/341#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://kelaenderkoe.pages.dev"},{"@type":"ListItem","position":2,"name":"`replace`/`replaceAll` mishandle ` `replace`/`replaceAll` mishandle `$` substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub `replace`/`replaceAll` mishandle `$` substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub `replace`/`replaceAll` mishandle `$` substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub `replace`/`replaceAll` mishandle `$` substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub `replace`/`replaceAll` mishandle `$` substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub substitution patterns and insert `undefined` for an unmatched group · Issue #341 · Rich-Harris/magic-string · GitHub","item":"https://kelaenderkoe.pages.dev/post/java-https-github.com/Rich-Harris/magic-string/issues/341"}]}]}
Skip to content

replace/replaceAll mishandle $ substitution patterns and insert undefined for an unmatched group #341

Description

@theRizwan

Version

v1.2.3 / master @ 5473bfb

What happens

A capture group that does not participate in the match puts the text undefined into the
output:

import MagicString from 'magic-string'

new MagicString('ac').replace(/a(b)?/, '[$1]').toString()
// '[undefined]c'
'ac'.replace(/a(b)?/, '[$1]')
// '[]c'

Nothing is thrown. An optional group that happens not to match is ordinary, so this
silently writes undefined into generated code.

The rest of the table

_replaceRegexp links to the MDN table of $ patterns, and implements three of its six
entries. Every other entry is either wrong or absent:

// `$0` names no group, so it is literal - it expands to the whole match instead
new MagicString('abcabc').replace(/(a)/, '[$0]').toString() // '[a]bcabc'
'abcabc'.replace(/(a)/, '[$0]')                             // '[$0]bcabc'

// `$nn` never falls back to `$n`, so it stays literal when there is no group nn
new MagicString('ab').replace(/(a)/, '[$12]').toString()    // '[$12]b'
'ab'.replace(/(a)/, '[$12]')                                // '[a2]b'

// `$<name>` is not recognised
new MagicString('ab').replace(/(?<first>a)/, '[$<first>]').toString() // '[$<first>]b'
'ab'.replace(/(?<first>a)/, '[$<first>]')                             // '[a]b'

// `` $` `` and `$'` are not recognised
new MagicString('abcabc').replace(/b/, '$`').toString()  // 'a$`cabc'
'abcabc'.replace(/b/, '$`')                              // 'aacabc'
new MagicString('abcabc').replace(/b/, '$\'').toString() // "a$'cabc"
'abcabc'.replace(/b/, '$\'')                             // 'acabccabc'

A string search value expands nothing at all

_replaceString and _replaceAllString pass the replacement through untouched, so the
same replacement behaves differently depending only on whether the pattern is a string or
an equivalent regexp:

new MagicString('abcabc').replace('b', '$$').toString() // 'a$$cabc'
new MagicString('abcabc').replace(/b/, '$$').toString() // 'a$cabc'
'abcabc'.replace('b', '$$')                             // 'a$cabc'  <- both should be this

$&, $` and $' are affected the same way. $n and $<name> are correctly literal
for a string search value, because there are no capture groups of either kind — that part
String.prototype.replace also does.

Cause

The substitution is one pass of replacement.replace(/\$(\$|&|\d+)/g, ...):

const num = +i
if (num < match.length)
  return match[+i]
return `$${i}`
  • for a group that did not participate, match[n] is undefined. It is returned from the
    replacer and coerced to "undefined" when concatenated.
  • the same guard admits i === '0', and match[0] is the whole match, so $0 expands to
    it rather than staying literal.
  • \d+ is greedy, so $12 is always read as index 12. String.prototype.replace prefers
    the two-digit reading only when that group exists, and otherwise falls back to one digit,
    which is why $12 is group 1 followed by a literal 2 when the pattern has one group.
  • $` , $' and $<name> are not in the pattern, so they were never candidates.

Scope

Comparing against String.prototype.replace/replaceAll as the reference, over random
sources, patterns (0–3 groups, named groups, optional groups, zero-length matches, global
and not) and replacement strings assembled from the substitution tokens: 32,902 of
136,000 comparisons disagree on master
. Every disagreement is one of the cases above.

The undefined case is the only one that corrupts output silently. The others leak a $
sequence into the output verbatim, or — for $0 — substitute text that was not asked for.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions