Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ v5.10.1
* Fixed `selfDefending`, `disableConsoleOutput`, `debugProtection` and `domainLock` code helpers being placed before the `'use strict'` directive, which disabled strict mode. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1475
* Fixed `numbersToExpressions` producing invalid code for non-computed numeric keys of class members. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1476
* Template literals are now transformed into `String.prototype.concat` calls instead of `+` concatenation, so substitutions are converted to strings with `toString` instead of `valueOf`. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1477
* Fixed `controlFlowFlattening` dropping the `?.` short-circuit on an optional call in the middle of a chain (`foo?.(x).bar()`), causing `TypeError: <X> is not a function`. Follow-up to https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1408

v5.10.0
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ export class CallExpressionControlFlowReplacer extends AbstractControlFlowReplac

const isChainExpressionParent = NodeGuards.isChainExpressionNode(parentNode);

// An optional call that is not the last element of the chain cannot be hoisted into a
// wrapper: `foo?.(x).bar()` short-circuits the whole chain, while the wrapper would only
// guard the call and return `undefined` to `.bar()`. Skipping the flattening keeps the
// semantics (follow-up to issue #1408).
const isOptionalCall: boolean = 'optional' in callExpressionNode && callExpressionNode.optional;

if (isOptionalCall && !isChainExpressionParent) {
return callExpressionNode;
}

const expressionArguments: (ESTree.Expression | ESTree.SpreadElement)[] = callExpressionNode.arguments;

// Bucket reuse-eligible wrappers by the exact argument *shape* (which positions are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,5 +303,33 @@ describe('CallExpressionControlFlowReplacer', function () {
}
});
});

describe('Variant #10 - optional call in the middle of a chain', () => {
const samplesCount: number = 50;

it('should keep the `?.` short-circuit when an optional call is followed by a member access', () => {
const code: string = readFileAsString(
__dirname + '/fixtures/optional-call-in-the-middle-of-a-chain.js'
);

for (let i = 0; i < samplesCount; i++) {
const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
...NO_ADDITIONAL_NODES_PRESET,
controlFlowFlattening: true,
controlFlowFlatteningThreshold: 1,
deadCodeInjection: true,
deadCodeInjectionThreshold: 1
}).getObfuscatedCode();

const result: unknown = eval(obfuscatedCode);

assert.strictEqual(
result,
'ok',
`iteration ${i}: optional ?.() call in the middle of a chain lost its short-circuit`
);
}
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
(function () {
var maybeHandler = undefined;
var handler = function () {
return ['a', 'b'];
};

maybeHandler?.(1).map(function (value) {
return value;
});

var result = handler?.(1).map(function (value) {
return value;
}).join('');

return result === 'ab' ? 'ok' : 'unexpected';
})();