Backport some fixes from Pro version (#1374)

This commit is contained in:
Timofey Kachalov
2026-01-27 17:20:23 +04:00
committed by GitHub
parent 99c635a6d2
commit 3848bca794
12 changed files with 350 additions and 0 deletions
@@ -2,6 +2,7 @@ import 'reflect-metadata';
import { assert } from 'chai';
import { evalLocal } from '../../../helpers/evalLocal';
import { readFileAsString } from '../../../helpers/readFileAsString';
import { JavaScriptObfuscator } from '../../../../src/JavaScriptObfuscatorFacade';
@@ -43,5 +44,82 @@ describe('ScopeAnalyzer', () => {
assert.equal(error, null);
});
});
describe('Variant #2: Annex B function hoisting', () => {
describe('Variant #1: basic block-scoped function hoisting', () => {
const samplesCount: number = 50;
let testFunc: () => void;
beforeEach(() => {
const code: string = readFileAsString(__dirname + '/fixtures/annex-b-function-hoisting.js');
testFunc = () => {
for (let i = 0; i < samplesCount; i++) {
const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
seed: i
}).getObfuscatedCode();
const result = evalLocal(obfuscatedCode);
if (result.test1 !== 'foo') {
throw new Error('test1 failed: expected foo, got ' + result.test1);
}
if (result.test2 !== 'bar') {
throw new Error('test2 failed: expected bar, got ' + result.test2);
}
}
};
});
it('should correctly handle Annex B function hoisting references', () => {
assert.doesNotThrow(testFunc);
});
});
describe('Variant #2: strict mode should not apply Annex B hoisting', () => {
let testFunc: () => void;
beforeEach(() => {
const code: string = readFileAsString(__dirname + '/fixtures/annex-b-strict-mode.js');
testFunc = () => {
const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
seed: 12345
}).getObfuscatedCode();
eval(obfuscatedCode);
};
});
it('should correctly handle strict mode block-scoped functions', () => {
assert.doesNotThrow(testFunc);
});
});
describe('Variant #3: let/const shadowing should prevent Annex B hoisting', () => {
let testFunc: () => void;
beforeEach(() => {
const code: string = readFileAsString(__dirname + '/fixtures/annex-b-let-const-shadowing.js');
testFunc = () => {
const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, {
seed: 12345
}).getObfuscatedCode();
const result = eval(obfuscatedCode);
if (result !== 'outer') {
throw new Error('Expected outer, got: ' + result);
}
};
});
it('should not hoist when let/const shadows the function name', () => {
assert.doesNotThrow(testFunc);
});
});
});
});
});
@@ -0,0 +1,39 @@
// Basic Annex B case: function in block referenced after block
function test1() {
if (true) {
function foo() {
return 'foo';
}
}
return foo();
}
// Function in switch case
function test2(x) {
switch (x) {
case 1:
function bar() {
return 'bar';
}
break;
}
return bar();
}
// Multiple blocks with same function name
function test3() {
if (true) {
function baz() {
return 'first';
}
}
if (false) {
function baz() {
return 'second';
}
}
return baz();
}
// Return results for testing
({ test1: test1(), test2: test2(1) });
@@ -0,0 +1,9 @@
function test() {
let foo = 'outer';
if (true) {
function foo() { return 'inner'; }
foo(); // block-scoped foo
}
return foo; // should be 'outer', not the function
}
test();
@@ -0,0 +1,11 @@
'use strict';
function test() {
let foo;
if (true) {
function foo() { return 'inner'; }
foo(); // This refers to block-scoped foo
}
// foo here is the outer let, which is undefined
return typeof foo;
}
test();
@@ -3,6 +3,7 @@ import { assert } from 'chai';
import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
import { readFileAsString } from '../../../../helpers/readFileAsString';
import { evalLocal } from '../../../../helpers/evalLocal';
import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade';
@@ -142,4 +143,62 @@ describe('ObjectPatternPropertiesTransformer', () => {
});
});
});
describe('Variant #3: destructuring default parameter with var shadowing', () => {
describe('Variant #1: destructuring default should reference outer var, not inner var', () => {
let code: string;
let obfuscatedCode: string;
before(() => {
code = readFileAsString(__dirname + '/fixtures/destructuring-default-outer-var.js');
obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
...NO_ADDITIONAL_NODES_PRESET
}).getObfuscatedCode();
});
it('should correctly resolve destructuring default to outer variable', () => {
// Default parameter `a = x` should use outer 'x' value ('outer'),
// NOT the inner var x = 'inner' which is in the function body scope
assert.equal(evalLocal(code), 'outer');
assert.equal(evalLocal(obfuscatedCode), evalLocal(code));
});
});
describe('Variant #2: nested destructuring default should reference outer var', () => {
let code: string;
let obfuscatedCode: string;
before(() => {
code = readFileAsString(__dirname + '/fixtures/nested-destructuring-default-outer-var.js');
obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
...NO_ADDITIONAL_NODES_PRESET
}).getObfuscatedCode();
});
it('should correctly resolve nested destructuring default to outer variable', () => {
assert.equal(evalLocal(code), 'outer');
assert.equal(evalLocal(obfuscatedCode), evalLocal(code));
});
});
describe('Variant #3: simple default param with var shadow', () => {
let code: string;
let obfuscatedCode: string;
before(() => {
code = readFileAsString(__dirname + '/fixtures/simple-default-param-outer-var.js');
obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
...NO_ADDITIONAL_NODES_PRESET
}).getObfuscatedCode();
});
it('should correctly resolve default param to outer variable', () => {
assert.equal(evalLocal(code), 'outer');
assert.equal(evalLocal(obfuscatedCode), evalLocal(code));
});
});
});
});
@@ -0,0 +1,8 @@
(function() {
var x = 'outer';
function f({ a = x } = {}) {
var x = 'inner';
return a;
}
return f();
})();
@@ -0,0 +1,8 @@
(function() {
var x = 'outer';
function f({ a: { b = x } } = { a: {} }) {
var x = 'inner';
return b;
}
return f();
})();
@@ -0,0 +1,8 @@
(function() {
var x = 'outer';
function f(a = x) {
var x = 'inner';
return a;
}
return f();
})();
+9
View File
@@ -0,0 +1,9 @@
/**
* Evaluates code using indirect eval.
* Indirect eval runs in global scope without inheriting strict mode from the calling context.
* This is needed for testing features like Annex B function hoisting.
*
* @param {string} code
* @returns {any}
*/
export const evalLocal = (code: string): any => (0, eval)(code);