Skip to content

Fixed controlFlowFlattening dropping the ?. short-circuit on optional calls in the middle of a chain - #1479

Merged
sanex3339 merged 2 commits into
javascript-obfuscator:masterfrom
lounya:fix/optional-call-in-middle-of-chain
Nov 11, 2026
Merged

sanex3339 merged 2 commits into
javascript-obfuscator:masterfrom
lounya:fix/optional-call-in-middle-of-chain

Conversation

@lounya

@lounya lounya commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

controlFlowFlattening drops the ?. short-circuit when an optional call is followed by a member access:

const fn = undefined;
fn?.(1).map((x) => x); // undefined, no throw

With controlFlowFlattening: true the call is hoisted into an unguarded wrapper and throws TypeError: _0x... is not a function.

This is the gap left by #1409. The replacer treats a call as optional when its parent is a ChainExpression, but that only covers the last call of the chain. In fn?.(1).map(...) the call's parent is a MemberExpression, so it is hoisted into a regular wrapper without ?.. The call node itself has optional: true, so this PR uses that flag and leaves the call in place when it is not the chain tail. Hoisting it into a wrapper cannot work here: the wrapper would return undefined to .map(), while the original expression short-circuits the whole chain.

The tradeoff is that such calls are no longer flattened. I did not find a way to hoist them without changing the chain semantics.

Added a regression test that obfuscates and evaluates the shape with a nullish callee. It fails on master with TypeError: _0x... is not a function and passes with the fix. Verified with yarn build and yarn test:mocha (2119 passing).

Follow-up to #1408.

lounya and others added 2 commits November 10, 2026 11:18
…onal calls in the middle of a chain

`foo?.(x).bar()` short-circuits the whole chain when `foo` is nullish.
The replacer decided whether a call was optional by checking if its parent
is a `ChainExpression`, which is only true for the last call in the chain.
In `foo?.(x).bar()` the call's parent is a `MemberExpression`, so it was
hoisted into a regular wrapper without `?.` and threw
`TypeError: <X> is not a function` at runtime.

Use the `optional` flag on the call node itself, and skip the replacement
when the optional call is not the chain tail. A wrapper cannot preserve
the semantics on its own: it would return `undefined` to the following
member access instead of short-circuiting the whole chain.

Follow-up to javascript-obfuscator#1408
@sanex3339
sanex3339 merged commit 0bb6e48 into javascript-obfuscator:master Nov 11, 2026
@lounya
lounya deleted the fix/optional-call-in-middle-of-chain branch November 12, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants