Compatibility

Does Obfuscation Break Named Capture Groups?

A named capture group is one of the few places in JavaScript where the same name is written twice in two different alphabets. Once inside a regular expression, where it is text the engine parses, and once in your code as a property read. Protection treats those two halves completely differently, and the result is a contract that half works with no error anywhere in between.

What was measured

One file with five regular expressions and the code that normally surrounds them: a date parse, a destructure of the groups object, a numbered-group read, a replacement pattern, a log line scanned with a global expression into records, an optional group with a fallback, an identifier validator, and two reads of the expression's own properties. Driven through five protection profiles for the base column and both member-renaming profiles after that.

The base column is clean. Every profile reproduced the unprotected output exactly, including the reformatted date and the record built from the log line. Protecting code that uses named capture groups changes nothing about how it runs.

The site has already said that a regular expression literal comes out of protection identical to the way you wrote it, and that is worth repeating here because it is the mechanism behind everything below. The pattern text is frozen. The property reads around it are not.

The half that keeps working is what makes this hard to see

With member renaming on and a pattern matching the three group names in the date expression, the parsed year and month read undefined and the assembled date came out as nothing but its separators. The destructured form failed identically, because a destructure of the groups object is the same read in shorthand.

In the same run, two other lines were untouched. The numbered read, which asks for the first, second and third capture rather than their names, printed the correct date. The replacement, which names the groups inside a replacement string, printed the correctly reformatted date as well. Both are text or index positions, and neither is a property name, so neither moves.

That is a genuinely unpleasant combination. A file where the same expression feeds a working reformatting path and a broken extraction path does not look like a build problem. It looks like the input changed shape, which is exactly what everyone investigating a parser assumes first.

A pattern matching the container itself is the loud version: reading the year off an undefined groups object threw a TypeError on the first line that touched it. Between those two extremes sits the whole spectrum this series keeps finding, and which end you land on is decided by whether the code reached for the container or for a property on it.

The record that collapses to one null field

The log-scanning arm matched the four group names that a global expression pulls out of a line and the record keys built from them, which is realistic: the names in the pattern and the names in the object are usually the same word.

The record went from four populated fields to a single generated key with a null value. Three of the four reads returned undefined, and serialising an object drops keys whose value is undefined, so they vanished from the document rather than arriving empty. The fourth was a number parsed from undefined, which is not a number, and serialises as null.

The totals followed. The summed amount became not-a-number, and the severity test that asked whether the level equalled the error string went from true to false. A log pipeline in this state does not report an error: it reports that nothing was an error.

A validator that rejects the input it was written to accept

The identifier validator in the sample is the shape every codebase has: match the string, reject it if it does not match, then check one named group against an expected prefix and return another.

Renaming those two group names inverted it. A valid identifier came back as a wrong-prefix rejection, and the rejection message named undefined rather than the prefix it found. The invalid identifier was rejected too, with the same useless message, and the malformed input was still correctly reported as malformed because that path never reads a group.

So the validator fails closed, which is the direction you want, and it is still a production incident: valid data is rejected and the error text points at nothing. It is worth noticing which third of the function survived. The match test itself is unaffected, because a null test is not a property read, so a monitoring check that only counts unmatched lines sees a completely healthy system.

The fields the engine writes, and the length that is still a number

A match object carries more than the groups. It has the index the match began at and the input string it came from, and both are written by the engine rather than by your code, which puts them on the same side of the contract as the group names.

Renaming them printed the index as undefined, which is easy to spot. The input length printed as 9, which is not. The code converts the input to a string before taking its length, and the string form of undefined is nine characters long, so the failure produced a plausible small number where a plausible larger one used to be. A metric that never stops being a number is the hardest kind to alarm on.

The last arm reads the expression's own properties. Renaming them made the flags print as undefined and, more pointedly, made a self-check that tests whether the pattern source contains named groups report false. Code that inspects its own regular expressions to decide which parsing path to take will take the wrong one and log nothing.

What to do about it

Keep group names out of the member pattern. That means the names inside every named capture group in the codebase, the groups container itself, and the match fields the engine writes. Anchoring the pattern to a private prefix handles all of them at once, which matters here because group names are ordinary English words like year, level or amount and no exclusion list stays complete.

If a pattern must be broad, prefer the numbered form at the boundary. The numbered read was correct in every arm measured, because an index is not a name. That is a real mitigation rather than a workaround, though it costs the readability the named form was introduced for.

Test by comparing an extracted value against a literal you typed, and do it on the protected build. Do not test by checking that the match succeeded, and do not compare one read of the groups object with another read of the same object: both are renamed together, so they agree with each other while both are wrong.

Frequently asked questions

Does obfuscation break named capture groups?

Not in the default configuration. A file with five expressions covering date parsing, log scanning, an optional group, a validator and reads of the expression's own properties produced identical output on all five protection profiles measured. Named groups become a surface only when member renaming is switched on and the pattern matches the group names.

Why does the replacement still work when the read does not?

Because a replacement pattern names groups inside a string, and a string is data. In the measured run the reformatted date printed correctly while the parsed year read undefined, in the same file, from the same expression. That combination is why the failure reads as bad input rather than as a build step.

Do numbered capture groups have the same problem?

No. A numbered read asks for a position rather than a name, so it is not a rename site. It printed the correct value in every arm measured, which makes it a usable fallback at a boundary where the member pattern has to stay broad.

What happens to a record built from named groups?

It loses fields rather than showing empty ones. In the measured run a four-field record serialised as a single generated key with a null value, because undefined values are dropped during serialisation and a number parsed from undefined serialises as null. The summed total became not-a-number and a severity test flipped from true to false.

Why did my validator start rejecting valid input?

Because the prefix it compares against a named group read undefined, so the comparison failed and the value was rejected. The measured run rejected a valid identifier and printed undefined in place of the prefix it had found, while genuinely malformed input was still reported correctly, since that path never reads a group.

Can the match index or input string be affected?

Yes, both are written by the engine and read as properties. Renaming them printed the index as undefined and made the input length read as 9, because the string form of undefined is nine characters long. A wrong number that is still a number is the least visible outcome in this whole area.

How should regular expression code be verified against a protected build?

Print an extracted value and compare it with a literal you typed, on the protected build. Testing that the match succeeded proves nothing, because the match test is not a property read, and comparing two reads of the same groups object proves nothing either, since both reads are renamed together.

Related reading