From 093da5cefc95b105320eceea14cb97eb4741bdd4 Mon Sep 17 00:00:00 2001 From: Ben Baryo <19845603+ctrl-escp@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:32:38 +0200 Subject: [PATCH 1/4] Refactor ESLint configuration and update test cases for improved clarity and consistency - Enhanced ESLint configuration by restructuring rules and adding detailed comments for better organization. - Updated test cases in `modules.safe.test.js` to improve readability and maintainability, ensuring consistent formatting and structure across all tests. - Adjusted expected results in tests to align with updated code logic. --- eslint.config.js | 126 +- tests/modules.safe.test.js | 3952 ++++++++++++++++++------------------ 2 files changed, 2052 insertions(+), 2026 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 884fbf5..50ab96c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,52 +1,78 @@ -import js from '@eslint/js'; -import path from 'node:path'; -import globals from 'globals'; -import {fileURLToPath} from 'node:url'; -import {FlatCompat} from '@eslint/eslintrc'; -import babelParser from "@babel/eslint-parser"; +export default [ + { + ignores: [ + 'tests/resources/', + '**/jquery*.js', + '**/*tmp*.*', + '**/*tmp*/', + 'eslint.config.js', + 'node_modules/', + ], + }, + { + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all, -}); + rules: { + /* + * ───────── Formatting ───────── + */ + indent: ['error', 2, {SwitchCase: 1}], + semi: ['error', 'always'], + quotes: ['error', 'single', {avoidEscape: true}], + 'comma-dangle': ['error', 'always-multiline'], + 'object-curly-spacing': ['error', 'never'], + 'array-bracket-spacing': ['error', 'never'], + 'no-trailing-spaces': 'error', + 'eol-last': ['error', 'always'], + 'no-multiple-empty-lines': ['error', {max: 1, maxEOF: 0}], -export default [ - { - ignores: [ - 'tests/resources/', - '**/jquery*.js', - '**/*tmp*.*', - '**/*tmp*/', - "eslint.config.js", - "node_modules/", - ], - }, - ...compat.extends('eslint:recommended'), - { - languageOptions: { - parser: babelParser, - parserOptions: { - requireConfigFile: false, - }, - globals: { - ...globals.browser, - ...globals.nodeBuiltin, - }, - ecmaVersion: 'latest', - sourceType: 'module', - }, - rules: { - indent: ['error', 'tab', { - SwitchCase: 1, - }], - 'linebreak-style': ['error', 'unix'], - quotes: ['error', 'single', { - allowTemplateLiterals: true, - }], - semi: ['error', 'always'], - 'no-empty': ['off'], - }, - }]; \ No newline at end of file + /* + * ───────── Strictness ───────── + */ + eqeqeq: ['error', 'always'], + 'no-var': 'error', + 'prefer-const': 'error', + 'no-redeclare': 'error', + 'no-shadow': 'error', + 'no-return-await': 'error', + 'no-useless-catch': 'error', + + /* + * ───────── Predictability ───────── + */ + 'consistent-return': 'error', + 'default-case': 'error', + 'dot-notation': 'error', + 'no-fallthrough': 'error', + 'no-unreachable': 'error', + 'no-throw-literal': 'error', + radix: ['error', 'always'], + yoda: ['error', 'never'], + + /* + * ───────── Clean Refactors ───────── + */ + 'no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'no-debugger': 'error', + + /* + * ───────── Modern JS Discipline ───────── + */ + 'prefer-arrow-callback': 'error', + 'prefer-template': 'error', + 'prefer-spread': 'error', + 'prefer-rest-params': 'error', + 'object-shorthand': ['error', 'always'], + }, + }, +]; diff --git a/tests/modules.safe.test.js b/tests/modules.safe.test.js index deee7a7..0c27dc8 100644 --- a/tests/modules.safe.test.js +++ b/tests/modules.safe.test.js @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars */ + import assert from 'node:assert'; import {describe, it} from 'node:test'; import {Arborist, applyIteratively} from 'flast'; @@ -11,236 +11,236 @@ import {Arborist, applyIteratively} from 'flast'; * @return {string} The result of the operation */ function applyModuleToCode(code, func, looped = false) { - let result; - if (looped) { - result = applyIteratively(code, [func]); - } else { - const arb = new Arborist(code); - result = func(arb); - result.applyChanges(); - result = result.script; - } - return result; + let result; + if (looped) { + result = applyIteratively(code, [func]); + } else { + const arb = new Arborist(code); + result = func(arb); + result.applyChanges(); + result = result.script; + } + return result; } describe('SAFE: removeRedundantBlockStatements', async () => { - const targetModule = (await import('../src/modules/safe/removeRedundantBlockStatements.js')).default; - it('TP-1', () => { - const code = `if (a) {{do_a();}}`; - const expected = `if (a) {\n do_a();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2', () => { - const code = `if (a) {{do_a();}{do_b();}}`; - const expected = `if (a) {\n do_a();\n do_b();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3', () => { - const code = `if (a) {{do_a();}{do_b(); do_c();}{do_d();}}`; - const expected = `if (a) {\n do_a();\n do_b();\n do_c();\n do_d();\n}`; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TP-4', () => { - const code = `if (a) {{{{{do_a();}}}} do_b();}`; - const expected = `if (a) {\n do_a();\n do_b();\n}`; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/removeRedundantBlockStatements.js')).default; + it('TP-1', () => { + const code = 'if (a) {{do_a();}}'; + const expected = 'if (a) {\n do_a();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2', () => { + const code = 'if (a) {{do_a();}{do_b();}}'; + const expected = 'if (a) {\n do_a();\n do_b();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3', () => { + const code = 'if (a) {{do_a();}{do_b(); do_c();}{do_d();}}'; + const expected = 'if (a) {\n do_a();\n do_b();\n do_c();\n do_d();\n}'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TP-4', () => { + const code = 'if (a) {{{{{do_a();}}}} do_b();}'; + const expected = 'if (a) {\n do_a();\n do_b();\n}'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); }); describe('SAFE: normalizeComputed', async () => { - const targetModule = (await import('../src/modules/safe/normalizeComputed.js')).default; - it('TP-1: Convert valid string identifiers to dot notation', () => { - const code = `hello['world'][0]['%32']['valid']`; - const expected = `hello.world[0]['%32'].valid;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Convert object properties with valid identifiers', () => { - const code = `const obj = {['validProp']: 1, ['invalid-prop']: 2, ['$valid']: 3};`; - const expected = `const obj = {\n validProp: 1,\n ['invalid-prop']: 2,\n $valid: 3\n};`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Convert class method definitions with valid identifiers', () => { - const code = `class Test { ['method']() {} ['123invalid']() {} ['_valid']() {} }`; - const expected = `class Test {\n method() {\n }\n ['123invalid']() {\n }\n _valid() {\n }\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not convert invalid identifiers', () => { - const code = `obj['123']['-invalid']['spa ce']['@special'];`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, code); - }); - it('TN-2: Do not convert numeric indices but convert valid string', () => { - const code = `arr[0][42]['string'];`; - const expected = `arr[0][42].string;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/normalizeComputed.js')).default; + it('TP-1: Convert valid string identifiers to dot notation', () => { + const code = 'hello[\'world\'][0][\'%32\'][\'valid\']'; + const expected = 'hello.world[0][\'%32\'].valid;'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TP-2: Convert object properties with valid identifiers', () => { + const code = 'const obj = {[\'validProp\']: 1, [\'invalid-prop\']: 2, [\'$valid\']: 3};'; + const expected = 'const obj = {\n validProp: 1,\n [\'invalid-prop\']: 2,\n $valid: 3\n};'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Convert class method definitions with valid identifiers', () => { + const code = 'class Test { [\'method\']() {} [\'123invalid\']() {} [\'_valid\']() {} }'; + const expected = 'class Test {\n method() {\n }\n [\'123invalid\']() {\n }\n _valid() {\n }\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not convert invalid identifiers', () => { + const code = 'obj[\'123\'][\'-invalid\'][\'spa ce\'][\'@special\'];'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, code); + }); + it('TN-2: Do not convert numeric indices but convert valid string', () => { + const code = 'arr[0][42][\'string\'];'; + const expected = 'arr[0][42].string;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: normalizeEmptyStatements', async () => { - const targetModule = (await import('../src/modules/safe/normalizeEmptyStatements.js')).default; - it('TP-1: Remove standalone empty statements', () => { - const code = `;;var a = 3;;`; - const expected = `var a = 3;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Remove empty statements in blocks', () => { - const code = `if (true) {;; var x = 1; ;;;};`; - const expected = `if (true) {\n var x = 1;\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Preserve empty statements in for-loops', () => { - const code = `;for (;;);;`; - const expected = `for (;;);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Preserve empty statements in while-loops', () => { - const code = `;while (true);;`; - const expected = `while (true);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Preserve empty statements in if-statements', () => { - const code = `;if (condition); else;;`; - const expected = `if (condition);\nelse ;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Preserve empty statements in do-while loops', () => { - const code = `;do; while(true);;`; - const expected = `do ;\nwhile (true);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Preserve empty statements in for-in loops', () => { - const code = `;for (;;);;`; - const expected = `for (;;);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/normalizeEmptyStatements.js')).default; + it('TP-1: Remove standalone empty statements', () => { + const code = ';;var a = 3;;'; + const expected = 'var a = 3;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Remove empty statements in blocks', () => { + const code = 'if (true) {;; var x = 1; ;;;};'; + const expected = 'if (true) {\n var x = 1;\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Preserve empty statements in for-loops', () => { + const code = ';for (;;);;'; + const expected = 'for (;;);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Preserve empty statements in while-loops', () => { + const code = ';while (true);;'; + const expected = 'while (true);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Preserve empty statements in if-statements', () => { + const code = ';if (condition); else;;'; + const expected = 'if (condition);\nelse ;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Preserve empty statements in do-while loops', () => { + const code = ';do; while(true);;'; + const expected = 'do ;\nwhile (true);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Preserve empty statements in for-in loops', () => { + const code = ';for (;;);;'; + const expected = 'for (;;);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: parseTemplateLiteralsIntoStringLiterals', async () => { - const targetModule = (await import('../src/modules/safe/parseTemplateLiteralsIntoStringLiterals.js')).default; - it('TP-1: Convert template literal with string expression', () => { - const code = '`hello ${"world"}!`;'; - const expected = `'hello world!';`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Convert template literal with multiple expressions', () => { - const code = '`start ${42} middle ${"end"} finish`;'; - const expected = `'start 42 middle end finish';`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Convert template literal with no expressions', () => { - const code = '`just plain text`;'; - const expected = `'just plain text';`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Convert template literal with boolean and number expressions', () => { - const code = '`flag: ${true}, count: ${123.456}`;'; - const expected = `'flag: true, count: 123.456';`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Convert empty template literal', () => { - const code = '``;'; - const expected = `'';`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not convert template literal with variable expression', () => { - const code = '`hello ${name}!`;'; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not convert template literal with function call expression', () => { - const code = '`result: ${getValue()}`;'; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not convert template literal with mixed literal and non-literal expressions', () => { - const code = '`hello ${"world"} and ${name}!`;'; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/parseTemplateLiteralsIntoStringLiterals.js')).default; + it('TP-1: Convert template literal with string expression', () => { + const code = '`hello ${"world"}!`;'; + const expected = '\'hello world!\';'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Convert template literal with multiple expressions', () => { + const code = '`start ${42} middle ${"end"} finish`;'; + const expected = '\'start 42 middle end finish\';'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Convert template literal with no expressions', () => { + const code = '`just plain text`;'; + const expected = '\'just plain text\';'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Convert template literal with boolean and number expressions', () => { + const code = '`flag: ${true}, count: ${123.456}`;'; + const expected = '\'flag: true, count: 123.456\';'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Convert empty template literal', () => { + const code = '``;'; + const expected = '\'\';'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not convert template literal with variable expression', () => { + const code = '`hello ${name}!`;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not convert template literal with function call expression', () => { + const code = '`result: ${getValue()}`;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not convert template literal with mixed literal and non-literal expressions', () => { + const code = '`hello ${"world"} and ${name}!`;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: rearrangeSequences', async () => { - const targetModule = (await import('../src/modules/safe/rearrangeSequences.js')).default; - it('TP-1: Split sequenced calls to standalone expressions', () => { - const code = `function f() { return a(), b(), c(); }`; - const expected = `function f() {\n a();\n b();\n return c();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Split sequenced calls to standalone expressions in if-statements', () => { - const code = `function f() { if (x) return a(), b(), c(); else d(); }`; - const expected = `function f() {\n if (x) {\n a();\n b();\n return c();\n } else\n d();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Split sequenced calls in if-statements to cascading if-statements', () => { - const code = `function f() { if (a(), b()) c(); }`; - const expected = `function f() {\n a();\n if (b())\n c();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Split sequenced calls in nested if-statements to cascading if-statements', () => { - const code = `function f() { if (x) if (a(), b()) c(); }`; - const expected = `function f() {\n if (x) {\n a();\n if (b())\n c();\n }\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Split sequences with more than three expressions', () => { - const code = `function f() { return a(), b(), c(), d(), e(); }`; - const expected = `function f() {\n a();\n b();\n c();\n d();\n return e();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Split sequences in if condition with else clause', () => { - const code = `if (setup(), check(), validate()) action(); else fallback();`; - const expected = `{\n setup();\n check();\n if (validate())\n action();\n else\n fallback();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not transform single expression returns', () => { - const code = `function f() { return a(); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not transform single expression if conditions', () => { - const code = `if (condition()) action();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not transform non-sequence expressions', () => { - const code = `function f() { return func(a, b, c); if (obj.prop) x(); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/rearrangeSequences.js')).default; + it('TP-1: Split sequenced calls to standalone expressions', () => { + const code = 'function f() { return a(), b(), c(); }'; + const expected = 'function f() {\n a();\n b();\n return c();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Split sequenced calls to standalone expressions in if-statements', () => { + const code = 'function f() { if (x) return a(), b(), c(); else d(); }'; + const expected = 'function f() {\n if (x) {\n a();\n b();\n return c();\n } else\n d();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Split sequenced calls in if-statements to cascading if-statements', () => { + const code = 'function f() { if (a(), b()) c(); }'; + const expected = 'function f() {\n a();\n if (b())\n c();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Split sequenced calls in nested if-statements to cascading if-statements', () => { + const code = 'function f() { if (x) if (a(), b()) c(); }'; + const expected = 'function f() {\n if (x) {\n a();\n if (b())\n c();\n }\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Split sequences with more than three expressions', () => { + const code = 'function f() { return a(), b(), c(), d(), e(); }'; + const expected = 'function f() {\n a();\n b();\n c();\n d();\n return e();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Split sequences in if condition with else clause', () => { + const code = 'if (setup(), check(), validate()) action(); else fallback();'; + const expected = '{\n setup();\n check();\n if (validate())\n action();\n else\n fallback();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not transform single expression returns', () => { + const code = 'function f() { return a(); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not transform single expression if conditions', () => { + const code = 'if (condition()) action();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not transform non-sequence expressions', () => { + const code = 'function f() { return func(a, b, c); if (obj.prop) x(); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: rearrangeSwitches', async () => { - const targetModule = (await import('../src/modules/safe/rearrangeSwitches.js')).default; - it('TP-1: Complex switch with multiple cases and return statement', () => { - const code = `(() => {let a = 1;\twhile (true) {switch (a) {case 3: return console.log(3); case 2: console.log(2); a = 3; break; + const targetModule = (await import('../src/modules/safe/rearrangeSwitches.js')).default; + it('TP-1: Complex switch with multiple cases and return statement', () => { + const code = `(() => {let a = 1;\twhile (true) {switch (a) {case 3: return console.log(3); case 2: console.log(2); a = 3; break; case 1: console.log(1); a = 2; break;}}})();`; - const expected = `((() => { + const expected = `((() => { let a = 1; while (true) { { @@ -252,1551 +252,1551 @@ case 1: console.log(1); a = 2; break;}}})();`; } } })());`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Simple switch with sequential cases', () => { - const code = `var state = 0; switch (state) { case 0: first(); state = 1; break; case 1: second(); break; }`; - const expected = `var state = 0; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Simple switch with sequential cases', () => { + const code = 'var state = 0; switch (state) { case 0: first(); state = 1; break; case 1: second(); break; }'; + const expected = `var state = 0; { first(); state = 1; second(); }`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Switch with default case', () => { - const code = `var x = 1; switch (x) { case 1: action1(); x = 2; break; default: defaultAction(); break; case 2: action2(); break; }`; - const expected = `var x = 1; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Switch with default case', () => { + const code = 'var x = 1; switch (x) { case 1: action1(); x = 2; break; default: defaultAction(); break; case 2: action2(); break; }'; + const expected = `var x = 1; { action1(); x = 2; defaultAction(); }`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Switch starting with non-initial case via default', () => { - const code = `var val = 99; switch (val) { case 1: step1(); val = 2; break; case 2: step2(); break; default: val = 1; break; }`; - const expected = `var val = 99; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Switch starting with non-initial case via default', () => { + const code = 'var val = 99; switch (val) { case 1: step1(); val = 2; break; case 2: step2(); break; default: val = 1; break; }'; + const expected = `var val = 99; { val = 1; step1(); val = 2; step2(); }`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not transform switch without literal discriminant initialization', () => { - const code = `var a; switch (a) { case 1: doSomething(); break; }`; - const expected = `var a; switch (a) { case 1: doSomething(); break; }`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Transform switch but stop at multiple assignments to discriminant', () => { - const code = `var state = 0; switch (state) { case 0: state = 1; state = 2; break; case 1: action(); break; }`; - const expected = `var state = 0; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not transform switch without literal discriminant initialization', () => { + const code = 'var a; switch (a) { case 1: doSomething(); break; }'; + const expected = 'var a; switch (a) { case 1: doSomething(); break; }'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Transform switch but stop at multiple assignments to discriminant', () => { + const code = 'var state = 0; switch (state) { case 0: state = 1; state = 2; break; case 1: action(); break; }'; + const expected = `var state = 0; { state = 1; state = 2; }`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not transform switch with non-literal case value', () => { - const code = `var x = 0; switch (x) { case variable: doSomething(); break; }`; - const expected = `var x = 0; switch (x) { case variable: doSomething(); break; }`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not transform switch with non-literal case value', () => { + const code = 'var x = 0; switch (x) { case variable: doSomething(); break; }'; + const expected = 'var x = 0; switch (x) { case variable: doSomething(); break; }'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: removeDeadNodes', async () => { - const targetModule = (await import('../src/modules/safe/removeDeadNodes.js')).default; - it('TP-1', () => { - const code = `var a = 3, b = 12; console.log(b);`; - const expected = `var b = 12;\nconsole.log(b);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/removeDeadNodes.js')).default; + it('TP-1', () => { + const code = 'var a = 3, b = 12; console.log(b);'; + const expected = 'var b = 12;\nconsole.log(b);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: replaceCallExpressionsWithUnwrappedIdentifier', async () => { - const targetModule = (await import('../src/modules/safe/replaceCallExpressionsWithUnwrappedIdentifier.js')).default; - it('TP-1: Replace call expression with identifier behind an arrow function', () => { - const code = `const a = () => btoa; a()('yo');`; - const expected = `const a = () => btoa;\nbtoa('yo');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace call expression with identifier behind a function declaration', () => { - const code = `function a() {return btoa;} a()('yo');`; - const expected = `function a() {\n return btoa;\n}\nbtoa('yo');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace call expression with function expression assigned to variable', () => { - const code = `const a = function() {return btoa;}; a()('data');`; - const expected = `const a = function () {\n return btoa;\n};\nbtoa('data');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace call expression with arrow function using block statement', () => { - const code = `const a = () => {return btoa;}; a()('test');`; - const expected = `const a = () => {\n return btoa;\n};\nbtoa('test');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace call expression returning parameterless call', () => { - const code = `function a() {return someFunc();} a()('arg');`; - const expected = `function a() {\n return someFunc();\n}\nsomeFunc()('arg');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not replace function returning call expression with arguments', () => { - const code = `function a() {return someFunc('param');} a()('arg');`; - const expected = `function a() {return someFunc('param');} a()('arg');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not replace function with multiple statements', () => { - const code = `function a() {console.log('test'); return btoa;} a()('data');`; - const expected = `function a() {console.log('test'); return btoa;} a()('data');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace function with no return statement', () => { - const code = `function a() {console.log('test');} a()('data');`; - const expected = `function a() {console.log('test');} a()('data');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace non-function callee', () => { - const code = `const a = 'notAFunction'; a()('data');`; - const expected = `const a = 'notAFunction'; a()('data');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/replaceCallExpressionsWithUnwrappedIdentifier.js')).default; + it('TP-1: Replace call expression with identifier behind an arrow function', () => { + const code = 'const a = () => btoa; a()(\'yo\');'; + const expected = 'const a = () => btoa;\nbtoa(\'yo\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace call expression with identifier behind a function declaration', () => { + const code = 'function a() {return btoa;} a()(\'yo\');'; + const expected = 'function a() {\n return btoa;\n}\nbtoa(\'yo\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace call expression with function expression assigned to variable', () => { + const code = 'const a = function() {return btoa;}; a()(\'data\');'; + const expected = 'const a = function () {\n return btoa;\n};\nbtoa(\'data\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace call expression with arrow function using block statement', () => { + const code = 'const a = () => {return btoa;}; a()(\'test\');'; + const expected = 'const a = () => {\n return btoa;\n};\nbtoa(\'test\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace call expression returning parameterless call', () => { + const code = 'function a() {return someFunc();} a()(\'arg\');'; + const expected = 'function a() {\n return someFunc();\n}\nsomeFunc()(\'arg\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not replace function returning call expression with arguments', () => { + const code = 'function a() {return someFunc(\'param\');} a()(\'arg\');'; + const expected = 'function a() {return someFunc(\'param\');} a()(\'arg\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not replace function with multiple statements', () => { + const code = 'function a() {console.log(\'test\'); return btoa;} a()(\'data\');'; + const expected = 'function a() {console.log(\'test\'); return btoa;} a()(\'data\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace function with no return statement', () => { + const code = 'function a() {console.log(\'test\');} a()(\'data\');'; + const expected = 'function a() {console.log(\'test\');} a()(\'data\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace non-function callee', () => { + const code = 'const a = \'notAFunction\'; a()(\'data\');'; + const expected = 'const a = \'notAFunction\'; a()(\'data\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: replaceEvalCallsWithLiteralContent', async () => { - const targetModule = (await import('../src/modules/safe/replaceEvalCallsWithLiteralContent.js')).default; - it('TP-1: Replace eval call with the code parsed from the argument string', () => { - const code = `eval('console.log("hello world")');`; - const expected = `console.log('hello world');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace eval call with a block statement with multiple expression statements', () => { - const code = `eval('a; b;');`; - const expected = `{\n a;\n b;\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace eval call with the code in a return statement', () => { - const code = `function q() {return (eval('a, b;'));}`; - const expected = `function q() {\n return a, b;\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace eval call wrapped in a call expression', () => { - const code = `eval('()=>1')();`; - const expected = `((() => 1)());`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace eval call wrapped in a binary expression', () => { - const code = `eval('3 * 5') + 1;`; - const expected = `3 * 5 + 1;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Unwrap expression statement from replacement where needed', () => { - const code = `console.log(eval('1;'));`; - const expected = `console.log(1);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-7: Replace eval with single expression in conditional', () => { - const code = `if (eval('true')) console.log('test');`; - const expected = `if (true)\n console.log('test');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-8: Replace eval with function declaration', () => { - const code = `eval('function test() { return 42; }');`; - const expected = `(function test() {\n return 42;\n});`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not replace eval with non-literal argument', () => { - const code = `const x = 'alert(1)'; eval(x);`; - const expected = `const x = 'alert(1)'; eval(x);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not replace non-eval function calls', () => { - const code = `myEval('console.log("test")');`; - const expected = `myEval('console.log("test")');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace eval with invalid syntax', () => { - const code = `eval('invalid syntax {{{');`; - const expected = `eval('invalid syntax {{{');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace eval with no arguments', () => { - const code = `eval();`; - const expected = `eval();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/replaceEvalCallsWithLiteralContent.js')).default; + it('TP-1: Replace eval call with the code parsed from the argument string', () => { + const code = 'eval(\'console.log("hello world")\');'; + const expected = 'console.log(\'hello world\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace eval call with a block statement with multiple expression statements', () => { + const code = 'eval(\'a; b;\');'; + const expected = '{\n a;\n b;\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace eval call with the code in a return statement', () => { + const code = 'function q() {return (eval(\'a, b;\'));}'; + const expected = 'function q() {\n return a, b;\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace eval call wrapped in a call expression', () => { + const code = 'eval(\'()=>1\')();'; + const expected = '((() => 1)());'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace eval call wrapped in a binary expression', () => { + const code = 'eval(\'3 * 5\') + 1;'; + const expected = '3 * 5 + 1;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Unwrap expression statement from replacement where needed', () => { + const code = 'console.log(eval(\'1;\'));'; + const expected = 'console.log(1);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-7: Replace eval with single expression in conditional', () => { + const code = 'if (eval(\'true\')) console.log(\'test\');'; + const expected = 'if (true)\n console.log(\'test\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-8: Replace eval with function declaration', () => { + const code = 'eval(\'function test() { return 42; }\');'; + const expected = '(function test() {\n return 42;\n});'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not replace eval with non-literal argument', () => { + const code = 'const x = \'alert(1)\'; eval(x);'; + const expected = 'const x = \'alert(1)\'; eval(x);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not replace non-eval function calls', () => { + const code = 'myEval(\'console.log("test")\');'; + const expected = 'myEval(\'console.log("test")\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace eval with invalid syntax', () => { + const code = 'eval(\'invalid syntax {{{\');'; + const expected = 'eval(\'invalid syntax {{{\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace eval with no arguments', () => { + const code = 'eval();'; + const expected = 'eval();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: replaceFunctionShellsWithWrappedValue', async () => { - const targetModule = (await import('../src/modules/safe/replaceFunctionShellsWithWrappedValue.js')).default; - it('TP-1: Replace references with identifier', () => { - const code = `function a() {return String}\na()(val);`; - const expected = `function a() {\n return String;\n}\nString(val);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace function returning literal number', () => { - const code = `function getValue() { return 42; }\nconsole.log(getValue());`; - const expected = `function getValue() {\n return 42;\n}\nconsole.log(42);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace function returning literal string', () => { - const code = `function getName() { return "test"; }\nalert(getName());`; - const expected = `function getName() {\n return 'test';\n}\nalert('test');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace function returning boolean literal', () => { - const code = `function isTrue() { return true; }\nif (isTrue()) console.log("yes");`; - const expected = `function isTrue() {\n return true;\n}\nif (true)\n console.log('yes');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace multiple calls to same function', () => { - const code = `function getX() { return x; }\ngetX() + getX();`; - const expected = `function getX() {\n return x;\n}\nx + x;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Should not replace literals 1', () => { - const code = `function a() {\n return 0;\n}\nconst o = { key: a }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Should not replace literals 2', () => { - const code = `function a() {\n return 0;\n}\nconsole.log(a);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace function with multiple statements', () => { - const code = `function complex() { console.log("side effect"); return 42; }\ncomplex();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace function with no return statement', () => { - const code = `function noReturn() { console.log("void"); }\nnoReturn();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not replace function returning complex expression', () => { - const code = `function calc() { return a + b; }\ncalc();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-6: Do not replace function used as callback', () => { - const code = `function getValue() { return 42; }\n[1,2,3].map(getValue);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/replaceFunctionShellsWithWrappedValue.js')).default; + it('TP-1: Replace references with identifier', () => { + const code = 'function a() {return String}\na()(val);'; + const expected = 'function a() {\n return String;\n}\nString(val);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace function returning literal number', () => { + const code = 'function getValue() { return 42; }\nconsole.log(getValue());'; + const expected = 'function getValue() {\n return 42;\n}\nconsole.log(42);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace function returning literal string', () => { + const code = 'function getName() { return "test"; }\nalert(getName());'; + const expected = 'function getName() {\n return \'test\';\n}\nalert(\'test\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace function returning boolean literal', () => { + const code = 'function isTrue() { return true; }\nif (isTrue()) console.log("yes");'; + const expected = 'function isTrue() {\n return true;\n}\nif (true)\n console.log(\'yes\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace multiple calls to same function', () => { + const code = 'function getX() { return x; }\ngetX() + getX();'; + const expected = 'function getX() {\n return x;\n}\nx + x;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Should not replace literals 1', () => { + const code = 'function a() {\n return 0;\n}\nconst o = { key: a }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Should not replace literals 2', () => { + const code = 'function a() {\n return 0;\n}\nconsole.log(a);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace function with multiple statements', () => { + const code = 'function complex() { console.log("side effect"); return 42; }\ncomplex();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace function with no return statement', () => { + const code = 'function noReturn() { console.log("void"); }\nnoReturn();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not replace function returning complex expression', () => { + const code = 'function calc() { return a + b; }\ncalc();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-6: Do not replace function used as callback', () => { + const code = 'function getValue() { return 42; }\n[1,2,3].map(getValue);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: replaceFunctionShellsWithWrappedValueIIFE', async () => { - const targetModule = (await import('../src/modules/safe/replaceFunctionShellsWithWrappedValueIIFE.js')).default; - it('TP-1: Replace with wrapped value in-place', () => { - const code = `(function a() {return String}\n)()(val);`; - const expected = `String(val);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace IIFE returning literal number', () => { - const code = `(function() { return 42; })() + 1;`; - const expected = `42 + 1;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace IIFE returning literal string', () => { - const code = `console.log((function() { return "hello"; })());`; - const expected = `console.log('hello');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace IIFE returning boolean literal', () => { - const code = `if ((function() { return true; })()) console.log("yes");`; - const expected = `if (true)\n console.log('yes');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace IIFE returning identifier', () => { - const code = `var result = (function() { return someValue; })();`; - const expected = `var result = someValue;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Replace multiple IIFEs in expression', () => { - const code = `(function() { return 5; })() + (function() { return 3; })();`; - const expected = `5 + 3;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not replace IIFE with arguments', () => { - const code = `(function() { return 42; })(arg);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not replace IIFE with multiple statements', () => { - const code = `(function() { console.log("side effect"); return 42; })();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace IIFE with no return statement', () => { - const code = `(function() { console.log("void"); })();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace IIFE returning complex expression', () => { - const code = `(function() { return a + b; })();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not replace function expression not used as IIFE', () => { - const code = `var fn = function() { return 42; }; fn();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-6: Do not replace function expression without a return value', () => { - const code = `var fn = function() { return; };`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/replaceFunctionShellsWithWrappedValueIIFE.js')).default; + it('TP-1: Replace with wrapped value in-place', () => { + const code = '(function a() {return String}\n)()(val);'; + const expected = 'String(val);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace IIFE returning literal number', () => { + const code = '(function() { return 42; })() + 1;'; + const expected = '42 + 1;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace IIFE returning literal string', () => { + const code = 'console.log((function() { return "hello"; })());'; + const expected = 'console.log(\'hello\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace IIFE returning boolean literal', () => { + const code = 'if ((function() { return true; })()) console.log("yes");'; + const expected = 'if (true)\n console.log(\'yes\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace IIFE returning identifier', () => { + const code = 'var result = (function() { return someValue; })();'; + const expected = 'var result = someValue;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Replace multiple IIFEs in expression', () => { + const code = '(function() { return 5; })() + (function() { return 3; })();'; + const expected = '5 + 3;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not replace IIFE with arguments', () => { + const code = '(function() { return 42; })(arg);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not replace IIFE with multiple statements', () => { + const code = '(function() { console.log("side effect"); return 42; })();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace IIFE with no return statement', () => { + const code = '(function() { console.log("void"); })();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace IIFE returning complex expression', () => { + const code = '(function() { return a + b; })();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not replace function expression not used as IIFE', () => { + const code = 'var fn = function() { return 42; }; fn();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-6: Do not replace function expression without a return value', () => { + const code = 'var fn = function() { return; };'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: replaceIdentifierWithFixedAssignedValue', async () => { - const targetModule = (await import('../src/modules/safe/replaceIdentifierWithFixedAssignedValue.js')).default; - it('TP-1: Replace references with number literal', () => { - const code = `const a = 3; const b = a * 2; console.log(b + a);`; - const expected = `const a = 3;\nconst b = 3 * 2;\nconsole.log(b + 3);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace references with string literal', () => { - const code = `const msg = "hello"; console.log(msg + " world");`; - const expected = `const msg = 'hello';\nconsole.log('hello' + ' world');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace references with boolean literal', () => { - const code = `const flag = true; if (flag) console.log("yes");`; - const expected = `const flag = true;\nif (true)\n console.log('yes');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace multiple different variables', () => { - const code = `const x = 5; const y = "test"; console.log(x, y);`; - const expected = `const x = 5;\nconst y = 'test';\nconsole.log(5, 'test');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace with null literal', () => { - const code = `const val = null; if (val === null) console.log("null");`; - const expected = `const val = null;\nif (null === null)\n console.log('null');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Replace with let declaration', () => { - const code = `let count = 0; console.log(count + 1);`; - const expected = `let count = 0;\nconsole.log(0 + 1);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-7: Replace with var declaration', () => { - const code = `var total = 100; console.log(total / 2);`; - const expected = `var total = 100;\nconsole.log(100 / 2);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do no replace a value used in a for-in-loop', () => { - const code = `var a = 3; for (a in [1, 2]) console.log(a);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do no replace a value used in a for-of-loop', () => { - const code = `var a = 3; for (a of [1, 2]) console.log(a);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace variable with non-literal initializer', () => { - const code = `const result = getValue(); console.log(result);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace object property names', () => { - const code = `const key = "name"; const obj = { key: "value" };`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not replace modified variables', () => { - const code = `let counter = 0; counter++; console.log(counter);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-6: Do not replace reassigned variables', () => { - const code = `let status = true; status = false; console.log(status);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-7: Do not replace variable without declaration', () => { - const code = `console.log(undeclaredVar);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/replaceIdentifierWithFixedAssignedValue.js')).default; + it('TP-1: Replace references with number literal', () => { + const code = 'const a = 3; const b = a * 2; console.log(b + a);'; + const expected = 'const a = 3;\nconst b = 3 * 2;\nconsole.log(b + 3);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace references with string literal', () => { + const code = 'const msg = "hello"; console.log(msg + " world");'; + const expected = 'const msg = \'hello\';\nconsole.log(\'hello\' + \' world\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace references with boolean literal', () => { + const code = 'const flag = true; if (flag) console.log("yes");'; + const expected = 'const flag = true;\nif (true)\n console.log(\'yes\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace multiple different variables', () => { + const code = 'const x = 5; const y = "test"; console.log(x, y);'; + const expected = 'const x = 5;\nconst y = \'test\';\nconsole.log(5, \'test\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace with null literal', () => { + const code = 'const val = null; if (val === null) console.log("null");'; + const expected = 'const val = null;\nif (null === null)\n console.log(\'null\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Replace with let declaration', () => { + const code = 'let count = 0; console.log(count + 1);'; + const expected = 'let count = 0;\nconsole.log(0 + 1);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-7: Replace with var declaration', () => { + const code = 'var total = 100; console.log(total / 2);'; + const expected = 'var total = 100;\nconsole.log(100 / 2);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do no replace a value used in a for-in-loop', () => { + const code = 'var a = 3; for (a in [1, 2]) console.log(a);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do no replace a value used in a for-of-loop', () => { + const code = 'var a = 3; for (a of [1, 2]) console.log(a);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace variable with non-literal initializer', () => { + const code = 'const result = getValue(); console.log(result);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace object property names', () => { + const code = 'const key = "name"; const obj = { key: "value" };'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not replace modified variables', () => { + const code = 'let counter = 0; counter++; console.log(counter);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-6: Do not replace reassigned variables', () => { + const code = 'let status = true; status = false; console.log(status);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-7: Do not replace variable without declaration', () => { + const code = 'console.log(undeclaredVar);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: replaceIdentifierWithFixedValueNotAssignedAtDeclaration', async () => { - const targetModule = (await import('../src/modules/safe/replaceIdentifierWithFixedValueNotAssignedAtDeclaration.js')).default; - it('TP-1: Replace identifier with number literal', () => { - const code = `let a; a = 3; const b = a * 2; console.log(b + a);`; - const expected = `let a;\na = 3;\nconst b = 3 * 2;\nconsole.log(b + 3);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace identifier with string literal', () => { - const code = `let name; name = 'test'; alert(name);`; - const expected = `let name;\nname = 'test';\nalert('test');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace identifier with boolean literal', () => { - const code = `let flag; flag = true; if (flag) console.log('yes');`; - const expected = `let flag;\nflag = true;\nif (true)\n console.log('yes');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace identifier with null literal', () => { - const code = `let value; value = null; console.log(value);`; - const expected = `let value;\nvalue = null;\nconsole.log(null);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace var declaration', () => { - const code = `var x; x = 42; console.log(x);`; - const expected = `var x;\nx = 42;\nconsole.log(42);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Replace with multiple references', () => { - const code = `let count; count = 5; alert(count); console.log(count);`; - const expected = `let count;\ncount = 5;\nalert(5);\nconsole.log(5);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not replace variable used in for-in loop', () => { - const code = `let a; a = 'prop'; for (a in obj) console.log(a);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not replace variable used in for-of loop', () => { - const code = `let item; item = 1; for (item of arr) console.log(item);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace variable in conditional expression context', () => { - const code = `let a; b === c ? (a = 1) : (a = 2); console.log(a);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace variable with multiple assignments', () => { - const code = `let a; a = 1; a = 2; console.log(a);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not replace variable assigned non-literal value', () => { - const code = `let a; a = someFunction(); console.log(a);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-6: Do not replace function callee', () => { - const code = `let func; func = alert; func('hello');`; - const expected = `let func; func = alert; func('hello');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-7: Do not replace variable with initial value', () => { - const code = `let a = 1; a = 2; console.log(a);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-8: Do not replace when references are modified', () => { - const code = `let a; a = 1; a++; console.log(a);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/replaceIdentifierWithFixedValueNotAssignedAtDeclaration.js')).default; + it('TP-1: Replace identifier with number literal', () => { + const code = 'let a; a = 3; const b = a * 2; console.log(b + a);'; + const expected = 'let a;\na = 3;\nconst b = 3 * 2;\nconsole.log(b + 3);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace identifier with string literal', () => { + const code = 'let name; name = \'test\'; alert(name);'; + const expected = 'let name;\nname = \'test\';\nalert(\'test\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace identifier with boolean literal', () => { + const code = 'let flag; flag = true; if (flag) console.log(\'yes\');'; + const expected = 'let flag;\nflag = true;\nif (true)\n console.log(\'yes\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace identifier with null literal', () => { + const code = 'let value; value = null; console.log(value);'; + const expected = 'let value;\nvalue = null;\nconsole.log(null);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace var declaration', () => { + const code = 'var x; x = 42; console.log(x);'; + const expected = 'var x;\nx = 42;\nconsole.log(42);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Replace with multiple references', () => { + const code = 'let count; count = 5; alert(count); console.log(count);'; + const expected = 'let count;\ncount = 5;\nalert(5);\nconsole.log(5);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not replace variable used in for-in loop', () => { + const code = 'let a; a = \'prop\'; for (a in obj) console.log(a);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not replace variable used in for-of loop', () => { + const code = 'let item; item = 1; for (item of arr) console.log(item);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace variable in conditional expression context', () => { + const code = 'let a; b === c ? (a = 1) : (a = 2); console.log(a);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace variable with multiple assignments', () => { + const code = 'let a; a = 1; a = 2; console.log(a);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not replace variable assigned non-literal value', () => { + const code = 'let a; a = someFunction(); console.log(a);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-6: Do not replace function callee', () => { + const code = 'let func; func = alert; func(\'hello\');'; + const expected = 'let func; func = alert; func(\'hello\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-7: Do not replace variable with initial value', () => { + const code = 'let a = 1; a = 2; console.log(a);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-8: Do not replace when references are modified', () => { + const code = 'let a; a = 1; a++; console.log(a);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: replaceNewFuncCallsWithLiteralContent', async () => { - const targetModule = (await import('../src/modules/safe/replaceNewFuncCallsWithLiteralContent.js')).default; - it('TP-1: Replace Function constructor with IIFE', () => { - const code = `new Function("!function() {console.log('hello world')}()")();`; - const expected = `!(function () {\n console.log('hello world');\n}());`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace Function constructor with single expression', () => { - const code = `new Function("console.log('test')")();`; - const expected = `console.log('test');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace Function constructor with multiple statements', () => { - const code = `new Function("var x = 1; var y = 2; console.log(x + y);")();`; - const expected = `{\n var x = 1;\n var y = 2;\n console.log(x + y);\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace Function constructor with empty string', () => { - const code = `new Function("")();`; - const expected = `'';`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace Function constructor with variable declaration', () => { - const code = `new Function("let x = 'hello'; console.log(x);")();`; - const expected = `{\n let x = 'hello';\n console.log(x);\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not replace Function constructor with arguments', () => { - const code = `new Function("return a + b")(1, 2);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not replace Function constructor with multiple parameters', () => { - const code = `new Function("a", "b", "return a + b")();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace Function constructor with non-literal argument', () => { - const code = `new Function(someVariable)();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace non-Function constructor', () => { - const code = `new Array("1,2,3")();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not replace Function constructor not used as callee', () => { - const code = `var func = new Function("console.log('test')");`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-6: Do not replace Function constructor with invalid syntax', () => { - const code = `new Function("invalid syntax {{{")();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/replaceNewFuncCallsWithLiteralContent.js')).default; + it('TP-1: Replace Function constructor with IIFE', () => { + const code = 'new Function("!function() {console.log(\'hello world\')}()")();'; + const expected = '!(function () {\n console.log(\'hello world\');\n}());'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace Function constructor with single expression', () => { + const code = 'new Function("console.log(\'test\')")();'; + const expected = 'console.log(\'test\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace Function constructor with multiple statements', () => { + const code = 'new Function("var x = 1; var y = 2; console.log(x + y);")();'; + const expected = '{\n var x = 1;\n var y = 2;\n console.log(x + y);\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace Function constructor with empty string', () => { + const code = 'new Function("")();'; + const expected = '\'\';'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace Function constructor with variable declaration', () => { + const code = 'new Function("let x = \'hello\'; console.log(x);")();'; + const expected = '{\n let x = \'hello\';\n console.log(x);\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not replace Function constructor with arguments', () => { + const code = 'new Function("return a + b")(1, 2);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not replace Function constructor with multiple parameters', () => { + const code = 'new Function("a", "b", "return a + b")();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace Function constructor with non-literal argument', () => { + const code = 'new Function(someVariable)();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace non-Function constructor', () => { + const code = 'new Array("1,2,3")();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not replace Function constructor not used as callee', () => { + const code = 'var func = new Function("console.log(\'test\')");'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-6: Do not replace Function constructor with invalid syntax', () => { + const code = 'new Function("invalid syntax {{{")();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: replaceBooleanExpressionsWithIf', async () => { - const targetModule = (await import('../src/modules/safe/replaceBooleanExpressionsWithIf.js')).default; - it('TP-1: Simple logical AND', () => { - const code = `x && y();`; - const expected = `if (x) {\n y();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Simple logical OR', () => { - const code = `x || y();`; - const expected = `if (!x) {\n y();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Chained logical AND', () => { - const code = `x && y && z();`; - const expected = `if (x && y) {\n z();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Chained logical OR', () => { - const code = `x || y || z();`; - const expected = `if (!(x || y)) {\n z();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Function call in condition', () => { - const code = `isValid() && doAction();`; - const expected = `if (isValid()) {\n doAction();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Member expression in condition', () => { - const code = `obj.prop && execute();`; - const expected = `if (obj.prop) {\n execute();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not transform non-logical expressions', () => { - const code = `x + y;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, code); - }); - it('TN-2: Do not transform logical expressions not in expression statements', () => { - const code = `var result = x && y;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, code); - }); - it('TN-3: Do not transform bitwise operators', () => { - const code = `x & y();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, code); - }); + const targetModule = (await import('../src/modules/safe/replaceBooleanExpressionsWithIf.js')).default; + it('TP-1: Simple logical AND', () => { + const code = 'x && y();'; + const expected = 'if (x) {\n y();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Simple logical OR', () => { + const code = 'x || y();'; + const expected = 'if (!x) {\n y();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Chained logical AND', () => { + const code = 'x && y && z();'; + const expected = 'if (x && y) {\n z();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Chained logical OR', () => { + const code = 'x || y || z();'; + const expected = 'if (!(x || y)) {\n z();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Function call in condition', () => { + const code = 'isValid() && doAction();'; + const expected = 'if (isValid()) {\n doAction();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Member expression in condition', () => { + const code = 'obj.prop && execute();'; + const expected = 'if (obj.prop) {\n execute();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not transform non-logical expressions', () => { + const code = 'x + y;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, code); + }); + it('TN-2: Do not transform logical expressions not in expression statements', () => { + const code = 'var result = x && y;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, code); + }); + it('TN-3: Do not transform bitwise operators', () => { + const code = 'x & y();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, code); + }); }); describe('SAFE: replaceSequencesWithExpressions', async () => { - const targetModule = (await import('../src/modules/safe/replaceSequencesWithExpressions.js')).default; - it('TP-1: Replace sequence with 2 expressions in if statement', () => { - const code = `if (a) (b(), c());`; - const expected = `if (a) {\n b();\n c();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace sequence with 3 expressions within existing block', () => { - const code = `if (a) { (b(), c()); d() }`; - const expected = `if (a) {\n b();\n c();\n d();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace sequence in while loop', () => { - const code = `while (x) (y++, z());`; - const expected = `while (x) {\n y++;\n z();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace sequence with 4 expressions', () => { - const code = `if (condition) (a(), b(), c(), d());`; - const expected = `if (condition) {\n a();\n b();\n c();\n d();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace sequence in for loop body', () => { - const code = `for (let i = 0; i < 10; i++) (foo(i), bar(i));`; - const expected = `for (let i = 0; i < 10; i++) {\n foo(i);\n bar(i);\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Replace sequence with mixed expression types', () => { - const code = `if (test) (x = 5, func(), obj.method());`; - const expected = `if (test) {\n x = 5;\n func();\n obj.method();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-7: Replace sequence in else clause', () => { - const code = `if (a) doSomething(); else (first(), second());`; - const expected = `if (a)\n doSomething();\nelse {\n first();\n second();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not replace single expression (not a sequence)', () => { - const code = `if (a) b();`; - const expected = `if (a) b();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not replace sequence with only one expression', () => { - const code = `if (a) b;`; - const expected = `if (a) b;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace sequence in non-ExpressionStatement context', () => { - const code = `const result = (a(), b());`; - const expected = `const result = (a(), b());`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace sequence in return statement', () => { - const code = `function test() { return (x(), y()); }`; - const expected = `function test() { return (x(), y()); }`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not replace sequence in assignment', () => { - const code = `let value = (init(), compute());`; - const expected = `let value = (init(), compute());`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/replaceSequencesWithExpressions.js')).default; + it('TP-1: Replace sequence with 2 expressions in if statement', () => { + const code = 'if (a) (b(), c());'; + const expected = 'if (a) {\n b();\n c();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace sequence with 3 expressions within existing block', () => { + const code = 'if (a) { (b(), c()); d() }'; + const expected = 'if (a) {\n b();\n c();\n d();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace sequence in while loop', () => { + const code = 'while (x) (y++, z());'; + const expected = 'while (x) {\n y++;\n z();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace sequence with 4 expressions', () => { + const code = 'if (condition) (a(), b(), c(), d());'; + const expected = 'if (condition) {\n a();\n b();\n c();\n d();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace sequence in for loop body', () => { + const code = 'for (let i = 0; i < 10; i++) (foo(i), bar(i));'; + const expected = 'for (let i = 0; i < 10; i++) {\n foo(i);\n bar(i);\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Replace sequence with mixed expression types', () => { + const code = 'if (test) (x = 5, func(), obj.method());'; + const expected = 'if (test) {\n x = 5;\n func();\n obj.method();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-7: Replace sequence in else clause', () => { + const code = 'if (a) doSomething(); else (first(), second());'; + const expected = 'if (a)\n doSomething();\nelse {\n first();\n second();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not replace single expression (not a sequence)', () => { + const code = 'if (a) b();'; + const expected = 'if (a) b();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not replace sequence with only one expression', () => { + const code = 'if (a) b;'; + const expected = 'if (a) b;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace sequence in non-ExpressionStatement context', () => { + const code = 'const result = (a(), b());'; + const expected = 'const result = (a(), b());'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace sequence in return statement', () => { + const code = 'function test() { return (x(), y()); }'; + const expected = 'function test() { return (x(), y()); }'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not replace sequence in assignment', () => { + const code = 'let value = (init(), compute());'; + const expected = 'let value = (init(), compute());'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: resolveDeterministicIfStatements', async () => { - const targetModule = (await import('../src/modules/safe/resolveDeterministicIfStatements.js')).default; - it('TP-1: Resolve true and false literals', () => { - const code = `if (true) do_a(); else do_b(); if (false) do_c(); else do_d();`; - const expected = `do_a();\ndo_d();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Resolve truthy number literal', () => { - const code = `if (1) console.log('truthy'); else console.log('falsy');`; - const expected = `console.log('truthy');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Resolve falsy number literal (0)', () => { - const code = `if (0) console.log('truthy'); else console.log('falsy');`; - const expected = `console.log('falsy');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Resolve truthy string literal', () => { - const code = `if ('hello') console.log('truthy'); else console.log('falsy');`; - const expected = `console.log('truthy');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Resolve falsy string literal (empty)', () => { - const code = `if ('') console.log('truthy'); else console.log('falsy');`; - const expected = `console.log('falsy');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Resolve null literal', () => { - const code = `if (null) console.log('truthy'); else console.log('falsy');`; - const expected = `console.log('falsy');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-7: Resolve if statement with no else clause (truthy)', () => { - const code = `if (true) console.log('executed');`; - const expected = `console.log('executed');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-8: Remove if statement with no else clause (falsy)', () => { - const code = `before(); if (false) console.log('never'); after();`; - const expected = `before();\nafter();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-9: Resolve negative number literal', () => { - const code = `if (-1) console.log('truthy'); else console.log('falsy');`; - const expected = `console.log('truthy');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-10: Resolve nested if statements', () => { - const code = `if (true) { if (false) inner(); else other(); }`; - const expected = `{\n other();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not resolve if with variable condition', () => { - const code = `if (someVar) console.log('maybe');`; - const expected = `if (someVar) console.log('maybe');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not resolve if with function call condition', () => { - const code = `if (getValue()) console.log('maybe');`; - const expected = `if (getValue()) console.log('maybe');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not resolve if with expression condition', () => { - const code = `if (x + y) console.log('maybe');`; - const expected = `if (x + y) console.log('maybe');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not resolve if with member expression condition', () => { - const code = `if (obj.prop) console.log('maybe');`; - const expected = `if (obj.prop) console.log('maybe');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/resolveDeterministicIfStatements.js')).default; + it('TP-1: Resolve true and false literals', () => { + const code = 'if (true) do_a(); else do_b(); if (false) do_c(); else do_d();'; + const expected = 'do_a();\ndo_d();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Resolve truthy number literal', () => { + const code = 'if (1) console.log(\'truthy\'); else console.log(\'falsy\');'; + const expected = 'console.log(\'truthy\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Resolve falsy number literal (0)', () => { + const code = 'if (0) console.log(\'truthy\'); else console.log(\'falsy\');'; + const expected = 'console.log(\'falsy\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Resolve truthy string literal', () => { + const code = 'if (\'hello\') console.log(\'truthy\'); else console.log(\'falsy\');'; + const expected = 'console.log(\'truthy\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Resolve falsy string literal (empty)', () => { + const code = 'if (\'\') console.log(\'truthy\'); else console.log(\'falsy\');'; + const expected = 'console.log(\'falsy\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Resolve null literal', () => { + const code = 'if (null) console.log(\'truthy\'); else console.log(\'falsy\');'; + const expected = 'console.log(\'falsy\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-7: Resolve if statement with no else clause (truthy)', () => { + const code = 'if (true) console.log(\'executed\');'; + const expected = 'console.log(\'executed\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-8: Remove if statement with no else clause (falsy)', () => { + const code = 'before(); if (false) console.log(\'never\'); after();'; + const expected = 'before();\nafter();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-9: Resolve negative number literal', () => { + const code = 'if (-1) console.log(\'truthy\'); else console.log(\'falsy\');'; + const expected = 'console.log(\'truthy\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-10: Resolve nested if statements', () => { + const code = 'if (true) { if (false) inner(); else other(); }'; + const expected = '{\n other();\n}'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not resolve if with variable condition', () => { + const code = 'if (someVar) console.log(\'maybe\');'; + const expected = 'if (someVar) console.log(\'maybe\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not resolve if with function call condition', () => { + const code = 'if (getValue()) console.log(\'maybe\');'; + const expected = 'if (getValue()) console.log(\'maybe\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not resolve if with expression condition', () => { + const code = 'if (x + y) console.log(\'maybe\');'; + const expected = 'if (x + y) console.log(\'maybe\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not resolve if with member expression condition', () => { + const code = 'if (obj.prop) console.log(\'maybe\');'; + const expected = 'if (obj.prop) console.log(\'maybe\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: resolveFunctionConstructorCalls', async () => { - const targetModule = (await import('../src/modules/safe/resolveFunctionConstructorCalls.js')).default; - it('TP-1: Replace Function.constructor with no parameters', () => { - const code = `const func = Function.constructor('', "console.log('hello world!');");`; - const expected = `const func = function () {\n console.log('hello world!');\n};`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Part of a member expression', () => { - const code = `a = Function.constructor('return /" + this + "/')().constructor('^([^ ]+( +[^ ]+)+)+[^ ]}');`; - const expected = `a = function () {\n return /" + this + "/;\n}().constructor('^([^ ]+( +[^ ]+)+)+[^ ]}');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace Function.constructor with single parameter', () => { - const code = `const func = Function.constructor('x', 'return x * 2;');`; - const expected = `const func = function (x) {\n return x * 2;\n};`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace Function.constructor with multiple parameters', () => { - const code = `const func = Function.constructor('a', 'b', 'return a + b;');`; - const expected = `const func = function (a, b) {\n return a + b;\n};`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace Function.constructor with complex body', () => { - const code = `const func = Function.constructor('if (true) { return 42; } else { return 0; }');`; - const expected = `const func = function () {\n if (true) {\n return 42;\n } else {\n return 0;\n }\n};`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Replace Function.constructor with empty body', () => { - const code = `const func = Function.constructor('');`; - const expected = `const func = function () {\n};`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-7: Replace Function.constructor in variable assignment', () => { - const code = `var myFunc = Function.constructor('n', 'return n > 0;');`; - const expected = `var myFunc = function (n) {\n return n > 0;\n};`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-8: Replace Function.constructor in call expression', () => { - const code = `console.log(Function.constructor('return "test"')());`; - const expected = `console.log((function () {\n return 'test';\n}()));`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not replace Function.constructor with non-literal arguments', () => { - const code = `const func = Function.constructor(param, 'return value;');`; - const expected = `const func = Function.constructor(param, 'return value;');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not replace Function.constructor with no arguments', () => { - const code = `const func = Function.constructor();`; - const expected = `const func = Function.constructor();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace non-constructor calls', () => { - const code = `const func = Function.prototype('test');`; - const expected = `const func = Function.prototype('test');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace Function.constructor with invalid syntax body', () => { - const code = `const func = Function.constructor('invalid syntax {{{');`; - const expected = `const func = Function.constructor('invalid syntax {{{');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-9: Replace any constructor call with literal arguments', () => { - const code = `const result = obj.constructor('test');`; - const expected = `const result = function () {\n test;\n};`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/resolveFunctionConstructorCalls.js')).default; + it('TP-1: Replace Function.constructor with no parameters', () => { + const code = 'const func = Function.constructor(\'\', "console.log(\'hello world!\');");'; + const expected = 'const func = function () {\n console.log(\'hello world!\');\n};'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Part of a member expression', () => { + const code = 'a = Function.constructor(\'return /" + this + "/\')().constructor(\'^([^ ]+( +[^ ]+)+)+[^ ]}\');'; + const expected = 'a = function () {\n return /" + this + "/;\n}().constructor(\'^([^ ]+( +[^ ]+)+)+[^ ]}\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace Function.constructor with single parameter', () => { + const code = 'const func = Function.constructor(\'x\', \'return x * 2;\');'; + const expected = 'const func = function (x) {\n return x * 2;\n};'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace Function.constructor with multiple parameters', () => { + const code = 'const func = Function.constructor(\'a\', \'b\', \'return a + b;\');'; + const expected = 'const func = function (a, b) {\n return a + b;\n};'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace Function.constructor with complex body', () => { + const code = 'const func = Function.constructor(\'if (true) { return 42; } else { return 0; }\');'; + const expected = 'const func = function () {\n if (true) {\n return 42;\n } else {\n return 0;\n }\n};'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Replace Function.constructor with empty body', () => { + const code = 'const func = Function.constructor(\'\');'; + const expected = 'const func = function () {\n};'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-7: Replace Function.constructor in variable assignment', () => { + const code = 'var myFunc = Function.constructor(\'n\', \'return n > 0;\');'; + const expected = 'var myFunc = function (n) {\n return n > 0;\n};'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-8: Replace Function.constructor in call expression', () => { + const code = 'console.log(Function.constructor(\'return "test"\')());'; + const expected = 'console.log((function () {\n return \'test\';\n}()));'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not replace Function.constructor with non-literal arguments', () => { + const code = 'const func = Function.constructor(param, \'return value;\');'; + const expected = 'const func = Function.constructor(param, \'return value;\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not replace Function.constructor with no arguments', () => { + const code = 'const func = Function.constructor();'; + const expected = 'const func = Function.constructor();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace non-constructor calls', () => { + const code = 'const func = Function.prototype(\'test\');'; + const expected = 'const func = Function.prototype(\'test\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace Function.constructor with invalid syntax body', () => { + const code = 'const func = Function.constructor(\'invalid syntax {{{\');'; + const expected = 'const func = Function.constructor(\'invalid syntax {{{\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-9: Replace any constructor call with literal arguments', () => { + const code = 'const result = obj.constructor(\'test\');'; + const expected = 'const result = function () {\n test;\n};'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: resolveMemberExpressionReferencesToArrayIndex', async () => { - const targetModule = (await import('../src/modules/safe/resolveMemberExpressionReferencesToArrayIndex.js')).default; - it('TP-1', () => { - const code = `const a = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; b = a[0]; c = a[20];`; - const expected = `const a = [\n 1,\n 1,\n 1,\n 1,\n 1,\n 1,\n 1,\n 1,\n 1,\n 1, + const targetModule = (await import('../src/modules/safe/resolveMemberExpressionReferencesToArrayIndex.js')).default; + it('TP-1', () => { + const code = 'const a = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; b = a[0]; c = a[20];'; + const expected = `const a = [\n 1,\n 1,\n 1,\n 1,\n 1,\n 1,\n 1,\n 1,\n 1,\n 1, 2,\n 2,\n 2,\n 2,\n 2,\n 2,\n 2,\n 2,\n 2,\n 2,\n 3\n];\nb = 1;\nc = 3;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace multiple array accesses on same array', () => { - const code = `const arr = [5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,7]; const x = arr[0], y = arr[10], z = arr[20];`; - const expected = `const arr = [\n 5,\n 5,\n 5,\n 5,\n 5,\n 5,\n 5,\n 5,\n 5,\n 5,\n 6,\n 6,\n 6,\n 6,\n 6,\n 6,\n 6,\n 6,\n 6,\n 6,\n 7\n];\nconst x = 5, y = 6, z = 7;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace array access with string literal elements', () => { - const code = `const words = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u']; const first = words[0]; const last = words[20];`; - const expected = `const words = [\n 'a',\n 'b',\n 'c',\n 'd',\n 'e',\n 'f',\n 'g',\n 'h',\n 'i',\n 'j',\n 'k',\n 'l',\n 'm',\n 'n',\n 'o',\n 'p',\n 'q',\n 'r',\n 's',\n 't',\n 'u'\n];\nconst first = 'a';\nconst last = 'u';`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace array access in function call arguments', () => { - const code = `const nums = [9,9,9,9,9,9,9,9,9,9,8,8,8,8,8,8,8,8,8,8,7]; console.log(nums[0], nums[10], nums[20]);`; - const expected = `const nums = [\n 9,\n 9,\n 9,\n 9,\n 9,\n 9,\n 9,\n 9,\n 9,\n 9,\n 8,\n 8,\n 8,\n 8,\n 8,\n 8,\n 8,\n 8,\n 8,\n 8,\n 7\n];\nconsole.log(9, 8, 7);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it(`TN-1: Don't resolve references to array methods`, () => { - const code = `const a = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; b = a['indexOf']; c = a['length'];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not resolve arrays smaller than minimum length', () => { - const code = `const small = [1,2,3,4,5]; const x = small[0]; const y = small[2];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not resolve assignment to array elements', () => { - const code = `const items = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; items[0] = 99; items[10] = 88;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not resolve computed property access with variables', () => { - const code = `const data = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; const i = 5; const val = data[i];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not resolve out-of-bounds array access', () => { - const code = `const bounds = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; const invalid = bounds[100];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-6: Do not resolve negative array indices', () => { - const code = `const negTest = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; const neg = negTest[-1];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-7: Do not resolve floating point indices', () => { - const code = `const floatTest = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; const flt = floatTest[1.5];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace multiple array accesses on same array', () => { + const code = 'const arr = [5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,7]; const x = arr[0], y = arr[10], z = arr[20];'; + const expected = 'const arr = [\n 5,\n 5,\n 5,\n 5,\n 5,\n 5,\n 5,\n 5,\n 5,\n 5,\n 6,\n 6,\n 6,\n 6,\n 6,\n 6,\n 6,\n 6,\n 6,\n 6,\n 7\n];\nconst x = 5, y = 6, z = 7;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace array access with string literal elements', () => { + const code = 'const words = [\'a\',\'b\',\'c\',\'d\',\'e\',\'f\',\'g\',\'h\',\'i\',\'j\',\'k\',\'l\',\'m\',\'n\',\'o\',\'p\',\'q\',\'r\',\'s\',\'t\',\'u\']; const first = words[0]; const last = words[20];'; + const expected = 'const words = [\n \'a\',\n \'b\',\n \'c\',\n \'d\',\n \'e\',\n \'f\',\n \'g\',\n \'h\',\n \'i\',\n \'j\',\n \'k\',\n \'l\',\n \'m\',\n \'n\',\n \'o\',\n \'p\',\n \'q\',\n \'r\',\n \'s\',\n \'t\',\n \'u\'\n];\nconst first = \'a\';\nconst last = \'u\';'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace array access in function call arguments', () => { + const code = 'const nums = [9,9,9,9,9,9,9,9,9,9,8,8,8,8,8,8,8,8,8,8,7]; console.log(nums[0], nums[10], nums[20]);'; + const expected = 'const nums = [\n 9,\n 9,\n 9,\n 9,\n 9,\n 9,\n 9,\n 9,\n 9,\n 9,\n 8,\n 8,\n 8,\n 8,\n 8,\n 8,\n 8,\n 8,\n 8,\n 8,\n 7\n];\nconsole.log(9, 8, 7);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Don\'t resolve references to array methods', () => { + const code = 'const a = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; b = a[\'indexOf\']; c = a[\'length\'];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not resolve arrays smaller than minimum length', () => { + const code = 'const small = [1,2,3,4,5]; const x = small[0]; const y = small[2];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not resolve assignment to array elements', () => { + const code = 'const items = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; items[0] = 99; items[10] = 88;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not resolve computed property access with variables', () => { + const code = 'const data = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; const i = 5; const val = data[i];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not resolve out-of-bounds array access', () => { + const code = 'const bounds = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; const invalid = bounds[100];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-6: Do not resolve negative array indices', () => { + const code = 'const negTest = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; const neg = negTest[-1];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-7: Do not resolve floating point indices', () => { + const code = 'const floatTest = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3]; const flt = floatTest[1.5];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: resolveMemberExpressionsWithDirectAssignment', async () => { - const targetModule = (await import('../src/modules/safe/resolveMemberExpressionsWithDirectAssignment.js')).default; - it('TP-1: Replace direct property assignments with literal values', () => { - const code = `function a() {} a.b = 3; a.c = '5'; console.log(a.b + a.c);`; - const expected = `function a() {\n}\na.b = 3;\na.c = '5';\nconsole.log(3 + '5');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace object property assignments', () => { - const code = `const obj = {}; obj.name = 'test'; obj.value = 42; const result = obj.name + obj.value;`; - const expected = `const obj = {};\nobj.name = 'test';\nobj.value = 42;\nconst result = 'test' + 42;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace computed property assignments', () => { - const code = `const data = {}; data['key'] = 'value'; console.log(data['key']);`; - const expected = `const data = {};\ndata['key'] = 'value';\nconsole.log('value');`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace boolean and null assignments', () => { - const code = `const state = {}; state.flag = true; state.data = null; if (state.flag) console.log(state.data);`; - const expected = `const state = {};\nstate.flag = true;\nstate.data = null;\nif (true)\n console.log(null);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace multiple references to same property', () => { - const code = `let config = {}; config.timeout = 5000; const a = config.timeout; const b = config.timeout + 1000;`; - const expected = `let config = {};\nconfig.timeout = 5000;\nconst a = 5000;\nconst b = 5000 + 1000;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it(`TN-1: Don't resolve with multiple assignments`, () => { - const code = `const a = {}; a.b = ''; a.b = 3;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it(`TN-2: Don't resolve with update expressions`, () => { - const code = `const a = {}; a.b = 0; ++a.b + 2;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not resolve when assigned non-literal value', () => { - const code = `const obj = {}; obj.prop = getValue(); console.log(obj.prop);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not resolve when object has no declaration', () => { - const code = `unknown.prop = 'value'; console.log(unknown.prop);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not resolve when property is reassigned', () => { - const code = `const obj = {}; obj.data = 'first'; obj.data = 'second'; console.log(obj.data);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-6: Do not resolve when used in assignment expression', () => { - const code = `const obj = {}; obj.counter = 0; obj.counter += 5; console.log(obj.counter);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-7: Do not resolve when property is computed with variable', () => { - const code = `const obj = {}; const key = 'prop'; obj[key] = 'value'; console.log(obj[key]);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-8: Do not resolve when no references exist', () => { - const code = `const obj = {}; obj.unused = 'value';`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/resolveMemberExpressionsWithDirectAssignment.js')).default; + it('TP-1: Replace direct property assignments with literal values', () => { + const code = 'function a() {} a.b = 3; a.c = \'5\'; console.log(a.b + a.c);'; + const expected = 'function a() {\n}\na.b = 3;\na.c = \'5\';\nconsole.log(3 + \'5\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace object property assignments', () => { + const code = 'const obj = {}; obj.name = \'test\'; obj.value = 42; const result = obj.name + obj.value;'; + const expected = 'const obj = {};\nobj.name = \'test\';\nobj.value = 42;\nconst result = \'test\' + 42;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace computed property assignments', () => { + const code = 'const data = {}; data[\'key\'] = \'value\'; console.log(data[\'key\']);'; + const expected = 'const data = {};\ndata[\'key\'] = \'value\';\nconsole.log(\'value\');'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace boolean and null assignments', () => { + const code = 'const state = {}; state.flag = true; state.data = null; if (state.flag) console.log(state.data);'; + const expected = 'const state = {};\nstate.flag = true;\nstate.data = null;\nif (true)\n console.log(null);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace multiple references to same property', () => { + const code = 'let config = {}; config.timeout = 5000; const a = config.timeout; const b = config.timeout + 1000;'; + const expected = 'let config = {};\nconfig.timeout = 5000;\nconst a = 5000;\nconst b = 5000 + 1000;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Don\'t resolve with multiple assignments', () => { + const code = 'const a = {}; a.b = \'\'; a.b = 3;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Don\'t resolve with update expressions', () => { + const code = 'const a = {}; a.b = 0; ++a.b + 2;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not resolve when assigned non-literal value', () => { + const code = 'const obj = {}; obj.prop = getValue(); console.log(obj.prop);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not resolve when object has no declaration', () => { + const code = 'unknown.prop = \'value\'; console.log(unknown.prop);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not resolve when property is reassigned', () => { + const code = 'const obj = {}; obj.data = \'first\'; obj.data = \'second\'; console.log(obj.data);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-6: Do not resolve when used in assignment expression', () => { + const code = 'const obj = {}; obj.counter = 0; obj.counter += 5; console.log(obj.counter);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-7: Do not resolve when property is computed with variable', () => { + const code = 'const obj = {}; const key = \'prop\'; obj[key] = \'value\'; console.log(obj[key]);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-8: Do not resolve when no references exist', () => { + const code = 'const obj = {}; obj.unused = \'value\';'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: resolveProxyCalls', async () => { - const targetModule = (await import('../src/modules/safe/resolveProxyCalls.js')).default; - it('TP-1: Replace chained proxy calls with direct function calls', () => { - const code = `function call1(a, b) {return a + b;} function call2(c, d) {return call1(c, d);} function call3(e, f) {return call2(e, f);}`; - const expected = `function call1(a, b) {\n return a + b;\n}\nfunction call2(c, d) {\n return call1(c, d);\n}\nfunction call3(e, f) {\n return call1(e, f);\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace proxy with no parameters', () => { - const code = `function target() { return 42; } function proxy() { return target(); } const result = proxy();`; - const expected = `function target() {\n return 42;\n}\nfunction proxy() {\n return target();\n}\nconst result = target();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace proxy with multiple parameters', () => { - const code = `function add(a, b, c) { return a + b + c; } function addProxy(x, y, z) { return add(x, y, z); } const sum = addProxy(1, 2, 3);`; - const expected = `function add(a, b, c) {\n return a + b + c;\n}\nfunction addProxy(x, y, z) {\n return add(x, y, z);\n}\nconst sum = add(1, 2, 3);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace proxy that calls another proxy (single-step resolution)', () => { - const code = `function base() { return 'test'; } function proxy1() { return base(); } function proxy2() { return proxy1(); } console.log(proxy2());`; - const expected = `function base() {\n return 'test';\n}\nfunction proxy1() {\n return base();\n}\nfunction proxy2() {\n return base();\n}\nconsole.log(proxy1());`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not replace function with multiple statements', () => { - const code = `function target() { return 42; } function notProxy() { console.log('side effect'); return target(); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not replace function with no return statement', () => { - const code = `function target() { return 42; } function notProxy() { target(); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace function that returns non-call expression', () => { - const code = `function notProxy() { return 42; }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace function that calls member expression', () => { - const code = `const obj = { method: () => 42 }; function notProxy() { return obj.method(); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not replace function with parameter count mismatch', () => { - const code = `function target(a, b) { return a + b; } function notProxy(x) { return target(x, 0); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-6: Do not replace function with reordered parameters', () => { - const code = `function target(a, b) { return a - b; } function notProxy(x, y) { return target(y, x); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-7: Do not replace function with modified parameters', () => { - const code = `function target(a) { return a * 2; } function notProxy(x) { return target(x + 1); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-8: Do not replace function with no references', () => { - const code = `function target() { return 42; } function unreferencedProxy() { return target(); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/resolveProxyCalls.js')).default; + it('TP-1: Replace chained proxy calls with direct function calls', () => { + const code = 'function call1(a, b) {return a + b;} function call2(c, d) {return call1(c, d);} function call3(e, f) {return call2(e, f);}'; + const expected = 'function call1(a, b) {\n return a + b;\n}\nfunction call2(c, d) {\n return call1(c, d);\n}\nfunction call3(e, f) {\n return call1(e, f);\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace proxy with no parameters', () => { + const code = 'function target() { return 42; } function proxy() { return target(); } const result = proxy();'; + const expected = 'function target() {\n return 42;\n}\nfunction proxy() {\n return target();\n}\nconst result = target();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace proxy with multiple parameters', () => { + const code = 'function add(a, b, c) { return a + b + c; } function addProxy(x, y, z) { return add(x, y, z); } const sum = addProxy(1, 2, 3);'; + const expected = 'function add(a, b, c) {\n return a + b + c;\n}\nfunction addProxy(x, y, z) {\n return add(x, y, z);\n}\nconst sum = add(1, 2, 3);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace proxy that calls another proxy (single-step resolution)', () => { + const code = 'function base() { return \'test\'; } function proxy1() { return base(); } function proxy2() { return proxy1(); } console.log(proxy2());'; + const expected = 'function base() {\n return \'test\';\n}\nfunction proxy1() {\n return base();\n}\nfunction proxy2() {\n return base();\n}\nconsole.log(proxy1());'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not replace function with multiple statements', () => { + const code = 'function target() { return 42; } function notProxy() { console.log(\'side effect\'); return target(); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not replace function with no return statement', () => { + const code = 'function target() { return 42; } function notProxy() { target(); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace function that returns non-call expression', () => { + const code = 'function notProxy() { return 42; }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace function that calls member expression', () => { + const code = 'const obj = { method: () => 42 }; function notProxy() { return obj.method(); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not replace function with parameter count mismatch', () => { + const code = 'function target(a, b) { return a + b; } function notProxy(x) { return target(x, 0); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-6: Do not replace function with reordered parameters', () => { + const code = 'function target(a, b) { return a - b; } function notProxy(x, y) { return target(y, x); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-7: Do not replace function with modified parameters', () => { + const code = 'function target(a) { return a * 2; } function notProxy(x) { return target(x + 1); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-8: Do not replace function with no references', () => { + const code = 'function target() { return 42; } function unreferencedProxy() { return target(); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: resolveProxyReferences', async () => { - const targetModule = (await import('../src/modules/safe/resolveProxyReferences.js')).default; - it('TP-1: Replace proxy reference with direct reference', () => { - const code = `const a = ['']; const b = a; const c = b[0];`; - const expected = `const a = [''];\nconst b = a;\nconst c = a[0];`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Replace multiple proxy references to same target', () => { - const code = `const arr = [1, 2, 3]; const proxy = arr; const x = proxy[0]; const y = proxy[1];`; - const expected = `const arr = [\n 1,\n 2,\n 3\n];\nconst proxy = arr;\nconst x = arr[0];\nconst y = arr[1];`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace member expression proxy references', () => { - const code = `const obj = {prop: 42}; const alias = obj.prop; const result = alias;`; - const expected = `const obj = { prop: 42 };\nconst alias = obj.prop;\nconst result = obj.prop;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Replace chained proxy references', () => { - const code = `const original = 'test'; const proxy1 = original; const proxy2 = proxy1; const final = proxy2;`; - const expected = `const original = 'test';\nconst proxy1 = original;\nconst proxy2 = original;\nconst final = proxy1;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace variable with let declaration', () => { - const code = `let source = 'value'; let reference = source; console.log(reference);`; - const expected = `let source = 'value';\nlet reference = source;\nconsole.log(source);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Replace variable with var declaration', () => { - const code = `var base = [1, 2]; var link = base; var item = link[0];`; - const expected = `var base = [\n 1,\n 2\n];\nvar link = base;\nvar item = base[0];`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not replace proxy in for-in statement', () => { - const code = `const obj = {a: 1}; for (const key in obj) { const proxy = key; }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not replace proxy in for-of statement', () => { - const code = `const arr = [1, 2]; for (const item of arr) { const proxy = item; }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace proxy in for statement', () => { - const code = `const arr = [1, 2]; for (let i = 0; i < arr.length; i++) { const proxy = arr[i]; }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace circular references', () => { - const code = `let a; let b; a = b; b = a;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not replace self-referencing variables', () => { - const code = `const a = someFunction(a);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-6: Do not replace when proxy is modified', () => { - const code = `const original = [1]; const proxy = original; proxy.push(2);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-7: Do not replace when target is modified', () => { - const code = `let original = [1]; const proxy = original; original = [2];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-8: Do not replace when proxy has no references', () => { - const code = `const original = 'test'; const unused = original;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-9: Do not replace non-identifier/non-member expression variables', () => { - const code = `const a = func(); const b = a;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-7: Replace when target comes from function call (still safe)', () => { - const code = `const a = getValue(); const b = a; console.log(b);`; - const expected = `const a = getValue();\nconst b = a;\nconsole.log(a);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/resolveProxyReferences.js')).default; + it('TP-1: Replace proxy reference with direct reference', () => { + const code = 'const a = [\'\']; const b = a; const c = b[0];'; + const expected = 'const a = [\'\'];\nconst b = a;\nconst c = a[0];'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Replace multiple proxy references to same target', () => { + const code = 'const arr = [1, 2, 3]; const proxy = arr; const x = proxy[0]; const y = proxy[1];'; + const expected = 'const arr = [\n 1,\n 2,\n 3\n];\nconst proxy = arr;\nconst x = arr[0];\nconst y = arr[1];'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace member expression proxy references', () => { + const code = 'const obj = {prop: 42}; const alias = obj.prop; const result = alias;'; + const expected = 'const obj = { prop: 42 };\nconst alias = obj.prop;\nconst result = obj.prop;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Replace chained proxy references', () => { + const code = 'const original = \'test\'; const proxy1 = original; const proxy2 = proxy1; const final = proxy2;'; + const expected = 'const original = \'test\';\nconst proxy1 = original;\nconst proxy2 = original;\nconst final = proxy1;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace variable with let declaration', () => { + const code = 'let source = \'value\'; let reference = source; console.log(reference);'; + const expected = 'let source = \'value\';\nlet reference = source;\nconsole.log(source);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Replace variable with var declaration', () => { + const code = 'var base = [1, 2]; var link = base; var item = link[0];'; + const expected = 'var base = [\n 1,\n 2\n];\nvar link = base;\nvar item = base[0];'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not replace proxy in for-in statement', () => { + const code = 'const obj = {a: 1}; for (const key in obj) { const proxy = key; }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not replace proxy in for-of statement', () => { + const code = 'const arr = [1, 2]; for (const item of arr) { const proxy = item; }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace proxy in for statement', () => { + const code = 'const arr = [1, 2]; for (let i = 0; i < arr.length; i++) { const proxy = arr[i]; }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace circular references', () => { + const code = 'let a; let b; a = b; b = a;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not replace self-referencing variables', () => { + const code = 'const a = someFunction(a);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-6: Do not replace when proxy is modified', () => { + const code = 'const original = [1]; const proxy = original; proxy.push(2);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-7: Do not replace when target is modified', () => { + const code = 'let original = [1]; const proxy = original; original = [2];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-8: Do not replace when proxy has no references', () => { + const code = 'const original = \'test\'; const unused = original;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-9: Do not replace non-identifier/non-member expression variables', () => { + const code = 'const a = func(); const b = a;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-7: Replace when target comes from function call (still safe)', () => { + const code = 'const a = getValue(); const b = a; console.log(b);'; + const expected = 'const a = getValue();\nconst b = a;\nconsole.log(a);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: resolveProxyVariables', async () => { - const targetModule = (await import('../src/modules/safe/resolveProxyVariables.js')).default; - it('TP-1: Replace proxy variable references with target identifier', () => { - const code = `const a2b = atob; console.log(a2b('NDI='));`; - const expected = `console.log(atob('NDI='));`; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TP-2: Remove unused proxy variable declaration', () => { - const code = `const a2b = atob, a = 3; console.log(a2b('NDI='));`; - const expected = `const a = 3;\nconsole.log(atob('NDI='));`; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TP-3: Replace multiple references to same proxy', () => { - const code = `const alias = original; console.log(alias); console.log(alias);`; - const expected = `console.log(original);\nconsole.log(original);`; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TP-4: Remove proxy variable with no references', () => { - const code = `const unused = target; console.log('other');`; - const expected = `console.log('other');`; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TP-5: Replace with let declaration', () => { - const code = `let proxy = original; console.log(proxy);`; - const expected = `console.log(original);`; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TP-6: Replace with var declaration', () => { - const code = `var proxy = original; console.log(proxy);`; - const expected = `console.log(original);`; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not replace when proxy is assigned non-identifier', () => { - const code = `const proxy = getValue(); console.log(proxy);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not replace when proxy is modified', () => { - const code = `const proxy = original; proxy = 'modified'; console.log(proxy);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not replace when proxy is updated', () => { - const code = `const proxy = original; proxy++; console.log(proxy);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not replace when reference is used in assignment', () => { - const code = `const proxy = original; const x = proxy = 'new';`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not replace non-identifier initialization', () => { - const code = `const proxy = obj.prop; console.log(proxy);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/resolveProxyVariables.js')).default; + it('TP-1: Replace proxy variable references with target identifier', () => { + const code = 'const a2b = atob; console.log(a2b(\'NDI=\'));'; + const expected = 'console.log(atob(\'NDI=\'));'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TP-2: Remove unused proxy variable declaration', () => { + const code = 'const a2b = atob, a = 3; console.log(a2b(\'NDI=\'));'; + const expected = 'const a = 3;\nconsole.log(atob(\'NDI=\'));'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TP-3: Replace multiple references to same proxy', () => { + const code = 'const alias = original; console.log(alias); console.log(alias);'; + const expected = 'console.log(original);\nconsole.log(original);'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TP-4: Remove proxy variable with no references', () => { + const code = 'const unused = target; console.log(\'other\');'; + const expected = 'console.log(\'other\');'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TP-5: Replace with let declaration', () => { + const code = 'let proxy = original; console.log(proxy);'; + const expected = 'console.log(original);'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TP-6: Replace with var declaration', () => { + const code = 'var proxy = original; console.log(proxy);'; + const expected = 'console.log(original);'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not replace when proxy is assigned non-identifier', () => { + const code = 'const proxy = getValue(); console.log(proxy);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not replace when proxy is modified', () => { + const code = 'const proxy = original; proxy = \'modified\'; console.log(proxy);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not replace when proxy is updated', () => { + const code = 'const proxy = original; proxy++; console.log(proxy);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not replace when reference is used in assignment', () => { + const code = 'const proxy = original; const x = proxy = \'new\';'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not replace non-identifier initialization', () => { + const code = 'const proxy = obj.prop; console.log(proxy);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: resolveRedundantLogicalExpressions', async () => { - const targetModule = (await import('../src/modules/safe/resolveRedundantLogicalExpressions.js')).default; - it('TP-1: Simplify basic true and false literals with && and ||', () => { - const code = `if (false && true) {} if (false || true) {} if (true && false) {} if (true || false) {}`; - const expected = `if (false) {\n}\nif (true) {\n}\nif (false) {\n}\nif (true) {\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Simplify AND expressions with truthy left operand', () => { - const code = `if (true && someVar) {} if (1 && someFunc()) {} if ("str" && obj.prop) {}`; - const expected = `if (someVar) {\n}\nif (someFunc()) {\n}\nif (obj.prop) {\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Simplify AND expressions with falsy left operand', () => { - const code = `if (false && someVar) {} if (0 && someFunc()) {} if ("" && obj.prop) {} if (null && x) {}`; - const expected = `if (false) {\n}\nif (0) {\n}\nif ('') {\n}\nif (null) {\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Simplify AND expressions with truthy right operand', () => { - const code = `if (someVar && true) {} if (someFunc() && 1) {} if (obj.prop && "str") {}`; - const expected = `if (someVar) {\n}\nif (someFunc()) {\n}\nif (obj.prop) {\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Simplify AND expressions with falsy right operand', () => { - const code = `if (someVar && false) {} if (someFunc() && 0) {} if (obj.prop && "") {} if (x && null) {}`; - const expected = `if (false) {\n}\nif (0) {\n}\nif ('') {\n}\nif (null) {\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Simplify OR expressions with truthy left operand', () => { - const code = `if (true || someVar) {} if (1 || someFunc()) {} if ("str" || obj.prop) {}`; - const expected = `if (true) {\n}\nif (1) {\n}\nif ('str') {\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-7: Simplify OR expressions with falsy left operand', () => { - const code = `if (false || someVar) {} if (0 || someFunc()) {} if ("" || obj.prop) {} if (null || x) {}`; - const expected = `if (someVar) {\n}\nif (someFunc()) {\n}\nif (obj.prop) {\n}\nif (x) {\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-8: Simplify OR expressions with truthy right operand', () => { - const code = `if (someVar || true) {} if (someFunc() || 1) {} if (obj.prop || "str") {}`; - const expected = `if (true) {\n}\nif (1) {\n}\nif ('str') {\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-9: Simplify OR expressions with falsy right operand', () => { - const code = `if (someVar || false) {} if (someFunc() || 0) {} if (obj.prop || "") {} if (x || null) {}`; - const expected = `if (someVar) {\n}\nif (someFunc()) {\n}\nif (obj.prop) {\n}\nif (x) {\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-10: Handle complex expressions with nested logical operators', () => { - const code = `if (true && (someVar && false)) {} if (false || (x || true)) {}`; - const expected = `if (someVar && false) {\n}\nif (x || true) {\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not simplify when both operands are non-literals', () => { - const code = `if (someVar && otherVar) {} if (func1() || func2()) {} if (obj.a && obj.b) {}`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not simplify non-logical expressions', () => { - const code = `if (a + b) {} if (a === b) {} if (a > b) {} if (!a) {}`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not simplify logical expressions outside if statements', () => { - const code = `if (someVar) { const x = true && someVar; const y = false || someFunc(); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not simplify unsupported logical operators (if any)', () => { - const code = `if (a & b) {} if (a | b) {} if (a ^ b) {}`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/resolveRedundantLogicalExpressions.js')).default; + it('TP-1: Simplify basic true and false literals with && and ||', () => { + const code = 'if (false && true) {} if (false || true) {} if (true && false) {} if (true || false) {}'; + const expected = 'if (false) {\n}\nif (true) {\n}\nif (false) {\n}\nif (true) {\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Simplify AND expressions with truthy left operand', () => { + const code = 'if (true && someVar) {} if (1 && someFunc()) {} if ("str" && obj.prop) {}'; + const expected = 'if (someVar) {\n}\nif (someFunc()) {\n}\nif (obj.prop) {\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Simplify AND expressions with falsy left operand', () => { + const code = 'if (false && someVar) {} if (0 && someFunc()) {} if ("" && obj.prop) {} if (null && x) {}'; + const expected = 'if (false) {\n}\nif (0) {\n}\nif (\'\') {\n}\nif (null) {\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Simplify AND expressions with truthy right operand', () => { + const code = 'if (someVar && true) {} if (someFunc() && 1) {} if (obj.prop && "str") {}'; + const expected = 'if (someVar) {\n}\nif (someFunc()) {\n}\nif (obj.prop) {\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Simplify AND expressions with falsy right operand', () => { + const code = 'if (someVar && false) {} if (someFunc() && 0) {} if (obj.prop && "") {} if (x && null) {}'; + const expected = 'if (false) {\n}\nif (0) {\n}\nif (\'\') {\n}\nif (null) {\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Simplify OR expressions with truthy left operand', () => { + const code = 'if (true || someVar) {} if (1 || someFunc()) {} if ("str" || obj.prop) {}'; + const expected = 'if (true) {\n}\nif (1) {\n}\nif (\'str\') {\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-7: Simplify OR expressions with falsy left operand', () => { + const code = 'if (false || someVar) {} if (0 || someFunc()) {} if ("" || obj.prop) {} if (null || x) {}'; + const expected = 'if (someVar) {\n}\nif (someFunc()) {\n}\nif (obj.prop) {\n}\nif (x) {\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-8: Simplify OR expressions with truthy right operand', () => { + const code = 'if (someVar || true) {} if (someFunc() || 1) {} if (obj.prop || "str") {}'; + const expected = 'if (true) {\n}\nif (1) {\n}\nif (\'str\') {\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-9: Simplify OR expressions with falsy right operand', () => { + const code = 'if (someVar || false) {} if (someFunc() || 0) {} if (obj.prop || "") {} if (x || null) {}'; + const expected = 'if (someVar) {\n}\nif (someFunc()) {\n}\nif (obj.prop) {\n}\nif (x) {\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-10: Handle complex expressions with nested logical operators', () => { + const code = 'if (true && (someVar && false)) {} if (false || (x || true)) {}'; + const expected = 'if (someVar && false) {\n}\nif (x || true) {\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not simplify when both operands are non-literals', () => { + const code = 'if (someVar && otherVar) {} if (func1() || func2()) {} if (obj.a && obj.b) {}'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not simplify non-logical expressions', () => { + const code = 'if (a + b) {} if (a === b) {} if (a > b) {} if (!a) {}'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not simplify logical expressions outside if statements', () => { + const code = 'if (someVar) { const x = true && someVar; const y = false || someFunc(); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not simplify unsupported logical operators (if any)', () => { + const code = 'if (a & b) {} if (a | b) {} if (a ^ b) {}'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: unwrapFunctionShells', async () => { - const targetModule = (await import('../src/modules/safe/unwrapFunctionShells.js')).default; - it('TP-1: Unwrap and rename', () => { - const code = `function a(x) {return function b() {return x + 3}.apply(this, arguments);}`; - const expected = `function b(x) {\n return x + 3;\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Unwrap anonymous without renaming', () => { - const code = `function a(x) {return function() {return x + 3}.apply(this, arguments);}`; - const expected = `function a(x) {\n return x + 3;\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Unwrap function expression assigned to variable', () => { - const code = `const outer = function(param) { return function inner() { return param * 2; }.apply(this, arguments); };`; - const expected = `const outer = function inner(param) {\n return param * 2;\n};`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Inner function already has parameters', () => { - const code = `function wrapper() { return function inner(existing) { return existing + 1; }.apply(this, arguments); }`; - const expected = `function inner(existing) {\n return existing + 1;\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Outer function has multiple parameters', () => { - const code = `function multi(a, b, c) { return function() { return a + b + c; }.apply(this, arguments); }`; - const expected = `function multi(a, b, c) {\n return a + b + c;\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Complex inner function body', () => { - const code = `function complex(x) { return function process() { const temp = x * 2; return temp + 1; }.apply(this, arguments); }`; - const expected = `function process(x) {\n const temp = x * 2;\n return temp + 1;\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not unwrap function with multiple statements', () => { - const code = `function multi() { console.log('test'); return function() { return 42; }.apply(this, arguments); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not unwrap function with no return statement', () => { - const code = `function noReturn() { console.log('no return'); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not unwrap function returning .call instead of .apply', () => { - const code = `function useCall(x) { return function() { return x + 1; }.call(this, x); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not unwrap .apply with wrong argument count', () => { - const code = `function wrongArgs(x) { return function() { return x; }.apply(this); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not unwrap when callee object is not FunctionExpression', () => { - const code = `function notFunc(x) { return someFunc.apply(this, arguments); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-6: Do not unwrap function returning non-call expression', () => { - const code = `function nonCall(x) { return x + 1; }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-7: Do not unwrap function with empty body', () => { - const code = `function empty() {}`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-8: Do not unwrap function with BlockStatement but no statements', () => { - const code = `function emptyBlock() { }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-9: Do not unwrap arrow function as outer function', () => { - const code = `const arrow = (x) => { return function inner() { return x * 3; }.apply(this, arguments); };`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/unwrapFunctionShells.js')).default; + it('TP-1: Unwrap and rename', () => { + const code = 'function a(x) {return function b() {return x + 3}.apply(this, arguments);}'; + const expected = 'function b(x) {\n return x + 3;\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Unwrap anonymous without renaming', () => { + const code = 'function a(x) {return function() {return x + 3}.apply(this, arguments);}'; + const expected = 'function a(x) {\n return x + 3;\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Unwrap function expression assigned to variable', () => { + const code = 'const outer = function(param) { return function inner() { return param * 2; }.apply(this, arguments); };'; + const expected = 'const outer = function inner(param) {\n return param * 2;\n};'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Inner function already has parameters', () => { + const code = 'function wrapper() { return function inner(existing) { return existing + 1; }.apply(this, arguments); }'; + const expected = 'function inner(existing) {\n return existing + 1;\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Outer function has multiple parameters', () => { + const code = 'function multi(a, b, c) { return function() { return a + b + c; }.apply(this, arguments); }'; + const expected = 'function multi(a, b, c) {\n return a + b + c;\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Complex inner function body', () => { + const code = 'function complex(x) { return function process() { const temp = x * 2; return temp + 1; }.apply(this, arguments); }'; + const expected = 'function process(x) {\n const temp = x * 2;\n return temp + 1;\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not unwrap function with multiple statements', () => { + const code = 'function multi() { console.log(\'test\'); return function() { return 42; }.apply(this, arguments); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not unwrap function with no return statement', () => { + const code = 'function noReturn() { console.log(\'no return\'); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not unwrap function returning .call instead of .apply', () => { + const code = 'function useCall(x) { return function() { return x + 1; }.call(this, x); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not unwrap .apply with wrong argument count', () => { + const code = 'function wrongArgs(x) { return function() { return x; }.apply(this); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not unwrap when callee object is not FunctionExpression', () => { + const code = 'function notFunc(x) { return someFunc.apply(this, arguments); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-6: Do not unwrap function returning non-call expression', () => { + const code = 'function nonCall(x) { return x + 1; }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-7: Do not unwrap function with empty body', () => { + const code = 'function empty() {}'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-8: Do not unwrap function with BlockStatement but no statements', () => { + const code = 'function emptyBlock() { }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-9: Do not unwrap arrow function as outer function', () => { + const code = 'const arrow = (x) => { return function inner() { return x * 3; }.apply(this, arguments); };'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: unwrapIIFEs', async () => { - const targetModule = (await import('../src/modules/safe/unwrapIIFEs.js')).default; - it('TP-1: Arrow functions', () => { - const code = `var a = (() => { + const targetModule = (await import('../src/modules/safe/unwrapIIFEs.js')).default; + it('TP-1: Arrow functions', () => { + const code = `var a = (() => { return b => { return c(b - 40); }; })();`; - const expected = `var a = b => {\n return c(b - 40);\n};`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Function expressions', () => { - const code = `var a = (function () { + const expected = 'var a = b => {\n return c(b - 40);\n};'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Function expressions', () => { + const code = `var a = (function () { return b => c(b - 40); })();`; - const expected = `var a = b => c(b - 40);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: In-place unwrapping', () => { - const code = `!function() { + const expected = 'var a = b => c(b - 40);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: In-place unwrapping', () => { + const code = `!function() { var a = 'message'; console.log(a); }();`; - const expected = `var a = 'message';\nconsole.log(a);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Unary declarator init', () => { - const code = `var b = !function() { + const expected = 'var a = \'message\';\nconsole.log(a);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Unary declarator init', () => { + const code = `var b = !function() { var a = 'message'; console.log(a); }();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Unary assignment right', () => { - const code = `b = !function() { + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Unary assignment right', () => { + const code = `b = !function() { var a = 'message'; console.log(a); }();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: IIFE with multiple statements unwrapped', () => { - const code = `!function() { + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: IIFE with multiple statements unwrapped', () => { + const code = `!function() { var x = 1; var y = 2; console.log(x + y); }();`; - const expected = `var x = 1;\nvar y = 2;\nconsole.log(x + y);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not unwrap IIFE with arguments', () => { - const code = `var result = (function(x) { return x * 2; })(5);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not unwrap named function IIFE', () => { - const code = `var result = (function named() { return 42; })();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not unwrap IIFE in assignment context', () => { - const code = `obj.prop = (function() { return getValue(); })();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Arrow function IIFE with expression body', () => { - const code = `var result = (() => someValue)();`; - const expected = `var result = someValue;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const expected = 'var x = 1;\nvar y = 2;\nconsole.log(x + y);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not unwrap IIFE with arguments', () => { + const code = 'var result = (function(x) { return x * 2; })(5);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not unwrap named function IIFE', () => { + const code = 'var result = (function named() { return 42; })();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not unwrap IIFE in assignment context', () => { + const code = 'obj.prop = (function() { return getValue(); })();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Arrow function IIFE with expression body', () => { + const code = 'var result = (() => someValue)();'; + const expected = 'var result = someValue;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: unwrapSimpleOperations', async () => { - const targetModule = (await import('../src/modules/safe/unwrapSimpleOperations.js')).default; - it('TP-1: Binary operations', () => { - const code = `function add(b,c){return b + c;} + const targetModule = (await import('../src/modules/safe/unwrapSimpleOperations.js')).default; + it('TP-1: Binary operations', () => { + const code = `function add(b,c){return b + c;} function minus(b,c){return b - c;} function mul(b,c){return b * c;} function div(b,c){return b / c;} @@ -1849,7 +1849,7 @@ instanceofOp(1, 2); typeofOp(1); nullishCoalescingOp(1, 2); `; - const expected = `function add(b, c) { + const expected = `function add(b, c) { return b + c; } function minus(b, c) { @@ -1953,11 +1953,11 @@ function nullishCoalescingOp(b, c) { 1 instanceof 2; typeof 1; 1 ?? 2;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2 Unary operations', () => { - const code = `function unaryNegation(v) {return -v;} + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2 Unary operations', () => { + const code = `function unaryNegation(v) {return -v;} function unaryPlus(v) {return +v;} function logicalNot(v) {return !v;} function bitwiseNot(v) {return ~v;} @@ -1966,312 +1966,312 @@ typeof 1; function voidOp(v) {return void v;} (unaryNegation(1), unaryPlus(2), logicalNot(3), bitwiseNot(4), typeofOp(5), deleteOp(6), voidOp(7)); `; - const expected = `function unaryNegation(v) {\n return -v;\n}\nfunction unaryPlus(v) {\n return +v;\n}\nfunction logicalNot(v) {\n return !v;\n}\nfunction bitwiseNot(v) {\n return ~v;\n}\nfunction typeofOp(v) {\n return typeof v;\n}\nfunction deleteOp(v) {\n return delete v;\n}\nfunction voidOp(v) {\n return void v;\n}\n-1, +2, !3, ~4, typeof 5, delete 6, void 7;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3 Update operations', () => { - const code = `function incrementPre(v) {return ++v;} + const expected = 'function unaryNegation(v) {\n return -v;\n}\nfunction unaryPlus(v) {\n return +v;\n}\nfunction logicalNot(v) {\n return !v;\n}\nfunction bitwiseNot(v) {\n return ~v;\n}\nfunction typeofOp(v) {\n return typeof v;\n}\nfunction deleteOp(v) {\n return delete v;\n}\nfunction voidOp(v) {\n return void v;\n}\n-1, +2, !3, ~4, typeof 5, delete 6, void 7;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3 Update operations', () => { + const code = `function incrementPre(v) {return ++v;} function decrementPost(v) {return v--;} (incrementPre(a), decrementPost(b)); `; - const expected = `function incrementPre(v) {\n return ++v;\n}\nfunction decrementPost(v) {\n return v--;\n}\n++a, b--;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not unwrap function with multiple statements', () => { - const code = `function complexAdd(a, b) { + const expected = 'function incrementPre(v) {\n return ++v;\n}\nfunction decrementPost(v) {\n return v--;\n}\n++a, b--;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not unwrap function with multiple statements', () => { + const code = `function complexAdd(a, b) { console.log('adding'); return a + b; } complexAdd(1, 2);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not unwrap function with wrong parameter count', () => { - const code = `function singleParam(a) { return a + 1; } + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not unwrap function with wrong parameter count', () => { + const code = `function singleParam(a) { return a + 1; } singleParam(5);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not unwrap operation not using parameters', () => { - const code = `function fixedAdd(a, b) { return 5 + 10; } + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not unwrap operation not using parameters', () => { + const code = `function fixedAdd(a, b) { return 5 + 10; } fixedAdd(1, 2);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not unwrap function with no return statement', () => { - const code = `function noReturn(a, b) { + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not unwrap function with no return statement', () => { + const code = `function noReturn(a, b) { var result = a + b; } noReturn(1, 2);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not unwrap unsupported operator', () => { - const code = `function assignmentOp(a, b) { return a = b; } + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not unwrap unsupported operator', () => { + const code = `function assignmentOp(a, b) { return a = b; } assignmentOp(x, 5);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: separateChainedDeclarators', async () => { - const targetModule = (await import('../src/modules/safe/separateChainedDeclarators.js')).default; - it('TP-1: A single const', () => { - const code = `const foo = 5, bar = 7;`; - const expected = `const foo = 5;\nconst bar = 7;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: A single let', () => { - const code = `const a = 1; let foo = 5, bar = 7;`; - const expected = `const a = 1;\nlet foo = 5;\nlet bar = 7;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: A var and a let', () => { - const code = `!function() {var a, b = 2; let c, d = 3;}();`; - const expected = `!(function () {\n var a;\n var b = 2;\n let c;\n let d = 3;\n}());`; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TP-3: Wrap in a block statement for a one-liner', () => { - const code = `if (a) var b, c; while (true) var e = 3, d = 3;`; - const expected = `if (a) {\n var b;\n var c;\n}\nwhile (true) {\n var e = 3;\n var d = 3;\n}`; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TP-5: Mixed initialization patterns', () => { - const code = `var a, b = 2, c;`; - const expected = `var a;\nvar b = 2;\nvar c;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Mixed declaration types with complex expressions', () => { - const code = `const x = func(), y = [1, 2, 3], z = {prop: 'value'};`; - const expected = `const x = func();\nconst y = [\n 1,\n 2,\n 3\n];\nconst z = { prop: 'value' };`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-7: Three or more declarations', () => { - const code = `let a = 1, b = 2, c = 3, d = 4, e = 5;`; - const expected = `let a = 1;\nlet b = 2;\nlet c = 3;\nlet d = 4;\nlet e = 5;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-8: Declarations in function scope', () => { - const code = `function test() { const x = 1, y = 2; return x + y; }`; - const expected = `function test() {\n const x = 1;\n const y = 2;\n return x + y;\n}`; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TN-1: Variable declarators are not chained in for statement', () => { - const code = `for (let i, b = 2, c = 3;;);`; - const expected = code; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TN-2: Variable declarators are not chained in for-in statement', () => { - const code = `for (let a, b in obj);`; - const expected = code; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TN-3: Variable declarators are not chained in for-of statement', () => { - const code = `for (let a, b of arr);`; - const expected = code; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TN-4: Single declarator should not be transformed', () => { - const code = `const singleVar = 42;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: ForAwaitStatement declarations should be preserved', () => { - const code = `for await (let a, b of asyncIterable);`; - const expected = code; - const result = applyModuleToCode(code, targetModule, true); - assert.strictEqual(result, expected); - }); - it('TN-6: Destructuring patterns should not be separated', () => { - const code = `const {a, b} = obj, c = 3;`; - const expected = `const {a, b} = obj;\nconst c = 3;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/separateChainedDeclarators.js')).default; + it('TP-1: A single const', () => { + const code = 'const foo = 5, bar = 7;'; + const expected = 'const foo = 5;\nconst bar = 7;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: A single let', () => { + const code = 'const a = 1; let foo = 5, bar = 7;'; + const expected = 'const a = 1;\nlet foo = 5;\nlet bar = 7;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: A var and a let', () => { + const code = '!function() {var a, b = 2; let c, d = 3;}();'; + const expected = '!(function () {\n var a;\n var b = 2;\n let c;\n let d = 3;\n}());'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TP-3: Wrap in a block statement for a one-liner', () => { + const code = 'if (a) var b, c; while (true) var e = 3, d = 3;'; + const expected = 'if (a) {\n var b;\n var c;\n}\nwhile (true) {\n var e = 3;\n var d = 3;\n}'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TP-5: Mixed initialization patterns', () => { + const code = 'var a, b = 2, c;'; + const expected = 'var a;\nvar b = 2;\nvar c;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Mixed declaration types with complex expressions', () => { + const code = 'const x = func(), y = [1, 2, 3], z = {prop: \'value\'};'; + const expected = 'const x = func();\nconst y = [\n 1,\n 2,\n 3\n];\nconst z = { prop: \'value\' };'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-7: Three or more declarations', () => { + const code = 'let a = 1, b = 2, c = 3, d = 4, e = 5;'; + const expected = 'let a = 1;\nlet b = 2;\nlet c = 3;\nlet d = 4;\nlet e = 5;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-8: Declarations in function scope', () => { + const code = 'function test() { const x = 1, y = 2; return x + y; }'; + const expected = 'function test() {\n const x = 1;\n const y = 2;\n return x + y;\n}'; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TN-1: Variable declarators are not chained in for statement', () => { + const code = 'for (let i, b = 2, c = 3;;);'; + const expected = code; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TN-2: Variable declarators are not chained in for-in statement', () => { + const code = 'for (let a, b in obj);'; + const expected = code; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TN-3: Variable declarators are not chained in for-of statement', () => { + const code = 'for (let a, b of arr);'; + const expected = code; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TN-4: Single declarator should not be transformed', () => { + const code = 'const singleVar = 42;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: ForAwaitStatement declarations should be preserved', () => { + const code = 'for await (let a, b of asyncIterable);'; + const expected = code; + const result = applyModuleToCode(code, targetModule, true); + assert.strictEqual(result, expected); + }); + it('TN-6: Destructuring patterns should not be separated', () => { + const code = 'const {a, b} = obj, c = 3;'; + const expected = 'const {a, b} = obj;\nconst c = 3;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: simplifyCalls', async () => { - const targetModule = (await import('../src/modules/safe/simplifyCalls.js')).default; - it('TP-1: With args', () => { - const code = `func1.apply(this, [arg1, arg2]); func2.call(this, arg1, arg2);`; - const expected = `func1(arg1, arg2);\nfunc2(arg1, arg2);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Without args', () => { - const code = `func1.apply(this); func2.call(this);`; - const expected = `func1();\nfunc2();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Mixed calls with complex arguments', () => { - const code = `func.call(this, a + b, getValue()); obj.method.apply(this, [x, y, z]);`; - const expected = `func(a + b, getValue());\nobj.method(x, y, z);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Calls on member expressions', () => { - const code = `obj.method.call(this, arg1); nested.obj.func.apply(this, [arg2]);`; - const expected = `obj.method(arg1);\nnested.obj.func(arg2);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Apply with empty array', () => { - const code = `func.apply(this, []);`; - const expected = `func();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Call and apply in same expression', () => { - const code = `func1.call(this, arg) + func2.apply(this, [arg]);`; - const expected = `func1(arg) + func2(arg);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-7: Call and apply with null for context', () => { - const code = `func1.call(null, arg); func2.apply(null, [arg]);`; - const expected = `func1(arg);\nfunc2(arg);`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Ignore calls without ThisExpression', () => { - const code = `func1.apply({}); func2.call(undefined); func3.apply(obj);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not transform Function constructor calls', () => { - const code = `Function.call(this, 'return 42'); Function.apply(this, ['return 42']);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not transform calls on function expressions', () => { - const code = `(function() {}).call(this); (function() {}).apply(this, []);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not transform other method names', () => { - const code = `func.bind(this, arg); func.toString(this); func.valueOf(this);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-5: Do not transform calls with this in wrong position', () => { - const code = `func.call(arg, this); func.apply(arg1, this, arg2);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/safe/simplifyCalls.js')).default; + it('TP-1: With args', () => { + const code = 'func1.apply(this, [arg1, arg2]); func2.call(this, arg1, arg2);'; + const expected = 'func1(arg1, arg2);\nfunc2(arg1, arg2);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Without args', () => { + const code = 'func1.apply(this); func2.call(this);'; + const expected = 'func1();\nfunc2();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Mixed calls with complex arguments', () => { + const code = 'func.call(this, a + b, getValue()); obj.method.apply(this, [x, y, z]);'; + const expected = 'func(a + b, getValue());\nobj.method(x, y, z);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Calls on member expressions', () => { + const code = 'obj.method.call(this, arg1); nested.obj.func.apply(this, [arg2]);'; + const expected = 'obj.method(arg1);\nnested.obj.func(arg2);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Apply with empty array', () => { + const code = 'func.apply(this, []);'; + const expected = 'func();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Call and apply in same expression', () => { + const code = 'func1.call(this, arg) + func2.apply(this, [arg]);'; + const expected = 'func1(arg) + func2(arg);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-7: Call and apply with null for context', () => { + const code = 'func1.call(null, arg); func2.apply(null, [arg]);'; + const expected = 'func1(arg);\nfunc2(arg);'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Ignore calls without ThisExpression', () => { + const code = 'func1.apply({}); func2.call(undefined); func3.apply(obj);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not transform Function constructor calls', () => { + const code = 'Function.call(this, \'return 42\'); Function.apply(this, [\'return 42\']);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not transform calls on function expressions', () => { + const code = '(function() {}).call(this); (function() {}).apply(this, []);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not transform other method names', () => { + const code = 'func.bind(this, arg); func.toString(this); func.valueOf(this);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-5: Do not transform calls with this in wrong position', () => { + const code = 'func.call(arg, this); func.apply(arg1, this, arg2);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('SAFE: simplifyIfStatements', async () => { - const targetModule = (await import('../src/modules/safe/simplifyIfStatements.js')).default; - it('TP-1: Empty blocks', () => { - const code = `if (J) {} else {}`; - const expected = `J;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-2: Empty blocks with an empty alternate statement', () => { - const code = `if (J) {} else;`; - const expected = `J;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-3: Empty blocks with a populated alternate statement', () => { - const code = `if (J) {} else J();`; - const expected = `if (!J)\n J();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-4: Empty blocks with a populated alternate block', () => { - const code = `if (J) {} else {J()}`; - const expected = `if (!J) {\n J();\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-5: Empty statements', () => { - const code = `if (J); else;`; - const expected = `J;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-6: Empty statements with no alternate', () => { - const code = `if (J);`; - const expected = `J;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-7: Empty statements with an empty alternate', () => { - const code = `if (J) {}`; - const expected = `J;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-8: Populated consequent with empty alternate block', () => { - const code = `if (test) doSomething(); else {}`; - const expected = `if (test)\n doSomething();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-9: Populated consequent with empty alternate statement', () => { - const code = `if (condition) action(); else;`; - const expected = `if (condition)\n action();`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-10: Complex expression in test with empty branches', () => { - const code = `if (a && b || c) {} else {}`; - const expected = `a && b || c;`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TP-11: Nested empty if statements', () => { - const code = `if (outer) { if (inner) {} else {} }`; - const expected = `if (outer) {\n inner;\n}`; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-1: Do not transform if with populated consequent and alternate', () => { - const code = `if (test) doThis(); else doThat();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-2: Do not transform if with populated block statements', () => { - const code = `if (condition) { action1(); action2(); } else { action3(); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-3: Do not transform if with only populated consequent block', () => { - const code = `if (test) { performAction(); }`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - it('TN-4: Do not transform complex if-else chains', () => { - const code = `if (a) first(); else if (b) second(); else third();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); -}); \ No newline at end of file + const targetModule = (await import('../src/modules/safe/simplifyIfStatements.js')).default; + it('TP-1: Empty blocks', () => { + const code = 'if (J) {} else {}'; + const expected = 'J;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-2: Empty blocks with an empty alternate statement', () => { + const code = 'if (J) {} else;'; + const expected = 'J;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-3: Empty blocks with a populated alternate statement', () => { + const code = 'if (J) {} else J();'; + const expected = 'if (!J)\n J();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-4: Empty blocks with a populated alternate block', () => { + const code = 'if (J) {} else {J()}'; + const expected = 'if (!J) {\n J();\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-5: Empty statements', () => { + const code = 'if (J); else;'; + const expected = 'J;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-6: Empty statements with no alternate', () => { + const code = 'if (J);'; + const expected = 'J;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-7: Empty statements with an empty alternate', () => { + const code = 'if (J) {}'; + const expected = 'J;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-8: Populated consequent with empty alternate block', () => { + const code = 'if (test) doSomething(); else {}'; + const expected = 'if (test)\n doSomething();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-9: Populated consequent with empty alternate statement', () => { + const code = 'if (condition) action(); else;'; + const expected = 'if (condition)\n action();'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-10: Complex expression in test with empty branches', () => { + const code = 'if (a && b || c) {} else {}'; + const expected = 'a && b || c;'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TP-11: Nested empty if statements', () => { + const code = 'if (outer) { if (inner) {} else {} }'; + const expected = 'if (outer) {\n inner;\n}'; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-1: Do not transform if with populated consequent and alternate', () => { + const code = 'if (test) doThis(); else doThat();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-2: Do not transform if with populated block statements', () => { + const code = 'if (condition) { action1(); action2(); } else { action3(); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-3: Do not transform if with only populated consequent block', () => { + const code = 'if (test) { performAction(); }'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + it('TN-4: Do not transform complex if-else chains', () => { + const code = 'if (a) first(); else if (b) second(); else third();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); +}); From e2a1ca323a7a43e0347a4e6dd407fc67e5904de3 Mon Sep 17 00:00:00 2001 From: Ben Baryo <19845603+ctrl-escp@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:36:09 +0200 Subject: [PATCH 2/4] Update dependencies in package.json and package-lock.json - Upgrade 'flast' to version 2.2.8 and 'isolated-vm' to version 6.1.2. - Bump development dependencies: '@babel/eslint-parser' to 7.28.6, '@babel/plugin-syntax-import-assertions' to 7.27.6, 'eslint' to 9.39.4, and 'globals' to 17.4.0 --- package-lock.json | 808 +++++++++++++--------------------------------- package.json | 14 +- 2 files changed, 226 insertions(+), 596 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a6bab7..e91e122 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,30 +10,30 @@ "license": "MIT", "dependencies": { "commander": "^14.0.2", - "flast": "2.2.5", - "isolated-vm": "5.0.4", + "flast": "2.2.8", + "isolated-vm": "6.1.2", "obfuscation-detector": "^2.0.7" }, "bin": { "restringer": "bin/deobfuscate.js" }, "devDependencies": { - "@babel/eslint-parser": "^7.28.5", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "eslint": "^9.39.1", - "globals": "^16.5.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-syntax-import-assertions": "^7.27.6", + "eslint": "^9.39.4", + "globals": "^17.4.0", "husky": "^9.1.7" } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -42,9 +42,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "peer": true, @@ -53,22 +53,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -85,9 +85,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.5.tgz", - "integrity": "sha512-fcdRcWahONYo+JRnJg1/AekOacGvKx12Gu0qXJXFi2WBqQA1i7+O5PaxRB7kxE/Op94dExnCiiar6T09pvdHpA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.6.tgz", + "integrity": "sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==", "dev": true, "license": "MIT", "dependencies": { @@ -104,15 +104,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -122,14 +122,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -151,31 +151,31 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -185,9 +185,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -228,29 +228,29 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -260,13 +260,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -276,35 +276,35 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -312,9 +312,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "peer": true, @@ -327,9 +327,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -369,15 +369,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -410,20 +410,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -447,9 +447,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -609,11 +609,16 @@ "eslint-scope": "5.1.1" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -624,9 +629,9 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -645,9 +650,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -691,46 +696,18 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { - "version": "2.8.31", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", - "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==", + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", + "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", "dev": true, "license": "Apache-2.0", "peer": true, "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/brace-expansion": { @@ -745,9 +722,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -766,11 +743,11 @@ "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -779,30 +756,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -814,9 +767,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001757", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", - "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", "dev": true, "funding": [ { @@ -852,12 +805,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -879,9 +826,9 @@ "license": "MIT" }, "node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "license": "MIT", "engines": { "node": ">=20" @@ -935,62 +882,20 @@ } } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "license": "MIT" }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/electron-to-chromium": { - "version": "1.5.260", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz", - "integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==", + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", "dev": true, "license": "ISC", "peer": true }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1017,9 +922,9 @@ }, "node_modules/escodegen": { "name": "@javascript-obfuscator/escodegen", - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@javascript-obfuscator/escodegen/-/escodegen-2.3.0.tgz", - "integrity": "sha512-QVXwMIKqYMl3KwtTirYIA6gOCiJ0ZDtptXqAv/8KWLG9uQU2fZqTVy7a/A5RvcoZhbDoFfveTxuGxJ5ibzQtkw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@javascript-obfuscator/escodegen/-/escodegen-2.4.0.tgz", + "integrity": "sha512-h9cJ/qb3Y3c1jMQPWypt2CGTFgP34V5OtWLqoOCjV6CT/DUXMZFpoTAfDHpuUrRP0oxNd0UwnVAsPtPuYsoXxQ==", "license": "BSD-2-Clause", "dependencies": { "@javascript-obfuscator/estraverse": "^5.3.0", @@ -1085,25 +990,25 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -1122,7 +1027,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -1212,6 +1117,7 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.15.0", @@ -1229,6 +1135,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1251,9 +1158,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1313,15 +1220,6 @@ "node": ">=0.10.0" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1373,27 +1271,58 @@ } }, "node_modules/flast": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/flast/-/flast-2.2.5.tgz", - "integrity": "sha512-ZLYKBunsigVXxTdODDV7MMGbtrszDzYMlQbkuF2X/axgpeknjSnlqlgOzXGaONdC9wk+fty6ytPxyANzD+qC5Q==", + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/flast/-/flast-2.2.8.tgz", + "integrity": "sha512-hAgukc9Thr6uxK/W9ArKm3wDZQjEFoQVhhzQwEufati+CfM6bYqy1kpBeGhSJY9NBVoQxjSu5/K0dIiNIDaLJg==", "license": "MIT", "dependencies": { "escodegen": "npm:@javascript-obfuscator/escodegen", - "eslint-scope": "^8.3.0", - "espree": "^10.3.0" + "eslint-scope": "^9.1.2", + "espree": "^11.2.0" } }, "node_modules/flast/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/flast/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/flast/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1423,18 +1352,12 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1446,12 +1369,6 @@ "node": ">=6.9.0" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1466,9 +1383,9 @@ } }, "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "dev": true, "license": "MIT", "engines": { @@ -1504,26 +1421,6 @@ "url": "https://github.com/sponsors/typicode" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1561,18 +1458,6 @@ "node": ">=0.8.19" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1604,16 +1489,16 @@ "license": "ISC" }, "node_modules/isolated-vm": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/isolated-vm/-/isolated-vm-5.0.4.tgz", - "integrity": "sha512-RYUf/JC4ldWz/oi2BVs8a1XIprQ71q6eQPBwySaF5Apu0KMyf2gIpElbCyPh2OEmRT+FYw1GOKSdkv7jw2KLxw==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/isolated-vm/-/isolated-vm-6.1.2.tgz", + "integrity": "sha512-GGfsHqtlZiiurZaxB/3kY7LLAXR3sgzDul0fom4cSyBjx6ZbjpTrFWiH3z/nUfLJGJ8PIq9LQmQFiAxu24+I7A==", "hasInstallScript": true, "license": "ISC", "dependencies": { - "prebuild-install": "^7.1.2" + "node-gyp-build": "^4.8.4" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/js-tokens": { @@ -1744,22 +1629,10 @@ "yallist": "^3.0.2" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1769,21 +1642,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1791,12 +1649,6 @@ "dev": true, "license": "MIT" }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -1804,59 +1656,37 @@ "dev": true, "license": "MIT" }, - "node_modules/node-abi": { - "version": "3.85.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", - "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT", "peer": true }, "node_modules/obfuscation-detector": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/obfuscation-detector/-/obfuscation-detector-2.0.7.tgz", - "integrity": "sha512-OhMPPDwx9s7SCavwK0E5Mg4zugbV9+o4NnhMgKRdds7YdvNkHaxNTkTZGpSaReaa8QHQB4UVihjdTAQ+KuQWgg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/obfuscation-detector/-/obfuscation-detector-2.0.8.tgz", + "integrity": "sha512-YrLHl0qmX/R+Fhmx2y3TEMvTlTVDyeqLPNHXnX6jMU60+lAomRMQ1AVwoL0C7Qd5gXoK6Qia1kvg57gct8ajsw==", "license": "MIT", "dependencies": { - "flast": "^2.2.5" + "flast": "^2.2.6" }, "bin": { "obfuscation-detector": "bin/obfuscation-detector.js" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -1948,32 +1778,6 @@ "license": "ISC", "peer": true }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -1984,16 +1788,6 @@ "node": ">= 0.8.0" } }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2004,44 +1798,6 @@ "node": ">=6" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2052,26 +1808,6 @@ "node": ">=4" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -2105,51 +1841,6 @@ "node": ">=8" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2160,15 +1851,6 @@ "node": ">=0.10.0" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2195,46 +1877,6 @@ "node": ">=8" } }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2249,9 +1891,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -2290,12 +1932,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2321,12 +1957,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index a97a3e8..c083baa 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ }, "dependencies": { "commander": "^14.0.2", - "flast": "2.2.5", - "isolated-vm": "5.0.4", + "flast": "2.2.8", + "isolated-vm": "6.1.2", "obfuscation-detector": "^2.0.7" }, "scripts": { @@ -49,10 +49,10 @@ }, "homepage": "https://github.com/HumanSecurity/restringer#readme", "devDependencies": { - "@babel/eslint-parser": "^7.28.5", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "eslint": "^9.39.1", - "globals": "^16.5.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-syntax-import-assertions": "^7.27.6", + "eslint": "^9.39.4", + "globals": "^17.4.0", "husky": "^9.1.7" } -} +} \ No newline at end of file From 27187e6ad9c644961f7e83b4f39857374b25fc9f Mon Sep 17 00:00:00 2001 From: Ben Baryo <19845603+ctrl-escp@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:23:16 +0200 Subject: [PATCH 3/4] Refactor ESLint configuration and improve deobfuscation script readability - Removed unnecessary ESLint rules for cleaner configuration. - Enhanced the deobfuscate.js script by restructuring the argument parsing logic for clarity and improved user experience. - Updated the REstringer class and related modules to streamline the deobfuscation process, ensuring better organization and maintainability. - Refactored multiple modules to separate matching and transformation logic, enhancing code clarity and performance. - Improved documentation across various modules for better understanding of functionality and usage. --- bin/deobfuscate.js | 48 +- eslint.config.js | 5 - src/modules/config.js | 16 +- src/modules/safe/normalizeComputed.js | 98 +- src/modules/safe/normalizeEmptyStatements.js | 60 +- ...parseTemplateLiteralsIntoStringLiterals.js | 72 +- src/modules/safe/rearrangeSequences.js | 136 +- src/modules/safe/rearrangeSwitches.js | 158 +- src/modules/safe/removeDeadNodes.js | 78 +- .../safe/removeRedundantBlockStatements.js | 122 +- .../safe/replaceBooleanExpressionsWithIf.js | 112 +- ...eCallExpressionsWithUnwrappedIdentifier.js | 146 +- .../replaceEvalCallsWithLiteralContent.js | 200 +- .../replaceFunctionShellsWithWrappedValue.js | 94 +- ...placeFunctionShellsWithWrappedValueIIFE.js | 86 +- ...replaceIdentifierWithFixedAssignedValue.js | 94 +- ...rWithFixedValueNotAssignedAtDeclaration.js | 180 +- .../replaceNewFuncCallsWithLiteralContent.js | 160 +- .../safe/replaceSequencesWithExpressions.js | 178 +- .../safe/resolveDeterministicIfStatements.js | 204 +- .../safe/resolveFunctionConstructorCalls.js | 184 +- ...eMemberExpressionReferencesToArrayIndex.js | 196 +- ...veMemberExpressionsWithDirectAssignment.js | 312 +- src/modules/safe/resolveProxyCalls.js | 196 +- src/modules/safe/resolveProxyReferences.js | 214 +- src/modules/safe/resolveProxyVariables.js | 144 +- .../resolveRedundantLogicalExpressions.js | 190 +- .../safe/separateChainedDeclarators.js | 140 +- src/modules/safe/simplifyCalls.js | 128 +- src/modules/safe/simplifyIfStatements.js | 158 +- src/modules/safe/unwrapFunctionShells.js | 132 +- src/modules/safe/unwrapIIFEs.js | 152 +- src/modules/safe/unwrapSimpleOperations.js | 144 +- .../unsafe/normalizeRedundantNotOperator.js | 134 +- ...gmentedFunctionWrappedArrayReplacements.js | 188 +- src/modules/unsafe/resolveBuiltinCalls.js | 124 +- .../resolveDefiniteBinaryExpressions.js | 168 +- .../resolveDefiniteMemberExpressions.js | 94 +- ...olveDeterministicConditionalExpressions.js | 56 +- .../unsafe/resolveEvalCallsOnNonLiterals.js | 128 +- src/modules/unsafe/resolveFunctionToArray.js | 98 +- .../resolveInjectedPrototypeMethodCalls.js | 100 +- src/modules/unsafe/resolveLocalCalls.js | 180 +- ...resolveMemberExpressionsLocalReferences.js | 144 +- src/modules/unsafe/resolveMinimalAlphabet.js | 70 +- src/modules/utils/areReferencesModified.js | 248 +- src/modules/utils/createNewNode.js | 326 +- src/modules/utils/createOrderedSrc.js | 172 +- .../utils/doesDescendantMatchCondition.js | 46 +- src/modules/utils/evalInVm.js | 92 +- src/modules/utils/generateHash.js | 58 +- src/modules/utils/getCache.js | 26 +- src/modules/utils/getCalleeName.js | 68 +- .../utils/getDeclarationWithContext.js | 376 +-- src/modules/utils/getDescendants.js | 62 +- ...getMainDeclaredObjectOfMemberExpression.js | 32 +- src/modules/utils/getObjType.js | 2 +- src/modules/utils/index.js | 32 +- src/modules/utils/isNodeInRanges.js | 26 +- src/modules/utils/normalizeScript.js | 10 +- src/modules/utils/safe-atob.js | 2 +- src/modules/utils/safe-btoa.js | 2 +- src/modules/utils/sandbox.js | 92 +- src/processors/augmentedArray.js | 156 +- src/processors/caesarp.js | 34 +- src/processors/functionToArray.js | 10 +- src/processors/index.js | 14 +- src/processors/obfuscator.io.js | 98 +- src/restringer.js | 214 +- src/utils/parseArgs.js | 262 +- tests/deobfuscation.test.js | 504 +-- tests/functionality.test.js | 23 +- tests/modules.unsafe.test.js | 1973 ++++++------ tests/modules.utils.test.js | 2720 ++++++++--------- tests/processors.test.js | 384 +-- tests/samples.test.js | 188 +- tests/utils.test.js | 240 +- 77 files changed, 7249 insertions(+), 7264 deletions(-) diff --git a/bin/deobfuscate.js b/bin/deobfuscate.js index 1ec79b3..70781ae 100755 --- a/bin/deobfuscate.js +++ b/bin/deobfuscate.js @@ -3,30 +3,30 @@ import {REstringer} from '../src/restringer.js'; import {parseArgs} from '../src/utils/parseArgs.js'; try { - const args = parseArgs(process.argv.slice(2)); - - // Skip processing if help was displayed - if (args.help) process.exit(0); - - const fs = await import('node:fs'); - let content = fs.readFileSync(args.inputFilename, 'utf-8'); - const startTime = Date.now(); + const args = parseArgs(process.argv.slice(2)); - const restringer = new REstringer(content); - if (args.quiet) restringer.logger.setLogLevelNone(); - else if (args.verbose) restringer.logger.setLogLevelDebug(); - restringer.logger.log(`[!] REstringer v${REstringer.__version__}`); - restringer.logger.log(`[!] Deobfuscating ${args.inputFilename}...`); - if (args.maxIterations) { - restringer.maxIterations.value = args.maxIterations; - restringer.logger.log(`[!] Running at most ${args.maxIterations} iterations`); - } - if (restringer.deobfuscate()) { - restringer.logger.log(`[+] Saved ${args.outputFilename}`); - restringer.logger.log(`[!] Deobfuscation took ${(Date.now() - startTime) / 1000} seconds.`); - if (args.outputToFile) fs.writeFileSync(args.outputFilename, restringer.script, {encoding: 'utf-8'}); - else console.log(restringer.script); - } else restringer.logger.log(`[-] Nothing was deobfuscated ¯\\_(ツ)_/¯`); + // Skip processing if help was displayed + if (args.help) process.exit(0); + + const fs = await import('node:fs'); + const content = fs.readFileSync(args.inputFilename, 'utf-8'); + const startTime = Date.now(); + + const restringer = new REstringer(content); + if (args.quiet) restringer.logger.setLogLevelNone(); + else if (args.verbose) restringer.logger.setLogLevelDebug(); + restringer.logger.log(`[!] REstringer v${REstringer.__version__}`); + restringer.logger.log(`[!] Deobfuscating ${args.inputFilename}...`); + if (args.maxIterations) { + restringer.maxIterations.value = args.maxIterations; + restringer.logger.log(`[!] Running at most ${args.maxIterations} iterations`); + } + if (restringer.deobfuscate()) { + restringer.logger.log(`[+] Saved ${args.outputFilename}`); + restringer.logger.log(`[!] Deobfuscation took ${(Date.now() - startTime) / 1000} seconds.`); + if (args.outputToFile) fs.writeFileSync(args.outputFilename, restringer.script, {encoding: 'utf-8'}); + else console.log(restringer.script); + } else restringer.logger.log('[-] Nothing was deobfuscated ¯\\_(ツ)_/¯'); } catch (e) { - console.error(`[-] Critical Error: ${e}`); + console.error(`[-] Critical Error: ${e}`); } diff --git a/eslint.config.js b/eslint.config.js index 50ab96c..a894767 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -26,7 +26,6 @@ export default [ 'object-curly-spacing': ['error', 'never'], 'array-bracket-spacing': ['error', 'never'], 'no-trailing-spaces': 'error', - 'eol-last': ['error', 'always'], 'no-multiple-empty-lines': ['error', {max: 1, maxEOF: 0}], /* @@ -44,13 +43,10 @@ export default [ * ───────── Predictability ───────── */ 'consistent-return': 'error', - 'default-case': 'error', 'dot-notation': 'error', 'no-fallthrough': 'error', 'no-unreachable': 'error', 'no-throw-literal': 'error', - radix: ['error', 'always'], - yoda: ['error', 'never'], /* * ───────── Clean Refactors ───────── @@ -69,7 +65,6 @@ export default [ * ───────── Modern JS Discipline ───────── */ 'prefer-arrow-callback': 'error', - 'prefer-template': 'error', 'prefer-spread': 'error', 'prefer-rest-params': 'error', 'object-shorthand': ['error', 'always'], diff --git a/src/modules/config.js b/src/modules/config.js index e78b9dd..883507b 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -5,23 +5,23 @@ export const BAD_VALUE = '--BAD-VAL--'; // Behaves like a number, but decrements each time it's used. // Use DEFAULT_MAX_ITERATIONS.value = 300 to set a new value. export const DEFAULT_MAX_ITERATIONS = { - value: 500, - valueOf() {return this.value--;}, + value: 500, + valueOf() {return this.value--;}, }; export const PROPERTIES_THAT_MODIFY_CONTENT = [ - 'push', 'forEach', 'pop', 'insert', 'add', 'set', 'delete', 'shift', 'unshift', 'splice', - 'sort', 'reverse', 'fill', 'copyWithin' + 'push', 'forEach', 'pop', 'insert', 'add', 'set', 'delete', 'shift', 'unshift', 'splice', + 'sort', 'reverse', 'fill', 'copyWithin', ]; // Identifiers that shouldn't be touched since they're either session-based or resolve inconsisstently. export const SKIP_IDENTIFIERS = [ - 'window', 'this', 'self', 'document', 'module', '$', 'jQuery', 'navigator', 'typeof', 'new', 'Date', 'Math', - 'Promise', 'Error', 'fetch', 'XMLHttpRequest', 'performance', 'globalThis', + 'window', 'this', 'self', 'document', 'module', '$', 'jQuery', 'navigator', 'typeof', 'new', 'Date', 'Math', + 'Promise', 'Error', 'fetch', 'XMLHttpRequest', 'performance', 'globalThis', ]; // Properties that shouldn't be resolved since they're either based on context which can't be determined or resolve inconsistently. export const SKIP_PROPERTIES = [ - 'test', 'exec', 'match', 'length', 'freeze', 'call', 'apply', 'create', 'getTime', 'now', - 'getMilliseconds', ...PROPERTIES_THAT_MODIFY_CONTENT, + 'test', 'exec', 'match', 'length', 'freeze', 'call', 'apply', 'create', 'getTime', 'now', + 'getMilliseconds', ...PROPERTIES_THAT_MODIFY_CONTENT, ]; \ No newline at end of file diff --git a/src/modules/safe/normalizeComputed.js b/src/modules/safe/normalizeComputed.js index 8b7cb09..357aed0 100644 --- a/src/modules/safe/normalizeComputed.js +++ b/src/modules/safe/normalizeComputed.js @@ -10,47 +10,47 @@ const VALID_IDENTIFIER_BEGINNING = /^[A-Za-z$_]/; * @return {ASTNode[]} Array of nodes that match the criteria for normalization */ export function normalizeComputedMatch(arb, candidateFilter = () => true) { - const matchingNodes = []; - - // Process MemberExpression nodes: obj['prop'] -> obj.prop - const memberExpressions = arb.ast[0].typeMap.MemberExpression; - for (let i = 0; i < memberExpressions.length; i++) { - const n = memberExpressions[i]; - if (n.computed && + const matchingNodes = []; + + // Process MemberExpression nodes: obj['prop'] -> obj.prop + const memberExpressions = arb.ast[0].typeMap.MemberExpression; + for (let i = 0; i < memberExpressions.length; i++) { + const n = memberExpressions[i]; + if (n.computed && n.property.type === 'Literal' && VALID_IDENTIFIER_BEGINNING.test(n.property.value) && !BAD_IDENTIFIER_CHARS_REGEX.test(n.property.value) && candidateFilter(n)) { - matchingNodes.push(n); - } - } - - // Process MethodDefinition nodes: ['method']() {} -> method() {} - const methodDefinitions = arb.ast[0].typeMap.MethodDefinition; - for (let i = 0; i < methodDefinitions.length; i++) { - const n = methodDefinitions[i]; - if (n.computed && + matchingNodes.push(n); + } + } + + // Process MethodDefinition nodes: ['method']() {} -> method() {} + const methodDefinitions = arb.ast[0].typeMap.MethodDefinition; + for (let i = 0; i < methodDefinitions.length; i++) { + const n = methodDefinitions[i]; + if (n.computed && n.key.type === 'Literal' && VALID_IDENTIFIER_BEGINNING.test(n.key.value) && !BAD_IDENTIFIER_CHARS_REGEX.test(n.key.value) && candidateFilter(n)) { - matchingNodes.push(n); - } - } - - // Process Property nodes: {['prop']: value} -> {prop: value}, and also {'string': value} -> {string: value} - const properties = arb.ast[0].typeMap.Property; - for (let i = 0; i < properties.length; i++) { - const n = properties[i]; - if (n.key.type === 'Literal' && + matchingNodes.push(n); + } + } + + // Process Property nodes: {['prop']: value} -> {prop: value}, and also {'string': value} -> {string: value} + const properties = arb.ast[0].typeMap.Property; + for (let i = 0; i < properties.length; i++) { + const n = properties[i]; + if (n.key.type === 'Literal' && VALID_IDENTIFIER_BEGINNING.test(n.key.value) && !BAD_IDENTIFIER_CHARS_REGEX.test(n.key.value) && candidateFilter(n)) { - matchingNodes.push(n); - } - } - - return matchingNodes; + matchingNodes.push(n); + } + } + + return matchingNodes; } /** @@ -60,39 +60,39 @@ export function normalizeComputedMatch(arb, candidateFilter = () => true) { * @return {Arborist} */ export function normalizeComputedTransform(arb, n) { - const relevantProperty = n.type === 'MemberExpression' ? 'property' : 'key'; - arb.markNode(n, { - ...n, - computed: false, - [relevantProperty]: { - type: 'Identifier', - name: n[relevantProperty].value, - }, - }); - return arb; + const relevantProperty = n.type === 'MemberExpression' ? 'property' : 'key'; + arb.markNode(n, { + ...n, + computed: false, + [relevantProperty]: { + type: 'Identifier', + name: n[relevantProperty].value, + }, + }); + return arb; } /** * Convert computed property access to dot notation where the property is a valid identifier. * This normalizes bracket notation to more readable dot notation. - * + * * Transforms: * console['log'] -> console.log * obj['methodName']() -> obj.methodName() * {['propName']: value} -> {propName: value} - * + * * Only applies to string literals that form valid JavaScript identifiers * (start with letter/$/_, contain only alphanumeric/_/$ characters). - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list * @return {Arborist} */ export default function normalizeComputed(arb, candidateFilter = () => true) { - const matchingNodes = normalizeComputedMatch(arb, candidateFilter); - - for (let i = 0; i < matchingNodes.length; i++) { - arb = normalizeComputedTransform(arb, matchingNodes[i]); - } - return arb; + const matchingNodes = normalizeComputedMatch(arb, candidateFilter); + + for (let i = 0; i < matchingNodes.length; i++) { + arb = normalizeComputedTransform(arb, matchingNodes[i]); + } + return arb; } \ No newline at end of file diff --git a/src/modules/safe/normalizeEmptyStatements.js b/src/modules/safe/normalizeEmptyStatements.js index e8e3039..81e7f63 100644 --- a/src/modules/safe/normalizeEmptyStatements.js +++ b/src/modules/safe/normalizeEmptyStatements.js @@ -8,23 +8,23 @@ const CONTROL_FLOW_STATEMENT_TYPES = ['ForStatement', 'ForInStatement', 'ForOfSt * @return {ASTNode[]} Array of empty statement nodes that can be safely removed */ export function normalizeEmptyStatementsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.EmptyStatement; - - const matchingNodes = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - if (candidateFilter(n)) { - // Control flow statements can have empty statements as their body. - // If we delete that empty statement the syntax breaks - // e.g. for (var i = 0, b = 8;;); - valid for statement - // e.g. if (condition); - valid if statement with empty consequent - if (!CONTROL_FLOW_STATEMENT_TYPES.includes(n.parentNode.type)) { - matchingNodes.push(n); - } - } - } - return matchingNodes; + const relevantNodes = arb.ast[0].typeMap.EmptyStatement; + + const matchingNodes = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + if (candidateFilter(n)) { + // Control flow statements can have empty statements as their body. + // If we delete that empty statement the syntax breaks + // e.g. for (var i = 0, b = 8;;); - valid for statement + // e.g. if (condition); - valid if statement with empty consequent + if (!CONTROL_FLOW_STATEMENT_TYPES.includes(n.parentNode.type)) { + matchingNodes.push(n); + } + } + } + return matchingNodes; } /** @@ -34,36 +34,36 @@ export function normalizeEmptyStatementsMatch(arb, candidateFilter = () => true) * @return {Arborist} */ export function normalizeEmptyStatementsTransform(arb, node) { - arb.markNode(node); - return arb; + arb.markNode(node); + return arb; } /** * Remove empty statements that are not required for syntax correctness. - * + * * Empty statements (just semicolons) can be safely removed in most contexts, * but must be preserved in control flow statements where they serve as the statement body: * - for (var i = 0; i < 10; i++); // The semicolon is the empty loop body * - while (condition); // The semicolon is the empty loop body * - if (condition); else;// The semicolon is the empty if consequent and the empty else alternate - * + * * Safe to remove: * - Standalone empty statements: "var x = 1;;" * - Empty statements in blocks: "if (true) {;}" - * + * * Must preserve: * - Control flow body empty statements: "for(;;);", "while(true);", "if(condition);" - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list. Defaults to true. * @return {Arborist} */ export default function normalizeEmptyStatements(arb, candidateFilter = () => true) { - const matchingNodes = normalizeEmptyStatementsMatch(arb, candidateFilter); - - for (let i = 0; i < matchingNodes.length; i++) { - arb = normalizeEmptyStatementsTransform(arb, matchingNodes[i]); - } - - return arb; + const matchingNodes = normalizeEmptyStatementsMatch(arb, candidateFilter); + + for (let i = 0; i < matchingNodes.length; i++) { + arb = normalizeEmptyStatementsTransform(arb, matchingNodes[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/parseTemplateLiteralsIntoStringLiterals.js b/src/modules/safe/parseTemplateLiteralsIntoStringLiterals.js index df27457..1858008 100644 --- a/src/modules/safe/parseTemplateLiteralsIntoStringLiterals.js +++ b/src/modules/safe/parseTemplateLiteralsIntoStringLiterals.js @@ -7,18 +7,18 @@ import {createNewNode} from '../utils/createNewNode.js'; * @return {ASTNode[]} Array of template literal nodes that can be converted to string literals */ export function parseTemplateLiteralsIntoStringLiteralsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.TemplateLiteral; - const matchingNodes = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - // Only process template literals where all expressions are literals (not variables or function calls) - if (!n.expressions.some(exp => exp.type !== 'Literal') && + const relevantNodes = arb.ast[0].typeMap.TemplateLiteral; + const matchingNodes = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + // Only process template literals where all expressions are literals (not variables or function calls) + if (!n.expressions.some(exp => exp.type !== 'Literal') && candidateFilter(n)) { - matchingNodes.push(n); - } - } - return matchingNodes; + matchingNodes.push(n); + } + } + return matchingNodes; } /** @@ -28,45 +28,45 @@ export function parseTemplateLiteralsIntoStringLiteralsMatch(arb, candidateFilte * @return {Arborist} */ export function parseTemplateLiteralsIntoStringLiteralsTransform(arb, node) { - // Template literals have alternating quasis (string parts) and expressions - // e.g. `hello ${name}!` has quasis=["hello ", "!"] and expressions=[name] - // The build process is: quasi[0] + expr[0] + quasi[1] + expr[1] + ... + final_quasi - let newStringLiteral = ''; - - // Process all expressions, adding the preceding quasi each time - for (let i = 0; i < node.expressions.length; i++) { - newStringLiteral += node.quasis[i].value.raw + node.expressions[i].value; - } - - // Add the final quasi (there's always one more quasi than expressions) - newStringLiteral += node.quasis.slice(-1)[0].value.raw; - - arb.markNode(node, createNewNode(newStringLiteral)); - return arb; + // Template literals have alternating quasis (string parts) and expressions + // e.g. `hello ${name}!` has quasis=["hello ", "!"] and expressions=[name] + // The build process is: quasi[0] + expr[0] + quasi[1] + expr[1] + ... + final_quasi + let newStringLiteral = ''; + + // Process all expressions, adding the preceding quasi each time + for (let i = 0; i < node.expressions.length; i++) { + newStringLiteral += node.quasis[i].value.raw + node.expressions[i].value; + } + + // Add the final quasi (there's always one more quasi than expressions) + newStringLiteral += node.quasis.slice(-1)[0].value.raw; + + arb.markNode(node, createNewNode(newStringLiteral)); + return arb; } /** * Convert template literals that contain only literal expressions into regular string literals. * This simplifies expressions by replacing template syntax with plain strings when no dynamic content exists. - * + * * Transforms: * `hello ${'world'}!` -> 'hello world!' * `static ${42} text` -> 'static 42 text' * `just text` -> 'just text' - * + * * Only processes template literals where all interpolated expressions are literals (strings, numbers, booleans), * not variables or function calls which could change at runtime. - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list. Defaults to true. * @return {Arborist} */ export default function parseTemplateLiteralsIntoStringLiterals(arb, candidateFilter = () => true) { - const matchingNodes = parseTemplateLiteralsIntoStringLiteralsMatch(arb, candidateFilter); - - for (let i = 0; i < matchingNodes.length; i++) { - arb = parseTemplateLiteralsIntoStringLiteralsTransform(arb, matchingNodes[i]); - } - - return arb; + const matchingNodes = parseTemplateLiteralsIntoStringLiteralsMatch(arb, candidateFilter); + + for (let i = 0; i < matchingNodes.length; i++) { + arb = parseTemplateLiteralsIntoStringLiteralsTransform(arb, matchingNodes[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/rearrangeSequences.js b/src/modules/safe/rearrangeSequences.js index e0cfcb9..72e4713 100644 --- a/src/modules/safe/rearrangeSequences.js +++ b/src/modules/safe/rearrangeSequences.js @@ -5,22 +5,22 @@ * @return {ASTNode[]} Array of nodes with sequence expressions that can be rearranged */ export function rearrangeSequencesMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.ReturnStatement - .concat(arb.ast[0].typeMap.IfStatement); - const matchingNodes = []; + const relevantNodes = arb.ast[0].typeMap.ReturnStatement + .concat(arb.ast[0].typeMap.IfStatement); + const matchingNodes = []; - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - // Check if node has a sequence expression that can be rearranged - const hasSequenceExpression = + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + // Check if node has a sequence expression that can be rearranged + const hasSequenceExpression = (n.type === 'ReturnStatement' && n.argument?.type === 'SequenceExpression') || (n.type === 'IfStatement' && n.test?.type === 'SequenceExpression'); - if (hasSequenceExpression && candidateFilter(n)) { - matchingNodes.push(n); - } - } - return matchingNodes; + if (hasSequenceExpression && candidateFilter(n)) { + matchingNodes.push(n); + } + } + return matchingNodes; } /** @@ -31,81 +31,81 @@ export function rearrangeSequencesMatch(arb, candidateFilter = () => true) { * @return {Arborist} */ export function rearrangeSequencesTransform(arb, node) { - const parent = node.parentNode; - // Get the sequence expression from either return argument or if test - const sequenceExpression = node.argument || node.test; - const { expressions } = sequenceExpression; + const parent = node.parentNode; + // Get the sequence expression from either return argument or if test + const sequenceExpression = node.argument || node.test; + const {expressions} = sequenceExpression; - // Create expression statements for all but the last expression - const extractedStatements = expressions.slice(0, -1).map(expr => ({ - type: 'ExpressionStatement', - expression: expr - })); + // Create expression statements for all but the last expression + const extractedStatements = expressions.slice(0, -1).map(expr => ({ + type: 'ExpressionStatement', + expression: expr, + })); - // Create the replacement node with only the last expression - const replacementNode = node.type === 'IfStatement' ? { - type: 'IfStatement', - test: expressions[expressions.length - 1], - consequent: node.consequent, - alternate: node.alternate - } : { - type: 'ReturnStatement', - argument: expressions[expressions.length - 1] - }; + // Create the replacement node with only the last expression + const replacementNode = node.type === 'IfStatement' ? { + type: 'IfStatement', + test: expressions[expressions.length - 1], + consequent: node.consequent, + alternate: node.alternate, + } : { + type: 'ReturnStatement', + argument: expressions[expressions.length - 1], + }; - // Handle different parent contexts - if (parent.type === 'BlockStatement') { - // Insert extracted statements before the current statement in the block - const currentIdx = parent.body.indexOf(node); - const newBlockBody = [ - ...parent.body.slice(0, currentIdx), - ...extractedStatements, - replacementNode, - ...parent.body.slice(currentIdx + 1) - ]; - - arb.markNode(parent, { - type: 'BlockStatement', - body: newBlockBody, - }); - } else { - // Wrap in a new block statement if parent is not a block - arb.markNode(node, { - type: 'BlockStatement', - body: [ - ...extractedStatements, - replacementNode - ] - }); - } - return arb; + // Handle different parent contexts + if (parent.type === 'BlockStatement') { + // Insert extracted statements before the current statement in the block + const currentIdx = parent.body.indexOf(node); + const newBlockBody = [ + ...parent.body.slice(0, currentIdx), + ...extractedStatements, + replacementNode, + ...parent.body.slice(currentIdx + 1), + ]; + + arb.markNode(parent, { + type: 'BlockStatement', + body: newBlockBody, + }); + } else { + // Wrap in a new block statement if parent is not a block + arb.markNode(node, { + type: 'BlockStatement', + body: [ + ...extractedStatements, + replacementNode, + ], + }); + } + return arb; } /** * Rearrange sequence expressions in return statements and if conditions by extracting * all expressions except the last one into separate expression statements. - * + * * This improves code readability by converting: * return a(), b(), c(); -> a(); b(); return c(); * if (x(), y(), z()) {...} -> x(); y(); if (z()) {...} - * + * * Algorithm: * 1. Find return statements with sequence expression arguments - * 2. Find if statements with sequence expression tests + * 2. Find if statements with sequence expression tests * 3. Extract all but the last expression into separate expression statements * 4. Replace the original statement with one containing only the last expression * 5. Handle both block statement parents and single statement contexts - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list. Defaults to true. * @return {Arborist} */ export default function rearrangeSequences(arb, candidateFilter = () => true) { - const matchingNodes = rearrangeSequencesMatch(arb, candidateFilter); - - for (let i = 0; i < matchingNodes.length; i++) { - arb = rearrangeSequencesTransform(arb, matchingNodes[i]); - } - - return arb; + const matchingNodes = rearrangeSequencesMatch(arb, candidateFilter); + + for (let i = 0; i < matchingNodes.length; i++) { + arb = rearrangeSequencesTransform(arb, matchingNodes[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/rearrangeSwitches.js b/src/modules/safe/rearrangeSwitches.js index 4719a05..97b90c2 100644 --- a/src/modules/safe/rearrangeSwitches.js +++ b/src/modules/safe/rearrangeSwitches.js @@ -4,34 +4,34 @@ const MAX_REPETITION = 50; /** * Find switch statements that can be linearized into sequential code. - * + * * Identifies switch statements that use a discriminant variable which: * - Is an identifier with literal initialization * - Has deterministic flow through cases via assignments - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list. Defaults to true. * @return {ASTNode[]} Array of matching switch statement nodes */ export function rearrangeSwitchesMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.SwitchStatement; - const matchingNodes = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - // Check if switch discriminant is an identifier with literal initialization - if (n.discriminant.type === 'Identifier' && + const relevantNodes = arb.ast[0].typeMap.SwitchStatement; + const matchingNodes = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + // Check if switch discriminant is an identifier with literal initialization + if (n.discriminant.type === 'Identifier' && n?.discriminant.declNode?.parentNode?.init?.type === 'Literal' && candidateFilter(n)) { - matchingNodes.push(n); - } - } - return matchingNodes; + matchingNodes.push(n); + } + } + return matchingNodes; } /** * Transform a switch statement into a sequential block of statements. - * + * * Algorithm: * 1. Start with the initial discriminant value from variable initialization * 2. Find the matching case (or default case) for current value @@ -39,76 +39,76 @@ export function rearrangeSwitchesMatch(arb, candidateFilter = () => true) { * 4. Look for assignments to the discriminant variable to find next case * 5. Repeat until no more valid transitions found or max iterations reached * 6. Replace switch with sequential block of collected statements - * + * * @param {Arborist} arb * @param {Object} switchNode - The switch statement node to transform * @return {Arborist} */ export function rearrangeSwitchesTransform(arb, switchNode) { - const ordered = []; - const cases = switchNode.cases; - let currentVal = switchNode.discriminant.declNode.parentNode.init.value; - let counter = 0; - - // Trace execution path through switch cases - while (currentVal !== undefined && counter < MAX_REPETITION) { - // Find the matching case for current value (or default case) - let currentCase; - for (let i = 0; i < cases.length; i++) { - if (cases[i].test?.value === currentVal || !cases[i].test) { - currentCase = cases[i]; - break; - } - } - if (!currentCase) break; - - // Collect all statements from this case (except break statements) - for (let i = 0; i < currentCase.consequent.length; i++) { - if (currentCase.consequent[i].type !== 'BreakStatement') { - ordered.push(currentCase.consequent[i]); - } - } - - // Find assignments to discriminant variable to determine next case - let allDescendants = []; - for (let i = 0; i < currentCase.consequent.length; i++) { - allDescendants.push(...getDescendants(currentCase.consequent[i])); - } - - // Look for assignments to the switch discriminant variable - const assignments2Next = allDescendants.filter(d => - d.declNode === switchNode.discriminant.declNode && + const ordered = []; + const cases = switchNode.cases; + let currentVal = switchNode.discriminant.declNode.parentNode.init.value; + let counter = 0; + + // Trace execution path through switch cases + while (currentVal !== undefined && counter < MAX_REPETITION) { + // Find the matching case for current value (or default case) + let currentCase; + for (let i = 0; i < cases.length; i++) { + if (cases[i].test?.value === currentVal || !cases[i].test) { + currentCase = cases[i]; + break; + } + } + if (!currentCase) break; + + // Collect all statements from this case (except break statements) + for (let i = 0; i < currentCase.consequent.length; i++) { + if (currentCase.consequent[i].type !== 'BreakStatement') { + ordered.push(currentCase.consequent[i]); + } + } + + // Find assignments to discriminant variable to determine next case + const allDescendants = []; + for (let i = 0; i < currentCase.consequent.length; i++) { + allDescendants.push(...getDescendants(currentCase.consequent[i])); + } + + // Look for assignments to the switch discriminant variable + const assignments2Next = allDescendants.filter(d => + d.declNode === switchNode.discriminant.declNode && d.parentKey === 'left' && - d.parentNode.type === 'AssignmentExpression' - ); - - if (assignments2Next.length === 1) { - // Single assignment found - use its value for next iteration - currentVal = assignments2Next[0].parentNode.right.value; - } else { - // Multiple or no assignments - can't determine next case reliably - currentVal = undefined; - } - ++counter; - } - - // Replace switch with sequential block if we collected any statements - if (ordered.length) { - arb.markNode(switchNode, { - type: 'BlockStatement', - body: ordered, - }); - } - return arb; + d.parentNode.type === 'AssignmentExpression', + ); + + if (assignments2Next.length === 1) { + // Single assignment found - use its value for next iteration + currentVal = assignments2Next[0].parentNode.right.value; + } else { + // Multiple or no assignments - can't determine next case reliably + currentVal = undefined; + } + ++counter; + } + + // Replace switch with sequential block if we collected any statements + if (ordered.length) { + arb.markNode(switchNode, { + type: 'BlockStatement', + body: ordered, + }); + } + return arb; } /** * Rearrange switch statements with deterministic flow into sequential code blocks. - * + * * Converts switch statements that use a control variable to sequence operations * into a linear sequence of statements. This is commonly seen in obfuscated code * where a simple sequence of operations is disguised as a switch statement. - * + * * Example transformation: * var state = 0; * switch (state) { @@ -116,20 +116,20 @@ export function rearrangeSwitchesTransform(arb, switchNode) { * case 1: doSecond(); state = 2; break; * case 2: doThird(); break; * } - * + * * Becomes: * doFirst(); - * doSecond(); + * doSecond(); * doThird(); - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list. Defaults to true. * @return {Arborist} */ export default function rearrangeSwitches(arb, candidateFilter = () => true) { - const matchingNodes = rearrangeSwitchesMatch(arb, candidateFilter); - for (let i = 0; i < matchingNodes.length; i++) { - arb = rearrangeSwitchesTransform(arb, matchingNodes[i]); - } - return arb; + const matchingNodes = rearrangeSwitchesMatch(arb, candidateFilter); + for (let i = 0; i < matchingNodes.length; i++) { + arb = rearrangeSwitchesTransform(arb, matchingNodes[i]); + } + return arb; } \ No newline at end of file diff --git a/src/modules/safe/removeDeadNodes.js b/src/modules/safe/removeDeadNodes.js index 912dda4..80a6b95 100644 --- a/src/modules/safe/removeDeadNodes.js +++ b/src/modules/safe/removeDeadNodes.js @@ -1,93 +1,93 @@ const RELEVANT_PARENTS = [ - 'VariableDeclarator', - 'AssignmentExpression', - 'FunctionDeclaration', - 'ClassDeclaration', + 'VariableDeclarator', + 'AssignmentExpression', + 'FunctionDeclaration', + 'ClassDeclaration', ]; /** * Find identifiers that are declared but never referenced (dead code). - * + * * Identifies identifiers in declaration contexts that have no references, * indicating they are declared but never used anywhere in the code. - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list * @return {ASTNode[]} Array of dead identifier nodes */ function removeDeadNodesMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.Identifier; - const matchingNodes = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - // Check if identifier is in a declaration context and has no references - if (RELEVANT_PARENTS.includes(n.parentNode.type) && + const relevantNodes = arb.ast[0].typeMap.Identifier; + const matchingNodes = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + // Check if identifier is in a declaration context and has no references + if (RELEVANT_PARENTS.includes(n.parentNode.type) && (!n?.declNode?.references?.length && !n?.references?.length) && candidateFilter(n)) { - const parent = n.parentNode; - // Skip root-level declarations as they might be referenced externally - if (parent.parentNode.type === 'Program') continue; - matchingNodes.push(n); - } - } - return matchingNodes; + const parent = n.parentNode; + // Skip root-level declarations as they might be referenced externally + if (parent.parentNode.type === 'Program') continue; + matchingNodes.push(n); + } + } + return matchingNodes; } /** * Remove a dead code declaration node. - * + * * Determines the appropriate node to remove based on the declaration type: * - For expression statements: removes the entire expression statement * - For other declarations: removes the declaration itself - * + * * @param {Arborist} arb * @param {Object} identifierNode - The dead identifier node * @return {Arborist} */ function removeDeadNodesTransform(arb, identifierNode) { - const parent = identifierNode.parentNode; - // Remove expression statement wrapper if present, otherwise remove the declaration - const nodeToRemove = parent?.parentNode?.type === 'ExpressionStatement' - ? parent.parentNode - : parent; - arb.markNode(nodeToRemove); - return arb; + const parent = identifierNode.parentNode; + // Remove expression statement wrapper if present, otherwise remove the declaration + const nodeToRemove = parent?.parentNode?.type === 'ExpressionStatement' + ? parent.parentNode + : parent; + arb.markNode(nodeToRemove); + return arb; } /** * Remove declared but unused code (dead code elimination). - * + * * This function identifies and removes variables, functions, and classes that are * declared but never referenced in the code. This helps clean up obfuscated code * that may contain many unused declarations. - * + * * ⚠️ **WARNING**: This is a potentially dangerous operation that should be used with caution. * Dynamic references (e.g., `eval`, `window[varName]`) cannot be detected statically, * so removing "dead" code might break functionality that relies on dynamic access. - * + * * Algorithm: * 1. Find all identifiers in declaration contexts (variables, functions, classes) * 2. Check if they have any references in the AST * 3. Skip root-level declarations (might be used by external scripts) * 4. Remove unreferenced declarations - * + * * Handles these declaration types: * - Variable declarations: `var unused = 5;` * - Function declarations: `function unused() {}` * - Class declarations: `class Unused {}` * - Assignment expressions: `unused = value;` (if unused is unreferenced) - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list * @return {Arborist} */ function removeDeadNodes(arb, candidateFilter = () => true) { - const matchingNodes = removeDeadNodesMatch(arb, candidateFilter); - for (let i = 0; i < matchingNodes.length; i++) { - arb = removeDeadNodesTransform(arb, matchingNodes[i]); - } - return arb; + const matchingNodes = removeDeadNodesMatch(arb, candidateFilter); + for (let i = 0; i < matchingNodes.length; i++) { + arb = removeDeadNodesTransform(arb, matchingNodes[i]); + } + return arb; } export default removeDeadNodes; \ No newline at end of file diff --git a/src/modules/safe/removeRedundantBlockStatements.js b/src/modules/safe/removeRedundantBlockStatements.js index 8786373..73ab83b 100644 --- a/src/modules/safe/removeRedundantBlockStatements.js +++ b/src/modules/safe/removeRedundantBlockStatements.js @@ -3,107 +3,107 @@ const REDUNDANT_BLOCK_PARENT_TYPES = ['BlockStatement', 'Program']; /** * Find all block statements that are redundant and can be flattened. - * + * * Identifies block statements that create unnecessary nesting by being * direct children of other block statements or the Program node. - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list. Defaults to true. * @return {ASTNode[]} Array of redundant block statement nodes */ export function removeRedundantBlockStatementsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.BlockStatement; - const matchingNodes = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - // Block statements are redundant if: - // 1. Their parent is a node type that creates unnecessary nesting - // 2. They pass the candidate filter - if (REDUNDANT_BLOCK_PARENT_TYPES.includes(n.parentNode.type) && candidateFilter(n)) { - matchingNodes.push(n); - } - } - return matchingNodes; + const relevantNodes = arb.ast[0].typeMap.BlockStatement; + const matchingNodes = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + // Block statements are redundant if: + // 1. Their parent is a node type that creates unnecessary nesting + // 2. They pass the candidate filter + if (REDUNDANT_BLOCK_PARENT_TYPES.includes(n.parentNode.type) && candidateFilter(n)) { + matchingNodes.push(n); + } + } + return matchingNodes; } /** * Transform a redundant block statement by flattening it into its parent. - * + * * Handles three transformation scenarios: * 1. Single child replacement: parent becomes this block - * 2. Single statement replacement: block becomes its single statement + * 2. Single statement replacement: block becomes its single statement * 3. Content flattening: block's contents spread into parent's body - * + * * @param {Arborist} arb * @param {Object} blockNode The redundant block statement node to flatten * @return {Arborist} */ export function removeRedundantBlockStatementsTransform(arb, blockNode) { - const parent = blockNode.parentNode; - - // Case 1: Parent has only one child (this block) - replace parent with this block - if (parent.body?.length === 1) { - arb.markNode(parent, blockNode); - } - // Case 2: Parent has multiple children - need to flatten this block's contents - else if (parent.body?.length > 1) { - // If this block has only one statement, replace it directly - if (blockNode.body.length === 1) { - arb.markNode(blockNode, blockNode.body[0]); - } else { - // Flatten this block's contents into the parent's body - const currentIdx = parent.body.indexOf(blockNode); - const replacementNode = { - type: parent.type, - body: [ - ...parent.body.slice(0, currentIdx), - ...blockNode.body, - ...parent.body.slice(currentIdx + 1) - ], - }; - arb.markNode(parent, replacementNode); - } - } - - return arb; + const parent = blockNode.parentNode; + + // Case 1: Parent has only one child (this block) - replace parent with this block + if (parent.body?.length === 1) { + arb.markNode(parent, blockNode); + } + // Case 2: Parent has multiple children - need to flatten this block's contents + else if (parent.body?.length > 1) { + // If this block has only one statement, replace it directly + if (blockNode.body.length === 1) { + arb.markNode(blockNode, blockNode.body[0]); + } else { + // Flatten this block's contents into the parent's body + const currentIdx = parent.body.indexOf(blockNode); + const replacementNode = { + type: parent.type, + body: [ + ...parent.body.slice(0, currentIdx), + ...blockNode.body, + ...parent.body.slice(currentIdx + 1), + ], + }; + arb.markNode(parent, replacementNode); + } + } + + return arb; } /** * Remove redundant block statements by flattening unnecessarily nested blocks. - * + * * This module eliminates redundant block statements that create unnecessary nesting: * 1. Block statements that are direct children of other block statements * 2. Block statements that are direct children of the Program node - * + * * Transformations: * if (a) {{do_a();}} → if (a) {do_a();} * if (a) {{do_a();}{do_b();}} → if (a) {do_a(); do_b();} * {{{{{statement;}}}}} → statement; - * + * * Algorithm: * 1. Find all block statements whose parent is BlockStatement or Program * 2. For each redundant block: * - If parent has single child: replace parent with the block * - If block has single statement: replace block with the statement * - Otherwise: flatten block's contents into parent's body - * + * * Note: Processing stops after Program node replacement since the root changes. - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list. Defaults to true. * @return {Arborist} */ export default function removeRedundantBlockStatements(arb, candidateFilter = () => true) { - const matchingNodes = removeRedundantBlockStatementsMatch(arb, candidateFilter); - - for (let i = 0; i < matchingNodes.length; i++) { - arb = removeRedundantBlockStatementsTransform(arb, matchingNodes[i]); - - // Stop processing if we replaced the Program node since the AST structure changed - if (matchingNodes[i].parentNode.type === 'Program') { - break; - } - } - return arb; + const matchingNodes = removeRedundantBlockStatementsMatch(arb, candidateFilter); + + for (let i = 0; i < matchingNodes.length; i++) { + arb = removeRedundantBlockStatementsTransform(arb, matchingNodes[i]); + + // Stop processing if we replaced the Program node since the AST structure changed + if (matchingNodes[i].parentNode.type === 'Program') { + break; + } + } + return arb; } \ No newline at end of file diff --git a/src/modules/safe/replaceBooleanExpressionsWithIf.js b/src/modules/safe/replaceBooleanExpressionsWithIf.js index 3887ca3..5677a2e 100644 --- a/src/modules/safe/replaceBooleanExpressionsWithIf.js +++ b/src/modules/safe/replaceBooleanExpressionsWithIf.js @@ -3,107 +3,107 @@ const LOGICAL_OPERATORS = ['&&', '||']; /** * Find all expression statements containing logical expressions that can be converted to if statements. - * + * * Identifies expression statements where the expression is a logical operation (&&, ||) * that can be converted from short-circuit evaluation to explicit if statements. - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list. Defaults to true. * @return {ASTNode[]} Array of expression statement nodes with logical expressions */ export function replaceBooleanExpressionsWithIfMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.ExpressionStatement; - const matchingNodes = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - // Check if the expression statement contains a logical expression with && or || - if (n.expression?.type === 'LogicalExpression' && - LOGICAL_OPERATORS.includes(n.expression.operator) && + const relevantNodes = arb.ast[0].typeMap.ExpressionStatement; + const matchingNodes = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + // Check if the expression statement contains a logical expression with && or || + if (n.expression?.type === 'LogicalExpression' && + LOGICAL_OPERATORS.includes(n.expression.operator) && candidateFilter(n)) { - matchingNodes.push(n); - } - } - return matchingNodes; + matchingNodes.push(n); + } + } + return matchingNodes; } /** * Transform a logical expression into an if statement. - * + * * Converts logical expressions using short-circuit evaluation into explicit if statements: - * - For &&: if (left) { right; } + * - For &&: if (left) { right; } * - For ||: if (!left) { right; } (inverted logic) - * + * * The transformation preserves the original semantics where: * - && only executes right side if left is truthy * - || only executes right side if left is falsy - * + * * @param {Arborist} arb * @param {Object} expressionStatementNode The expression statement node to transform * @return {Arborist} */ export function replaceBooleanExpressionsWithIfTransform(arb, expressionStatementNode) { - const logicalExpr = expressionStatementNode.expression; - - // For ||, we need to invert the test condition since || executes right side when left is falsy - const testExpression = logicalExpr.operator === '||' - ? { - type: 'UnaryExpression', - operator: '!', - argument: logicalExpr.left, - } - : logicalExpr.left; - - // Create the if statement with the right operand as the consequent - const ifStatement = { - type: 'IfStatement', - test: testExpression, - consequent: { - type: 'BlockStatement', - body: [{ - type: 'ExpressionStatement', - expression: logicalExpr.right - }] - }, - }; - - arb.markNode(expressionStatementNode, ifStatement); - return arb; + const logicalExpr = expressionStatementNode.expression; + + // For ||, we need to invert the test condition since || executes right side when left is falsy + const testExpression = logicalExpr.operator === '||' + ? { + type: 'UnaryExpression', + operator: '!', + argument: logicalExpr.left, + } + : logicalExpr.left; + + // Create the if statement with the right operand as the consequent + const ifStatement = { + type: 'IfStatement', + test: testExpression, + consequent: { + type: 'BlockStatement', + body: [{ + type: 'ExpressionStatement', + expression: logicalExpr.right, + }], + }, + }; + + arb.markNode(expressionStatementNode, ifStatement); + return arb; } /** * Replace logical expressions with equivalent if statements for better readability. - * + * * This module converts short-circuit logical expressions into explicit if statements, * making the control flow more obvious and easier to understand. - * + * * Transformations: * x && y(); → if (x) { y(); } * x || y(); → if (!x) { y(); } * a && b && c(); → if (a && b) { c(); } * a || b || c(); → if (!(a || b)) { c(); } - * + * * Algorithm: * 1. Find expression statements containing logical expressions (&& or ||) * 2. Extract the rightmost operand as the consequent action * 3. Use the left operand(s) as the test condition * 4. For ||, invert the test condition to preserve semantics * 5. Wrap the consequent in a block statement for proper syntax - * + * * Note: This transformation maintains the original short-circuit evaluation semantics * where && executes the right side only if left is truthy, and || executes the right * side only if left is falsy. - * + * * @param {Arborist} arb * @param {Function} [candidateFilter] a filter to apply on the candidates list. Defaults to true. * @return {Arborist} */ export default function replaceBooleanExpressionsWithIf(arb, candidateFilter = () => true) { - const matchingNodes = replaceBooleanExpressionsWithIfMatch(arb, candidateFilter); - - for (let i = 0; i < matchingNodes.length; i++) { - arb = replaceBooleanExpressionsWithIfTransform(arb, matchingNodes[i]); - } - - return arb; + const matchingNodes = replaceBooleanExpressionsWithIfMatch(arb, candidateFilter); + + for (let i = 0; i < matchingNodes.length; i++) { + arb = replaceBooleanExpressionsWithIfTransform(arb, matchingNodes[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/replaceCallExpressionsWithUnwrappedIdentifier.js b/src/modules/safe/replaceCallExpressionsWithUnwrappedIdentifier.js index ce8e78f..4b21556 100644 --- a/src/modules/safe/replaceCallExpressionsWithUnwrappedIdentifier.js +++ b/src/modules/safe/replaceCallExpressionsWithUnwrappedIdentifier.js @@ -3,135 +3,135 @@ const FUNCTION_EXPRESSION_TYPES = ['FunctionExpression', 'ArrowFunctionExpressio /** * Find all call expressions that can be replaced with unwrapped identifiers. - * + * * This function identifies call expressions where the callee is a function that * only returns an identifier or a call expression with no arguments. Such calls * can be safely replaced with the returned value directly. - * + * * Algorithm: * 1. Find all CallExpression nodes in the AST * 2. Check if the callee references a function declaration or function expression * 3. Analyze the function body to determine if it only returns an identifier * 4. Return matching nodes for transformation - * + * * @param {Arborist} arb - The arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {ASTNode[]} Array of call expression nodes that can be unwrapped */ export function replaceCallExpressionsWithUnwrappedIdentifierMatch(arb, candidateFilter = () => true) { - // Direct access to typeMap without spread operator for better performance - const relevantNodes = arb.ast[0].typeMap.CallExpression; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const node = relevantNodes[i]; - - // Check if the callee references a function declaration or expression - const calleeDecl = node.callee?.declNode; - if (!calleeDecl || !candidateFilter(node)) continue; - - const parentNode = calleeDecl.parentNode; - const parentType = parentNode?.type; - - // Check if callee is from a variable declarator with function expression - const isVariableFunction = parentType === 'VariableDeclarator' && + // Direct access to typeMap without spread operator for better performance + const relevantNodes = arb.ast[0].typeMap.CallExpression; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const node = relevantNodes[i]; + + // Check if the callee references a function declaration or expression + const calleeDecl = node.callee?.declNode; + if (!calleeDecl || !candidateFilter(node)) continue; + + const parentNode = calleeDecl.parentNode; + const parentType = parentNode?.type; + + // Check if callee is from a variable declarator with function expression + const isVariableFunction = parentType === 'VariableDeclarator' && FUNCTION_EXPRESSION_TYPES.includes(parentNode.init?.type); - - // Check if callee is from a function declaration - const isFunctionDeclaration = parentType === 'FunctionDeclaration' && + + // Check if callee is from a function declaration + const isFunctionDeclaration = parentType === 'FunctionDeclaration' && calleeDecl.parentKey === 'id'; - - if (isVariableFunction || isFunctionDeclaration) { - matches.push(node); - } - } - - return matches; + + if (isVariableFunction || isFunctionDeclaration) { + matches.push(node); + } + } + + return matches; } /** * Transform call expressions by replacing them with their unwrapped identifiers. - * + * * This function analyzes the function body referenced by each call expression * and replaces the call with the identifier or call expression that the function * returns, effectively unwrapping the function shell. - * + * * @param {Arborist} arb - The arborist instance to modify * @param {Object} node - The call expression node to transform * @return {Arborist} The modified arborist instance */ export function replaceCallExpressionsWithUnwrappedIdentifierTransform(arb, node) { - const calleeDecl = node.callee.declNode; - const parentNode = calleeDecl.parentNode; - - // Get the function body (either from init for expressions or body for declarations) - const declBody = parentNode.init?.body || parentNode.body; - - // Handle function bodies (arrow functions without blocks or block statements) - if (!Array.isArray(declBody)) { - // Case 1: Arrow function with direct return (no block statement) - if (isUnwrappableExpression(declBody)) { - // Mark all references to this function for replacement - for (const ref of calleeDecl.references) { - arb.markNode(ref.parentNode, declBody); - } - } - // Case 2: Block statement with single return statement - else if (declBody.type === 'BlockStatement' && - declBody.body.length === 1 && + const calleeDecl = node.callee.declNode; + const parentNode = calleeDecl.parentNode; + + // Get the function body (either from init for expressions or body for declarations) + const declBody = parentNode.init?.body || parentNode.body; + + // Handle function bodies (arrow functions without blocks or block statements) + if (!Array.isArray(declBody)) { + // Case 1: Arrow function with direct return (no block statement) + if (isUnwrappableExpression(declBody)) { + // Mark all references to this function for replacement + for (const ref of calleeDecl.references) { + arb.markNode(ref.parentNode, declBody); + } + } + // Case 2: Block statement with single return statement + else if (declBody.type === 'BlockStatement' && + declBody.body.length === 1 && declBody.body[0].type === 'ReturnStatement') { - - const returnArg = declBody.body[0].argument; - if (isUnwrappableExpression(returnArg)) { - arb.markNode(node, returnArg); - } - } - } - - return arb; + + const returnArg = declBody.body[0].argument; + if (isUnwrappableExpression(returnArg)) { + arb.markNode(node, returnArg); + } + } + } + + return arb; } /** * Check if an expression can be safely unwrapped. - * + * * An expression is unwrappable if it's: * - An identifier (variable reference) * - A call expression with no arguments - * + * * @param {Object} expr - The expression node to check * @return {boolean} True if the expression can be unwrapped */ function isUnwrappableExpression(expr) { - return expr.type === 'Identifier' || + return expr.type === 'Identifier' || (expr.type === 'CallExpression' && !expr.arguments?.length); } /** * Replace call expressions with unwrapped identifiers when the called function * only returns an identifier or parameterless call expression. - * + * * This transformation removes unnecessary function wrappers that only return * simple values, effectively flattening the call chain for better readability * and potential performance improvements. - * + * * Examples: * - function a() {return String} a()(val) → String(val) * - const b = () => btoa; b()('data') → btoa('data') - * + * * @param {Arborist} arb - The arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified arborist instance */ function replaceCallExpressionsWithUnwrappedIdentifier(arb, candidateFilter = () => true) { - // Find all matching call expressions - const matches = replaceCallExpressionsWithUnwrappedIdentifierMatch(arb, candidateFilter); - - // Transform each matching node - for (let i = 0; i < matches.length; i++) { - arb = replaceCallExpressionsWithUnwrappedIdentifierTransform(arb, matches[i]); - } - - return arb; + // Find all matching call expressions + const matches = replaceCallExpressionsWithUnwrappedIdentifierMatch(arb, candidateFilter); + + // Transform each matching node + for (let i = 0; i < matches.length; i++) { + arb = replaceCallExpressionsWithUnwrappedIdentifierTransform(arb, matches[i]); + } + + return arb; } export default replaceCallExpressionsWithUnwrappedIdentifier; \ No newline at end of file diff --git a/src/modules/safe/replaceEvalCallsWithLiteralContent.js b/src/modules/safe/replaceEvalCallsWithLiteralContent.js index 485d744..9aec1bb 100644 --- a/src/modules/safe/replaceEvalCallsWithLiteralContent.js +++ b/src/modules/safe/replaceEvalCallsWithLiteralContent.js @@ -4,172 +4,172 @@ import {generateHash} from '../utils/generateHash.js'; /** * Parse the string argument of an eval call into an AST node. - * + * * This function takes the string content from an eval() call and converts it * into the appropriate AST representation, handling single statements, * multiple statements, and expression statements appropriately. - * + * * @param {string} code - The code string to parse * @return {ASTNode} The parsed AST node */ function parseEvalArgument(code) { - let body = generateFlatAST(code, {detailed: false, includeSrc: false})[0].body; - - // Multiple statements become a block statement - if (body.length > 1) { - return { - type: 'BlockStatement', - body, - }; - } - - // Single statement processing - body = body[0]; - - // Unwrap expression statements to just the expression when appropriate - if (body.type === 'ExpressionStatement') { - body = body.expression; - } - - return body; + let body = generateFlatAST(code, {detailed: false, includeSrc: false})[0].body; + + // Multiple statements become a block statement + if (body.length > 1) { + return { + type: 'BlockStatement', + body, + }; + } + + // Single statement processing + body = body[0]; + + // Unwrap expression statements to just the expression when appropriate + if (body.type === 'ExpressionStatement') { + body = body.expression; + } + + return body; } /** * Handle replacement when eval is used as a callee in a call expression. - * + * * This handles the edge case where eval returns a function that is immediately * called, such as eval('Function')('alert("hacked!")'). - * + * * @param {ASTNode} evalNode - The original eval call node * @param {ASTNode} replacementNode - The parsed replacement AST node * @return {ASTNode} The modified call expression with eval replaced */ function handleCalleeReplacement(evalNode, replacementNode) { - // Unwrap expression statement if needed - if (replacementNode.type === 'ExpressionStatement') { - replacementNode = replacementNode.expression; - } - - // Create new call expression with eval replaced by the parsed content - return { - ...evalNode.parentNode, - callee: replacementNode - }; + // Unwrap expression statement if needed + if (replacementNode.type === 'ExpressionStatement') { + replacementNode = replacementNode.expression; + } + + // Create new call expression with eval replaced by the parsed content + return { + ...evalNode.parentNode, + callee: replacementNode, + }; } /** * Find all eval call expressions that can be replaced with their literal content. - * + * * This function identifies eval() calls where the argument is a string literal * that can be safely parsed and replaced with the actual AST nodes without * executing the eval. - * + * * Algorithm: * 1. Find all CallExpression nodes in the AST * 2. Check if callee is 'eval' and first argument is a string literal * 3. Apply candidate filter for additional constraints * 4. Return matching nodes for transformation - * + * * @param {Arborist} arb - The arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {ASTNode[]} Array of eval call expression nodes that can be replaced */ export function replaceEvalCallsWithLiteralContentMatch(arb, candidateFilter = () => true) { - // Direct access to typeMap without spread operator for better performance - const relevantNodes = arb.ast[0].typeMap.CallExpression; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const node = relevantNodes[i]; - - // Check if this is an eval call with a literal string argument - if (node.callee?.name === 'eval' && + // Direct access to typeMap without spread operator for better performance + const relevantNodes = arb.ast[0].typeMap.CallExpression; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const node = relevantNodes[i]; + + // Check if this is an eval call with a literal string argument + if (node.callee?.name === 'eval' && node.arguments[0]?.type === 'Literal' && candidateFilter(node)) { - matches.push(node); - } - } - - return matches; + matches.push(node); + } + } + + return matches; } /** * Transform eval call expressions by replacing them with their parsed content. - * + * * This function takes an eval() call with a string literal and replaces it with * the actual AST nodes that the string represents. It handles various edge cases * including block statements, expression statements, and nested call expressions. - * + * * @param {Arborist} arb - The arborist instance to modify * @param {ASTNode} node - The eval call expression node to transform * @return {Arborist} The modified arborist instance */ export function replaceEvalCallsWithLiteralContentTransform(arb, node) { - const cache = getCache(arb.ast[0].scriptHash); - const cacheName = `replaceEval-${generateHash(node.src)}`; - - try { - // Generate or retrieve cached AST for the eval argument - if (!cache[cacheName]) { - cache[cacheName] = parseEvalArgument(node.arguments[0].value); - } - - let replacementNode = cache[cacheName]; - let targetNode = node; - - // Handle edge case: eval used as callee in call expression - // Example: eval('Function')('alert("hacked!")'); - if (node.parentKey === 'callee') { - targetNode = node.parentNode; - replacementNode = handleCalleeReplacement(node, replacementNode); - } - - // Handle block statement placement - if (targetNode.parentNode.type === 'ExpressionStatement' && + const cache = getCache(arb.ast[0].scriptHash); + const cacheName = `replaceEval-${generateHash(node.src)}`; + + try { + // Generate or retrieve cached AST for the eval argument + if (!cache[cacheName]) { + cache[cacheName] = parseEvalArgument(node.arguments[0].value); + } + + let replacementNode = cache[cacheName]; + let targetNode = node; + + // Handle edge case: eval used as callee in call expression + // Example: eval('Function')('alert("hacked!")'); + if (node.parentKey === 'callee') { + targetNode = node.parentNode; + replacementNode = handleCalleeReplacement(node, replacementNode); + } + + // Handle block statement placement + if (targetNode.parentNode.type === 'ExpressionStatement' && replacementNode.type === 'BlockStatement') { - targetNode = targetNode.parentNode; - } - - // Handle expression statement unwrapping - // Example: console.log(eval('1;')) → console.log(1) - if (targetNode.parentNode.type !== 'ExpressionStatement' && + targetNode = targetNode.parentNode; + } + + // Handle expression statement unwrapping + // Example: console.log(eval('1;')) → console.log(1) + if (targetNode.parentNode.type !== 'ExpressionStatement' && replacementNode.type === 'ExpressionStatement') { - replacementNode = replacementNode.expression; - } - - arb.markNode(targetNode, replacementNode); - } catch (e) { - logger.debug(`[-] Unable to replace eval's body with call expression: ${e}`); - } - - return arb; + replacementNode = replacementNode.expression; + } + + arb.markNode(targetNode, replacementNode); + } catch (e) { + logger.debug(`[-] Unable to replace eval's body with call expression: ${e}`); + } + + return arb; } /** * Replace eval call expressions with their literal content without executing eval. - * + * * This transformation safely replaces eval() calls that contain string literals * with the actual AST nodes that the strings represent. This improves code * readability and removes the security concerns associated with eval. - * + * * Examples: * - eval('console.log("hello")') → console.log("hello") * - eval('a; b;') → {a; b;} * - console.log(eval('1;')) → console.log(1) * - eval('Function')('code') → Function('code') - * + * * @param {Arborist} arb - The arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified arborist instance */ export default function replaceEvalCallsWithLiteralContent(arb, candidateFilter = () => true) { - // Find all matching eval call expressions - const matches = replaceEvalCallsWithLiteralContentMatch(arb, candidateFilter); - - // Transform each matching node - for (let i = 0; i < matches.length; i++) { - arb = replaceEvalCallsWithLiteralContentTransform(arb, matches[i]); - } - - return arb; + // Find all matching eval call expressions + const matches = replaceEvalCallsWithLiteralContentMatch(arb, candidateFilter); + + // Transform each matching node + for (let i = 0; i < matches.length; i++) { + arb = replaceEvalCallsWithLiteralContentTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/replaceFunctionShellsWithWrappedValue.js b/src/modules/safe/replaceFunctionShellsWithWrappedValue.js index cea1090..82a0a7d 100644 --- a/src/modules/safe/replaceFunctionShellsWithWrappedValue.js +++ b/src/modules/safe/replaceFunctionShellsWithWrappedValue.js @@ -2,110 +2,110 @@ const RETURNABLE_TYPES = ['Literal', 'Identifier']; /** * Find all function declarations that only return a simple literal or identifier. - * + * * This function identifies function declarations that act as "shells" around simple * values, containing only a single return statement that returns either a literal * or an identifier. Such functions can be optimized by replacing calls to them * with their return values directly. - * + * * Algorithm: * 1. Find all function declarations in the AST * 2. Check if function body contains exactly one return statement * 3. Verify the return argument is a literal or identifier * 4. Apply candidate filter for additional constraints * 5. Return matching function declaration nodes - * + * * @param {Arborist} arb - The arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {ASTNode[]} Array of function declaration nodes that can be replaced */ export function replaceFunctionShellsWithWrappedValueMatch(arb, candidateFilter = () => true) { - // Direct access to typeMap without spread operator for better performance - const relevantNodes = arb.ast[0].typeMap.FunctionDeclaration; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const node = relevantNodes[i]; - - // Check if function has exactly one return statement with simple argument - if (node.body.body?.[0]?.type === 'ReturnStatement' && + // Direct access to typeMap without spread operator for better performance + const relevantNodes = arb.ast[0].typeMap.FunctionDeclaration; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const node = relevantNodes[i]; + + // Check if function has exactly one return statement with simple argument + if (node.body.body?.[0]?.type === 'ReturnStatement' && RETURNABLE_TYPES.includes(node.body.body[0]?.argument?.type) && candidateFilter(node)) { - matches.push(node); - } - } - - return matches; + matches.push(node); + } + } + + return matches; } /** * Transform function shell calls by replacing them with their wrapped values. - * + * * This function replaces call expressions to function shells with the actual * values they return. It only transforms actual function calls (not references) * to ensure the transformation maintains the original semantics. - * + * * Safety considerations: * - Only replaces call expressions where the function is the callee * - Preserves function references that are not called * - Maintains original execution semantics - * + * * @param {Arborist} arb - The arborist instance to modify * @param {Object} node - The function declaration node to process * @return {Arborist} The modified arborist instance */ export function replaceFunctionShellsWithWrappedValueTransform(arb, node) { - // Extract the return value from the function body - const replacementNode = node.body.body[0].argument; - - // Process all references to this function - for (const ref of (node.id?.references || [])) { - // Only replace call expressions, not function references - // This ensures we don't break code that passes the function around - if (ref.parentNode.type === 'CallExpression' && ref.parentNode.callee === ref) { - arb.markNode(ref.parentNode, replacementNode); - } - } - - return arb; + // Extract the return value from the function body + const replacementNode = node.body.body[0].argument; + + // Process all references to this function + for (const ref of (node.id?.references || [])) { + // Only replace call expressions, not function references + // This ensures we don't break code that passes the function around + if (ref.parentNode.type === 'CallExpression' && ref.parentNode.callee === ref) { + arb.markNode(ref.parentNode, replacementNode); + } + } + + return arb; } /** * Replace function shells with their wrapped values for optimization. - * + * * This module identifies and optimizes "function shells" - functions that serve * no purpose other than wrapping a simple literal or identifier value. Such * functions are common in obfuscated code where simple values are hidden * behind function calls. - * + * * Transformations: * function a() { return 42; } → (calls to a() become 42) * function b() { return String; } → (calls to b() become String) * function c() { return x; } → (calls to c() become x) - * + * * Safety features: * - Only processes functions with exactly one return statement * - Only replaces function calls, not function references * - Preserves functions passed as arguments or assigned to properties * - Only handles simple return types (literals and identifiers) - * + * * Performance benefits: * - Eliminates unnecessary function call overhead * - Reduces code size by removing wrapper functions * - Improves readability by exposing actual values - * + * * @param {Arborist} arb - The arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified arborist instance */ export default function replaceFunctionShellsWithWrappedValue(arb, candidateFilter = () => true) { - // Find all matching function declaration nodes - const matches = replaceFunctionShellsWithWrappedValueMatch(arb, candidateFilter); - - // Transform each matching function by replacing its calls - for (let i = 0; i < matches.length; i++) { - arb = replaceFunctionShellsWithWrappedValueTransform(arb, matches[i]); - } - - return arb; + // Find all matching function declaration nodes + const matches = replaceFunctionShellsWithWrappedValueMatch(arb, candidateFilter); + + // Transform each matching function by replacing its calls + for (let i = 0; i < matches.length; i++) { + arb = replaceFunctionShellsWithWrappedValueTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/replaceFunctionShellsWithWrappedValueIIFE.js b/src/modules/safe/replaceFunctionShellsWithWrappedValueIIFE.js index 5831d1a..21bc13b 100644 --- a/src/modules/safe/replaceFunctionShellsWithWrappedValueIIFE.js +++ b/src/modules/safe/replaceFunctionShellsWithWrappedValueIIFE.js @@ -3,12 +3,12 @@ const RETURNABLE_TYPES = ['Literal', 'Identifier']; /** * Find all IIFE function expressions that only return a simple literal or identifier. - * + * * This function identifies Immediately Invoked Function Expressions (IIFEs) that act * as "shells" around simple values. These are function expressions that are immediately * called with no arguments and contain only a single return statement returning either * a literal or an identifier. - * + * * Algorithm: * 1. Find all function expressions in the AST * 2. Check if they are used as callees (IIFE pattern) @@ -17,98 +17,98 @@ const RETURNABLE_TYPES = ['Literal', 'Identifier']; * 5. Verify the return argument is a literal or identifier * 6. Apply candidate filter for additional constraints * 7. Return matching function expression nodes - * + * * @param {Arborist} arb - The arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {ASTNode[]} Array of function expression nodes that can be replaced */ export function replaceFunctionShellsWithWrappedValueIIFEMatch(arb, candidateFilter = () => true) { - // Direct access to typeMap without spread operator for better performance - const relevantNodes = arb.ast[0].typeMap.FunctionExpression; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const node = relevantNodes[i]; - - // Optimized condition ordering: cheapest checks first for better performance - // Also added safety checks to prevent potential runtime errors - if (candidateFilter(node) && + // Direct access to typeMap without spread operator for better performance + const relevantNodes = arb.ast[0].typeMap.FunctionExpression; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const node = relevantNodes[i]; + + // Optimized condition ordering: cheapest checks first for better performance + // Also added safety checks to prevent potential runtime errors + if (candidateFilter(node) && node.parentKey === 'callee' && node.parentNode && !node.parentNode.arguments.length && node.body?.body?.[0]?.type === 'ReturnStatement' && RETURNABLE_TYPES.includes(node.body.body[0].argument?.type)) { - matches.push(node); - } - } - - return matches; + matches.push(node); + } + } + + return matches; } /** * Transform IIFE function shells by replacing them with their wrapped values. - * + * * This function replaces Immediately Invoked Function Expression (IIFE) calls * that only return simple values with the actual values themselves. This removes * the overhead of function creation and invocation for simple value wrapping. - * + * * The transformation changes patterns like (function(){return value})() to just value. - * + * * @param {Arborist} arb - The arborist instance to modify * @param {Object} node - The function expression node to process * @return {Arborist} The modified arborist instance */ export function replaceFunctionShellsWithWrappedValueIIFETransform(arb, node) { - // Extract the return value from the function body - const replacementNode = node.body.body[0].argument; - - // Replace the entire IIFE call expression with the return value - // node.parentNode is the call expression (function(){...})(), we replace it with just the value - arb.markNode(node.parentNode, replacementNode); - - return arb; + // Extract the return value from the function body + const replacementNode = node.body.body[0].argument; + + // Replace the entire IIFE call expression with the return value + // node.parentNode is the call expression (function(){...})(), we replace it with just the value + arb.markNode(node.parentNode, replacementNode); + + return arb; } /** * Replace IIFE function shells with their wrapped values for optimization. - * + * * This module identifies and optimizes Immediately Invoked Function Expression (IIFE) * "shells" - function expressions that are immediately called with no arguments and * serve no purpose other than wrapping a simple literal or identifier value. Such * patterns are common in obfuscated code where simple values are hidden behind * function calls. - * + * * Transformations: * (function() { return 42; })() → 42 * (function() { return String; })() → String * (function() { return x; })() → x * (function() { return "test"; })() → "test" - * + * * Safety features: * - Only processes function expressions used as callees (IIFE pattern) * - Only handles calls with no arguments to preserve semantics * - Only processes functions with exactly one return statement * - Only handles simple return types (literals and identifiers) * - Preserves execution order and side effects - * + * * Performance benefits: * - Eliminates unnecessary function creation and invocation overhead * - Reduces code size by removing wrapper functions * - Improves readability by exposing actual values * - Enables further optimization opportunities - * + * * @param {Arborist} arb - The arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified arborist instance */ export default function replaceFunctionShellsWithWrappedValueIIFE(arb, candidateFilter = () => true) { - // Find all matching IIFE function expression nodes - const matches = replaceFunctionShellsWithWrappedValueIIFEMatch(arb, candidateFilter); - - // Transform each matching IIFE by replacing it with its return value - for (let i = 0; i < matches.length; i++) { - arb = replaceFunctionShellsWithWrappedValueIIFETransform(arb, matches[i]); - } - - return arb; + // Find all matching IIFE function expression nodes + const matches = replaceFunctionShellsWithWrappedValueIIFEMatch(arb, candidateFilter); + + // Transform each matching IIFE by replacing it with its return value + for (let i = 0; i < matches.length; i++) { + arb = replaceFunctionShellsWithWrappedValueIIFETransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/replaceIdentifierWithFixedAssignedValue.js b/src/modules/safe/replaceIdentifierWithFixedAssignedValue.js index 2f9ec3e..8258835 100644 --- a/src/modules/safe/replaceIdentifierWithFixedAssignedValue.js +++ b/src/modules/safe/replaceIdentifierWithFixedAssignedValue.js @@ -2,27 +2,27 @@ import {areReferencesModified} from '../utils/areReferencesModified.js'; /** * Check if an identifier is a property name in an object expression. - * + * * This helper function determines if an identifier node is being used as a property * name in an object literal, which should not be replaced with its literal value * as that would change the object's structure. - * + * * @param {Object} n - The identifier node to check * @return {boolean} True if the identifier is a property name in an object expression */ function isObjectPropertyName(n) { - return n.parentKey === 'property' && + return n.parentKey === 'property' && n.parentNode?.type === 'ObjectExpression'; } /** * Find all identifiers with fixed literal assigned values that can be replaced. - * + * * This function identifies identifier nodes that: * - Have a declaration with a literal initializer (e.g., const x = 42) * - Are not used as property names in object expressions * - Have references that are not modified elsewhere in the code - * + * * Algorithm: * 1. Find all identifier nodes in the AST * 2. Check if they have a declaration with a literal init value @@ -30,95 +30,95 @@ function isObjectPropertyName(n) { * 4. Ensure their references aren't modified * 5. Apply candidate filter for additional constraints * 6. Return matching identifier nodes - * + * * @param {Arborist} arb - The arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {ASTNode[]} Array of identifier nodes that can have their references replaced */ export function replaceIdentifierWithFixedAssignedValueMatch(arb, candidateFilter = () => true) { - // Direct access to typeMap without spread operator for better performance - const relevantNodes = arb.ast[0].typeMap.Identifier; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Optimized condition ordering: cheapest checks first for better performance - // Added safety checks to prevent potential runtime errors - if (candidateFilter(n) && + // Direct access to typeMap without spread operator for better performance + const relevantNodes = arb.ast[0].typeMap.Identifier; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Optimized condition ordering: cheapest checks first for better performance + // Added safety checks to prevent potential runtime errors + if (candidateFilter(n) && !isObjectPropertyName(n) && n.declNode?.parentNode?.init?.type === 'Literal' && n.declNode.references && !areReferencesModified(arb.ast, n.declNode.references)) { - matches.push(n); - } - } - - return matches; + matches.push(n); + } + } + + return matches; } /** * Transform identifier references by replacing them with their fixed literal values. - * + * * This function replaces all references to an identifier with its literal value, * effectively performing constant propagation optimization. It ensures that the * original declaration and its literal value are preserved while replacing only * the references. - * + * * @param {Arborist} arb - The arborist instance to modify * @param {Object} n - The identifier node whose references should be replaced * @return {Arborist} The modified arborist instance */ export function replaceIdentifierWithFixedAssignedValueTransform(arb, n) { - // Extract the literal value from the declaration - const valueNode = n.declNode.parentNode.init; - const refs = n.declNode.references; - - // Replace all references with the literal value - // Note: We use traditional for loop for better performance - for (let i = 0; i < refs.length; i++) { - arb.markNode(refs[i], valueNode); - } - - return arb; + // Extract the literal value from the declaration + const valueNode = n.declNode.parentNode.init; + const refs = n.declNode.references; + + // Replace all references with the literal value + // Note: We use traditional for loop for better performance + for (let i = 0; i < refs.length; i++) { + arb.markNode(refs[i], valueNode); + } + + return arb; } /** * Replace identifier references with their fixed assigned literal values. - * + * * This module performs constant propagation by identifying variables that are * assigned literal values and never modified, then replacing all references to * those variables with their literal values directly. This optimization improves * code readability and enables further optimizations. - * + * * Transformations: * const x = 42; y = x + 1; → const x = 42; y = 42 + 1; * let msg = "hello"; console.log(msg); → let msg = "hello"; console.log("hello"); * var flag = true; if (flag) {...} → var flag = true; if (true) {...} - * + * * Safety features: * - Only processes identifiers with literal initializers * - Skips identifiers used as object property names to preserve structure * - Uses reference analysis to ensure variables are never modified * - Preserves original declaration for debugging and readability - * + * * Performance benefits: * - Eliminates variable lookups at runtime * - Enables further optimization opportunities (dead code elimination, etc.) * - Improves code clarity by making values explicit * - Reduces memory usage by eliminating variable references - * + * * @param {Arborist} arb - The arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified arborist instance */ export default function replaceIdentifierWithFixedAssignedValue(arb, candidateFilter = () => true) { - // Find all matching identifier nodes - const matches = replaceIdentifierWithFixedAssignedValueMatch(arb, candidateFilter); - - // Transform each matching identifier by replacing its references - for (let i = 0; i < matches.length; i++) { - arb = replaceIdentifierWithFixedAssignedValueTransform(arb, matches[i]); - } - return arb; + // Find all matching identifier nodes + const matches = replaceIdentifierWithFixedAssignedValueMatch(arb, candidateFilter); + + // Transform each matching identifier by replacing its references + for (let i = 0; i < matches.length; i++) { + arb = replaceIdentifierWithFixedAssignedValueTransform(arb, matches[i]); + } + return arb; } \ No newline at end of file diff --git a/src/modules/safe/replaceIdentifierWithFixedValueNotAssignedAtDeclaration.js b/src/modules/safe/replaceIdentifierWithFixedValueNotAssignedAtDeclaration.js index b97b594..d2c6480 100644 --- a/src/modules/safe/replaceIdentifierWithFixedValueNotAssignedAtDeclaration.js +++ b/src/modules/safe/replaceIdentifierWithFixedValueNotAssignedAtDeclaration.js @@ -13,7 +13,7 @@ const FOR_STATEMENT_REGEX = /For.*Statement/; * @return {boolean} True if reference is a for-loop iterator */ function isForLoopIterator(ref) { - return FOR_STATEMENT_REGEX.test(ref.parentNode.type) && ref.parentKey === 'left'; + return FOR_STATEMENT_REGEX.test(ref.parentNode.type) && ref.parentKey === 'left'; } /** @@ -27,15 +27,15 @@ function isForLoopIterator(ref) { * @return {boolean} True if reference is in conditional context */ function isInConditionalContext(ref) { - // Check up to 3 levels up the AST for ConditionalExpression - let currentNode = ref.parentNode; - for (let depth = 0; depth < 3 && currentNode; depth++) { - if (currentNode.type === 'ConditionalExpression') { - return true; - } - currentNode = currentNode.parentNode; - } - return false; + // Check up to 3 levels up the AST for ConditionalExpression + let currentNode = ref.parentNode; + for (let depth = 0; depth < 3 && currentNode; depth++) { + if (currentNode.type === 'ConditionalExpression') { + return true; + } + currentNode = currentNode.parentNode; + } + return false; } /** @@ -48,14 +48,14 @@ function isInConditionalContext(ref) { * @return {Object|null} The assignment reference or null */ function getSingleAssignmentReference(n) { - if (!n.references?.length) return null; - - const assignmentRefs = n.references.filter(r => - r.parentNode.type === 'AssignmentExpression' && - getMainDeclaredObjectOfMemberExpression(r.parentNode.left) === r - ); - - return assignmentRefs.length === 1 ? assignmentRefs[0] : null; + if (!n.references?.length) return null; + + const assignmentRefs = n.references.filter(r => + r.parentNode.type === 'AssignmentExpression' && + getMainDeclaredObjectOfMemberExpression(r.parentNode.left) === r, + ); + + return assignmentRefs.length === 1 ? assignmentRefs[0] : null; } /** @@ -77,36 +77,36 @@ function getSingleAssignmentReference(n) { * @return {ASTNode[]} Array of identifier nodes that can be safely replaced */ export function replaceIdentifierWithFixedValueNotAssignedAtDeclarationMatch(arb, candidateFilter = () => true) { - // Direct access to typeMap without spread operator for better performance - const relevantNodes = arb.ast[0].typeMap.Identifier; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Optimized condition ordering: cheapest checks first for better performance - if (candidateFilter(n) && + // Direct access to typeMap without spread operator for better performance + const relevantNodes = arb.ast[0].typeMap.Identifier; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Optimized condition ordering: cheapest checks first for better performance + if (candidateFilter(n) && n.parentNode?.type === 'VariableDeclarator' && !n.parentNode.init && // Variable declared without initial value n.references.length) { - - // Check for exactly one assignment to a literal value - const assignmentRef = getSingleAssignmentReference(n); - if (assignmentRef && assignmentRef.parentNode.right.type === 'Literal') { - - // Ensure no unsafe usage patterns exist - const hasUnsafeReferences = n.references.some(r => - isForLoopIterator(r) || isInConditionalContext(r) - ); - - if (!hasUnsafeReferences) { - matches.push(n); - } - } - } - } - - return matches; + + // Check for exactly one assignment to a literal value + const assignmentRef = getSingleAssignmentReference(n); + if (assignmentRef && assignmentRef.parentNode.right.type === 'Literal') { + + // Ensure no unsafe usage patterns exist + const hasUnsafeReferences = n.references.some(r => + isForLoopIterator(r) || isInConditionalContext(r), + ); + + if (!hasUnsafeReferences) { + matches.push(n); + } + } + } + } + + return matches; } /** @@ -121,42 +121,42 @@ export function replaceIdentifierWithFixedValueNotAssignedAtDeclarationMatch(arb * @return {Arborist} The modified arborist instance */ export function replaceIdentifierWithFixedValueNotAssignedAtDeclarationTransform(arb, n) { - // Get the single assignment reference (validated in match function) - const assignmentRef = getSingleAssignmentReference(n); - const valueNode = assignmentRef.parentNode.right; - - // Get all references except the assignment itself - const referencesToReplace = n.references.filter(r => r !== assignmentRef); - - // Additional safety check: ensure references aren't modified in complex ways - if (!areReferencesModified(arb.ast, referencesToReplace)) { - for (let i = 0; i < referencesToReplace.length; i++) { - const ref = referencesToReplace[i]; - - // Skip function calls where identifier is the callee - // Example: let func; func = someFunction; func(); // Don't replace func() - if (ref.parentNode.type === 'CallExpression' && ref.parentKey === 'callee') { - continue; - } - - // Check if the reference is in the same scope as the assignment - let scopesMatches = true; - for (let j = 0; j < assignmentRef.lineage.length; j++) { - if (assignmentRef.lineage[j] !== ref.lineage[j]) { - scopesMatches = false; - break; - } - } - - if (scopesMatches) { - // Replace the reference with the literal value - arb.markNode(ref, valueNode); - } - - } - } - - return arb; + // Get the single assignment reference (validated in match function) + const assignmentRef = getSingleAssignmentReference(n); + const valueNode = assignmentRef.parentNode.right; + + // Get all references except the assignment itself + const referencesToReplace = n.references.filter(r => r !== assignmentRef); + + // Additional safety check: ensure references aren't modified in complex ways + if (!areReferencesModified(arb.ast, referencesToReplace)) { + for (let i = 0; i < referencesToReplace.length; i++) { + const ref = referencesToReplace[i]; + + // Skip function calls where identifier is the callee + // Example: let func; func = someFunction; func(); // Don't replace func() + if (ref.parentNode.type === 'CallExpression' && ref.parentKey === 'callee') { + continue; + } + + // Check if the reference is in the same scope as the assignment + let scopesMatches = true; + for (let j = 0; j < assignmentRef.lineage.length; j++) { + if (assignmentRef.lineage[j] !== ref.lineage[j]) { + scopesMatches = false; + break; + } + } + + if (scopesMatches) { + // Replace the reference with the literal value + arb.markNode(ref, valueNode); + } + + } + } + + return arb; } /** @@ -181,13 +181,13 @@ export function replaceIdentifierWithFixedValueNotAssignedAtDeclarationTransform * @return {Arborist} The modified arborist instance */ export default function replaceIdentifierWithFixedValueNotAssignedAtDeclaration(arb, candidateFilter = () => true) { - // Find all matching identifier nodes - const matches = replaceIdentifierWithFixedValueNotAssignedAtDeclarationMatch(arb, candidateFilter); - - // Transform each matching node - for (let i = 0; i < matches.length; i++) { - arb = replaceIdentifierWithFixedValueNotAssignedAtDeclarationTransform(arb, matches[i]); - } - - return arb; + // Find all matching identifier nodes + const matches = replaceIdentifierWithFixedValueNotAssignedAtDeclarationMatch(arb, candidateFilter); + + // Transform each matching node + for (let i = 0; i < matches.length; i++) { + arb = replaceIdentifierWithFixedValueNotAssignedAtDeclarationTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/replaceNewFuncCallsWithLiteralContent.js b/src/modules/safe/replaceNewFuncCallsWithLiteralContent.js index 2517201..5dc3fc2 100644 --- a/src/modules/safe/replaceNewFuncCallsWithLiteralContent.js +++ b/src/modules/safe/replaceNewFuncCallsWithLiteralContent.js @@ -14,35 +14,35 @@ import {generateHash} from '../utils/generateHash.js'; * @return {ASTNode} The parsed AST node */ function parseCodeStringToAST(codeStr) { - if (!codeStr) { - return { - type: 'Literal', - value: codeStr, - }; - } - - const body = generateFlatAST(codeStr, {detailed: false, includeSrc: false})[0].body; - - if (body.length > 1) { - return { - type: 'BlockStatement', - body, - }; - } - - const singleStatement = body[0]; - - // Unwrap single expressions from ExpressionStatement wrapper - if (singleStatement.type === 'ExpressionStatement') { - return singleStatement.expression; - } - - // For immediately-executed functions, unwrap single return statements - if (singleStatement.type === 'ReturnStatement' && singleStatement.argument) { - return singleStatement.argument; - } - - return singleStatement; + if (!codeStr) { + return { + type: 'Literal', + value: codeStr, + }; + } + + const body = generateFlatAST(codeStr, {detailed: false, includeSrc: false})[0].body; + + if (body.length > 1) { + return { + type: 'BlockStatement', + body, + }; + } + + const singleStatement = body[0]; + + // Unwrap single expressions from ExpressionStatement wrapper + if (singleStatement.type === 'ExpressionStatement') { + return singleStatement.expression; + } + + // For immediately-executed functions, unwrap single return statements + if (singleStatement.type === 'ReturnStatement' && singleStatement.argument) { + return singleStatement.argument; + } + + return singleStatement; } /** @@ -58,14 +58,14 @@ function parseCodeStringToAST(codeStr) { * @return {ASTNode} The node that should be replaced */ function getReplacementTarget(callNode, replacementNode) { - // For BlockStatement replacements in standalone expressions, replace the entire ExpressionStatement - if (callNode.parentNode.type === 'ExpressionStatement' && + // For BlockStatement replacements in standalone expressions, replace the entire ExpressionStatement + if (callNode.parentNode.type === 'ExpressionStatement' && replacementNode.type === 'BlockStatement') { - return callNode.parentNode; - } - - // For all other cases (including variable assignments), replace just the call expression - return callNode; + return callNode.parentNode; + } + + // For all other cases (including variable assignments), replace just the call expression + return callNode; } /** @@ -88,26 +88,26 @@ function getReplacementTarget(callNode, replacementNode) { * @return {ASTNode[]} Array of NewExpression nodes that can be safely replaced */ export function replaceNewFuncCallsWithLiteralContentMatch(arb, candidateFilter = () => true) { - // Direct access to typeMap without spread operator for better performance - const relevantNodes = arb.ast[0].typeMap.NewExpression || []; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Optimized condition ordering: cheapest checks first for better performance - if (candidateFilter(n) && + // Direct access to typeMap without spread operator for better performance + const relevantNodes = arb.ast[0].typeMap.NewExpression || []; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Optimized condition ordering: cheapest checks first for better performance + if (candidateFilter(n) && n.parentKey === 'callee' && // Used as callee in immediate call n.callee?.name === 'Function' && // Constructor is 'Function' n.arguments?.length === 1 && // Exactly one argument n.arguments[0].type === 'Literal' && // Argument is a literal string !n.parentNode?.arguments?.length) { // Immediate call has no arguments - - matches.push(n); - } - } - - return matches; + + matches.push(n); + } + } + + return matches; } /** @@ -123,26 +123,26 @@ export function replaceNewFuncCallsWithLiteralContentMatch(arb, candidateFilter * @return {Arborist} The modified arborist instance */ export function replaceNewFuncCallsWithLiteralContentTransform(arb, n) { - const cache = getCache(arb.ast[0].scriptHash); - const targetCodeStr = n.arguments[0].value; - const cacheName = `replaceEval-${generateHash(targetCodeStr)}`; - - try { - // Use cache to avoid re-parsing identical code strings - if (!cache[cacheName]) { - cache[cacheName] = parseCodeStringToAST(targetCodeStr); - } - - const replacementNode = cache[cacheName]; - const targetNode = getReplacementTarget(n.parentNode, replacementNode); - - arb.markNode(targetNode, replacementNode); - } catch (e) { - // Log parsing failures but don't crash the transformation - logger.debug(`[-] Unable to replace new function's body with call expression: ${e}`); - } - - return arb; + const cache = getCache(arb.ast[0].scriptHash); + const targetCodeStr = n.arguments[0].value; + const cacheName = `replaceEval-${generateHash(targetCodeStr)}`; + + try { + // Use cache to avoid re-parsing identical code strings + if (!cache[cacheName]) { + cache[cacheName] = parseCodeStringToAST(targetCodeStr); + } + + const replacementNode = cache[cacheName]; + const targetNode = getReplacementTarget(n.parentNode, replacementNode); + + arb.markNode(targetNode, replacementNode); + } catch (e) { + // Log parsing failures but don't crash the transformation + logger.debug(`[-] Unable to replace new function's body with call expression: ${e}`); + } + + return arb; } /** @@ -168,13 +168,13 @@ export function replaceNewFuncCallsWithLiteralContentTransform(arb, n) { * @return {Arborist} The modified arborist instance */ export default function replaceNewFuncCallsWithLiteralContent(arb, candidateFilter = () => true) { - // Find all matching NewExpression nodes - const matches = replaceNewFuncCallsWithLiteralContentMatch(arb, candidateFilter); - - // Transform each matching node - for (let i = 0; i < matches.length; i++) { - arb = replaceNewFuncCallsWithLiteralContentTransform(arb, matches[i]); - } - - return arb; + // Find all matching NewExpression nodes + const matches = replaceNewFuncCallsWithLiteralContentMatch(arb, candidateFilter); + + // Transform each matching node + for (let i = 0; i < matches.length; i++) { + arb = replaceNewFuncCallsWithLiteralContentTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/replaceSequencesWithExpressions.js b/src/modules/safe/replaceSequencesWithExpressions.js index 25fba8e..79757e1 100644 --- a/src/modules/safe/replaceSequencesWithExpressions.js +++ b/src/modules/safe/replaceSequencesWithExpressions.js @@ -1,156 +1,156 @@ /** * Creates individual expression statements from each expression in a sequence expression. - * + * * This helper function takes an array of expressions from a SequenceExpression * and converts each one into a standalone ExpressionStatement AST node. - * + * * @param {Array} expressions - Array of expression AST nodes from SequenceExpression * @return {ASTNode[]} Array of ExpressionStatement AST nodes */ function createExpressionStatements(expressions) { - const statements = []; - for (let i = 0; i < expressions.length; i++) { - statements.push({ - type: 'ExpressionStatement', - expression: expressions[i] - }); - } - return statements; + const statements = []; + for (let i = 0; i < expressions.length; i++) { + statements.push({ + type: 'ExpressionStatement', + expression: expressions[i], + }); + } + return statements; } /** * Creates a new BlockStatement body by replacing a target statement with multiple statements. - * + * * This optimized implementation avoids spread operators and builds the new array * incrementally for better performance with large parent bodies. - * + * * @param {Array} parentBody - Original body array from BlockStatement - * @param {number} targetIndex - Index of statement to replace + * @param {number} targetIndex - Index of statement to replace * @param {Array} replacementStatements - Array of statements to insert * @return {ASTNode[]} New body array with replacements */ function createReplacementBody(parentBody, targetIndex, replacementStatements) { - const newBody = []; - let newIndex = 0; - - // Copy statements before target - for (let i = 0; i < targetIndex; i++) { - newBody[newIndex++] = parentBody[i]; - } - - // Insert replacement statements - for (let i = 0; i < replacementStatements.length; i++) { - newBody[newIndex++] = replacementStatements[i]; - } - - // Copy statements after target - for (let i = targetIndex + 1; i < parentBody.length; i++) { - newBody[newIndex++] = parentBody[i]; - } - - return newBody; + const newBody = []; + let newIndex = 0; + + // Copy statements before target + for (let i = 0; i < targetIndex; i++) { + newBody[newIndex++] = parentBody[i]; + } + + // Insert replacement statements + for (let i = 0; i < replacementStatements.length; i++) { + newBody[newIndex++] = replacementStatements[i]; + } + + // Copy statements after target + for (let i = targetIndex + 1; i < parentBody.length; i++) { + newBody[newIndex++] = parentBody[i]; + } + + return newBody; } /** * Identifies ExpressionStatement nodes that contain SequenceExpressions suitable for transformation. - * + * * A sequence expression is a candidate for transformation when: - * 1. The node is an ExpressionStatement + * 1. The node is an ExpressionStatement * 2. Its expression property is a SequenceExpression * 3. The SequenceExpression contains multiple expressions to expand * 4. The node passes the candidate filter - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of nodes that can be transformed */ export function replaceSequencesWithExpressionsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.ExpressionStatement || []; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Check if this ExpressionStatement contains a SequenceExpression - if (n.expression && + const relevantNodes = arb.ast[0].typeMap.ExpressionStatement || []; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Check if this ExpressionStatement contains a SequenceExpression + if (n.expression && n.expression.type === 'SequenceExpression' && n.expression.expressions && n.expression.expressions.length > 1 && candidateFilter(n)) { - matches[matches.length] = n; - } - } - - return matches; + matches[matches.length] = n; + } + } + + return matches; } /** * Transforms a SequenceExpression into individual ExpressionStatements. - * + * * The transformation strategy depends on the parent context: - * - If parent is BlockStatement: Replace within the existing block by creating + * - If parent is BlockStatement: Replace within the existing block by creating * a new BlockStatement with the sequence expanded into individual statements - * - If parent is not BlockStatement: Replace the ExpressionStatement with a + * - If parent is not BlockStatement: Replace the ExpressionStatement with a * new BlockStatement containing the individual statements - * + * * This ensures proper AST structure while expanding sequence expressions into * separate executable statements. - * + * * @param {Arborist} arb - The Arborist instance to mark changes on * @param {Object} n - The ExpressionStatement node containing SequenceExpression * @return {Arborist} The modified Arborist instance */ export function replaceSequencesWithExpressionsTransform(arb, n) { - const parent = n.parentNode; - const statements = createExpressionStatements(n.expression.expressions); - - if (parent && parent.type === 'BlockStatement') { - // Find target statement position within parent block - const currentIdx = parent.body.indexOf(n); - - if (currentIdx !== -1) { - // Create new BlockStatement with sequence expanded inline - const replacementNode = { - type: 'BlockStatement', - body: createReplacementBody(parent.body, currentIdx, statements) - }; - arb.markNode(parent, replacementNode); - } - } else { - // Replace ExpressionStatement with BlockStatement containing individual statements - const blockStatement = { - type: 'BlockStatement', - body: statements - }; - arb.markNode(n, blockStatement); - } - - return arb; + const parent = n.parentNode; + const statements = createExpressionStatements(n.expression.expressions); + + if (parent && parent.type === 'BlockStatement') { + // Find target statement position within parent block + const currentIdx = parent.body.indexOf(n); + + if (currentIdx !== -1) { + // Create new BlockStatement with sequence expanded inline + const replacementNode = { + type: 'BlockStatement', + body: createReplacementBody(parent.body, currentIdx, statements), + }; + arb.markNode(parent, replacementNode); + } + } else { + // Replace ExpressionStatement with BlockStatement containing individual statements + const blockStatement = { + type: 'BlockStatement', + body: statements, + }; + arb.markNode(n, blockStatement); + } + + return arb; } /** * All expressions within a sequence will be replaced by their own expression statement. - * + * * This transformation converts SequenceExpressions into individual ExpressionStatements * to improve code readability and enable better analysis. For example: - * + * * Input: if (a) (b(), c()); * Output: if (a) { b(); c(); } - * + * * The transformation handles both cases where the sequence is: * 1. Already within a BlockStatement (inserts statements inline) * 2. Not within a BlockStatement (creates new BlockStatement) - * + * * @param {Arborist} arb - The Arborist instance containing the AST to transform * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified Arborist instance */ export default function replaceSequencesWithExpressions(arb, candidateFilter = () => true) { - const matches = replaceSequencesWithExpressionsMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = replaceSequencesWithExpressionsTransform(arb, matches[i]); - } - - return arb; + const matches = replaceSequencesWithExpressionsMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = replaceSequencesWithExpressionsTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/resolveDeterministicIfStatements.js b/src/modules/safe/resolveDeterministicIfStatements.js index d663395..e5a0dfd 100644 --- a/src/modules/safe/resolveDeterministicIfStatements.js +++ b/src/modules/safe/resolveDeterministicIfStatements.js @@ -1,183 +1,183 @@ /** * Determines whether a literal value is truthy in JavaScript context. - * + * * This helper evaluates literal values according to JavaScript truthiness rules: * - false, 0, -0, 0n, "", null, undefined, NaN are falsy * - All other values are truthy - * + * * @param {*} value - The literal value to evaluate * @return {boolean} Whether the value is truthy */ function isLiteralTruthy(value) { - // Handle special JavaScript falsy values - if (value === false || value === 0 || value === -0 || value === 0n || + // Handle special JavaScript falsy values + if (value === false || value === 0 || value === -0 || value === 0n || value === '' || value === null || value === undefined) { - return false; - } - - // Handle NaN (NaN !== NaN is true) - if (typeof value === 'number' && value !== value) { - return false; - } - - return true; + return false; + } + + // Handle NaN (NaN !== NaN is true) + if (typeof value === 'number' && value !== value) { + return false; + } + + return true; } /** * Evaluates a test condition to get its literal value for truthiness testing. - * + * * Handles both direct literals and unary expressions with literal arguments: * - Literal nodes: return the literal value directly * - UnaryExpression nodes: evaluate the unary operation and return result - * + * * @param {ASTNode} testNode - The test condition AST node (Literal or UnaryExpression) * @return {*} The evaluated literal value */ function evaluateTestValue(testNode) { - if (testNode.type === 'Literal') { - return testNode.value; - } - - if (testNode.type === 'UnaryExpression' && testNode.argument.type === 'Literal') { - const argument = testNode.argument.value; - const operator = testNode.operator; - - switch (operator) { - case '-': - return -argument; - case '+': - return +argument; - case '!': - return !argument; - case '~': - return ~argument; - default: - // For any other unary operators, return the original argument - return argument; - } - } - - // Fallback (should not reach here if match function works correctly) - return testNode.value; + if (testNode.type === 'Literal') { + return testNode.value; + } + + if (testNode.type === 'UnaryExpression' && testNode.argument.type === 'Literal') { + const argument = testNode.argument.value; + const operator = testNode.operator; + + switch (operator) { + case '-': + return -argument; + case '+': + return +argument; + case '!': + return !argument; + case '~': + return ~argument; + default: + // For any other unary operators, return the original argument + return argument; + } + } + + // Fallback (should not reach here if match function works correctly) + return testNode.value; } /** * Gets the appropriate replacement node for a resolved if statement. - * + * * When an if statement can be resolved deterministically: * - If test is truthy: return consequent (or null if no consequent) * - If test is falsy: return alternate (or null if no alternate) - * + * * Returning null indicates the if statement should be removed entirely. * Handles both Literal and UnaryExpression test conditions. - * + * * @param {ASTNode} ifNode - The IfStatement AST node to resolve * @return {ASTNode|null} The replacement node or null to remove */ function getReplacementNode(ifNode) { - const testValue = evaluateTestValue(ifNode.test); - const isTestTruthy = isLiteralTruthy(testValue); - - if (isTestTruthy) { - // Test condition is truthy - use consequent - return ifNode.consequent || null; - } else { - // Test condition is falsy - use alternate - return ifNode.alternate || null; - } + const testValue = evaluateTestValue(ifNode.test); + const isTestTruthy = isLiteralTruthy(testValue); + + if (isTestTruthy) { + // Test condition is truthy - use consequent + return ifNode.consequent || null; + } else { + // Test condition is falsy - use alternate + return ifNode.alternate || null; + } } /** * Identifies IfStatement nodes with literal test conditions that can be resolved deterministically. - * + * * An if statement is a candidate for resolution when: * 1. The node is an IfStatement * 2. The test condition is a Literal (constant value) or UnaryExpression with literal argument * 3. The node passes the candidate filter - * + * * These conditions ensure the if statement's outcome is known at static analysis time. * Handles cases like: if (true), if (false), if (1), if (0), if (""), if (-1), etc. - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of IfStatement nodes that can be resolved */ export function resolveDeterministicIfStatementsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.IfStatement || []; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - if (!n.test || !candidateFilter(n)) { - continue; - } - - // Check if test condition is a literal - if (n.test.type === 'Literal') { - matches.push(n); - } - // Check if test condition is a unary expression with literal argument (e.g., -1, +5) - else if (n.test.type === 'UnaryExpression' && + const relevantNodes = arb.ast[0].typeMap.IfStatement || []; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + if (!n.test || !candidateFilter(n)) { + continue; + } + + // Check if test condition is a literal + if (n.test.type === 'Literal') { + matches.push(n); + } + // Check if test condition is a unary expression with literal argument (e.g., -1, +5) + else if (n.test.type === 'UnaryExpression' && n.test.argument && n.test.argument.type === 'Literal') { - matches.push(n); - } - } - - return matches; + matches.push(n); + } + } + + return matches; } /** * Transforms an IfStatement with a literal test condition into its resolved form. - * + * * The transformation logic: * - If test value is truthy: replace with consequent (if exists) or remove entirely * - If test value is falsy: replace with alternate (if exists) or remove entirely - * + * * This transformation eliminates dead code by resolving conditional branches * that will always take the same path at runtime. - * + * * @param {Arborist} arb - The Arborist instance to mark changes on * @param {ASTNode} n - The IfStatement node to transform * @return {Arborist} The modified Arborist instance */ export function resolveDeterministicIfStatementsTransform(arb, n) { - const replacementNode = getReplacementNode(n); - - if (replacementNode) { - // Replace if statement with the appropriate branch - arb.markNode(n, replacementNode); - } else { - // Remove if statement entirely (no consequent/alternate to execute) - arb.markNode(n); - } - - return arb; + const replacementNode = getReplacementNode(n); + + if (replacementNode) { + // Replace if statement with the appropriate branch + arb.markNode(n, replacementNode); + } else { + // Remove if statement entirely (no consequent/alternate to execute) + arb.markNode(n); + } + + return arb; } /** * Replace if statements which will always resolve the same way with their relevant consequent or alternative. - * + * * This transformation eliminates deterministic conditional statements where the test condition * is a literal value, allowing static resolution of the control flow. For example: - * + * * Input: if (true) do_a(); else do_b(); if (false) do_c(); else do_d(); * Output: do_a(); do_d(); - * + * * The transformation handles all JavaScript falsy values correctly (false, 0, "", null, etc.) * and ensures proper cleanup of dead code branches. - * + * * @param {Arborist} arb - The Arborist instance containing the AST to transform * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified Arborist instance */ export default function resolveDeterministicIfStatements(arb, candidateFilter = () => true) { - const matches = resolveDeterministicIfStatementsMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = resolveDeterministicIfStatementsTransform(arb, matches[i]); - } - - return arb; + const matches = resolveDeterministicIfStatementsMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = resolveDeterministicIfStatementsTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/resolveFunctionConstructorCalls.js b/src/modules/safe/resolveFunctionConstructorCalls.js index 4760ebf..fa680a9 100644 --- a/src/modules/safe/resolveFunctionConstructorCalls.js +++ b/src/modules/safe/resolveFunctionConstructorCalls.js @@ -2,164 +2,164 @@ import {generateFlatAST} from 'flast'; /** * Builds the function arguments string from constructor arguments. - * + * * When Function.constructor is called with multiple arguments, all but the last * are parameter names, and the last is the function body. This helper extracts * and formats the parameter names properly. - * + * * @param {Array} args - Array of literal argument values * @return {string} Comma-separated parameter names */ function buildArgumentsString(args) { - if (args.length <= 1) { - return ''; - } - - // All arguments except the last are parameter names - const paramNames = []; - for (let i = 0; i < args.length - 1; i++) { - paramNames.push(args[i]); - } - - return paramNames.join(', '); + if (args.length <= 1) { + return ''; + } + + // All arguments except the last are parameter names + const paramNames = []; + for (let i = 0; i < args.length - 1; i++) { + paramNames.push(args[i]); + } + + return paramNames.join(', '); } /** * Generates a function expression AST node from constructor arguments. - * + * * This function recreates the same behavior as Function.constructor by: * 1. Taking all but the last argument as parameter names * 2. Using the last argument as the function body * 3. Wrapping in a function expression for valid syntax * 4. Generating AST without nodeIds to avoid conflicts - * + * * @param {Array} argumentValues - Array of literal values from constructor call * @return {ASTNode|null} Function expression AST node or null if generation fails */ function generateFunctionExpression(argumentValues) { - const argsString = buildArgumentsString(argumentValues); - const code = argumentValues[argumentValues.length - 1]; - - try { - // Create function expression string matching Function.constructor behavior - const functionCode = `(function (${argsString}) {${code}})`; - - // Generate AST without nodeIds to avoid duplicates with existing code - const ast = generateFlatAST(functionCode, {detailed: false, includeSrc: false}); - - // Return the function expression node (index 2 in the generated AST) - return ast[2] || null; - } catch { - // Return null if code generation fails (invalid syntax, etc.) - return null; - } + const argsString = buildArgumentsString(argumentValues); + const code = argumentValues[argumentValues.length - 1]; + + try { + // Create function expression string matching Function.constructor behavior + const functionCode = `(function (${argsString}) {${code}})`; + + // Generate AST without nodeIds to avoid duplicates with existing code + const ast = generateFlatAST(functionCode, {detailed: false, includeSrc: false}); + + // Return the function expression node (index 2 in the generated AST) + return ast[2] || null; + } catch { + // Return null if code generation fails (invalid syntax, etc.) + return null; + } } /** * Identifies CallExpression nodes that are Function.constructor calls with literal arguments. - * + * * A call expression is a candidate for transformation when: * 1. It's a call to Function.constructor (member expression with 'constructor' property) * 2. All arguments are literal values (required for static analysis) * 3. Has at least one argument (the function body) * 4. Passes the candidate filter - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of CallExpression nodes that can be transformed */ export function resolveFunctionConstructorCallsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.CallExpression || []; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Check if this is a .constructor call - if (!n.callee || + const relevantNodes = arb.ast[0].typeMap.CallExpression || []; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Check if this is a .constructor call + if (!n.callee || n.callee.type !== 'MemberExpression' || !n.callee.property || (n.callee.property.name !== 'constructor' && n.callee.property.value !== 'constructor')) { - continue; - } - - // Must have at least one argument (the function body) - if (!n.arguments || n.arguments.length === 0) { - continue; - } - - // All arguments must be literals for static evaluation - let allLiterals = true; - for (let j = 0; j < n.arguments.length; j++) { - if (n.arguments[j].type !== 'Literal') { - allLiterals = false; - break; - } - } - - if (allLiterals && candidateFilter(n)) { - matches.push(n); - } - } - - return matches; + continue; + } + + // Must have at least one argument (the function body) + if (!n.arguments || n.arguments.length === 0) { + continue; + } + + // All arguments must be literals for static evaluation + let allLiterals = true; + for (let j = 0; j < n.arguments.length; j++) { + if (n.arguments[j].type !== 'Literal') { + allLiterals = false; + break; + } + } + + if (allLiterals && candidateFilter(n)) { + matches.push(n); + } + } + + return matches; } /** * Transforms a Function.constructor call into a function expression. - * + * * The transformation process: * 1. Extract literal values from constructor arguments * 2. Generate equivalent function expression AST * 3. Replace constructor call with function expression - * + * * This transformation is safe because all arguments are literals, ensuring * the function can be statically analyzed and transformed. - * + * * @param {Arborist} arb - The Arborist instance to mark changes on * @param {Object} n - The CallExpression node to transform * @return {Arborist} The modified Arborist instance */ export function resolveFunctionConstructorCallsTransform(arb, n) { - // Extract literal values from arguments - const argumentValues = []; - for (let i = 0; i < n.arguments.length; i++) { - argumentValues.push(n.arguments[i].value); - } - - // Generate equivalent function expression - const functionExpression = generateFunctionExpression(argumentValues); - - if (functionExpression) { - arb.markNode(n, functionExpression); - } - - return arb; + // Extract literal values from arguments + const argumentValues = []; + for (let i = 0; i < n.arguments.length; i++) { + argumentValues.push(n.arguments[i].value); + } + + // Generate equivalent function expression + const functionExpression = generateFunctionExpression(argumentValues); + + if (functionExpression) { + arb.markNode(n, functionExpression); + } + + return arb; } /** * Typical for packers, function constructor calls where the last argument * is a code snippet, should be replaced with the code nodes. - * + * * This transformation converts Function.constructor calls into equivalent function expressions * when all arguments are literal values. For example: - * + * * Input: Function.constructor('a', 'b', 'return a + b') * Output: function (a, b) { return a + b } - * + * * The transformation preserves the exact semantics of Function.constructor while * making the code more readable and enabling further static analysis. - * + * * @param {Arborist} arb - The Arborist instance containing the AST to transform * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified Arborist instance */ export default function resolveFunctionConstructorCalls(arb, candidateFilter = () => true) { - const matches = resolveFunctionConstructorCallsMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = resolveFunctionConstructorCallsTransform(arb, matches[i]); - } - - return arb; + const matches = resolveFunctionConstructorCallsMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = resolveFunctionConstructorCallsTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/resolveMemberExpressionReferencesToArrayIndex.js b/src/modules/safe/resolveMemberExpressionReferencesToArrayIndex.js index cb2de63..dd4f0c6 100644 --- a/src/modules/safe/resolveMemberExpressionReferencesToArrayIndex.js +++ b/src/modules/safe/resolveMemberExpressionReferencesToArrayIndex.js @@ -4,176 +4,176 @@ const MIN_ARRAY_LENGTH = 20; /** * Validates if a property access represents a valid numeric array index. - * + * * Checks that the property is a literal, represents a valid integer, * and is within the bounds of the array. Non-numeric properties like * 'length' or 'indexOf' are excluded. - * + * * @param {ASTNode} memberExpr - The MemberExpression node * @param {number} arrayLength - Length of the array being accessed * @return {boolean} True if this is a valid numeric index access */ function isValidArrayIndex(memberExpr, arrayLength) { - if (!memberExpr.property || memberExpr.property.type !== 'Literal') { - return false; - } - - const value = memberExpr.property.value; - - // Must be a number (not string like 'indexOf' or 'length') - if (typeof value !== 'number') { - return false; - } - - // Must be a valid integer within array bounds - const index = Math.floor(value); - return index >= 0 && index < arrayLength && index === value; + if (!memberExpr.property || memberExpr.property.type !== 'Literal') { + return false; + } + + const value = memberExpr.property.value; + + // Must be a number (not string like 'indexOf' or 'length') + if (typeof value !== 'number') { + return false; + } + + // Must be a valid integer within array bounds + const index = Math.floor(value); + return index >= 0 && index < arrayLength && index === value; } /** * Checks if a reference is a valid candidate for array index resolution. - * + * * Valid candidates are MemberExpression nodes that: * 1. Are not on the left side of assignments (not being modified) * 2. Have numeric literal properties within array bounds * 3. Are not accessing array methods or properties - * + * * @param {ASTNode} ref - Reference node to check * @param {number} arrayLength - Length of the array being accessed * @return {boolean} True if reference can be resolved to array element */ function isResolvableReference(ref, arrayLength) { - // Must be a member expression (array[index] access) - if (ref.type !== 'MemberExpression') { - return false; - } - - // Skip if this reference is being assigned to (left side of assignment) - if (ref.parentNode.type === 'AssignmentExpression' && ref.parentKey === 'left') { - return false; - } - - // Must be a valid numeric array index - return isValidArrayIndex(ref, arrayLength); + // Must be a member expression (array[index] access) + if (ref.type !== 'MemberExpression') { + return false; + } + + // Skip if this reference is being assigned to (left side of assignment) + if (ref.parentNode.type === 'AssignmentExpression' && ref.parentKey === 'left') { + return false; + } + + // Must be a valid numeric array index + return isValidArrayIndex(ref, arrayLength); } /** * Identifies VariableDeclarator nodes with large array initializers that can have their references resolved. - * + * * A variable declarator is a candidate when: * 1. It's initialized with an ArrayExpression * 2. The array has more than MIN_ARRAY_LENGTH elements (performance threshold) * 3. The identifier has references that can be resolved * 4. It passes the candidate filter - * + * * Large arrays are targeted because this optimization is most beneficial for * obfuscated code that uses large lookup tables. - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of VariableDeclarator nodes that can be processed */ export function resolveMemberExpressionReferencesToArrayIndexMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.VariableDeclarator || []; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Must be array initialization with sufficient length - if (!n.init || + const relevantNodes = arb.ast[0].typeMap.VariableDeclarator || []; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Must be array initialization with sufficient length + if (!n.init || n.init.type !== 'ArrayExpression' || n.init.elements.length <= MIN_ARRAY_LENGTH) { - continue; - } - - // Must have identifier with references to resolve - if (!n.id || !n.id.references || n.id.references.length === 0) { - continue; - } - - if (candidateFilter(n)) { - matches.push(n); - } - } - - return matches; + continue; + } + + // Must have identifier with references to resolve + if (!n.id || !n.id.references || n.id.references.length === 0) { + continue; + } + + if (candidateFilter(n)) { + matches.push(n); + } + } + + return matches; } /** * Transforms array index references into their literal values. - * + * * For each reference to the array variable, if it's a valid numeric index access, * replace the member expression with the corresponding array element. - * + * * This transformation is safe because: * - Only literal numeric indices are replaced * - Array bounds are validated * - Assignment targets are excluded - * + * * @param {Arborist} arb - The Arborist instance to mark changes on * @param {ASTNode} n - The VariableDeclarator node with array initialization * @return {Arborist} The modified Arborist instance */ export function resolveMemberExpressionReferencesToArrayIndexTransform(arb, n) { - const arrayElements = n.init.elements; - const arrayLength = arrayElements.length; - - // Get parent nodes of all references (the actual member expressions) - const memberExpressions = []; - for (let i = 0; i < n.id.references.length; i++) { - memberExpressions.push(n.id.references[i].parentNode); - } - - // Process each member expression reference - for (let i = 0; i < memberExpressions.length; i++) { - const memberExpr = memberExpressions[i]; - - if (isResolvableReference(memberExpr, arrayLength)) { - const index = memberExpr.property.value; - const arrayElement = arrayElements[index]; - - // Only replace if the array element exists (handle sparse arrays) - if (arrayElement) { - try { - arb.markNode(memberExpr, arrayElement); - } catch (e) { - logger.debug(`[-] Unable to mark node for replacement: ${e}`); - } - } - } - } - - return arb; + const arrayElements = n.init.elements; + const arrayLength = arrayElements.length; + + // Get parent nodes of all references (the actual member expressions) + const memberExpressions = []; + for (let i = 0; i < n.id.references.length; i++) { + memberExpressions.push(n.id.references[i].parentNode); + } + + // Process each member expression reference + for (let i = 0; i < memberExpressions.length; i++) { + const memberExpr = memberExpressions[i]; + + if (isResolvableReference(memberExpr, arrayLength)) { + const index = memberExpr.property.value; + const arrayElement = arrayElements[index]; + + // Only replace if the array element exists (handle sparse arrays) + if (arrayElement) { + try { + arb.markNode(memberExpr, arrayElement); + } catch (e) { + logger.debug(`[-] Unable to mark node for replacement: ${e}`); + } + } + } + } + + return arb; } /** * Resolve member expressions to their targeted index in an array. - * + * * This transformation replaces array index access with the literal values * for large arrays (> 20 elements). This is particularly useful for deobfuscating * code that uses large lookup tables. - * + * * Example transformation: * Input: const a = [1, 2, 3, ...]; b = a[0]; c = a[2]; * Output: const a = [1, 2, 3, ...]; b = 1; c = 3; - * + * * Only safe transformations are performed: * - Numeric literal indices only * - Within array bounds * - Not modifying assignments * - Array methods/properties excluded - * + * * @param {Arborist} arb - The Arborist instance containing the AST to transform * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified Arborist instance */ export default function resolveMemberExpressionReferencesToArrayIndex(arb, candidateFilter = () => true) { - const matches = resolveMemberExpressionReferencesToArrayIndexMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = resolveMemberExpressionReferencesToArrayIndexTransform(arb, matches[i]); - } - - return arb; + const matches = resolveMemberExpressionReferencesToArrayIndexMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = resolveMemberExpressionReferencesToArrayIndexTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/resolveMemberExpressionsWithDirectAssignment.js b/src/modules/safe/resolveMemberExpressionsWithDirectAssignment.js index 4ecb7b2..d7ab759 100644 --- a/src/modules/safe/resolveMemberExpressionsWithDirectAssignment.js +++ b/src/modules/safe/resolveMemberExpressionsWithDirectAssignment.js @@ -1,240 +1,240 @@ /** * Gets the property name from a MemberExpression, handling both computed and non-computed access. - * + * * For computed access (obj['prop']), uses the value of the property only if it's a literal. * For non-computed access (obj.prop), uses the name of the property. - * + * * This function is conservative about computed access - it only resolves when the property * is a direct literal, not a variable that happens to have a literal value. - * + * * @param {ASTNode} memberExpr - The MemberExpression node * @return {string|number|null} The property name/value, or null if not determinable */ function getPropertyName(memberExpr) { - if (!memberExpr.property) { - return null; - } - - if (memberExpr.computed) { - // For computed access, only allow direct literals like obj['prop'] or obj[0] - // Do not allow variables like obj[key] even if key has a literal value - if (memberExpr.property.type === 'Literal') { - return memberExpr.property.value; - } else { - // Conservative approach: don't resolve computed access with variables - return null; - } - } else { - // For dot notation access like obj.prop - return memberExpr.property.name; - } + if (!memberExpr.property) { + return null; + } + + if (memberExpr.computed) { + // For computed access, only allow direct literals like obj['prop'] or obj[0] + // Do not allow variables like obj[key] even if key has a literal value + if (memberExpr.property.type === 'Literal') { + return memberExpr.property.value; + } else { + // Conservative approach: don't resolve computed access with variables + return null; + } + } else { + // For dot notation access like obj.prop + return memberExpr.property.name; + } } /** * Checks if a member expression reference represents a modification (assignment or update). - * + * * Identifies cases where the member expression is being modified rather than read: * - Assignment expressions where the member expression is on the left side * - Update expressions like ++obj.prop or obj.prop++ - * + * * @param {ASTNode} memberExpr - The MemberExpression node to check * @return {boolean} True if this is a modification, false if it's a read access */ function isModifyingReference(memberExpr) { - const parent = memberExpr.parentNode; - - if (!parent) { - return false; - } - - // Check for update expressions (++obj.prop, obj.prop++, --obj.prop, obj.prop--) - if (parent.type === 'UpdateExpression') { - return true; - } - - // Check for assignment expressions where member expression is on the left side - if (parent.type === 'AssignmentExpression' && memberExpr.parentKey === 'left') { - return true; - } - - return false; + const parent = memberExpr.parentNode; + + if (!parent) { + return false; + } + + // Check for update expressions (++obj.prop, obj.prop++, --obj.prop, obj.prop--) + if (parent.type === 'UpdateExpression') { + return true; + } + + // Check for assignment expressions where member expression is on the left side + if (parent.type === 'AssignmentExpression' && memberExpr.parentKey === 'left') { + return true; + } + + return false; } /** * Finds all references to a specific property on an object that can be replaced with a literal value. - * + * * Searches through all references to the object's declaration and identifies member expressions * that access the same property. Excludes references that modify the property to ensure * the transformation is safe. - * + * * @param {ASTNode} objectDeclNode - The declaration node of the object * @param {string|number} propertyName - The name/value of the property to find * @param {Object} assignmentMemberExpr - The original assignment member expression to exclude * @return {Object[]} Array of reference nodes that can be replaced */ function findReplaceablePropertyReferences(objectDeclNode, propertyName, assignmentMemberExpr) { - const replaceableRefs = []; - - if (!objectDeclNode.references) { - return replaceableRefs; - } - - for (let i = 0; i < objectDeclNode.references.length; i++) { - const ref = objectDeclNode.references[i]; - const memberExpr = ref.parentNode; - - // Skip if not a member expression or if it's the original assignment - if (!memberExpr || - memberExpr.type !== 'MemberExpression' || + const replaceableRefs = []; + + if (!objectDeclNode.references) { + return replaceableRefs; + } + + for (let i = 0; i < objectDeclNode.references.length; i++) { + const ref = objectDeclNode.references[i]; + const memberExpr = ref.parentNode; + + // Skip if not a member expression or if it's the original assignment + if (!memberExpr || + memberExpr.type !== 'MemberExpression' || memberExpr === assignmentMemberExpr) { - continue; - } - - // Check if this member expression accesses the same property - const refPropertyName = getPropertyName(memberExpr); - if (refPropertyName !== propertyName) { - continue; - } - - // Don't replace any reference if any of them are modifying the property - if (isModifyingReference(memberExpr)) { - return []; - } - - if (ref.scope !== assignmentMemberExpr.scope) { - return []; - } - - replaceableRefs.push(ref); - } - - return replaceableRefs; + continue; + } + + // Check if this member expression accesses the same property + const refPropertyName = getPropertyName(memberExpr); + if (refPropertyName !== propertyName) { + continue; + } + + // Don't replace any reference if any of them are modifying the property + if (isModifyingReference(memberExpr)) { + return []; + } + + if (ref.scope !== assignmentMemberExpr.scope) { + return []; + } + + replaceableRefs.push(ref); + } + + return replaceableRefs; } /** * Identifies MemberExpression nodes that are being assigned literal values and can have their references resolved. - * + * * A member expression is a candidate when: * 1. It's on the left side of an assignment expression * 2. The right side is a literal value * 3. The object has a declaration node with references * 4. There are other references to the same property that can be replaced * 5. No references modify the property (ensuring safe transformation) - * + * * This transformation is useful for resolving simple object property assignments * like `obj.prop = 'value'` where `obj.prop` is later accessed. - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {Object[]} Array of objects with memberExpr, propertyName, replacementNode, and references */ export function resolveMemberExpressionsWithDirectAssignmentMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.MemberExpression; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Must be a member expression with an object that has a declaration - if (!n.object || !n.object.declNode) { - continue; - } - - // Must be on the left side of an assignment expression - if (!n.parentNode || - n.parentNode.type !== 'AssignmentExpression' || + const relevantNodes = arb.ast[0].typeMap.MemberExpression; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Must be a member expression with an object that has a declaration + if (!n.object || !n.object.declNode) { + continue; + } + + // Must be on the left side of an assignment expression + if (!n.parentNode || + n.parentNode.type !== 'AssignmentExpression' || n.parentKey !== 'left') { - continue; - } - - // The assigned value must be a literal - if (!n.parentNode.right || n.parentNode.right.type !== 'Literal') { - continue; - } - - // Must pass the candidate filter - if (!candidateFilter(n)) { - continue; - } - - const propertyName = getPropertyName(n); - if (propertyName === null) { - continue; - } - - // Find all references to this property that can be replaced - const replaceableRefs = findReplaceablePropertyReferences( - n.object.declNode, - propertyName, - n - ); - - // Only add as candidate if there are references to replace - if (replaceableRefs.length) { - matches.push({ - memberExpr: n, - propertyName: propertyName, - replacementNode: n.parentNode.right, - references: replaceableRefs - }); - } - } - - return matches; + continue; + } + + // The assigned value must be a literal + if (!n.parentNode.right || n.parentNode.right.type !== 'Literal') { + continue; + } + + // Must pass the candidate filter + if (!candidateFilter(n)) { + continue; + } + + const propertyName = getPropertyName(n); + if (propertyName === null) { + continue; + } + + // Find all references to this property that can be replaced + const replaceableRefs = findReplaceablePropertyReferences( + n.object.declNode, + propertyName, + n, + ); + + // Only add as candidate if there are references to replace + if (replaceableRefs.length) { + matches.push({ + memberExpr: n, + propertyName, + replacementNode: n.parentNode.right, + references: replaceableRefs, + }); + } + } + + return matches; } /** * Transforms member expression references by replacing them with their assigned literal values. - * + * * For each match, replaces all found references to the property with the literal value * that was assigned to it. This is safe because the match function ensures no * modifications occur to the property after assignment. - * + * * @param {Arborist} arb - The Arborist instance to mark changes on * @param {Object} match - Match object containing memberExpr, propertyName, replacementNode, and references * @return {Arborist} The modified Arborist instance */ export function resolveMemberExpressionsWithDirectAssignmentTransform(arb, match) { - const {replacementNode, references} = match; - - // Replace each reference with the literal value - for (let i = 0; i < references.length; i++) { - const ref = references[i]; - const memberExpr = ref.parentNode; - - if (memberExpr && memberExpr.type === 'MemberExpression') { - arb.markNode(memberExpr, replacementNode); - } - } - - return arb; + const {replacementNode, references} = match; + + // Replace each reference with the literal value + for (let i = 0; i < references.length; i++) { + const ref = references[i]; + const memberExpr = ref.parentNode; + + if (memberExpr && memberExpr.type === 'MemberExpression') { + arb.markNode(memberExpr, replacementNode); + } + } + + return arb; } /** * Resolve the value of member expressions to objects which hold literals that were directly assigned to the expression. - * + * * This transformation replaces property access with literal values when the property * has been directly assigned a literal value and is not modified elsewhere. - * + * * Example transformation: * Input: function a() {} a.b = 3; a.c = '5'; console.log(a.b + a.c); * Output: function a() {} a.b = 3; a.c = '5'; console.log(3 + '5'); - * + * * Safety constraints: * - Only replaces when assigned value is a literal * - Skips if property is modified (assigned or updated) elsewhere * - Ensures all references are read-only accesses - * + * * @param {Arborist} arb - The Arborist instance containing the AST to transform * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified Arborist instance */ export default function resolveMemberExpressionsWithDirectAssignment(arb, candidateFilter = () => true) { - const matches = resolveMemberExpressionsWithDirectAssignmentMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = resolveMemberExpressionsWithDirectAssignmentTransform(arb, matches[i]); - } - - return arb; + const matches = resolveMemberExpressionsWithDirectAssignmentMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = resolveMemberExpressionsWithDirectAssignmentTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/resolveProxyCalls.js b/src/modules/safe/resolveProxyCalls.js index b380c23..3574168 100644 --- a/src/modules/safe/resolveProxyCalls.js +++ b/src/modules/safe/resolveProxyCalls.js @@ -1,173 +1,173 @@ /** * Checks if a function contains only a single return statement with no other code. - * + * * A proxy function candidate must have exactly one statement in its body, * and that statement must be a return statement. This ensures the function * doesn't perform any side effects beyond passing through arguments. - * + * * @param {ASTNode} funcNode - The FunctionDeclaration node to check * @return {boolean} True if function has only a return statement */ function hasOnlyReturnStatement(funcNode) { - if (!funcNode.body || - !funcNode.body.body || + if (!funcNode.body || + !funcNode.body.body || funcNode?.body?.body?.length !== 1) { - return false; - } - - return funcNode?.body?.body[0]?.type === 'ReturnStatement'; + return false; + } + + return funcNode?.body?.body[0]?.type === 'ReturnStatement'; } /** * Validates that parameter names are passed through in the same order to the target function. - * + * * For a valid proxy function, each parameter must be passed to the target function * in the exact same order and position. This ensures the proxy doesn't modify, * reorder, or omit any arguments. - * + * * @param {Array} params - Function parameters array * @param {Array} callArgs - Arguments passed to the target function call * @return {boolean} True if all parameters are passed through correctly */ function areParametersPassedThrough(params, callArgs) { - // Must have same number of parameters and arguments - if (!params || !callArgs || params.length !== callArgs.length) { - return false; - } - - // Each parameter must match corresponding argument by name - for (let i = 0; i < params.length; i++) { - const param = params[i]; - const arg = callArgs[i]; - - // Both must be identifiers with matching names - if (param?.type !== 'Identifier' || + // Must have same number of parameters and arguments + if (!params || !callArgs || params.length !== callArgs.length) { + return false; + } + + // Each parameter must match corresponding argument by name + for (let i = 0; i < params.length; i++) { + const param = params[i]; + const arg = callArgs[i]; + + // Both must be identifiers with matching names + if (param?.type !== 'Identifier' || arg?.type !== 'Identifier' || param?.name !== arg?.name) { - return false; - } - } - - return true; + return false; + } + } + + return true; } /** * Identifies FunctionDeclaration nodes that act as proxy calls to other functions. - * + * * A proxy function is one that: * 1. Contains only a single return statement * 2. Returns a call expression * 3. The call target is an identifier (not a complex expression) * 4. All parameters are passed through to the target in the same order * 5. No parameters are modified, reordered, or omitted - * + * * This pattern is common in obfuscated code where simple wrapper functions * are used to indirect function calls. - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {Object[]} Array of objects with funcNode, targetCallee, and references */ export function resolveProxyCallsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.FunctionDeclaration; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Must pass the candidate filter - if (!candidateFilter(n)) { - continue; - } - - // Must have only a return statement - if (!hasOnlyReturnStatement(n)) { - continue; - } - - const returnStmt = n.body.body[0]; - const returnArg = returnStmt.argument; - - // Must return a call expression - if (returnArg?.type !== 'CallExpression') { - continue; - } - - // Call target must be a simple identifier - if (returnArg.callee?.type !== 'Identifier') { - continue; - } - - // Must have a function name with references to replace - if (!n.id?.references?.length) { - continue; - } - - // All parameters must be passed through correctly - if (!areParametersPassedThrough(n.params, returnArg.arguments)) { - continue; - } - - matches.push({ - funcNode: n, - targetCallee: returnArg.callee, - references: n.id.references - }); - } - - return matches; + const relevantNodes = arb.ast[0].typeMap.FunctionDeclaration; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Must pass the candidate filter + if (!candidateFilter(n)) { + continue; + } + + // Must have only a return statement + if (!hasOnlyReturnStatement(n)) { + continue; + } + + const returnStmt = n.body.body[0]; + const returnArg = returnStmt.argument; + + // Must return a call expression + if (returnArg?.type !== 'CallExpression') { + continue; + } + + // Call target must be a simple identifier + if (returnArg.callee?.type !== 'Identifier') { + continue; + } + + // Must have a function name with references to replace + if (!n.id?.references?.length) { + continue; + } + + // All parameters must be passed through correctly + if (!areParametersPassedThrough(n.params, returnArg.arguments)) { + continue; + } + + matches.push({ + funcNode: n, + targetCallee: returnArg.callee, + references: n.id.references, + }); + } + + return matches; } /** * Transforms proxy function calls by replacing them with direct calls to the target function. - * + * * For each reference to the proxy function, replaces it with a reference to the * target function that the proxy was calling. This eliminates the unnecessary * indirection and simplifies the call chain. - * + * * @param {Arborist} arb - The Arborist instance to mark changes on * @param {Object} match - Match object containing funcNode, targetCallee, and references * @return {Arborist} The modified Arborist instance */ export function resolveProxyCallsTransform(arb, match) { - const {targetCallee, references} = match; - - // Replace each reference to the proxy function with the target function - for (let i = 0; i < references.length; i++) { - arb.markNode(references[i], targetCallee); - } - - return arb; + const {targetCallee, references} = match; + + // Replace each reference to the proxy function with the target function + for (let i = 0; i < references.length; i++) { + arb.markNode(references[i], targetCallee); + } + + return arb; } /** * Remove redundant call expressions which only pass the arguments to other call expression. - * + * * This transformation identifies proxy functions that simply pass their arguments * to another function and replaces calls to the proxy with direct calls to the target. * This is particularly useful for deobfuscating code that uses wrapper functions * to indirect function calls. - * + * * Example transformation: * Input: function call2(c, d) { return call1(c, d); } call2(1, 2); * Output: function call2(c, d) { return call1(c, d); } call1(1, 2); - * + * * Safety constraints: * - Only processes functions with single return statements * - Target must be a simple identifier (not complex expression) * - All parameters must be passed through in exact order * - No parameter modification, reordering, or omission allowed - * + * * @param {Arborist} arb - The Arborist instance containing the AST to transform * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified Arborist instance */ export default function resolveProxyCalls(arb, candidateFilter = () => true) { - const matches = resolveProxyCallsMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = resolveProxyCallsTransform(arb, matches[i]); - } - - return arb; + const matches = resolveProxyCallsMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = resolveProxyCallsTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/resolveProxyReferences.js b/src/modules/safe/resolveProxyReferences.js index 5d30f1b..a93e20b 100644 --- a/src/modules/safe/resolveProxyReferences.js +++ b/src/modules/safe/resolveProxyReferences.js @@ -10,187 +10,185 @@ const LOOP_STATEMENT_REGEX = /(For.*Statement|WhileStatement|DoWhileStatement)/; /** * Checks if a variable declarator represents a proxy reference. - * + * * A proxy reference is a variable that simply points to another variable * without modification. For example: `const b = a;` where `b` is a proxy to `a`. - * + * * @param {ASTNode} declaratorNode - The VariableDeclarator node to check * @return {boolean} True if this is a valid proxy reference pattern */ function isProxyReferencePattern(declaratorNode) { - // The variable being declared must be an Identifier or MemberExpression - if (!SUPPORTED_REFERENCE_TYPES.includes(declaratorNode.id?.type)) { - return false; - } - - // CRITICAL: The value being assigned must also be Identifier or MemberExpression - // This prevents transforming cases like: const b = getValue(); where getValue() is a CallExpression - if (!SUPPORTED_REFERENCE_TYPES.includes(declaratorNode.init?.type)) { - return false; - } - - // Avoid proxy variables in loop contexts (for, while, do-while) - // This prevents breaking loop semantics where variables may be modified during iteration - if (LOOP_STATEMENT_REGEX.test(declaratorNode.parentNode?.parentNode?.type)) { - return false; - } - - return true; + // The variable being declared must be an Identifier or MemberExpression + if (!SUPPORTED_REFERENCE_TYPES.includes(declaratorNode.id?.type)) { + return false; + } + + // CRITICAL: The value being assigned must also be Identifier or MemberExpression + // This prevents transforming cases like: const b = getValue(); where getValue() is a CallExpression + if (!SUPPORTED_REFERENCE_TYPES.includes(declaratorNode.init?.type)) { + return false; + } + + // Avoid proxy variables in loop contexts (for, while, do-while) + // This prevents breaking loop semantics where variables may be modified during iteration + if (LOOP_STATEMENT_REGEX.test(declaratorNode.parentNode?.parentNode?.type)) { + return false; + } + + return true; } /** * Validates that a proxy reference replacement is safe to perform. - * + * * Ensures that replacing the proxy with its target won't create circular * references or other problematic scenarios. This includes checking for * self-references and ensuring the proxy variable isn't used in its own * initialization. - * + * * @param {ASTNode} proxyIdentifier - The main identifier being proxied * @param {ASTNode} replacementNode - The node that will replace the proxy * @return {boolean} True if the replacement is safe */ function isReplacementSafe(proxyIdentifier, replacementNode) { - // Get the main identifier from the replacement to check for circular references - const replacementMainIdentifier = getMainDeclaredObjectOfMemberExpression(replacementNode)?.declNode; - - // Prevent circular references: proxy can't point to itself - // Example: const a = b; const b = a; (circular - not safe) - if (replacementMainIdentifier && replacementMainIdentifier === proxyIdentifier) { - return false; - } - - // Prevent self-reference in initialization - // Example: const a = someFunction(a); (not safe - uses itself in init) - if (doesDescendantMatchCondition(replacementNode, n => n === proxyIdentifier)) { - return false; - } - - return true; -} + // Get the main identifier from the replacement to check for circular references + const replacementMainIdentifier = getMainDeclaredObjectOfMemberExpression(replacementNode)?.declNode; + + // Prevent circular references: proxy can't point to itself + // Example: const a = b; const b = a; (circular - not safe) + if (replacementMainIdentifier && replacementMainIdentifier === proxyIdentifier) { + return false; + } + // Prevent self-reference in initialization + // Example: const a = someFunction(a); (not safe - uses itself in init) + if (doesDescendantMatchCondition(replacementNode, n => n === proxyIdentifier)) { + return false; + } + return true; +} /** * Identifies VariableDeclarator nodes that represent proxy references to other variables. - * + * * A proxy reference is a variable declaration where the variable simply points to * another variable without any modification. This pattern is common in obfuscated * code to create indirection layers. - * + * * Examples of proxy references: * const b = a; // Simple identifier proxy * const d = obj.prop; // Member expression proxy * const e = b; // Chained proxy (b -> a, e -> b) - * + * * Safety constraints: * - Both variable and value must be Identifier or MemberExpression * - Not in For statement context (to avoid breaking loop semantics) * - No circular references allowed * - References must not be modified after declaration * - Target must not be modified either - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {Object[]} Array of objects with proxyNode, targetNode, and references */ export function resolveProxyReferencesMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.VariableDeclarator; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Must pass the candidate filter - if (!candidateFilter(n)) { - continue; - } - - // Must follow the proxy reference pattern - if (!isProxyReferencePattern(n)) { - continue; - } - - // Get the main identifier that will be replaced - const proxyIdentifier = getMainDeclaredObjectOfMemberExpression(n.id)?.declNode || n.id; - const refs = proxyIdentifier.references || []; - - // Must have references to replace - if (!refs.length) { - continue; - } - - // Must be safe to replace - if (!isReplacementSafe(proxyIdentifier, n.init)) { - continue; - } - - // Both the proxy and target must not be modified - if (areReferencesModified(arb.ast, refs) || areReferencesModified(arb.ast, [n.init])) { - continue; - } - - matches.push({ - declaratorNode: n, - proxyIdentifier, - targetNode: n.init, - references: refs - }); - } - - return matches; + const relevantNodes = arb.ast[0].typeMap.VariableDeclarator; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Must pass the candidate filter + if (!candidateFilter(n)) { + continue; + } + + // Must follow the proxy reference pattern + if (!isProxyReferencePattern(n)) { + continue; + } + + // Get the main identifier that will be replaced + const proxyIdentifier = getMainDeclaredObjectOfMemberExpression(n.id)?.declNode || n.id; + const refs = proxyIdentifier.references || []; + + // Must have references to replace + if (!refs.length) { + continue; + } + + // Must be safe to replace + if (!isReplacementSafe(proxyIdentifier, n.init)) { + continue; + } + + // Both the proxy and target must not be modified + if (areReferencesModified(arb.ast, refs) || areReferencesModified(arb.ast, [n.init])) { + continue; + } + + matches.push({ + declaratorNode: n, + proxyIdentifier, + targetNode: n.init, + references: refs, + }); + } + + return matches; } /** * Transforms proxy references by replacing them with direct references to their targets. - * + * * For each reference to the proxy variable, replaces it with the target node * that the proxy was pointing to. This eliminates unnecessary indirection * in the code. - * + * * @param {Arborist} arb - The Arborist instance to mark changes on * @param {Object} match - Match object containing proxyIdentifier, targetNode, and references * @return {Arborist} The modified Arborist instance */ export function resolveProxyReferencesTransform(arb, match) { - const {targetNode, references} = match; - - // Replace each reference to the proxy with the target - for (let i = 0; i < references.length; i++) { - arb.markNode(references[i], targetNode); - } - - return arb; + const {targetNode, references} = match; + + // Replace each reference to the proxy with the target + for (let i = 0; i < references.length; i++) { + arb.markNode(references[i], targetNode); + } + + return arb; } /** * Replace variables which only point at other variables and do not change, with their target. - * + * * This transformation identifies proxy references where a variable simply points to * another variable without modification and replaces all references to the proxy * with direct references to the target. This is particularly useful for deobfuscating * code that uses multiple layers of variable indirection. - * + * * Example transformation: * Input: const a = ['hello']; const b = a; const c = b[0]; * Output: const a = ['hello']; const b = a; const c = a[0]; - * + * * Safety constraints: * - Only processes simple variable-to-variable assignments * - Avoids loop iterator variables to prevent breaking loop semantics * - Prevents circular references and self-references * - Ensures neither proxy nor target variables are modified after declaration - * + * * @param {Arborist} arb - The Arborist instance containing the AST to transform * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified Arborist instance */ export default function resolveProxyReferences(arb, candidateFilter = () => true) { - const matches = resolveProxyReferencesMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = resolveProxyReferencesTransform(arb, matches[i]); - } - - return arb; + const matches = resolveProxyReferencesMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = resolveProxyReferencesTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/resolveProxyVariables.js b/src/modules/safe/resolveProxyVariables.js index 95236df..49c91a1 100644 --- a/src/modules/safe/resolveProxyVariables.js +++ b/src/modules/safe/resolveProxyVariables.js @@ -2,129 +2,129 @@ import {areReferencesModified} from '../utils/areReferencesModified.js'; /** * Validates that a VariableDeclarator represents a proxy variable assignment. - * + * * A proxy variable is one that simply assigns another identifier without modification. * For example: `const alias = originalVar;` where `alias` is a proxy to `originalVar`. - * + * * @param {ASTNode} declaratorNode - The VariableDeclarator node to check * @param {Function} candidateFilter - Filter function to apply additional criteria * @return {boolean} True if this is a valid proxy variable pattern */ function isProxyVariablePattern(declaratorNode, candidateFilter) { - // Must have an identifier as the initialization value - if (!declaratorNode.init || declaratorNode.init.type !== 'Identifier') { - return false; - } - - // Must pass the candidate filter - if (!candidateFilter(declaratorNode)) { - return false; - } - - return true; + // Must have an identifier as the initialization value + if (!declaratorNode.init || declaratorNode.init.type !== 'Identifier') { + return false; + } + + // Must pass the candidate filter + if (!candidateFilter(declaratorNode)) { + return false; + } + + return true; } /** * Identifies VariableDeclarator nodes that represent proxy variables to other identifiers. - * + * * A proxy variable is a declaration like `const alias = originalVar;` where the variable * simply points to another identifier. These can either be removed (if unused) or have * all their references replaced with the target identifier. - * + * * This function finds all such proxy variables and returns them along with their * reference information for transformation. - * + * * @param {Arborist} arb - The AST tree manager * @param {Function} candidateFilter - Filter to apply on candidate nodes * @return {Object[]} Array of match objects containing proxy info */ export function resolveProxyVariablesMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.VariableDeclarator; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Must be a valid proxy variable pattern - if (!isProxyVariablePattern(n, candidateFilter)) { - continue; - } - - // Get references to this proxy variable - const refs = n.id?.references || []; - - // Add to matches - we'll handle both removal and replacement in transform - matches.push({ - declaratorNode: n, - targetIdentifier: n.init, - references: refs, - shouldRemove: refs.length === 0 - }); - } - - return matches; + const relevantNodes = arb.ast[0].typeMap.VariableDeclarator; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Must be a valid proxy variable pattern + if (!isProxyVariablePattern(n, candidateFilter)) { + continue; + } + + // Get references to this proxy variable + const refs = n.id?.references || []; + + // Add to matches - we'll handle both removal and replacement in transform + matches.push({ + declaratorNode: n, + targetIdentifier: n.init, + references: refs, + shouldRemove: refs.length === 0, + }); + } + + return matches; } /** * Transforms proxy variable declarations by either removing them or replacing references. - * + * * For proxy variables with no references, removes the entire declaration. * For proxy variables with references, replaces all references with the target identifier * if the references are not modified elsewhere. - * + * * @param {Arborist} arb - The AST tree manager * @param {Object} match - Match object from resolveProxyVariablesMatch * @return {Arborist} The modified AST tree manager */ export function resolveProxyVariablesTransform(arb, match) { - const {declaratorNode, targetIdentifier, references, shouldRemove} = match; - - if (shouldRemove) { - // Remove the proxy assignment if there are no references - arb.markNode(declaratorNode); - } else { - // Check if references are modified - if so, skip transformation - if (areReferencesModified(arb.ast, references)) { - return arb; - } - - // Replace all references with the target identifier - for (let i = 0; i < references.length; i++) { - const ref = references[i]; - arb.markNode(ref, targetIdentifier); - } - } - - return arb; + const {declaratorNode, targetIdentifier, references, shouldRemove} = match; + + if (shouldRemove) { + // Remove the proxy assignment if there are no references + arb.markNode(declaratorNode); + } else { + // Check if references are modified - if so, skip transformation + if (areReferencesModified(arb.ast, references)) { + return arb; + } + + // Replace all references with the target identifier + for (let i = 0; i < references.length; i++) { + const ref = references[i]; + arb.markNode(ref, targetIdentifier); + } + } + + return arb; } /** * Replace proxied variables with their intended target. - * + * * This module handles simple variable assignments where one identifier is assigned * to another identifier, creating a "proxy" relationship. It either removes unused * proxy assignments or replaces all references to the proxy with the original identifier. - * + * * Examples of transformations: * - `const alias = original; console.log(alias);` → `console.log(original);` * - `const unused = original;` → (removed entirely) * - `const a2b = atob; console.log(a2b('test'));` → `console.log(atob('test'));` - * + * * Safety considerations: * - Only transforms when references are not modified (no assignments or updates) * - Preserves program semantics by ensuring proxy and target are equivalent * - Removes unused declarations to clean up dead code - * + * * @param {Arborist} arb - The AST tree manager * @param {Function} [candidateFilter] - Optional filter to apply on candidates * @return {Arborist} The modified AST tree manager */ export default function resolveProxyVariables(arb, candidateFilter = () => true) { - const matches = resolveProxyVariablesMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = resolveProxyVariablesTransform(arb, matches[i]); - } - - return arb; + const matches = resolveProxyVariablesMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = resolveProxyVariablesTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/resolveRedundantLogicalExpressions.js b/src/modules/safe/resolveRedundantLogicalExpressions.js index e0f15aa..5e2d787 100644 --- a/src/modules/safe/resolveRedundantLogicalExpressions.js +++ b/src/modules/safe/resolveRedundantLogicalExpressions.js @@ -4,146 +4,146 @@ const TRUTHY_NODE_TYPES = ['ArrayExpression', 'ObjectExpression', 'FunctionExpre /** * Evaluates the truthiness of an AST node according to JavaScript rules. - * + * * In JavaScript, these are always truthy: * - Arrays (even empty: []) - * - Objects (even empty: {}) + * - Objects (even empty: {}) * - Functions * - Regular expressions - * + * * For literals, these values are falsy: false, 0, -0, 0n, "", null, undefined, NaN * All other literal values are truthy. - * + * * @param {ASTNode} node - The AST node to evaluate * @return {boolean|null} True if truthy, false if falsy, null if indeterminate */ function isNodeTruthy(node) { - // Arrays, objects, functions, and regex are always truthy - if (TRUTHY_NODE_TYPES.includes(node.type) || (node.type === 'Literal' && node.regex)) { - return true; - } - - // For literal values, evaluate using JavaScript truthiness rules - if (node.type === 'Literal') { - // JavaScript falsy values: false, 0, -0, 0n, "", null, undefined, NaN - return Boolean(node.value); - } - - // For other node types, we can't determine truthiness statically - return null; + // Arrays, objects, functions, and regex are always truthy + if (TRUTHY_NODE_TYPES.includes(node.type) || (node.type === 'Literal' && node.regex)) { + return true; + } + + // For literal values, evaluate using JavaScript truthiness rules + if (node.type === 'Literal') { + // JavaScript falsy values: false, 0, -0, 0n, "", null, undefined, NaN + return Boolean(node.value); + } + + // For other node types, we can't determine truthiness statically + return null; } /** * Determines the replacement node for a redundant logical expression. - * + * * Uses JavaScript's short-circuit evaluation rules. See truth table below: - * + * * AND (&&) operator - returns first falsy value or last value: * | Left | Right | Result | * |--------|--------|--------| * | truthy | any | right | * | falsy | any | left | - * + * * OR (||) operator - returns first truthy value or last value: * | Left | Right | Result | * |--------|--------|--------| * | truthy | any | left | * | falsy | any | right | - * + * * @param {ASTNode} logicalExpr - The LogicalExpression node to simplify * @return {ASTNode|null} The replacement node or null if no simplification possible */ function getSimplifiedLogicalExpression(logicalExpr) { - const {left, right, operator} = logicalExpr; - - // Check if left operand has deterministic truthiness - const leftTruthiness = isNodeTruthy(left); - if (leftTruthiness !== null) { - if (operator === '&&') { - // Apply AND truth table: truthy left → right, falsy left → left - return leftTruthiness ? right : left; - } else if (operator === '||') { - // Apply OR truth table: truthy left → left, falsy left → right - return leftTruthiness ? left : right; - } - } - - // Check if right operand has deterministic truthiness - const rightTruthiness = isNodeTruthy(right); - if (rightTruthiness !== null) { - if (operator === '&&') { - // Apply AND truth table: truthy right → left, falsy right → right - return rightTruthiness ? left : right; - } else if (operator === '||') { - // Apply OR truth table: truthy right → right, falsy right → left - return rightTruthiness ? right : left; - } - } - - return null; // No simplification possible + const {left, right, operator} = logicalExpr; + + // Check if left operand has deterministic truthiness + const leftTruthiness = isNodeTruthy(left); + if (leftTruthiness !== null) { + if (operator === '&&') { + // Apply AND truth table: truthy left → right, falsy left → left + return leftTruthiness ? right : left; + } else if (operator === '||') { + // Apply OR truth table: truthy left → left, falsy left → right + return leftTruthiness ? left : right; + } + } + + // Check if right operand has deterministic truthiness + const rightTruthiness = isNodeTruthy(right); + if (rightTruthiness !== null) { + if (operator === '&&') { + // Apply AND truth table: truthy right → left, falsy right → right + return rightTruthiness ? left : right; + } else if (operator === '||') { + // Apply OR truth table: truthy right → right, falsy right → left + return rightTruthiness ? right : left; + } + } + + return null; // No simplification possible } /** * Finds IfStatement nodes with redundant logical expressions that can be simplified. - * + * * Identifies if statements where the test condition is a logical expression (&&, ||) * with at least one operand that has deterministic truthiness, allowing the expression * to be simplified based on JavaScript's short-circuit evaluation rules. - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of IfStatement nodes that can be simplified */ export function resolveRedundantLogicalExpressionsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.IfStatement; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Must have a LogicalExpression with supported operator and pass candidate filter - if (n.test?.type !== 'LogicalExpression' || + const relevantNodes = arb.ast[0].typeMap.IfStatement; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Must have a LogicalExpression with supported operator and pass candidate filter + if (n.test?.type !== 'LogicalExpression' || !LOGICAL_OPERATORS.includes(n.test.operator) || !candidateFilter(n)) { - continue; - } - - // Check if this logical expression can be simplified - if (getSimplifiedLogicalExpression(n.test) !== null) { - matches.push(n); - } - } - - return matches; + continue; + } + + // Check if this logical expression can be simplified + if (getSimplifiedLogicalExpression(n.test) !== null) { + matches.push(n); + } + } + + return matches; } /** * Transforms an IfStatement by simplifying its redundant logical expression. - * + * * Replaces the test condition with the simplified expression determined by * JavaScript's logical operator short-circuit evaluation rules. - * + * * @param {Arborist} arb - The Arborist instance to mark nodes for transformation * @param {ASTNode} n - The IfStatement node to transform * @return {Arborist} The Arborist instance for chaining */ export function resolveRedundantLogicalExpressionsTransform(arb, n) { - const simplifiedExpr = getSimplifiedLogicalExpression(n.test); - - if (simplifiedExpr !== null) { - arb.markNode(n.test, simplifiedExpr); - } - - return arb; + const simplifiedExpr = getSimplifiedLogicalExpression(n.test); + + if (simplifiedExpr !== null) { + arb.markNode(n.test, simplifiedExpr); + } + + return arb; } /** * Remove redundant logical expressions which will always resolve in the same way. - * + * * This function simplifies logical expressions in if statement conditions where * one operand has deterministic truthiness, making the result predictable based on * JavaScript's short-circuit evaluation rules. - * + * * Handles literals, arrays, objects, functions, and regular expressions: * - `if (false && expr)` becomes `if (false)` (AND with falsy literal) * - `if ([] || expr)` becomes `if ([])` (OR with truthy array) @@ -151,44 +151,44 @@ export function resolveRedundantLogicalExpressionsTransform(arb, n) { * - `if (function() {} || expr)` becomes `if (function() {})` (OR with truthy function) * - `if (true && expr)` becomes `if (expr)` (AND with truthy literal) * - `if (0 || expr)` becomes `if (expr)` (OR with falsy literal) - * + * * ⚠️ EDGE CASES WHERE THIS OPTIMIZATION COULD BREAK CODE: - * + * * 1. Getter side effects: Properties with getters that have side effects * - `if (obj.prop && true)` → `if (obj.prop)` may change when getter is called * - `if (false && obj.sideEffectProp)` → `if (false)` prevents getter execution - * + * * 2. Function call side effects: When expr contains function calls with side effects * - `if (true && doSomething())` → `if (doSomething())` (still executes) * - `if (false && doSomething())` → `if (false)` (skips execution entirely) - * + * * 3. Proxy object traps: Objects wrapped in Proxy with get/has trap side effects * - Accessing properties can trigger custom proxy handlers - * + * * 4. Type coercion side effects: Objects with custom valueOf/toString methods * - `if (customObj && true)` might trigger valueOf() during evaluation - * + * * 5. Reactive/Observable systems: Frameworks like Vue, MobX, or RxJS * - Property access can trigger reactivity or subscription side effects - * + * * 6. Temporal dead zone: Variables accessed before declaration in let/const * - May throw ReferenceError that gets prevented by short-circuiting - * + * * This optimization is SAFE for obfuscated code analysis because: * - Obfuscated code typically avoids complex side effects for reliability * - We only transform when operands are deterministically truthy/falsy * - The logic outcome remains semantically equivalent for pure expressions - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply to candidates * @return {Arborist} The Arborist instance for chaining */ export default function resolveRedundantLogicalExpressions(arb, candidateFilter = () => true) { - const matches = resolveRedundantLogicalExpressionsMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = resolveRedundantLogicalExpressionsTransform(arb, matches[i]); - } - - return arb; + const matches = resolveRedundantLogicalExpressionsMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = resolveRedundantLogicalExpressionsTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/separateChainedDeclarators.js b/src/modules/safe/separateChainedDeclarators.js index b6da1ea..220922f 100644 --- a/src/modules/safe/separateChainedDeclarators.js +++ b/src/modules/safe/separateChainedDeclarators.js @@ -3,133 +3,133 @@ const FOR_STATEMENT_REGEX = /For.*Statement/; /** * Creates individual VariableDeclaration nodes from a single declarator. - * + * * @param {ASTNode} originalDeclaration - The original VariableDeclaration node * @param {ASTNode} declarator - The individual VariableDeclarator to wrap * @return {ASTNode} New VariableDeclaration node with single declarator */ function createSingleDeclaration(originalDeclaration, declarator) { - return { - type: 'VariableDeclaration', - kind: originalDeclaration.kind, - declarations: [declarator], - }; + return { + type: 'VariableDeclaration', + kind: originalDeclaration.kind, + declarations: [declarator], + }; } /** * Creates a replacement parent node with separated declarations. - * + * * Handles two cases: * 1. Parent accepts arrays - splice in separated declarations * 2. Parent accepts single nodes - wrap in BlockStatement - * + * * @param {ASTNode} n - The VariableDeclaration node to replace * @param {ASTNode[]} separatedDeclarations - Array of separated declaration nodes * @return {ASTNode} The replacement parent node */ function createReplacementParent(n, separatedDeclarations) { - let replacementValue; - - if (Array.isArray(n.parentNode[n.parentKey])) { - // Parent accepts multiple nodes - splice in the separated declarations - const replacedArr = n.parentNode[n.parentKey]; - const idx = replacedArr.indexOf(n); - replacementValue = [ - ...replacedArr.slice(0, idx), - ...separatedDeclarations, - ...replacedArr.slice(idx + 1) - ]; - } else { - // Parent accepts single node - wrap in BlockStatement - replacementValue = { - type: 'BlockStatement', - body: separatedDeclarations, - }; - } - - return { - ...n.parentNode, - [n.parentKey]: replacementValue, - }; + let replacementValue; + + if (Array.isArray(n.parentNode[n.parentKey])) { + // Parent accepts multiple nodes - splice in the separated declarations + const replacedArr = n.parentNode[n.parentKey]; + const idx = replacedArr.indexOf(n); + replacementValue = [ + ...replacedArr.slice(0, idx), + ...separatedDeclarations, + ...replacedArr.slice(idx + 1), + ]; + } else { + // Parent accepts single node - wrap in BlockStatement + replacementValue = { + type: 'BlockStatement', + body: separatedDeclarations, + }; + } + + return { + ...n.parentNode, + [n.parentKey]: replacementValue, + }; } /** * Finds VariableDeclaration nodes with multiple declarators that can be separated. - * + * * Identifies variable declarations with multiple declarators, excluding those inside * for-loop statements where multiple declarations serve a specific purpose. - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of VariableDeclaration nodes that can be separated */ export function separateChainedDeclaratorsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.VariableDeclaration; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Must have multiple declarations, not be in a for-loop, and pass filter - if (n.declarations.length > 1 && + const relevantNodes = arb.ast[0].typeMap.VariableDeclaration; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Must have multiple declarations, not be in a for-loop, and pass filter + if (n.declarations.length > 1 && !FOR_STATEMENT_REGEX.test(n.parentNode.type) && candidateFilter(n)) { - matches.push(n); - } - } - - return matches; + matches.push(n); + } + } + + return matches; } /** * Transforms a VariableDeclaration by separating its multiple declarators. - * + * * Converts a single VariableDeclaration with multiple declarators into * multiple VariableDeclaration nodes each with a single declarator. - * + * * @param {Arborist} arb - The Arborist instance to mark nodes for transformation * @param {ASTNode} n - The VariableDeclaration node to transform * @return {Arborist} The Arborist instance for chaining */ export function separateChainedDeclaratorsTransform(arb, n) { - // Create individual declarations for each declarator - const separatedDeclarations = []; - for (let i = 0; i < n.declarations.length; i++) { - separatedDeclarations.push(createSingleDeclaration(n, n.declarations[i])); - } - - // Create replacement parent node and mark for transformation - const replacementParent = createReplacementParent(n, separatedDeclarations); - arb.markNode(n.parentNode, replacementParent); - - return arb; + // Create individual declarations for each declarator + const separatedDeclarations = []; + for (let i = 0; i < n.declarations.length; i++) { + separatedDeclarations.push(createSingleDeclaration(n, n.declarations[i])); + } + + // Create replacement parent node and mark for transformation + const replacementParent = createReplacementParent(n, separatedDeclarations); + arb.markNode(n.parentNode, replacementParent); + + return arb; } /** * Separate multiple variable declarators under the same variable declaration into single variable declaration->variable declarator pairs. - * + * * This function improves code readability and simplifies analysis by converting * chained variable declarations into individual declaration statements. - * + * * Examples: * - `const foo = 5, bar = 7;` becomes `const foo = 5; const bar = 7;` * - `let a, b = 2, c = 3;` becomes `let a; let b = 2; let c = 3;` * - `var x = 1, y = 2;` becomes `var x = 1; var y = 2;` - * + * * Special handling: * - Preserves for-loop declarations: `for (let i = 0, len = arr.length; ...)` (unchanged) * - Wraps in BlockStatement when parent expects single node: `if (x) var a, b;` becomes `if (x) { var a; var b; }` - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply to candidates * @return {Arborist} The Arborist instance for chaining */ export default function separateChainedDeclarators(arb, candidateFilter = () => true) { - const matches = separateChainedDeclaratorsMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = separateChainedDeclaratorsTransform(arb, matches[i]); - } - - return arb; + const matches = separateChainedDeclaratorsMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = separateChainedDeclaratorsTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/simplifyCalls.js b/src/modules/safe/simplifyCalls.js index a6c0f72..415b4f4 100644 --- a/src/modules/safe/simplifyCalls.js +++ b/src/modules/safe/simplifyCalls.js @@ -3,122 +3,122 @@ const ALLOWED_CONTEXT_VARIABLE_TYPES = ['ThisExpression', 'Literal']; /** * Extracts arguments for the simplified call based on method type. - * + * * For 'apply': extracts elements from the array argument * For 'call': extracts arguments after the first (this) argument - * + * * @param {ASTNode} n - The CallExpression node * @param {string} methodName - Either 'apply' or 'call' * @return {ASTNode[]} Array of argument nodes for the simplified call */ function extractSimplifiedArguments(n, methodName) { - if (methodName === 'apply') { - // For apply: func.apply(this, [arg1, arg2]) -> get elements from array - const arrayArg = n.arguments?.[1]; - return Array.isArray(arrayArg?.elements) ? arrayArg.elements : []; - } else { - // For call: func.call(this, arg1, arg2) -> get args after 'this' - return n.arguments?.slice(1) || []; - } + if (methodName === 'apply') { + // For apply: func.apply(this, [arg1, arg2]) -> get elements from array + const arrayArg = n.arguments?.[1]; + return Array.isArray(arrayArg?.elements) ? arrayArg.elements : []; + } else { + // For call: func.call(this, arg1, arg2) -> get args after 'this' + return n.arguments?.slice(1) || []; + } } /** * Finds CallExpression nodes that use .call(this) or .apply(this) patterns. - * + * * Identifies function calls that can be simplified by removing unnecessary * .call(this) or .apply(this) wrappers when the context is 'this'. - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of CallExpression nodes that can be simplified */ export function simplifyCallsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.CallExpression; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Must be a call/apply on a member expression with 'this' as first argument - if (!ALLOWED_CONTEXT_VARIABLE_TYPES.includes(n.arguments?.[0]?.type) || + const relevantNodes = arb.ast[0].typeMap.CallExpression; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Must be a call/apply on a member expression with 'this' as first argument + if (!ALLOWED_CONTEXT_VARIABLE_TYPES.includes(n.arguments?.[0]?.type) || (n.arguments?.[0]?.type === 'Literal' && n.arguments?.[0]?.value !== null) || n.callee.type !== 'MemberExpression' || !candidateFilter(n)) { - continue; - } - - const propertyName = n.callee.property?.name || n.callee.property?.value; - - // Must be 'apply' or 'call' method - if (!CALL_APPLY_METHODS.includes(propertyName)) { - continue; - } - - // Exclude Function constructor calls and function expressions - const objectName = n.callee.object?.name || n.callee?.value; - if (objectName === 'Function' || n.callee.object.type.includes('unction')) { - continue; - } - - matches.push(n); - } - - return matches; + continue; + } + + const propertyName = n.callee.property?.name || n.callee.property?.value; + + // Must be 'apply' or 'call' method + if (!CALL_APPLY_METHODS.includes(propertyName)) { + continue; + } + + // Exclude Function constructor calls and function expressions + const objectName = n.callee.object?.name || n.callee?.value; + if (objectName === 'Function' || n.callee.object.type.includes('unction')) { + continue; + } + + matches.push(n); + } + + return matches; } /** * Transforms a .call(this) or .apply(this) call into a direct function call. - * + * * Converts patterns like: * - func.call(this, arg1, arg2) -> func(arg1, arg2) * - func.apply(this, [arg1, arg2]) -> func(arg1, arg2) * - func.apply(this) -> func() - * + * * @param {Arborist} arb - The Arborist instance to mark nodes for transformation * @param {ASTNode} n - The CallExpression node to transform * @return {Arborist} The Arborist instance for chaining */ export function simplifyCallsTransform(arb, n) { - const propertyName = n.callee.property?.name || n.callee.property?.value; - const simplifiedArgs = extractSimplifiedArguments(n, propertyName); - - const simplifiedCall = { - type: 'CallExpression', - callee: n.callee.object, - arguments: simplifiedArgs, - }; - - arb.markNode(n, simplifiedCall); - return arb; + const propertyName = n.callee.property?.name || n.callee.property?.value; + const simplifiedArgs = extractSimplifiedArguments(n, propertyName); + + const simplifiedCall = { + type: 'CallExpression', + callee: n.callee.object, + arguments: simplifiedArgs, + }; + + arb.markNode(n, simplifiedCall); + return arb; } /** * Remove unnecessary usage of .call(this) or .apply(this) when calling a function. - * + * * This function simplifies function calls that use .call(this, ...) or .apply(this, [...]) * by converting them to direct function calls, improving code readability and performance. - * + * * Examples: * - `func.call(this, arg1, arg2)` becomes `func(arg1, arg2)` * - `func.apply(this, [arg1, arg2])` becomes `func(arg1, arg2)` * - `func.apply(this)` becomes `func()` * - `func.call(this)` becomes `func()` - * + * * Restrictions: * - Only transforms calls where first argument is exactly 'this' * - Does not transform Function constructor calls * - Does not transform calls on function expressions - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply to candidates * @return {Arborist} The Arborist instance for chaining */ export default function simplifyCalls(arb, candidateFilter = () => true) { - const matches = simplifyCallsMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = simplifyCallsTransform(arb, matches[i]); - } - - return arb; + const matches = simplifyCallsMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = simplifyCallsTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/simplifyIfStatements.js b/src/modules/safe/simplifyIfStatements.js index f3ac736..4a06931 100644 --- a/src/modules/safe/simplifyIfStatements.js +++ b/src/modules/safe/simplifyIfStatements.js @@ -1,138 +1,138 @@ /** * Checks if an AST node represents an empty statement or block. - * + * * @param {ASTNode} node - The AST node to check * @return {boolean} True if the node is empty, false otherwise */ function isEmpty(node) { - if (!node) return true; - if (node.type === 'EmptyStatement') return true; - if (node.type === 'BlockStatement' && !node.body.length) return true; - return false; + if (!node) return true; + if (node.type === 'EmptyStatement') return true; + if (node.type === 'BlockStatement' && !node.body.length) return true; + return false; } /** * Creates an inverted test expression wrapped in UnaryExpression with '!' operator. - * + * * @param {ASTNode} test - The original test expression * @return {ASTNode} UnaryExpression node with '!' operator */ function createInvertedTest(test) { - return { - type: 'UnaryExpression', - operator: '!', - prefix: true, - argument: test, - }; + return { + type: 'UnaryExpression', + operator: '!', + prefix: true, + argument: test, + }; } /** * Finds IfStatement nodes that can be simplified by removing empty branches. - * + * * Identifies if statements where: * - Both consequent and alternate are empty (convert to expression) * - Consequent is empty but alternate has content (invert and swap) * - Alternate is empty but consequent has content (remove alternate) - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of IfStatement nodes that can be simplified */ export function simplifyIfStatementsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.IfStatement; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - if (!candidateFilter(n)) { - continue; - } - - const consequentEmpty = isEmpty(n.consequent); - const alternateEmpty = isEmpty(n.alternate); - - // Can simplify if: both empty, or consequent empty with populated alternate, or alternate empty with populated consequent - if ((consequentEmpty && alternateEmpty) || + const relevantNodes = arb.ast[0].typeMap.IfStatement; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + if (!candidateFilter(n)) { + continue; + } + + const consequentEmpty = isEmpty(n.consequent); + const alternateEmpty = isEmpty(n.alternate); + + // Can simplify if: both empty, or consequent empty with populated alternate, or alternate empty with populated consequent + if ((consequentEmpty && alternateEmpty) || (consequentEmpty && !alternateEmpty) || (!consequentEmpty && alternateEmpty)) { - matches.push(n); - } - } - - return matches; + matches.push(n); + } + } + + return matches; } /** * Transforms an IfStatement by simplifying empty branches. - * + * * Applies one of three transformations: * 1. Both branches empty: Convert to ExpressionStatement with test only * 2. Empty consequent with populated alternate: Invert test and move alternate to consequent * 3. Empty alternate with populated consequent: Remove the alternate clause - * + * * @param {Arborist} arb - The Arborist instance to mark nodes for transformation * @param {ASTNode} n - The IfStatement node to transform * @return {Arborist} The Arborist instance for chaining */ export function simplifyIfStatementsTransform(arb, n) { - const isConsequentEmpty = isEmpty(n.consequent); - const isAlternateEmpty = isEmpty(n.alternate); - let replacementNode; - - if (isConsequentEmpty) { - if (isAlternateEmpty) { - // Both branches empty - convert to expression statement - replacementNode = { - type: 'ExpressionStatement', - expression: n.test, - }; - } else { - // Empty consequent with populated alternate - invert test and swap - replacementNode = { - type: 'IfStatement', - test: createInvertedTest(n.test), - consequent: n.alternate, - alternate: null, - }; - } - } else if (isAlternateEmpty && n.alternate !== null) { - // Populated consequent with empty alternate - remove alternate - replacementNode = { - ...n, - alternate: null, - }; - } - - if (replacementNode) { - arb.markNode(n, replacementNode); - } - - return arb; + const isConsequentEmpty = isEmpty(n.consequent); + const isAlternateEmpty = isEmpty(n.alternate); + let replacementNode; + + if (isConsequentEmpty) { + if (isAlternateEmpty) { + // Both branches empty - convert to expression statement + replacementNode = { + type: 'ExpressionStatement', + expression: n.test, + }; + } else { + // Empty consequent with populated alternate - invert test and swap + replacementNode = { + type: 'IfStatement', + test: createInvertedTest(n.test), + consequent: n.alternate, + alternate: null, + }; + } + } else if (isAlternateEmpty && n.alternate !== null) { + // Populated consequent with empty alternate - remove alternate + replacementNode = { + ...n, + alternate: null, + }; + } + + if (replacementNode) { + arb.markNode(n, replacementNode); + } + + return arb; } /** * Simplify if statements by removing or restructuring empty branches. - * + * * This function optimizes if statements that have empty consequents or alternates, * improving code readability and reducing unnecessary branching. - * + * * Transformations applied: * - `if (test) {} else {}` becomes `test;` * - `if (test) {} else action()` becomes `if (!test) action()` * - `if (test) action() else {}` becomes `if (test) action()` * - `if (test);` becomes `test;` - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply to candidates * @return {Arborist} The Arborist instance for chaining */ export default function simplifyIfStatements(arb, candidateFilter = () => true) { - const matches = simplifyIfStatementsMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = simplifyIfStatementsTransform(arb, matches[i]); - } - - return arb; + const matches = simplifyIfStatementsMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = simplifyIfStatementsTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/unwrapFunctionShells.js b/src/modules/safe/unwrapFunctionShells.js index d5ad61b..8c68a23 100644 --- a/src/modules/safe/unwrapFunctionShells.js +++ b/src/modules/safe/unwrapFunctionShells.js @@ -2,140 +2,140 @@ const FUNCTION_TYPES = ['FunctionDeclaration', 'FunctionExpression']; /** * Gets the property name from a member expression property. - * + * * @param {ASTNode} property - The property node from MemberExpression * @return {string} The property name or an empty string if not extractable */ function getPropertyName(property) { - return property?.name || property?.value || ''; + return property?.name || property?.value || ''; } /** * Creates a replacement function by transferring outer function properties to inner function. - * + * * Preserves the inner function while adding: * - Outer function's identifier if inner function is anonymous * - Outer function's parameters if inner function has no parameters - * + * * @param {ASTNode} outerFunction - The outer function shell to unwrap * @param {ASTNode} innerFunction - The inner function to enhance * @return {ASTNode} The enhanced inner function node */ function createUnwrappedFunction(outerFunction, innerFunction) { - const replacementNode = { ...innerFunction }; - - // Transfer identifier from outer function if inner function is anonymous - if (outerFunction.id && !replacementNode.id) { - replacementNode.id = outerFunction.id; - } - - // Transfer parameters from outer function if inner function has no parameters - if (outerFunction.params.length && !replacementNode.params.length) { - replacementNode.params = outerFunction.params.slice(); - } - - return replacementNode; + const replacementNode = {...innerFunction}; + + // Transfer identifier from outer function if inner function is anonymous + if (outerFunction.id && !replacementNode.id) { + replacementNode.id = outerFunction.id; + } + + // Transfer parameters from outer function if inner function has no parameters + if (outerFunction.params.length && !replacementNode.params.length) { + replacementNode.params = outerFunction.params.slice(); + } + + return replacementNode; } /** * Finds function shells that can be unwrapped. - * + * * Identifies functions that: * - Only contain a single return statement * - Return the result of calling another function with .apply(this, arguments) * - Have a FunctionExpression as the callee object - * + * * Pattern: `function outer() { return inner().apply(this, arguments); }` - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of function nodes that can be unwrapped */ export function unwrapFunctionShellsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.FunctionExpression - .concat(arb.ast[0].typeMap.FunctionDeclaration); - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - if (!candidateFilter(n) || + const relevantNodes = arb.ast[0].typeMap.FunctionExpression + .concat(arb.ast[0].typeMap.FunctionDeclaration); + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + if (!candidateFilter(n) || !FUNCTION_TYPES.includes(n.type) || n.body?.body?.length !== 1) { - continue; - } - - const returnStmt = n.body.body[0]; - if (returnStmt?.type !== 'ReturnStatement') { - continue; - } - - const callExpr = returnStmt.argument; - if (callExpr?.type !== 'CallExpression' || + continue; + } + + const returnStmt = n.body.body[0]; + if (returnStmt?.type !== 'ReturnStatement') { + continue; + } + + const callExpr = returnStmt.argument; + if (callExpr?.type !== 'CallExpression' || callExpr.arguments?.length !== 2 || callExpr.callee?.type !== 'MemberExpression' || callExpr.callee.object?.type !== 'FunctionExpression') { - continue; - } - - const propertyName = getPropertyName(callExpr.callee.property); - if (propertyName === 'apply') { - matches.push(n); - } - } - - return matches; + continue; + } + + const propertyName = getPropertyName(callExpr.callee.property); + if (propertyName === 'apply') { + matches.push(n); + } + } + + return matches; } /** * Transforms a function shell by unwrapping it to reveal the inner function. - * + * * The transformation preserves the outer function's identifier and parameters * by transferring them to the inner function when appropriate. - * + * * @param {Arborist} arb - The Arborist instance to mark nodes for transformation * @param {ASTNode} n - The function shell node to unwrap * @return {Arborist} The Arborist instance for chaining */ export function unwrapFunctionShellsTransform(arb, n) { - const innerFunction = n.body.body[0].argument.callee.object; - const replacementNode = createUnwrappedFunction(n, innerFunction); - - arb.markNode(n, replacementNode); - return arb; + const innerFunction = n.body.body[0].argument.callee.object; + const replacementNode = createUnwrappedFunction(n, innerFunction); + + arb.markNode(n, replacementNode); + return arb; } /** * Remove functions which only return another function via .apply(this, arguments). - * + * * This optimization unwraps function shells that serve no purpose other than * forwarding calls to an inner function. The outer function's identifier and * parameters are preserved by transferring them to the inner function. - * + * * Transforms: * ```javascript * function outer(x) { * return function inner() { return x + 3; }.apply(this, arguments); * } * ``` - * + * * Into: * ```javascript * function inner(x) { * return x + 3; * } * ``` - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply to candidates * @return {Arborist} The Arborist instance for chaining */ export default function unwrapFunctionShells(arb, candidateFilter = () => true) { - const matches = unwrapFunctionShellsMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = unwrapFunctionShellsTransform(arb, matches[i]); - } - - return arb; + const matches = unwrapFunctionShellsMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = unwrapFunctionShellsTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/unwrapIIFEs.js b/src/modules/safe/unwrapIIFEs.js index f21947a..0c60a50 100644 --- a/src/modules/safe/unwrapIIFEs.js +++ b/src/modules/safe/unwrapIIFEs.js @@ -2,12 +2,12 @@ const IIFE_FUNCTION_TYPES = ['ArrowFunctionExpression', 'FunctionExpression']; /** * Determines if a node represents an unwrappable IIFE. - * + * * @param {ASTNode} n - The CallExpression node to check * @return {boolean} True if the node is an unwrappable IIFE */ function isUnwrappableIIFE(n) { - return !n.arguments.length && + return !n.arguments.length && IIFE_FUNCTION_TYPES.includes(n.callee.type) && !n.callee.id && // IIFEs with a single return statement for variable initialization @@ -22,127 +22,127 @@ function isUnwrappableIIFE(n) { /** * Computes target and replacement nodes for IIFE unwrapping. - * + * * @param {ASTNode} n - The IIFE CallExpression node * @return {Object|null} Object with targetNode and replacementNode, or null if unwrapping should be skipped */ function computeUnwrappingNodes(n) { - let targetNode = n; - let replacementNode = n.callee.body; - - if (replacementNode.type === 'BlockStatement') { - let targetChild = replacementNode; - - // IIFEs with a single return statement - if (replacementNode.body?.[0]?.argument) { - replacementNode = replacementNode.body[0].argument; - } - // IIFEs with multiple statements or expressions - else { - while (targetNode && !targetNode.body) { - // Skip cases where IIFE is used to initialize or set a value - if (targetNode.parentKey === 'init' || targetNode.type === 'AssignmentExpression') { - return null; // Signal to skip this candidate - } - targetChild = targetNode; - targetNode = targetNode.parentNode; - } - - if (!targetNode?.body?.filter) { - targetNode = n; - } else { - // Place the wrapped code instead of the wrapper node - replacementNode = { - ...targetNode, - body: targetNode.body.filter(t => t !== targetChild).concat(replacementNode.body), - }; - } - } - } - - return { targetNode, replacementNode }; + let targetNode = n; + let replacementNode = n.callee.body; + + if (replacementNode.type === 'BlockStatement') { + let targetChild = replacementNode; + + // IIFEs with a single return statement + if (replacementNode.body?.[0]?.argument) { + replacementNode = replacementNode.body[0].argument; + } + // IIFEs with multiple statements or expressions + else { + while (targetNode && !targetNode.body) { + // Skip cases where IIFE is used to initialize or set a value + if (targetNode.parentKey === 'init' || targetNode.type === 'AssignmentExpression') { + return null; // Signal to skip this candidate + } + targetChild = targetNode; + targetNode = targetNode.parentNode; + } + + if (!targetNode?.body?.filter) { + targetNode = n; + } else { + // Place the wrapped code instead of the wrapper node + replacementNode = { + ...targetNode, + body: targetNode.body.filter(t => t !== targetChild).concat(replacementNode.body), + }; + } + } + } + + return {targetNode, replacementNode}; } /** * Finds IIFE nodes that can be unwrapped. - * + * * Identifies Immediately Invoked Function Expressions (IIFEs) that: * - Have no arguments * - Use anonymous functions (arrow or function expressions) * - Are used for variable initialization or statement wrapping - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of IIFE CallExpression nodes that can be unwrapped */ export function unwrapIIFEsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.CallExpression; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - if (isUnwrappableIIFE(n) && candidateFilter(n)) { - // Verify that unwrapping is actually possible - const unwrappingNodes = computeUnwrappingNodes(n); - if (unwrappingNodes !== null) { - matches.push(n); - } - } - } - - return matches; + const relevantNodes = arb.ast[0].typeMap.CallExpression; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + if (isUnwrappableIIFE(n) && candidateFilter(n)) { + // Verify that unwrapping is actually possible + const unwrappingNodes = computeUnwrappingNodes(n); + if (unwrappingNodes !== null) { + matches.push(n); + } + } + } + + return matches; } /** * Transforms an IIFE by unwrapping it to reveal its content. - * + * * Handles two main transformation patterns: * 1. Variable initialization: Replace IIFE with returned function/value * 2. Statement unwrapping: Replace IIFE with its body statements - * + * * @param {Arborist} arb - The Arborist instance to mark nodes for transformation * @param {ASTNode} n - The IIFE CallExpression node to unwrap * @return {Arborist} The Arborist instance for chaining */ export function unwrapIIFEsTransform(arb, n) { - const unwrappingNodes = computeUnwrappingNodes(n); - - if (unwrappingNodes !== null) { - const { targetNode, replacementNode } = unwrappingNodes; - arb.markNode(targetNode, replacementNode); - } - - return arb; + const unwrappingNodes = computeUnwrappingNodes(n); + + if (unwrappingNodes !== null) { + const {targetNode, replacementNode} = unwrappingNodes; + arb.markNode(targetNode, replacementNode); + } + + return arb; } /** * Replace IIFEs that are unwrapping a function with the unwrapped function. - * + * * This optimization removes unnecessary IIFE wrappers around functions or statements * that serve no purpose other than immediate execution. The transformation handles * both variable initialization patterns and statement unwrapping scenarios. - * + * * Transforms: * ```javascript * var a = (() => { return b => c(b - 40); })(); * ``` - * + * * Into: * ```javascript * var a = b => c(b - 40); * ``` - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply to candidates * @return {Arborist} The Arborist instance for chaining */ export default function unwrapIIFEs(arb, candidateFilter = () => true) { - const matches = unwrapIIFEsMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = unwrapIIFEsTransform(arb, matches[i]); - } - - return arb; + const matches = unwrapIIFEsMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = unwrapIIFEsTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/safe/unwrapSimpleOperations.js b/src/modules/safe/unwrapSimpleOperations.js index db5320b..8d28329 100644 --- a/src/modules/safe/unwrapSimpleOperations.js +++ b/src/modules/safe/unwrapSimpleOperations.js @@ -1,17 +1,17 @@ const BINARY_OPERATORS = ['+', '-', '*', '/', '%', '&', '|', '&&', '||', '**', '^', '<=', '>=', '<', '>', '==', '===', '!=', - '!==', '<<', '>>', '>>>', 'in', 'instanceof', '??']; + '!==', '<<', '>>', '>>>', 'in', 'instanceof', '??']; const UNARY_OPERATORS = ['!', '~', '-', '+', 'typeof', 'void', 'delete', '--', '++']; const BINARY_EXPRESSION_TYPES = ['LogicalExpression', 'BinaryExpression']; const UNARY_EXPRESSION_TYPES = ['UnaryExpression', 'UpdateExpression']; /** * Determines if a node is a simple binary or logical operation within a function wrapper. - * + * * @param {ASTNode} n - The expression node to check * @return {boolean} True if the node is a binary/logical operation in a simple function wrapper */ function isBinaryOrLogicalWrapper(n) { - return BINARY_EXPRESSION_TYPES.includes(n.type) && + return BINARY_EXPRESSION_TYPES.includes(n.type) && BINARY_OPERATORS.includes(n.operator) && n.parentNode.type === 'ReturnStatement' && n.parentNode.parentNode?.body?.length === 1 && @@ -21,12 +21,12 @@ function isBinaryOrLogicalWrapper(n) { /** * Determines if a node is a simple unary or update operation within a function wrapper. - * + * * @param {ASTNode} n - The expression node to check * @return {boolean} True if the node is a unary/update operation in a simple function wrapper */ function isUnaryOrUpdateWrapper(n) { - return UNARY_EXPRESSION_TYPES.includes(n.type) && + return UNARY_EXPRESSION_TYPES.includes(n.type) && UNARY_OPERATORS.includes(n.operator) && n.parentNode.type === 'ReturnStatement' && n.parentNode.parentNode?.body?.length === 1 && @@ -35,132 +35,132 @@ function isUnaryOrUpdateWrapper(n) { /** * Creates a binary or logical expression node from the original operation. - * + * * @param {ASTNode} operationNode - The original binary/logical expression node * @param {ASTNode[]} args - The function call arguments to use as operands * @return {ASTNode} New binary or logical expression node */ function createBinaryOrLogicalExpression(operationNode, args) { - return { - type: operationNode.type, - operator: operationNode.operator, - left: args[0], - right: args[1], - }; + return { + type: operationNode.type, + operator: operationNode.operator, + left: args[0], + right: args[1], + }; } /** * Creates a unary or update expression node from the original operation. - * + * * @param {ASTNode} operationNode - The original unary/update expression node * @param {ASTNode[]} args - The function call arguments to use as operands * @return {ASTNode} New unary or update expression node */ function createUnaryOrUpdateExpression(operationNode, args) { - return { - type: operationNode.type, - operator: operationNode.operator, - prefix: operationNode.prefix, - argument: args[0], - }; + return { + type: operationNode.type, + operator: operationNode.operator, + prefix: operationNode.prefix, + argument: args[0], + }; } /** * Finds nodes representing simple operations wrapped in functions. - * + * * Identifies operation expressions (binary, logical, unary, update) that are: * - Single statements in function return statements * - Use function parameters as operands * - Can be safely unwrapped to direct operation calls - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of operation nodes that can be unwrapped */ export function unwrapSimpleOperationsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.BinaryExpression - .concat(arb.ast[0].typeMap.LogicalExpression) - .concat(arb.ast[0].typeMap.UnaryExpression) - .concat(arb.ast[0].typeMap.UpdateExpression); - - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - if ((isBinaryOrLogicalWrapper(n) || isUnaryOrUpdateWrapper(n)) && candidateFilter(n)) { - matches.push(n); - } - } - - return matches; + const relevantNodes = arb.ast[0].typeMap.BinaryExpression + .concat(arb.ast[0].typeMap.LogicalExpression) + .concat(arb.ast[0].typeMap.UnaryExpression) + .concat(arb.ast[0].typeMap.UpdateExpression); + + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + if ((isBinaryOrLogicalWrapper(n) || isUnaryOrUpdateWrapper(n)) && candidateFilter(n)) { + matches.push(n); + } + } + + return matches; } /** * Transforms a simple operation wrapper by replacing function calls with direct operations. - * + * * Replaces function calls that wrap simple operations with the actual operation. * For example, `add(1, 2)` where `add` is `function add(a,b) { return a + b; }` * becomes `1 + 2`. - * + * * @param {Arborist} arb - The Arborist instance to mark nodes for transformation * @param {ASTNode} n - The operation expression node within the function wrapper * @return {Arborist} The Arborist instance for chaining */ export function unwrapSimpleOperationsTransform(arb, n) { - const references = n.scope.block?.id?.references || []; - - for (let i = 0; i < references.length; i++) { - const ref = references[i]; - const callExpression = ref.parentNode; - - if (callExpression.type === 'CallExpression') { - let replacementNode = null; - - if (BINARY_EXPRESSION_TYPES.includes(n.type) && callExpression.arguments.length === 2) { - replacementNode = createBinaryOrLogicalExpression(n, callExpression.arguments); - } else if (UNARY_EXPRESSION_TYPES.includes(n.type) && callExpression.arguments.length === 1) { - replacementNode = createUnaryOrUpdateExpression(n, callExpression.arguments); - } - - if (replacementNode) { - arb.markNode(callExpression, replacementNode); - } - } - } - - return arb; + const references = n.scope.block?.id?.references || []; + + for (let i = 0; i < references.length; i++) { + const ref = references[i]; + const callExpression = ref.parentNode; + + if (callExpression.type === 'CallExpression') { + let replacementNode = null; + + if (BINARY_EXPRESSION_TYPES.includes(n.type) && callExpression.arguments.length === 2) { + replacementNode = createBinaryOrLogicalExpression(n, callExpression.arguments); + } else if (UNARY_EXPRESSION_TYPES.includes(n.type) && callExpression.arguments.length === 1) { + replacementNode = createUnaryOrUpdateExpression(n, callExpression.arguments); + } + + if (replacementNode) { + arb.markNode(callExpression, replacementNode); + } + } + } + + return arb; } /** * Replace calls to functions that wrap simple operations with the actual operations. - * + * * This optimization identifies function wrappers around simple operations (binary, logical, * unary, and update expressions) and replaces function calls with direct operations. * This removes unnecessary function call overhead for basic operations. - * + * * Transforms: * ```javascript * function add(a, b) { return a + b; } * add(1, 2); * ``` - * + * * Into: * ```javascript * function add(a, b) { return a + b; } * 1 + 2; * ``` - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply to candidates * @return {Arborist} The Arborist instance for chaining */ export default function unwrapSimpleOperations(arb, candidateFilter = () => true) { - const matches = unwrapSimpleOperationsMatch(arb, candidateFilter); - - for (let i = 0; i < matches.length; i++) { - arb = unwrapSimpleOperationsTransform(arb, matches[i]); - } - - return arb; + const matches = unwrapSimpleOperationsMatch(arb, candidateFilter); + + for (let i = 0; i < matches.length; i++) { + arb = unwrapSimpleOperationsTransform(arb, matches[i]); + } + + return arb; } \ No newline at end of file diff --git a/src/modules/unsafe/normalizeRedundantNotOperator.js b/src/modules/unsafe/normalizeRedundantNotOperator.js index 278d016..43990b6 100644 --- a/src/modules/unsafe/normalizeRedundantNotOperator.js +++ b/src/modules/unsafe/normalizeRedundantNotOperator.js @@ -11,124 +11,124 @@ const RESOLVABLE_ARGUMENT_TYPES = ['Literal', 'ArrayExpression', 'ObjectExpressi * @return {boolean} True if the argument can be resolved independently, false otherwise */ function canNotOperatorArgumentBeResolved(argument) { - switch (argument.type) { - case 'Literal': - return true; // All literals: !true, !"hello", !42, !null - - case 'ArrayExpression': - // All arrays evaluate to truthy (even empty ones), so all are resolvable - // E.g. ![] -> false, ![1, 2, 3] -> false - return true; - - case 'ObjectExpression': - // All objects evaluate to truthy (even empty ones), so all are resolvable - // E.g. !{} -> false, !{a: 1} -> false - return true; - - case 'Identifier': - // Only the undefined identifier has predictable truthiness - return argument.name === 'undefined'; - - case 'TemplateLiteral': - // Template literals with no dynamic expressions can be evaluated - // E.g. !`hello` -> false, !`` -> true, but not !`hello ${variable}` - return !argument.expressions.length; - - case 'UnaryExpression': - // Nested unary expressions: !!true, +!false, etc. - return canNotOperatorArgumentBeResolved(argument.argument); - } - - // Conservative approach: other expression types require runtime evaluation - return false; + switch (argument.type) { + case 'Literal': + return true; // All literals: !true, !"hello", !42, !null + + case 'ArrayExpression': + // All arrays evaluate to truthy (even empty ones), so all are resolvable + // E.g. ![] -> false, ![1, 2, 3] -> false + return true; + + case 'ObjectExpression': + // All objects evaluate to truthy (even empty ones), so all are resolvable + // E.g. !{} -> false, !{a: 1} -> false + return true; + + case 'Identifier': + // Only the undefined identifier has predictable truthiness + return argument.name === 'undefined'; + + case 'TemplateLiteral': + // Template literals with no dynamic expressions can be evaluated + // E.g. !`hello` -> false, !`` -> true, but not !`hello ${variable}` + return !argument.expressions.length; + + case 'UnaryExpression': + // Nested unary expressions: !!true, +!false, etc. + return canNotOperatorArgumentBeResolved(argument.argument); + } + + // Conservative approach: other expression types require runtime evaluation + return false; } /** * Finds UnaryExpression nodes with redundant NOT operators that can be normalized. - * + * * Identifies NOT operators (!expr) where the expression can be safely evaluated * to determine the boolean result. This includes NOT operations on: * - Literals (numbers, strings, booleans, null) * - Array expressions (empty or with literal elements) * - Object expressions (empty or with literal properties) * - Nested unary expressions - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} candidateFilter - Filter function to apply to candidates * @return {ASTNode[]} Array of UnaryExpression nodes with redundant NOT operators */ export function normalizeRedundantNotOperatorMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.UnaryExpression; - const matches = []; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - if (n.operator === '!' && + const relevantNodes = arb.ast[0].typeMap.UnaryExpression; + const matches = []; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + if (n.operator === '!' && RESOLVABLE_ARGUMENT_TYPES.includes(n.argument.type) && canNotOperatorArgumentBeResolved(n.argument) && candidateFilter(n)) { - matches.push(n); - } - } - - return matches; + matches.push(n); + } + } + + return matches; } /** * Transforms a redundant NOT operator by evaluating it to its boolean result. - * + * * Evaluates the NOT expression in a sandbox environment and replaces it with * the computed boolean literal. This normalizes expressions like `!true` to `false`, * `!0` to `true`, `![]` to `false`, etc. - * + * * @param {Arborist} arb - The Arborist instance to mark nodes for transformation * @param {ASTNode} n - The UnaryExpression node with redundant NOT operator * @param {Sandbox} sharedSandbox - Shared sandbox instance for evaluation * @return {Arborist} The Arborist instance for chaining */ export function normalizeRedundantNotOperatorTransform(arb, n, sharedSandbox) { - const replacementNode = evalInVm(n.src, sharedSandbox); - - if (replacementNode !== evalInVm.BAD_VALUE) { - arb.markNode(n, replacementNode); - } - - return arb; + const replacementNode = evalInVm(n.src, sharedSandbox); + + if (replacementNode !== evalInVm.BAD_VALUE) { + arb.markNode(n, replacementNode); + } + + return arb; } /** * Replace redundant NOT operators with their actual boolean values. - * + * * This optimization evaluates NOT expressions that can be safely computed at * transformation time, replacing them with boolean literals. This includes * expressions like `!true`, `!0`, `![]`, `!{}`, etc. - * + * * The evaluation is performed in a secure sandbox environment to prevent * code execution side effects. - * + * * Transforms: * ```javascript * !true || !false || !0 || !1 * ``` - * + * * Into: * ```javascript * false || true || true || false * ``` - * + * * @param {Arborist} arb - The Arborist instance containing the AST * @param {Function} [candidateFilter] - Optional filter to apply to candidates * @return {Arborist} The Arborist instance for chaining */ export default function normalizeRedundantNotOperator(arb, candidateFilter = () => true) { - const matches = normalizeRedundantNotOperatorMatch(arb, candidateFilter); - - if (matches.length) { - let sharedSandbox = new Sandbox(); - for (let i = 0; i < matches.length; i++) { - arb = normalizeRedundantNotOperatorTransform(arb, matches[i], sharedSandbox); - } - } - return arb; + const matches = normalizeRedundantNotOperatorMatch(arb, candidateFilter); + + if (matches.length) { + const sharedSandbox = new Sandbox(); + for (let i = 0; i < matches.length; i++) { + arb = normalizeRedundantNotOperatorTransform(arb, matches[i], sharedSandbox); + } + } + return arb; } \ No newline at end of file diff --git a/src/modules/unsafe/resolveAugmentedFunctionWrappedArrayReplacements.js b/src/modules/unsafe/resolveAugmentedFunctionWrappedArrayReplacements.js index 6613313..3298b7a 100644 --- a/src/modules/unsafe/resolveAugmentedFunctionWrappedArrayReplacements.js +++ b/src/modules/unsafe/resolveAugmentedFunctionWrappedArrayReplacements.js @@ -3,18 +3,16 @@ import {evalInVm} from '../utils/evalInVm.js'; import {getDescendants} from '../utils/getDescendants.js'; import {doesDescendantMatchCondition} from '../utils/doesDescendantMatchCondition.js'; - - /** * Resolves array reference from array candidate node by finding the assignment expression * where an array is assigned to a variable. - * - * This function returns the actual assignment/declaration node (e.g., `var arr = [1,2,3]` + * + * This function returns the actual assignment/declaration node (e.g., `var arr = [1,2,3]` * or `arr = someFunction()`). Having this assignment is crucial because it provides: * - The variable name that holds the array * - The ability to find all references to that array variable throughout the code * - The assignment expression needed for the sandbox evaluation context - * + * * Handles both: * - Global scope array declarations: `var arr = [1,2,3]` * - Call expression array initializations: `var arr = someArrayFunction()` @@ -23,17 +21,17 @@ import {doesDescendantMatchCondition} from '../utils/doesDescendantMatchConditio * @return {ASTNode|null} The assignment/declaration node containing the array, or null if not found */ function resolveArrayReference(ac) { - if (!ac.declNode) return null; - - if (ac.declNode.scope.type === 'global') { - if (ac.declNode.parentNode?.init?.type === 'ArrayExpression') { - return ac.declNode.parentNode?.parentNode || ac.declNode.parentNode; - } - } else if (ac.declNode.parentNode?.init?.type === 'CallExpression') { - return ac.declNode.parentNode.init.callee?.declNode?.parentNode; - } - - return null; + if (!ac.declNode) return null; + + if (ac.declNode.scope.type === 'global') { + if (ac.declNode.parentNode?.init?.type === 'ArrayExpression') { + return ac.declNode.parentNode?.parentNode || ac.declNode.parentNode; + } + } else if (ac.declNode.parentNode?.init?.type === 'CallExpression') { + return ac.declNode.parentNode.init.callee?.declNode?.parentNode; + } + + return null; } /** @@ -44,18 +42,18 @@ function resolveArrayReference(ac) { * @return {ASTNode|null} The matching expression statement or null if not found */ function findMatchingExpressionStatement(arb, ac) { - const expressionStatements = arb.ast[0].typeMap.ExpressionStatement; - for (let i = 0; i < expressionStatements.length; i++) { - const exp = expressionStatements[i]; - if (exp.expression.type === 'CallExpression' && + const expressionStatements = arb.ast[0].typeMap.ExpressionStatement; + for (let i = 0; i < expressionStatements.length; i++) { + const exp = expressionStatements[i]; + if (exp.expression.type === 'CallExpression' && exp.expression.callee.type === 'FunctionExpression' && exp.expression.arguments.length && exp.expression.arguments[0].type === 'Identifier' && exp.expression.arguments[0].declNode === ac.declNode) { - return exp; - } - } - return null; + return exp; + } + } + return null; } /** @@ -67,27 +65,27 @@ function findMatchingExpressionStatement(arb, ac) { * @return {ASTNode[]} Array of call expression nodes that are replacement candidates */ function findReplacementCandidates(arb, arrDecryptor, skipScopes) { - const callExpressions = arb.ast[0].typeMap.CallExpression; - const replacementCandidates = []; - - for (let i = 0; i < callExpressions.length; i++) { - const c = callExpressions[i]; - if (c.callee?.name === arrDecryptor.id.name && + const callExpressions = arb.ast[0].typeMap.CallExpression; + const replacementCandidates = []; + + for (let i = 0; i < callExpressions.length; i++) { + const c = callExpressions[i]; + if (c.callee?.name === arrDecryptor.id.name && !skipScopes.includes(c.scope)) { - replacementCandidates.push(c); - } - } - - return replacementCandidates; + replacementCandidates.push(c); + } + } + + return replacementCandidates; } /** * Finds FunctionDeclaration nodes that are potentially augmented functions. - * + * * Performs initial filtering for functions that: * - Are named (have an identifier) * - Contains assignment expressions that modify the function itself - * + * * Additional validation (checking if the function is used as an array decryptor) * is performed in the transform function since it's computationally expensive * and the results are needed for the actual transformation logic. @@ -97,25 +95,25 @@ function findReplacementCandidates(arb, arrDecryptor, skipScopes) { * @return {ASTNode[]} Array of FunctionDeclaration nodes that are potentially augmented */ export function resolveAugmentedFunctionWrappedArrayReplacementsMatch(arb, candidateFilter = () => true) { - const relevantNodes = arb.ast[0].typeMap.FunctionDeclaration; - const matches = []; + const relevantNodes = arb.ast[0].typeMap.FunctionDeclaration; + const matches = []; - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - if (n.id?.name && candidateFilter(n) && + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + if (n.id?.name && candidateFilter(n) && doesDescendantMatchCondition(n, d => - d.type === 'AssignmentExpression' && + d.type === 'AssignmentExpression' && d.left?.name === n.id.name)) { - matches.push(n); - } - } + matches.push(n); + } + } - return matches; + return matches; } /** * Transforms augmented function declarations by resolving array-wrapped function calls. - * + * * This handles a complex obfuscation pattern where: * 1. Array data is stored in variables (global or function-scoped) * 2. A decryptor function processes array indices to return string values @@ -132,61 +130,61 @@ export function resolveAugmentedFunctionWrappedArrayReplacementsMatch(arb, candi * @return {Arborist} The Arborist instance for chaining */ export function resolveAugmentedFunctionWrappedArrayReplacementsTransform(arb, n) { - const descendants = getDescendants(n); - const arrDecryptor = n; - - // Find and process MemberExpression nodes with Identifier objects as array candidates - for (let i = 0; i < descendants.length; i++) { - const d = descendants[i]; - if (d.type === 'MemberExpression' && d.object.type === 'Identifier') { - const ac = d.object; - const arrRef = resolveArrayReference(ac); - - if (arrRef) { - const exp = findMatchingExpressionStatement(arb, ac); - - if (exp) { - const context = [arrRef.src, arrDecryptor.src, exp.src].join('\n;'); - const skipScopes = [arrRef.scope, arrDecryptor.scope, exp.expression.callee.scope]; - const replacementCandidates = findReplacementCandidates(arb, arrDecryptor, skipScopes); - - if (!replacementCandidates.length) continue; - - const sb = new Sandbox(); - sb.run(context); - - for (let j = 0; j < replacementCandidates.length; j++) { - const rc = replacementCandidates[j]; - const replacementNode = evalInVm(`\n${rc.src}`, sb); - if (replacementNode !== evalInVm.BAD_VALUE) { - arb.markNode(rc, replacementNode); - } - } - break; - } - } - } - } - - return arb; + const descendants = getDescendants(n); + const arrDecryptor = n; + + // Find and process MemberExpression nodes with Identifier objects as array candidates + for (let i = 0; i < descendants.length; i++) { + const d = descendants[i]; + if (d.type === 'MemberExpression' && d.object.type === 'Identifier') { + const ac = d.object; + const arrRef = resolveArrayReference(ac); + + if (arrRef) { + const exp = findMatchingExpressionStatement(arb, ac); + + if (exp) { + const context = [arrRef.src, arrDecryptor.src, exp.src].join('\n;'); + const skipScopes = [arrRef.scope, arrDecryptor.scope, exp.expression.callee.scope]; + const replacementCandidates = findReplacementCandidates(arb, arrDecryptor, skipScopes); + + if (!replacementCandidates.length) continue; + + const sb = new Sandbox(); + sb.run(context); + + for (let j = 0; j < replacementCandidates.length; j++) { + const rc = replacementCandidates[j]; + const replacementNode = evalInVm(`\n${rc.src}`, sb); + if (replacementNode !== evalInVm.BAD_VALUE) { + arb.markNode(rc, replacementNode); + } + } + break; + } + } + } + } + + return arb; } /** * Resolves augmented function-wrapped array replacements in obfuscated code. - * + * * This transformation handles a sophisticated obfuscation pattern where array * access is disguised through function calls that decrypt array indices. The * pattern typically involves: - * + * * 1. An array of encoded strings stored in a variable * 2. A decryptor function that takes indices and returns decoded strings * 3. Assignment expressions that modify the decryptor function (augmentation) * 4. Function expressions that establish the array-decryptor relationship * 5. Call expressions throughout the code that use the decryptor - * + * * This module identifies such patterns and replaces the function calls with * their actual string literals, effectively deobfuscating the code. - * + * * Example transformation: * ```javascript * // Before: @@ -194,7 +192,7 @@ export function resolveAugmentedFunctionWrappedArrayReplacementsTransform(arb, n * function decrypt(i) { return arr[i]; } * decrypt = augmentFunction(decrypt, arr); * console.log(decrypt(0)); // obfuscated call - * + * * // After: * var arr = ['encoded1', 'encoded2']; * function decrypt(i) { return arr[i]; } @@ -207,11 +205,11 @@ export function resolveAugmentedFunctionWrappedArrayReplacementsTransform(arb, n * @return {Arborist} The Arborist instance for chaining */ export default function resolveAugmentedFunctionWrappedArrayReplacements(arb, candidateFilter = () => true) { - const matches = resolveAugmentedFunctionWrappedArrayReplacementsMatch(arb, candidateFilter); + const matches = resolveAugmentedFunctionWrappedArrayReplacementsMatch(arb, candidateFilter); - for (let i = 0; i < matches.length; i++) { - arb = resolveAugmentedFunctionWrappedArrayReplacementsTransform(arb, matches[i]); - } + for (let i = 0; i < matches.length; i++) { + arb = resolveAugmentedFunctionWrappedArrayReplacementsTransform(arb, matches[i]); + } - return arb; + return arb; } \ No newline at end of file diff --git a/src/modules/unsafe/resolveBuiltinCalls.js b/src/modules/unsafe/resolveBuiltinCalls.js index 3081470..debd646 100644 --- a/src/modules/unsafe/resolveBuiltinCalls.js +++ b/src/modules/unsafe/resolveBuiltinCalls.js @@ -8,7 +8,7 @@ import {SKIP_IDENTIFIERS, SKIP_PROPERTIES} from '../config.js'; const AVAILABLE_SAFE_IMPLEMENTATIONS = Object.keys(safeImplementations); // Builtin functions that shouldn't be resolved in the deobfuscation context. const SKIP_BUILTIN_FUNCTIONS = [ - 'Function', 'eval', 'Array', 'Object', 'fetch', 'XMLHttpRequest', 'Promise', 'console', 'performance', '$', + 'Function', 'eval', 'Array', 'Object', 'fetch', 'XMLHttpRequest', 'Promise', 'console', 'performance', '$', ]; /** @@ -20,46 +20,46 @@ const SKIP_BUILTIN_FUNCTIONS = [ * @return {ASTNode[]} Array of nodes that match the criteria */ export function resolveBuiltinCallsMatch(arb, candidateFilter = () => true) { - const matches = []; - const relevantNodes = arb.ast[0].typeMap.MemberExpression - .concat(arb.ast[0].typeMap.CallExpression) - .concat(arb.ast[0].typeMap.Identifier); - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - if (!candidateFilter(n)) continue; - - // Skip user-defined functions and objects, this expressions, constructor access - if (n.callee?.declNode || n?.callee?.object?.declNode || + const matches = []; + const relevantNodes = arb.ast[0].typeMap.MemberExpression + .concat(arb.ast[0].typeMap.CallExpression) + .concat(arb.ast[0].typeMap.Identifier); + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + if (!candidateFilter(n)) continue; + + // Skip user-defined functions and objects, this expressions, constructor access + if (n.callee?.declNode || n?.callee?.object?.declNode || 'ThisExpression' === (n.callee?.object?.type || n.callee?.type) || 'constructor' === (n.callee?.property?.name || n.callee?.property?.value)) { - continue; - } - - // Check for safe implementation calls - if (n.type === 'CallExpression' && AVAILABLE_SAFE_IMPLEMENTATIONS.includes(n.callee.name)) { - matches.push(n); - } - - // Check for calls with only literal arguments - else if (n.type === 'CallExpression' && !n.arguments.some(a => a.type !== 'Literal')) { - // Check if callee is builtin identifier - if (n.callee.type === 'Identifier' && !n.callee.declNode && + continue; + } + + // Check for safe implementation calls + if (n.type === 'CallExpression' && AVAILABLE_SAFE_IMPLEMENTATIONS.includes(n.callee.name)) { + matches.push(n); + } + + // Check for calls with only literal arguments + else if (n.type === 'CallExpression' && !n.arguments.some(a => a.type !== 'Literal')) { + // Check if callee is builtin identifier + if (n.callee.type === 'Identifier' && !n.callee.declNode && !SKIP_BUILTIN_FUNCTIONS.includes(n.callee.name)) { - matches.push(n); - continue; - } - - // Check if callee is builtin member expression - if (n.callee.type === 'MemberExpression' && !n.callee.object.declNode && + matches.push(n); + continue; + } + + // Check if callee is builtin member expression + if (n.callee.type === 'MemberExpression' && !n.callee.object.declNode && !SKIP_BUILTIN_FUNCTIONS.includes(n.callee.object?.name) && !SKIP_IDENTIFIERS.includes(n.callee.object?.name) && !SKIP_PROPERTIES.includes(n.callee.property?.name || n.callee.property?.value)) { - matches.push(n); - } - } - } - return matches; + matches.push(n); + } + } + } + return matches; } /** @@ -71,24 +71,24 @@ export function resolveBuiltinCallsMatch(arb, candidateFilter = () => true) { * @return {Arborist} The updated Arborist instance */ export function resolveBuiltinCallsTransform(arb, n, sharedSb) { - try { - const safeImplementation = safeImplementations[n.callee.name]; - if (safeImplementation) { - // Use safe implementation for known functions (btoa, atob, etc.) - const args = n.arguments.map(a => a.value); - const tempValue = safeImplementation(...args); - if (tempValue) { - arb.markNode(n, createNewNode(tempValue)); - } - } else { - // Evaluate unknown builtin calls in sandbox - const replacementNode = evalInVm(n.src, sharedSb); - if (replacementNode !== evalInVm.BAD_VALUE) arb.markNode(n, replacementNode); - } - } catch (e) { - logger.debug(e.message); - } - return arb; + try { + const safeImplementation = safeImplementations[n.callee.name]; + if (safeImplementation) { + // Use safe implementation for known functions (btoa, atob, etc.) + const args = n.arguments.map(a => a.value); + const tempValue = safeImplementation(...args); + if (tempValue) { + arb.markNode(n, createNewNode(tempValue)); + } + } else { + // Evaluate unknown builtin calls in sandbox + const replacementNode = evalInVm(n.src, sharedSb); + if (replacementNode !== evalInVm.BAD_VALUE) arb.markNode(n, replacementNode); + } + } catch (e) { + logger.debug(e.message); + } + return arb; } /** @@ -100,13 +100,13 @@ export function resolveBuiltinCallsTransform(arb, n, sharedSb) { * @return {Arborist} The updated Arborist instance */ export default function resolveBuiltinCalls(arb, candidateFilter = () => true) { - const matches = resolveBuiltinCallsMatch(arb, candidateFilter); - let sharedSb; - - for (let i = 0; i < matches.length; i++) { - // Create sandbox only when needed to avoid overhead - sharedSb = sharedSb || new Sandbox(); - arb = resolveBuiltinCallsTransform(arb, matches[i], sharedSb); - } - return arb; + const matches = resolveBuiltinCallsMatch(arb, candidateFilter); + let sharedSb; + + for (let i = 0; i < matches.length; i++) { + // Create sandbox only when needed to avoid overhead + sharedSb = sharedSb || new Sandbox(); + arb = resolveBuiltinCallsTransform(arb, matches[i], sharedSb); + } + return arb; } \ No newline at end of file diff --git a/src/modules/unsafe/resolveDefiniteBinaryExpressions.js b/src/modules/unsafe/resolveDefiniteBinaryExpressions.js index 81cb985..0c037ec 100644 --- a/src/modules/unsafe/resolveDefiniteBinaryExpressions.js +++ b/src/modules/unsafe/resolveDefiniteBinaryExpressions.js @@ -17,59 +17,59 @@ import {evalInVm} from '../utils/evalInVm.js'; * doesBinaryExpressionContainOnlyLiterals(parseCode('!true').body[0].expression); * doesBinaryExpressionContainOnlyLiterals(parseCode('true ? 1 : 2').body[0].expression); * - * // Returns false + * // Returns false * doesBinaryExpressionContainOnlyLiterals(parseCode('1 + x').body[0].expression); * doesBinaryExpressionContainOnlyLiterals(parseCode('func()').body[0].expression); */ export function doesBinaryExpressionContainOnlyLiterals(expression) { - // Early return for null/undefined to prevent errors - if (!expression || !expression.type) { - return false; - } - - switch (expression.type) { - case 'BinaryExpression': - // Both operands must contain only literals - return doesBinaryExpressionContainOnlyLiterals(expression.left) && + // Early return for null/undefined to prevent errors + if (!expression || !expression.type) { + return false; + } + + switch (expression.type) { + case 'BinaryExpression': + // Both operands must contain only literals + return doesBinaryExpressionContainOnlyLiterals(expression.left) && doesBinaryExpressionContainOnlyLiterals(expression.right); - - case 'UnaryExpression': - // Argument must contain only literals (e.g., !true, -5, +"hello") - return doesBinaryExpressionContainOnlyLiterals(expression.argument); - - case 'UpdateExpression': - // UpdateExpression requires lvalue (variable/property), never a literal - // Valid: ++x, invalid: ++5 (flast won't generate UpdateExpression for invalid syntax) - return false; - - case 'LogicalExpression': - // Both operands must contain only literals (e.g., true && false, 1 || 2) - return doesBinaryExpressionContainOnlyLiterals(expression.left) && + + case 'UnaryExpression': + // Argument must contain only literals (e.g., !true, -5, +"hello") + return doesBinaryExpressionContainOnlyLiterals(expression.argument); + + case 'UpdateExpression': + // UpdateExpression requires lvalue (variable/property), never a literal + // Valid: ++x, invalid: ++5 (flast won't generate UpdateExpression for invalid syntax) + return false; + + case 'LogicalExpression': + // Both operands must contain only literals (e.g., true && false, 1 || 2) + return doesBinaryExpressionContainOnlyLiterals(expression.left) && doesBinaryExpressionContainOnlyLiterals(expression.right); - - case 'ConditionalExpression': - // All three parts must contain only literals (e.g., true ? 1 : 2) - return doesBinaryExpressionContainOnlyLiterals(expression.test) && + + case 'ConditionalExpression': + // All three parts must contain only literals (e.g., true ? 1 : 2) + return doesBinaryExpressionContainOnlyLiterals(expression.test) && doesBinaryExpressionContainOnlyLiterals(expression.consequent) && doesBinaryExpressionContainOnlyLiterals(expression.alternate); - - case 'SequenceExpression': - // All expressions in sequence must contain only literals (e.g., (1, 2, 3)) - for (let i = 0; i < expression.expressions.length; i++) { - if (!doesBinaryExpressionContainOnlyLiterals(expression.expressions[i])) { - return false; - } - } - return true; - - case 'Literal': - // Base case: literals are always literal-only - return true; - - default: - // Any other node type (Identifier, CallExpression, etc.) is not literal-only - return false; - } + + case 'SequenceExpression': + // All expressions in sequence must contain only literals (e.g., (1, 2, 3)) + for (let i = 0; i < expression.expressions.length; i++) { + if (!doesBinaryExpressionContainOnlyLiterals(expression.expressions[i])) { + return false; + } + } + return true; + + case 'Literal': + // Base case: literals are always literal-only + return true; + + default: + // Any other node type (Identifier, CallExpression, etc.) is not literal-only + return false; + } } /** @@ -80,17 +80,17 @@ export function doesBinaryExpressionContainOnlyLiterals(expression) { * @return {ASTNode[]} Array of BinaryExpression nodes ready for evaluation */ export function resolveDefiniteBinaryExpressionsMatch(arb, candidateFilter = () => true) { - const matches = []; - const relevantNodes = arb.ast[0].typeMap.BinaryExpression; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - if (doesBinaryExpressionContainOnlyLiterals(n) && candidateFilter(n)) { - matches.push(n); - } - } - return matches; + const matches = []; + const relevantNodes = arb.ast[0].typeMap.BinaryExpression; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + if (doesBinaryExpressionContainOnlyLiterals(n) && candidateFilter(n)) { + matches.push(n); + } + } + return matches; } /** @@ -101,33 +101,33 @@ export function resolveDefiniteBinaryExpressionsMatch(arb, candidateFilter = () * @return {Arborist} The updated Arborist instance */ export function resolveDefiniteBinaryExpressionsTransform(arb, matches) { - if (!matches.length) return arb; - - const sharedSb = new Sandbox(); - - for (let i = 0; i < matches.length; i++) { - const n = matches[i]; - const replacementNode = evalInVm(n.src, sharedSb); - - if (replacementNode !== evalInVm.BAD_VALUE) { - try { - // Handle negative number edge case: when evaluating expressions like '5 - 10', - // the result may be a UnaryExpression with '-5' instead of a Literal with value -5. - // This ensures numeric operations remain as proper numeric literals. - if (replacementNode.type === 'UnaryExpression' && - typeof n?.left?.value === 'number' && + if (!matches.length) return arb; + + const sharedSb = new Sandbox(); + + for (let i = 0; i < matches.length; i++) { + const n = matches[i]; + const replacementNode = evalInVm(n.src, sharedSb); + + if (replacementNode !== evalInVm.BAD_VALUE) { + try { + // Handle negative number edge case: when evaluating expressions like '5 - 10', + // the result may be a UnaryExpression with '-5' instead of a Literal with value -5. + // This ensures numeric operations remain as proper numeric literals. + if (replacementNode.type === 'UnaryExpression' && + typeof n?.left?.value === 'number' && typeof n?.right?.value === 'number') { - const v = parseInt(replacementNode.argument.raw); - replacementNode.argument.value = v; - replacementNode.argument.raw = `${v}`; - } - arb.markNode(n, replacementNode); - } catch (e) { - logger.debug(e.message); - } - } - } - return arb; + const v = parseInt(replacementNode.argument.raw); + replacementNode.argument.value = v; + replacementNode.argument.raw = `${v}`; + } + arb.markNode(n, replacementNode); + } catch (e) { + logger.debug(e.message); + } + } + } + return arb; } /** @@ -139,6 +139,6 @@ export function resolveDefiniteBinaryExpressionsTransform(arb, matches) { * @return {Arborist} The updated Arborist instance */ export default function resolveDefiniteBinaryExpressions(arb, candidateFilter = () => true) { - const matches = resolveDefiniteBinaryExpressionsMatch(arb, candidateFilter); - return resolveDefiniteBinaryExpressionsTransform(arb, matches); + const matches = resolveDefiniteBinaryExpressionsMatch(arb, candidateFilter); + return resolveDefiniteBinaryExpressionsTransform(arb, matches); } \ No newline at end of file diff --git a/src/modules/unsafe/resolveDefiniteMemberExpressions.js b/src/modules/unsafe/resolveDefiniteMemberExpressions.js index ac46d2d..2dc596b 100644 --- a/src/modules/unsafe/resolveDefiniteMemberExpressions.js +++ b/src/modules/unsafe/resolveDefiniteMemberExpressions.js @@ -12,39 +12,39 @@ const VALID_OBJECT_TYPES = ['ArrayExpression', 'Literal']; * @return {ASTNode[]} Array of MemberExpression nodes ready for evaluation */ export function resolveDefiniteMemberExpressionsMatch(arb, candidateFilter = () => true) { - const matches = []; - const relevantNodes = arb.ast[0].typeMap.MemberExpression; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Prevent unsafe transformations that could break semantics - if (n.parentNode.type === 'UpdateExpression') { - // Prevent replacing (++[[]][0]) with (++1) which changes semantics - continue; - } - - if (n.parentKey === 'callee') { - // Prevent replacing obj.method() with undefined() calls - continue; - } - - // Property must be a literal or non-computed identifier (safe to evaluate) - const hasValidProperty = n.property.type === 'Literal' || + const matches = []; + const relevantNodes = arb.ast[0].typeMap.MemberExpression; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Prevent unsafe transformations that could break semantics + if (n.parentNode.type === 'UpdateExpression') { + // Prevent replacing (++[[]][0]) with (++1) which changes semantics + continue; + } + + if (n.parentKey === 'callee') { + // Prevent replacing obj.method() with undefined() calls + continue; + } + + // Property must be a literal or non-computed identifier (safe to evaluate) + const hasValidProperty = n.property.type === 'Literal' || (n.property.name && !n.computed); - if (!hasValidProperty) continue; - - // Object must be a literal or array expression (deterministic) - if (!VALID_OBJECT_TYPES.includes(n.object.type)) continue; - - // Object must have content to access (length or elements) - if (!(n.object?.value?.length || n.object?.elements?.length)) continue; - - if (candidateFilter(n)) { - matches.push(n); - } - } - return matches; + if (!hasValidProperty) continue; + + // Object must be a literal or array expression (deterministic) + if (!VALID_OBJECT_TYPES.includes(n.object.type)) continue; + + // Object must have content to access (length or elements) + if (!(n.object?.value?.length || n.object?.elements?.length)) continue; + + if (candidateFilter(n)) { + matches.push(n); + } + } + return matches; } /** @@ -55,19 +55,19 @@ export function resolveDefiniteMemberExpressionsMatch(arb, candidateFilter = () * @return {Arborist} The updated Arborist instance */ export function resolveDefiniteMemberExpressionsTransform(arb, matches) { - if (!matches.length) return arb; - - const sharedSb = new Sandbox(); - - for (let i = 0; i < matches.length; i++) { - const n = matches[i]; - const replacementNode = evalInVm(n.src, sharedSb); - - if (replacementNode !== evalInVm.BAD_VALUE) { - arb.markNode(n, replacementNode); - } - } - return arb; + if (!matches.length) return arb; + + const sharedSb = new Sandbox(); + + for (let i = 0; i < matches.length; i++) { + const n = matches[i]; + const replacementNode = evalInVm(n.src, sharedSb); + + if (replacementNode !== evalInVm.BAD_VALUE) { + arb.markNode(n, replacementNode); + } + } + return arb; } /** @@ -79,6 +79,6 @@ export function resolveDefiniteMemberExpressionsTransform(arb, matches) { * @return {Arborist} The updated Arborist instance */ export default function resolveDefiniteMemberExpressions(arb, candidateFilter = () => true) { - const matches = resolveDefiniteMemberExpressionsMatch(arb, candidateFilter); - return resolveDefiniteMemberExpressionsTransform(arb, matches); + const matches = resolveDefiniteMemberExpressionsMatch(arb, candidateFilter); + return resolveDefiniteMemberExpressionsTransform(arb, matches); } \ No newline at end of file diff --git a/src/modules/unsafe/resolveDeterministicConditionalExpressions.js b/src/modules/unsafe/resolveDeterministicConditionalExpressions.js index 7c1fd88..a88e809 100644 --- a/src/modules/unsafe/resolveDeterministicConditionalExpressions.js +++ b/src/modules/unsafe/resolveDeterministicConditionalExpressions.js @@ -9,17 +9,17 @@ import {evalInVm} from '../utils/evalInVm.js'; * @return {ASTNode[]} Array of ConditionalExpression nodes ready for evaluation */ export function resolveDeterministicConditionalExpressionsMatch(arb, candidateFilter = () => true) { - const matches = []; - const relevantNodes = arb.ast[0].typeMap.ConditionalExpression; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - // Only resolve conditionals where test is a literal (deterministic) - if (n.test.type === 'Literal' && candidateFilter(n)) { - matches.push(n); - } - } - return matches; + const matches = []; + const relevantNodes = arb.ast[0].typeMap.ConditionalExpression; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + // Only resolve conditionals where test is a literal (deterministic) + if (n.test.type === 'Literal' && candidateFilter(n)) { + matches.push(n); + } + } + return matches; } /** @@ -30,21 +30,21 @@ export function resolveDeterministicConditionalExpressionsMatch(arb, candidateFi * @return {Arborist} The updated Arborist instance */ export function resolveDeterministicConditionalExpressionsTransform(arb, matches) { - if (!matches.length) return arb; - - const sharedSb = new Sandbox(); - - for (let i = 0; i < matches.length; i++) { - const n = matches[i]; - // Evaluate the literal test value to determine truthiness - const replacementNode = evalInVm(`Boolean(${n.test.src});`, sharedSb); - - if (replacementNode.type === 'Literal') { - // Replace conditional with consequent if truthy, alternate if falsy - arb.markNode(n, replacementNode.value ? n.consequent : n.alternate); - } - } - return arb; + if (!matches.length) return arb; + + const sharedSb = new Sandbox(); + + for (let i = 0; i < matches.length; i++) { + const n = matches[i]; + // Evaluate the literal test value to determine truthiness + const replacementNode = evalInVm(`Boolean(${n.test.src});`, sharedSb); + + if (replacementNode.type === 'Literal') { + // Replace conditional with consequent if truthy, alternate if falsy + arb.markNode(n, replacementNode.value ? n.consequent : n.alternate); + } + } + return arb; } /** @@ -56,6 +56,6 @@ export function resolveDeterministicConditionalExpressionsTransform(arb, matches * @return {Arborist} The updated Arborist instance */ export default function resolveDeterministicConditionalExpressions(arb, candidateFilter = () => true) { - const matches = resolveDeterministicConditionalExpressionsMatch(arb, candidateFilter); - return resolveDeterministicConditionalExpressionsTransform(arb, matches); + const matches = resolveDeterministicConditionalExpressionsMatch(arb, candidateFilter); + return resolveDeterministicConditionalExpressionsTransform(arb, matches); } \ No newline at end of file diff --git a/src/modules/unsafe/resolveEvalCallsOnNonLiterals.js b/src/modules/unsafe/resolveEvalCallsOnNonLiterals.js index 32fa125..9bf4aa0 100644 --- a/src/modules/unsafe/resolveEvalCallsOnNonLiterals.js +++ b/src/modules/unsafe/resolveEvalCallsOnNonLiterals.js @@ -13,20 +13,20 @@ import {getDeclarationWithContext} from '../utils/getDeclarationWithContext.js'; * @return {ASTNode[]} Array of eval CallExpression nodes ready for resolution */ export function resolveEvalCallsOnNonLiteralsMatch(arb, candidateFilter = () => true) { - const matches = []; - const relevantNodes = arb.ast[0].typeMap.CallExpression; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - // Only process eval calls with exactly one non-literal argument - if (n.callee.name === 'eval' && + const matches = []; + const relevantNodes = arb.ast[0].typeMap.CallExpression; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + // Only process eval calls with exactly one non-literal argument + if (n.callee.name === 'eval' && n.arguments.length === 1 && n.arguments[0].type !== 'Literal' && candidateFilter(n)) { - matches.push(n); - } - } - return matches; + matches.push(n); + } + } + return matches; } /** @@ -38,57 +38,57 @@ export function resolveEvalCallsOnNonLiteralsMatch(arb, candidateFilter = () => * @return {Arborist} The updated Arborist instance */ export function resolveEvalCallsOnNonLiteralsTransform(arb, matches) { - if (!matches.length) return arb; - - const sharedSb = new Sandbox(); - - for (let i = 0; i < matches.length; i++) { - const n = matches[i]; - - // Gather context nodes that might be referenced by the eval argument - const contextNodes = getDeclarationWithContext(n, true); - - // Remove any nodes that are part of the eval expression itself to avoid circular references - const possiblyRedundantNodes = [n, n?.parentNode, n?.parentNode?.parentNode]; - for (let j = 0; j < possiblyRedundantNodes.length; j++) { - const redundantNode = possiblyRedundantNodes[j]; - const index = contextNodes.indexOf(redundantNode); - if (index !== -1) { - contextNodes.splice(index, 1); - } - } - - // Build evaluation context: dependencies + argument assignment + return value - const context = contextNodes.length ? createOrderedSrc(contextNodes) : ''; - const src = `${context}\n;${createOrderedSrc([n.arguments[0]])}\n;`; - - const newNode = evalInVm(src, sharedSb); - const targetNode = n.parentNode.type === 'ExpressionStatement' ? n.parentNode : n; - let replacementNode = newNode; - - // If result is a literal string, try to parse it as JavaScript code - try { - if (newNode.type === 'Literal') { - try { - replacementNode = parseCode(newNode.value); - } catch { - // Handle malformed code by adding newlines after closing brackets - // (except when part of regex patterns like "/}/") - replacementNode = parseCode(newNode.value.replace(/([)}])(?!\/)/g, '$1\n')); - } finally { - // Fallback to unparsed literal if parsing results in empty program - if (!replacementNode.body.length) replacementNode = newNode; - } - } - } catch { - // If all parsing attempts fail, keep the original evaluated result - } - - if (replacementNode !== evalInVm.BAD_VALUE) { - arb.markNode(targetNode, replacementNode); - } - } - return arb; + if (!matches.length) return arb; + + const sharedSb = new Sandbox(); + + for (let i = 0; i < matches.length; i++) { + const n = matches[i]; + + // Gather context nodes that might be referenced by the eval argument + const contextNodes = getDeclarationWithContext(n, true); + + // Remove any nodes that are part of the eval expression itself to avoid circular references + const possiblyRedundantNodes = [n, n?.parentNode, n?.parentNode?.parentNode]; + for (let j = 0; j < possiblyRedundantNodes.length; j++) { + const redundantNode = possiblyRedundantNodes[j]; + const index = contextNodes.indexOf(redundantNode); + if (index !== -1) { + contextNodes.splice(index, 1); + } + } + + // Build evaluation context: dependencies + argument assignment + return value + const context = contextNodes.length ? createOrderedSrc(contextNodes) : ''; + const src = `${context}\n;${createOrderedSrc([n.arguments[0]])}\n;`; + + const newNode = evalInVm(src, sharedSb); + const targetNode = n.parentNode.type === 'ExpressionStatement' ? n.parentNode : n; + let replacementNode = newNode; + + // If result is a literal string, try to parse it as JavaScript code + try { + if (newNode.type === 'Literal') { + try { + replacementNode = parseCode(newNode.value); + } catch { + // Handle malformed code by adding newlines after closing brackets + // (except when part of regex patterns like "/}/") + replacementNode = parseCode(newNode.value.replace(/([)}])(?!\/)/g, '$1\n')); + } finally { + // Fallback to unparsed literal if parsing results in empty program + if (!replacementNode.body.length) replacementNode = newNode; + } + } + } catch { + // If all parsing attempts fail, keep the original evaluated result + } + + if (replacementNode !== evalInVm.BAD_VALUE) { + arb.markNode(targetNode, replacementNode); + } + } + return arb; } /** @@ -100,6 +100,6 @@ export function resolveEvalCallsOnNonLiteralsTransform(arb, matches) { * @return {Arborist} The updated Arborist instance */ export default function resolveEvalCallsOnNonLiterals(arb, candidateFilter = () => true) { - const matches = resolveEvalCallsOnNonLiteralsMatch(arb, candidateFilter); - return resolveEvalCallsOnNonLiteralsTransform(arb, matches); + const matches = resolveEvalCallsOnNonLiteralsMatch(arb, candidateFilter); + return resolveEvalCallsOnNonLiteralsTransform(arb, matches); } \ No newline at end of file diff --git a/src/modules/unsafe/resolveFunctionToArray.js b/src/modules/unsafe/resolveFunctionToArray.js index a61a4b6..a27b6bf 100644 --- a/src/modules/unsafe/resolveFunctionToArray.js +++ b/src/modules/unsafe/resolveFunctionToArray.js @@ -16,28 +16,28 @@ const {createOrderedSrc, getDeclarationWithContext} = utils; * @return {ASTNode[]} Array of VariableDeclarator nodes ready for array resolution */ export function resolveFunctionToArrayMatch(arb, candidateFilter = () => true) { - const matches = []; - const relevantNodes = arb.ast[0].typeMap.VariableDeclarator; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Must be a variable assigned a function call result - if (n.init?.type !== 'CallExpression') continue; - - // All references must be member expressions that are NOT used as function callees - // Empty references array is allowed - if (n.id.references?.some(r => { - return r.parentNode.type !== 'MemberExpression' || - (r.parentNode.parentNode?.type === 'CallExpression' && + const matches = []; + const relevantNodes = arb.ast[0].typeMap.VariableDeclarator; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Must be a variable assigned a function call result + if (n.init?.type !== 'CallExpression') continue; + + // All references must be member expressions that are NOT used as function callees + // Empty references array is allowed + if (n.id.references?.some(r => { + return r.parentNode.type !== 'MemberExpression' || + (r.parentNode.parentNode?.type === 'CallExpression' && r.parentNode.parentNode.callee === r.parentNode); - })) continue; - - if (candidateFilter(n)) { - matches.push(n); - } - } - return matches; + })) continue; + + if (candidateFilter(n)) { + matches.push(n); + } + } + return matches; } /** @@ -49,32 +49,32 @@ export function resolveFunctionToArrayMatch(arb, candidateFilter = () => true) { * @return {Arborist} The updated Arborist instance */ export function resolveFunctionToArrayTransform(arb, matches) { - if (!matches.length) return arb; - - const sharedSb = new Sandbox(); - - for (let i = 0; i < matches.length; i++) { - const n = matches[i]; - - // Determine the target node that contains the function definition - const targetNode = n.init.callee?.declNode?.parentNode || n.init; - - // Build evaluation context - include function definition if it's separate - let src = ''; - if (![n.init, n.init?.parentNode].includes(targetNode)) { - // Function is defined elsewhere, include its context - src += createOrderedSrc(getDeclarationWithContext(targetNode)); - } - - // Add the function call to evaluate - src += `\n;${createOrderedSrc([n.init])}\n;`; - - const replacementNode = evalInVm(src, sharedSb); - if (replacementNode !== evalInVm.BAD_VALUE) { - arb.markNode(n.init, replacementNode); - } - } - return arb; + if (!matches.length) return arb; + + const sharedSb = new Sandbox(); + + for (let i = 0; i < matches.length; i++) { + const n = matches[i]; + + // Determine the target node that contains the function definition + const targetNode = n.init.callee?.declNode?.parentNode || n.init; + + // Build evaluation context - include function definition if it's separate + let src = ''; + if (![n.init, n.init?.parentNode].includes(targetNode)) { + // Function is defined elsewhere, include its context + src += createOrderedSrc(getDeclarationWithContext(targetNode)); + } + + // Add the function call to evaluate + src += `\n;${createOrderedSrc([n.init])}\n;`; + + const replacementNode = evalInVm(src, sharedSb); + if (replacementNode !== evalInVm.BAD_VALUE) { + arb.markNode(n.init, replacementNode); + } + } + return arb; } /** @@ -86,6 +86,6 @@ export function resolveFunctionToArrayTransform(arb, matches) { * @return {Arborist} The updated Arborist instance */ export default function resolveFunctionToArray(arb, candidateFilter = () => true) { - const matches = resolveFunctionToArrayMatch(arb, candidateFilter); - return resolveFunctionToArrayTransform(arb, matches); + const matches = resolveFunctionToArrayMatch(arb, candidateFilter); + return resolveFunctionToArrayTransform(arb, matches); } \ No newline at end of file diff --git a/src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js b/src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js index c4b09df..aa343b4 100644 --- a/src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js +++ b/src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js @@ -10,37 +10,37 @@ const VALID_PROTOTYPE_FUNCTION_TYPES = ['FunctionExpression', 'ArrowFunctionExpr /** * Identifies AssignmentExpression nodes that assign functions to prototype properties. - * Matches patterns like `String.prototype.method = function() {...}`, `Obj.prototype.prop = () => value`, + * Matches patterns like `String.prototype.method = function() {...}`, `Obj.prototype.prop = () => value`, * or `Obj.prototype.prop = identifier`. Arrow functions work fine when they don't rely on 'this' binding. * @param {Arborist} arb - The Arborist instance * @param {Function} [candidateFilter] - Optional filter for candidates * @return {Object[]} Array of match objects containing prototype assignments and method details */ export function resolveInjectedPrototypeMethodCallsMatch(arb, candidateFilter = () => true) { - const matches = []; - const relevantNodes = arb.ast[0].typeMap.AssignmentExpression; + const matches = []; + const relevantNodes = arb.ast[0].typeMap.AssignmentExpression; - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; - // Must be assignment to a prototype property with a function value - if (n.left?.type === 'MemberExpression' && + // Must be assignment to a prototype property with a function value + if (n.left?.type === 'MemberExpression' && n.left.object?.type === 'MemberExpression' && - 'prototype' === (n.left.object.property?.name || n.left.object.property?.value) && + (n.left.object.property?.name || n.left.object.property?.value) === 'prototype' && n.operator === '=' && VALID_PROTOTYPE_FUNCTION_TYPES.includes(n.right?.type) && candidateFilter(n)) { - const methodName = n.left.property?.name || n.left.property?.value; - if (methodName) { - matches.push({ - assignmentNode: n, - methodName: methodName - }); - } - } - } - return matches; + const methodName = n.left.property?.name || n.left.property?.value; + if (methodName) { + matches.push({ + assignmentNode: n, + methodName, + }); + } + } + } + return matches; } /** @@ -51,40 +51,40 @@ export function resolveInjectedPrototypeMethodCallsMatch(arb, candidateFilter = * @return {Arborist} The updated Arborist instance */ export function resolveInjectedPrototypeMethodCallsTransform(arb, matches) { - if (!matches.length) return arb; + if (!matches.length) return arb; - // Process each prototype method assignment - for (let i = 0; i < matches.length; i++) { - const match = matches[i]; - - try { - // Build execution context including the prototype assignment - const context = getDeclarationWithContext(match.assignmentNode); - const contextSb = new Sandbox(); - contextSb.run(createOrderedSrc(context)); + // Process each prototype method assignment + for (let i = 0; i < matches.length; i++) { + const match = matches[i]; - // Find and resolve calls to this injected method - const callNodes = arb.ast[0].typeMap.CallExpression; - for (let j = 0; j < callNodes.length; j++) { - const callNode = callNodes[j]; - - // Check if this call uses the injected prototype method - if (callNode.callee?.type === 'MemberExpression' && + try { + // Build execution context including the prototype assignment + const context = getDeclarationWithContext(match.assignmentNode); + const contextSb = new Sandbox(); + contextSb.run(createOrderedSrc(context)); + + // Find and resolve calls to this injected method + const callNodes = arb.ast[0].typeMap.CallExpression; + for (let j = 0; j < callNodes.length; j++) { + const callNode = callNodes[j]; + + // Check if this call uses the injected prototype method + if (callNode.callee?.type === 'MemberExpression' && (callNode.callee.property?.name === match.methodName || callNode.callee.property?.value === match.methodName)) { - - // Evaluate the method call in the prepared context - const replacementNode = evalInVm(`\n${createOrderedSrc([callNode])}`, contextSb); - if (replacementNode !== evalInVm.BAD_VALUE) { - arb.markNode(callNode, replacementNode); - } - } - } - } catch (e) { - logger.debug(`[-] Error resolving injected prototype method '${match.methodName}': ${e.message}`); - } - } - return arb; + + // Evaluate the method call in the prepared context + const replacementNode = evalInVm(`\n${createOrderedSrc([callNode])}`, contextSb); + if (replacementNode !== evalInVm.BAD_VALUE) { + arb.markNode(callNode, replacementNode); + } + } + } + } catch (e) { + logger.debug(`[-] Error resolving injected prototype method '${match.methodName}': ${e.message}`); + } + } + return arb; } /** @@ -96,6 +96,6 @@ export function resolveInjectedPrototypeMethodCallsTransform(arb, matches) { * @return {Arborist} The updated Arborist instance */ export default function resolveInjectedPrototypeMethodCalls(arb, candidateFilter = () => true) { - const matches = resolveInjectedPrototypeMethodCallsMatch(arb, candidateFilter); - return resolveInjectedPrototypeMethodCallsTransform(arb, matches); + const matches = resolveInjectedPrototypeMethodCallsMatch(arb, candidateFilter); + return resolveInjectedPrototypeMethodCallsTransform(arb, matches); } \ No newline at end of file diff --git a/src/modules/unsafe/resolveLocalCalls.js b/src/modules/unsafe/resolveLocalCalls.js index dfe2646..5434829 100644 --- a/src/modules/unsafe/resolveLocalCalls.js +++ b/src/modules/unsafe/resolveLocalCalls.js @@ -10,17 +10,17 @@ import {getDeclarationWithContext} from '../utils/getDeclarationWithContext.js'; const VALID_UNWRAP_TYPES = ['Literal', 'Identifier']; const CACHE_LIMIT = 100; -// Module-level variables for appearance tracking +// Module-level variables for appearance tracking let APPEARANCES = new Map(); /** * Sorts call expression nodes by their appearance frequency in descending order. * @param {ASTNode} a - First call expression node - * @param {ASTNode} b - Second call expression node + * @param {ASTNode} b - Second call expression node * @return {number} Comparison result for sorting */ function sortByApperanceFrequency(a, b) { - return APPEARANCES.get(getCalleeName(b)) - APPEARANCES.get(getCalleeName(a)); + return APPEARANCES.get(getCalleeName(b)) - APPEARANCES.get(getCalleeName(a)); } /** @@ -29,10 +29,10 @@ function sortByApperanceFrequency(a, b) { * @return {number} Updated appearance count */ function countAppearances(n) { - const calleeName = getCalleeName(n); - const count = (APPEARANCES.get(calleeName) || 0) + 1; - APPEARANCES.set(calleeName, count); - return count; + const calleeName = getCalleeName(n); + const count = (APPEARANCES.get(calleeName) || 0) + 1; + APPEARANCES.set(calleeName, count); + return count; } /** @@ -43,27 +43,27 @@ function countAppearances(n) { * @return {ASTNode[]} Array of call expression nodes that can be transformed */ export function resolveLocalCallsMatch(arb, candidateFilter = () => true) { - APPEARANCES = new Map(); - const matches = []; - const relevantNodes = arb.ast[0].typeMap.CallExpression; - - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - - // Check if call expression has proper declaration context - if ((n.callee?.declNode || + APPEARANCES = new Map(); + const matches = []; + const relevantNodes = arb.ast[0].typeMap.CallExpression; + + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + + // Check if call expression has proper declaration context + if ((n.callee?.declNode || (n.callee?.object?.declNode && !SKIP_PROPERTIES.includes(n.callee.property?.value || n.callee.property?.name)) || n.callee?.object?.type === 'Literal') && candidateFilter(n)) { - countAppearances(n); // Count appearances during the match phase to allow sorting by appearance frequency - matches.push(n); - } - } - - // Sort by appearance frequency for optimization (most frequent first) - matches.sort(sortByApperanceFrequency); - return matches; + countAppearances(n); // Count appearances during the match phase to allow sorting by appearance frequency + matches.push(n); + } + } + + // Sort by appearance frequency for optimization (most frequent first) + matches.sort(sortByApperanceFrequency); + return matches; } /** @@ -74,77 +74,77 @@ export function resolveLocalCallsMatch(arb, candidateFilter = () => true) { * @return {Arborist} The modified Arborist instance */ export function resolveLocalCallsTransform(arb, matches) { - if (!matches.length) return arb; - - const cache = getCache(arb.ast[0].scriptHash); - const modifiedRanges = []; - - candidateLoop: for (let i = 0; i < matches.length; i++) { - const c = matches[i]; - - // Skip if already modified in this iteration - if (isNodeInRanges(c, modifiedRanges)) continue; - - // Skip if any argument has problematic type - for (let j = 0; j < c.arguments.length; j++) { - if (c.arguments[j].type === 'ThisExpression') continue candidateLoop; - } - - const callee = c.callee?.object || c.callee; - const declNode = callee?.declNode || callee?.object?.declNode; - - // Skip simple wrappers that should be handled by safe modules - if (declNode?.parentNode?.body?.body?.[0]?.type === 'ReturnStatement') { - const returnArg = declNode.parentNode.body.body[0].argument; - // Leave simple literal/identifier returns to safe unwrapping modules - if (VALID_UNWRAP_TYPES.includes(returnArg.type) || returnArg.type.includes('unction')) continue; - // Leave function shell unwrapping to dedicated module - else if (returnArg.type === 'CallExpression' && + if (!matches.length) return arb; + + const cache = getCache(arb.ast[0].scriptHash); + const modifiedRanges = []; + + candidateLoop: for (let i = 0; i < matches.length; i++) { + const c = matches[i]; + + // Skip if already modified in this iteration + if (isNodeInRanges(c, modifiedRanges)) continue; + + // Skip if any argument has problematic type + for (let j = 0; j < c.arguments.length; j++) { + if (c.arguments[j].type === 'ThisExpression') continue candidateLoop; + } + + const callee = c.callee?.object || c.callee; + const declNode = callee?.declNode || callee?.object?.declNode; + + // Skip simple wrappers that should be handled by safe modules + if (declNode?.parentNode?.body?.body?.[0]?.type === 'ReturnStatement') { + const returnArg = declNode.parentNode.body.body[0].argument; + // Leave simple literal/identifier returns to safe unwrapping modules + if (VALID_UNWRAP_TYPES.includes(returnArg.type) || returnArg.type.includes('unction')) continue; + // Leave function shell unwrapping to dedicated module + else if (returnArg.type === 'CallExpression' && returnArg.callee?.object?.type === 'FunctionExpression' && (returnArg.callee.property?.name || returnArg.callee.property?.value) === 'apply') continue; - } - - // Cache management for performance - const cacheName = `rlc-${callee.name || callee.value}-${declNode?.nodeId}`; - if (!cache[cacheName]) { - cache[cacheName] = evalInVm.BAD_VALUE; - - // Skip problematic callee types that shouldn't be evaluated - if (SKIP_IDENTIFIERS.includes(callee.name) || + } + + // Cache management for performance + const cacheName = `rlc-${callee.name || callee.value}-${declNode?.nodeId}`; + if (!cache[cacheName]) { + cache[cacheName] = evalInVm.BAD_VALUE; + + // Skip problematic callee types that shouldn't be evaluated + if (SKIP_IDENTIFIERS.includes(callee.name) || (callee.type === 'ArrayExpression' && !callee.elements.length) || (callee.arguments || []).some(arg => SKIP_IDENTIFIERS.includes(arg) || arg?.type === 'ThisExpression')) continue; - - if (declNode) { - // Skip simple function wrappers (handled by safe modules) - if (declNode.parentNode.type === 'FunctionDeclaration' && + + if (declNode) { + // Skip simple function wrappers (handled by safe modules) + if (declNode.parentNode.type === 'FunctionDeclaration' && VALID_UNWRAP_TYPES.includes(declNode.parentNode?.body?.body?.[0]?.argument?.type)) continue; - - // Build execution context in sandbox - const contextSb = new Sandbox(); - try { - contextSb.run(createOrderedSrc(getDeclarationWithContext(declNode.parentNode))); - if (Object.keys(cache) >= CACHE_LIMIT) cache.flush(); - cache[cacheName] = contextSb; - } catch {} - } - } - - // Evaluate call expression in appropriate context - const contextVM = cache[cacheName]; - const nodeSrc = createOrderedSrc([c]); - const replacementNode = contextVM === evalInVm.BAD_VALUE ? evalInVm(nodeSrc) : evalInVm(nodeSrc, contextVM); - - if (replacementNode !== evalInVm.BAD_VALUE && replacementNode.type !== 'FunctionDeclaration' && replacementNode.name !== 'undefined') { - // Anti-debugging protection: avoid resolving function toString that might trigger detection - if (c.callee.type === 'MemberExpression' && + + // Build execution context in sandbox + const contextSb = new Sandbox(); + try { + contextSb.run(createOrderedSrc(getDeclarationWithContext(declNode.parentNode))); + if (Object.keys(cache) >= CACHE_LIMIT) cache.flush(); + cache[cacheName] = contextSb; + } catch {} + } + } + + // Evaluate call expression in appropriate context + const contextVM = cache[cacheName]; + const nodeSrc = createOrderedSrc([c]); + const replacementNode = contextVM === evalInVm.BAD_VALUE ? evalInVm(nodeSrc) : evalInVm(nodeSrc, contextVM); + + if (replacementNode !== evalInVm.BAD_VALUE && replacementNode.type !== 'FunctionDeclaration' && replacementNode.name !== 'undefined') { + // Anti-debugging protection: avoid resolving function toString that might trigger detection + if (c.callee.type === 'MemberExpression' && (c.callee.property?.name || c.callee.property?.value) === 'toString' && replacementNode?.value?.substring(0, 8) === 'function') continue; - - arb.markNode(c, replacementNode); - modifiedRanges.push(c.range); - } - } - return arb; + + arb.markNode(c, replacementNode); + modifiedRanges.push(c.range); + } + } + return arb; } /** @@ -156,6 +156,6 @@ export function resolveLocalCallsTransform(arb, matches) { * @return {Arborist} The modified Arborist instance */ export default function resolveLocalCalls(arb, candidateFilter = () => true) { - const matches = resolveLocalCallsMatch(arb, candidateFilter); - return resolveLocalCallsTransform(arb, matches); + const matches = resolveLocalCallsMatch(arb, candidateFilter); + return resolveLocalCallsTransform(arb, matches); } diff --git a/src/modules/unsafe/resolveMemberExpressionsLocalReferences.js b/src/modules/unsafe/resolveMemberExpressionsLocalReferences.js index 69bd1e1..21291d0 100644 --- a/src/modules/unsafe/resolveMemberExpressionsLocalReferences.js +++ b/src/modules/unsafe/resolveMemberExpressionsLocalReferences.js @@ -16,93 +16,93 @@ const VALID_PROPERTY_TYPES = ['Identifier', 'Literal']; * @return {ASTNode[]} Array of member expression nodes that can be resolved */ export function resolveMemberExpressionsLocalReferencesMatch(arb, candidateFilter = () => true) { - const matches = []; - const relevantNodes = arb.ast[0].typeMap.MemberExpression; + const matches = []; + const relevantNodes = arb.ast[0].typeMap.MemberExpression; - for (let i = 0; i < relevantNodes.length; i++) { - const n = relevantNodes[i]; - if (VALID_PROPERTY_TYPES.includes(n.property.type) && + for (let i = 0; i < relevantNodes.length; i++) { + const n = relevantNodes[i]; + if (VALID_PROPERTY_TYPES.includes(n.property.type) && !SKIP_PROPERTIES.includes(n.property?.name || n.property?.value) && !(n.parentKey === 'left' && n.parentNode.type === 'AssignmentExpression') && candidateFilter(n)) { - // Skip member expressions used as call expression callees - if (n.parentNode.type === 'CallExpression' && n.parentKey === 'callee') continue; - - // Find the main declared identifier for the member expression being processed - // E.g. processing 'c.d' in 'a.b[c.d]' -> mainObj is 'c' (declared identifier for c.d) - // E.g. processing 'data.user.name' in 'const data = {...}; data.user.name' -> mainObj is 'data' - const mainObj = getMainDeclaredObjectOfMemberExpression(n); - if (mainObj?.declNode) { - // Skip if identifier is assignment target - // E.g. const obj = {a: 1}; obj.a = 2; -> mainObj is 'obj', skip obj.a (obj on left side) - if (mainObj.parentNode.parentNode.type === 'AssignmentExpression' && + // Skip member expressions used as call expression callees + if (n.parentNode.type === 'CallExpression' && n.parentKey === 'callee') continue; + + // Find the main declared identifier for the member expression being processed + // E.g. processing 'c.d' in 'a.b[c.d]' -> mainObj is 'c' (declared identifier for c.d) + // E.g. processing 'data.user.name' in 'const data = {...}; data.user.name' -> mainObj is 'data' + const mainObj = getMainDeclaredObjectOfMemberExpression(n); + if (mainObj?.declNode) { + // Skip if identifier is assignment target + // E.g. const obj = {a: 1}; obj.a = 2; -> mainObj is 'obj', skip obj.a (obj on left side) + if (mainObj.parentNode.parentNode.type === 'AssignmentExpression' && mainObj.parentNode.parentKey === 'left') continue; - - const declNode = mainObj.declNode; - // Skip function parameters as they may have dynamic values - // E.g. function test(arr) { return arr[0]; } -> mainObj is 'arr', skip arr[0] (arr is parameter) - if (/Function/.test(declNode.parentNode.type) && + + const declNode = mainObj.declNode; + // Skip function parameters as they may have dynamic values + // E.g. function test(arr) { return arr[0]; } -> mainObj is 'arr', skip arr[0] (arr is parameter) + if (/Function/.test(declNode.parentNode.type) && (declNode.parentNode.params || []).find(p => p === declNode)) continue; - - const prop = n.property; - // Skip if property identifier has modified references (not safe to resolve) - // E.g. let idx = 0; idx = 1; const val = arr[idx]; -> mainObj is 'arr', prop is 'idx', skip because idx modified - if (prop.type === 'Identifier' && prop.declNode?.references && + + const prop = n.property; + // Skip if property identifier has modified references (not safe to resolve) + // E.g. let idx = 0; idx = 1; const val = arr[idx]; -> mainObj is 'arr', prop is 'idx', skip because idx modified + if (prop.type === 'Identifier' && prop.declNode?.references && areReferencesModified(arb.ast, prop.declNode.references)) continue; - - matches.push(n); - } - } - } - - return matches; + + matches.push(n); + } + } + } + + return matches; } /** * Transforms member expressions by resolving them to their evaluated values using local context. * Uses sandbox evaluation to safely determine replacement values and skips empty results. - * @param {Arborist} arb - Arborist instance + * @param {Arborist} arb - Arborist instance * @param {ASTNode[]} matches - Array of member expression nodes to transform * @return {Arborist} The modified Arborist instance */ export function resolveMemberExpressionsLocalReferencesTransform(arb, matches) { - if (!matches.length) return arb; + if (!matches.length) return arb; + + for (let i = 0; i < matches.length; i++) { + const n = matches[i]; + const relevantIdentifier = getMainDeclaredObjectOfMemberExpression(n); + const context = createOrderedSrc(getDeclarationWithContext(relevantIdentifier.declNode.parentNode)); + + if (context) { + const src = `${context}\n${n.src}`; + const replacementNode = evalInVm(src); + if (replacementNode !== evalInVm.BAD_VALUE) { + // Check if replacement would result in empty/meaningless values + let isEmptyReplacement = false; + switch (replacementNode.type) { + case 'ArrayExpression': + if (!replacementNode.elements.length) isEmptyReplacement = true; + break; + case 'ObjectExpression': + if (!replacementNode.properties.length) isEmptyReplacement = true; + break; + case 'Literal': + if (!String(replacementNode.value).length || replacementNode.raw === 'null') { + isEmptyReplacement = true; + } + break; + case 'Identifier': + if (replacementNode.name === 'undefined') isEmptyReplacement = true; + break; + } + if (!isEmptyReplacement) { + arb.markNode(n, replacementNode); + } + } + } + } - for (let i = 0; i < matches.length; i++) { - const n = matches[i]; - const relevantIdentifier = getMainDeclaredObjectOfMemberExpression(n); - const context = createOrderedSrc(getDeclarationWithContext(relevantIdentifier.declNode.parentNode)); - - if (context) { - const src = `${context}\n${n.src}`; - const replacementNode = evalInVm(src); - if (replacementNode !== evalInVm.BAD_VALUE) { - // Check if replacement would result in empty/meaningless values - let isEmptyReplacement = false; - switch (replacementNode.type) { - case 'ArrayExpression': - if (!replacementNode.elements.length) isEmptyReplacement = true; - break; - case 'ObjectExpression': - if (!replacementNode.properties.length) isEmptyReplacement = true; - break; - case 'Literal': - if (!String(replacementNode.value).length || replacementNode.raw === 'null') { - isEmptyReplacement = true; - } - break; - case 'Identifier': - if (replacementNode.name === 'undefined') isEmptyReplacement = true; - break; - } - if (!isEmptyReplacement) { - arb.markNode(n, replacementNode); - } - } - } - } - - return arb; + return arb; } /** @@ -120,6 +120,6 @@ export function resolveMemberExpressionsLocalReferencesTransform(arb, matches) { * @return {Arborist} The modified Arborist instance */ export default function resolveMemberExpressionsLocalReferences(arb, candidateFilter = () => true) { - const matches = resolveMemberExpressionsLocalReferencesMatch(arb, candidateFilter); - return resolveMemberExpressionsLocalReferencesTransform(arb, matches); + const matches = resolveMemberExpressionsLocalReferencesMatch(arb, candidateFilter); + return resolveMemberExpressionsLocalReferencesTransform(arb, matches); } \ No newline at end of file diff --git a/src/modules/unsafe/resolveMinimalAlphabet.js b/src/modules/unsafe/resolveMinimalAlphabet.js index d37fbe9..79a8e6f 100644 --- a/src/modules/unsafe/resolveMinimalAlphabet.js +++ b/src/modules/unsafe/resolveMinimalAlphabet.js @@ -1,48 +1,46 @@ import {evalInVm} from '../utils/evalInVm.js'; import {doesDescendantMatchCondition} from '../utils/doesDescendantMatchCondition.js'; - - /** * Identifies unary and binary expressions that can be resolved to simplified values. - * Targets JSFuck-style obfuscation patterns using non-numeric operands and excludes + * Targets JSFuck-style obfuscation patterns using non-numeric operands and excludes * expressions containing ThisExpression for safe evaluation. * @param {Arborist} arb - Arborist instance * @param {Function} [candidateFilter] - Optional filter function for additional candidate filtering * @return {ASTNode[]} Array of expression nodes that can be resolved */ export function resolveMinimalAlphabetMatch(arb, candidateFilter = () => true) { - const matches = []; - const unaryNodes = arb.ast[0].typeMap.UnaryExpression; - const binaryNodes = arb.ast[0].typeMap.BinaryExpression; + const matches = []; + const unaryNodes = arb.ast[0].typeMap.UnaryExpression; + const binaryNodes = arb.ast[0].typeMap.BinaryExpression; - // Process unary expressions: +true, +[], -false, ~[], etc. - for (let i = 0; i < unaryNodes.length; i++) { - const n = unaryNodes[i]; - if (((n.argument.type === 'Literal' && /^\D/.test(n.argument.raw[0])) || + // Process unary expressions: +true, +[], -false, ~[], etc. + for (let i = 0; i < unaryNodes.length; i++) { + const n = unaryNodes[i]; + if (((n.argument.type === 'Literal' && /^\D/.test(n.argument.raw[0])) || n.argument.type === 'ArrayExpression') && candidateFilter(n)) { - // Skip expressions containing ThisExpression for safe evaluation - if (doesDescendantMatchCondition(n, descendant => descendant.type === 'ThisExpression')) continue; - matches.push(n); - } - } + // Skip expressions containing ThisExpression for safe evaluation + if (doesDescendantMatchCondition(n, descendant => descendant.type === 'ThisExpression')) continue; + matches.push(n); + } + } - // Process binary expressions: [] + [], [+[]], etc. - for (let i = 0; i < binaryNodes.length; i++) { - const n = binaryNodes[i]; - if (n.operator === '+' && + // Process binary expressions: [] + [], [+[]], etc. + for (let i = 0; i < binaryNodes.length; i++) { + const n = binaryNodes[i]; + if (n.operator === '+' && (n.left.type !== 'MemberExpression' && Number.isNaN(parseFloat(n.left?.value))) && n.left?.type !== 'ThisExpression' && n.right?.type !== 'ThisExpression' && candidateFilter(n)) { - // Skip expressions containing ThisExpression for safe evaluation - if (doesDescendantMatchCondition(n, descendant => descendant.type === 'ThisExpression')) continue; - matches.push(n); - } - } + // Skip expressions containing ThisExpression for safe evaluation + if (doesDescendantMatchCondition(n, descendant => descendant.type === 'ThisExpression')) continue; + matches.push(n); + } + } - return matches; + return matches; } /** @@ -53,17 +51,17 @@ export function resolveMinimalAlphabetMatch(arb, candidateFilter = () => true) { * @return {Arborist} The modified Arborist instance */ export function resolveMinimalAlphabetTransform(arb, matches) { - if (!matches.length) return arb; + if (!matches.length) return arb; - for (let i = 0; i < matches.length; i++) { - const n = matches[i]; - const replacementNode = evalInVm(n.src); - if (replacementNode !== evalInVm.BAD_VALUE) { - arb.markNode(n, replacementNode); - } - } + for (let i = 0; i < matches.length; i++) { + const n = matches[i]; + const replacementNode = evalInVm(n.src); + if (replacementNode !== evalInVm.BAD_VALUE) { + arb.markNode(n, replacementNode); + } + } - return arb; + return arb; } /** @@ -75,6 +73,6 @@ export function resolveMinimalAlphabetTransform(arb, matches) { * @return {Arborist} The modified Arborist instance */ export default function resolveMinimalAlphabet(arb, candidateFilter = () => true) { - const matches = resolveMinimalAlphabetMatch(arb, candidateFilter); - return resolveMinimalAlphabetTransform(arb, matches); + const matches = resolveMinimalAlphabetMatch(arb, candidateFilter); + return resolveMinimalAlphabetTransform(arb, matches); } \ No newline at end of file diff --git a/src/modules/utils/areReferencesModified.js b/src/modules/utils/areReferencesModified.js index 9904308..1d8eeb0 100644 --- a/src/modules/utils/areReferencesModified.js +++ b/src/modules/utils/areReferencesModified.js @@ -1,7 +1,5 @@ import {PROPERTIES_THAT_MODIFY_CONTENT} from '../config.js'; - - // AST node types that indicate potential modification const ASSIGNMENT_TYPES = ['AssignmentExpression', 'ForInStatement', 'ForOfStatement', 'ForAwaitStatement']; @@ -13,51 +11,51 @@ const ASSIGNMENT_TYPES = ['AssignmentExpression', 'ForInStatement', 'ForOfStatem * @return {boolean} True if the member expression is being assigned to */ function isMemberExpressionAssignedTo(memberExpr, assignmentExpressions) { - for (let i = 0; i < assignmentExpressions.length; i++) { - const assignment = assignmentExpressions[i]; - if (assignment.left.type !== 'MemberExpression') continue; - - const leftObj = assignment.left.object; - const rightObj = memberExpr.object; - - // Compare object identities - both should refer to the same declared node - const leftDeclNode = leftObj.declNode || leftObj; - const rightDeclNode = rightObj.declNode || rightObj; - - if (leftDeclNode !== rightDeclNode) continue; - - // Compare property names/values - const leftProp = assignment.left.property?.name || assignment.left.property?.value; - const rightProp = memberExpr.property?.name || memberExpr.property?.value; - - if (leftProp === rightProp) return true; - } - return false; + for (let i = 0; i < assignmentExpressions.length; i++) { + const assignment = assignmentExpressions[i]; + if (assignment.left.type !== 'MemberExpression') continue; + + const leftObj = assignment.left.object; + const rightObj = memberExpr.object; + + // Compare object identities - both should refer to the same declared node + const leftDeclNode = leftObj.declNode || leftObj; + const rightDeclNode = rightObj.declNode || rightObj; + + if (leftDeclNode !== rightDeclNode) continue; + + // Compare property names/values + const leftProp = assignment.left.property?.name || assignment.left.property?.value; + const rightProp = memberExpr.property?.name || memberExpr.property?.value; + + if (leftProp === rightProp) return true; + } + return false; } /** * Checks if a reference is used as the target of a delete operation. - * E.g. delete obj.prop, delete arr[index] + * E.g. delete obj.prop, delete arr[index] * @param {ASTNode} ref - The reference to check * @return {boolean} True if the reference is being deleted */ function isReferenceDeleted(ref) { - // Direct deletion: delete ref - if (ref.parentNode.type === 'UnaryExpression' && - ref.parentNode.operator === 'delete' && + // Direct deletion: delete ref + if (ref.parentNode.type === 'UnaryExpression' && + ref.parentNode.operator === 'delete' && ref.parentNode.argument === ref) { - return true; - } - - // Member expression deletion: delete obj.prop, delete arr[index] - if (ref.parentNode.type === 'MemberExpression' && + return true; + } + + // Member expression deletion: delete obj.prop, delete arr[index] + if (ref.parentNode.type === 'MemberExpression' && ref.parentKey === 'object' && - ref.parentNode.parentNode.type === 'UnaryExpression' && + ref.parentNode.parentNode.type === 'UnaryExpression' && ref.parentNode.parentNode.operator === 'delete') { - return true; - } - - return false; + return true; + } + + return false; } /** @@ -68,14 +66,14 @@ function isReferenceDeleted(ref) { * @return {boolean} True if the reference is in a destructuring context */ function isInDestructuringPattern(ref) { - let current = ref; - while (current.parentNode) { - if (['ObjectPattern', 'ArrayPattern'].includes(current.parentNode.type)) { - return true; - } - current = current.parentNode; - } - return false; + let current = ref; + while (current.parentNode) { + if (['ObjectPattern', 'ArrayPattern'].includes(current.parentNode.type)) { + return true; + } + current = current.parentNode; + } + return false; } /** @@ -85,40 +83,40 @@ function isInDestructuringPattern(ref) { * @return {boolean} True if the reference is being incremented/decremented */ function isReferenceIncremented(ref) { - // Direct increment: ++ref, ref++, --ref, ref-- - if (ref.parentNode.type === 'UpdateExpression' && ref.parentNode.argument === ref) { - return true; - } - - // Member expression increment: ++obj.prop, obj.prop++ - if (ref.parentNode.type === 'MemberExpression' && + // Direct increment: ++ref, ref++, --ref, ref-- + if (ref.parentNode.type === 'UpdateExpression' && ref.parentNode.argument === ref) { + return true; + } + + // Member expression increment: ++obj.prop, obj.prop++ + if (ref.parentNode.type === 'MemberExpression' && ref.parentKey === 'object' && - ref.parentNode.parentNode.type === 'UpdateExpression' && + ref.parentNode.parentNode.type === 'UpdateExpression' && ref.parentNode.parentNode.argument === ref.parentNode) { - return true; - } - - return false; + return true; + } + + return false; } /** * Determines if any of the given references are potentially modified in ways that would * make code transformations unsafe. This function performs comprehensive checks for various * modification patterns including assignments, method calls, destructuring, and more. - * + * * Critical for safe transformations: if this returns true, the variable/object should not * be replaced or transformed as its value may change during execution. - * + * * @param {ASTNode[]} ast - The AST array (expects ast[0] to contain typeMap) * @param {ASTNode[]} refs - Array of reference nodes to analyze for modifications * @return {boolean} True if any reference might be modified, false if all are safe to transform - * + * * @example * // Safe cases (returns false): * const arr = [1, 2, 3]; const x = arr[0]; // No modification * const obj = {a: 1}; console.log(obj.a); // Read-only access - * - * @example + * + * @example * // Unsafe cases (returns true): * const arr = [1, 2, 3]; arr[0] = 5; // Direct assignment * const obj = {a: 1}; obj.a = 2; // Property assignment @@ -127,75 +125,75 @@ function isReferenceIncremented(ref) { * const obj = {a: 1}; delete obj.a; // Delete operation */ export function areReferencesModified(ast, refs) { - if (!refs.length) return false; - - // Cache assignment expressions for performance - const assignmentExpressions = ast[0].typeMap.AssignmentExpression || []; - - for (let i = 0; i < refs.length; i++) { - const ref = refs[i]; - - // Check for direct assignment: ref = value, ref += value, etc. - if (ref.parentKey === 'left' && ASSIGNMENT_TYPES.includes(ref.parentNode.type)) { - return true; - } - - // Check for for-in/for-of/for-await with member expression: for (obj.prop in/of/await ...) - if (ref.parentNode.type === 'MemberExpression' && + if (!refs.length) return false; + + // Cache assignment expressions for performance + const assignmentExpressions = ast[0].typeMap.AssignmentExpression || []; + + for (let i = 0; i < refs.length; i++) { + const ref = refs[i]; + + // Check for direct assignment: ref = value, ref += value, etc. + if (ref.parentKey === 'left' && ASSIGNMENT_TYPES.includes(ref.parentNode.type)) { + return true; + } + + // Check for for-in/for-of/for-await with member expression: for (obj.prop in/of/await ...) + if (ref.parentNode.type === 'MemberExpression' && ref.parentKey === 'object' && ref.parentNode.parentKey === 'left' && ['ForInStatement', 'ForOfStatement', 'ForAwaitStatement'].includes(ref.parentNode.parentNode.type)) { - return true; - } - - // Check for increment/decrement: ++ref, ref++, --ref, ref-- - if (isReferenceIncremented(ref)) { - return true; - } - - // Check for variable redeclaration in subscope: const ref = ... - if (ref.parentNode.type === 'VariableDeclarator' && ref.parentKey === 'id') { - return true; - } - - // Check for delete operations: delete ref, delete obj.prop - if (isReferenceDeleted(ref)) { - return true; - } - - // Check for destructuring patterns (conservative approach) - if (isInDestructuringPattern(ref)) { - return true; - } - - // Check for member expression modifications: obj.method(), obj.prop = value - if (ref.parentNode.type === 'MemberExpression') { - const memberExpr = ref.parentNode; - const grandParent = memberExpr.parentNode; - - // Check for mutating method calls: arr.push(), obj.sort() - if (grandParent.type === 'CallExpression' && + return true; + } + + // Check for increment/decrement: ++ref, ref++, --ref, ref-- + if (isReferenceIncremented(ref)) { + return true; + } + + // Check for variable redeclaration in subscope: const ref = ... + if (ref.parentNode.type === 'VariableDeclarator' && ref.parentKey === 'id') { + return true; + } + + // Check for delete operations: delete ref, delete obj.prop + if (isReferenceDeleted(ref)) { + return true; + } + + // Check for destructuring patterns (conservative approach) + if (isInDestructuringPattern(ref)) { + return true; + } + + // Check for member expression modifications: obj.method(), obj.prop = value + if (ref.parentNode.type === 'MemberExpression') { + const memberExpr = ref.parentNode; + const grandParent = memberExpr.parentNode; + + // Check for mutating method calls: arr.push(), obj.sort() + if (grandParent.type === 'CallExpression' && grandParent.callee === memberExpr && memberExpr.object === ref) { - const methodName = memberExpr.property?.value || memberExpr.property?.name; - if (PROPERTIES_THAT_MODIFY_CONTENT.includes(methodName)) { - return true; - } - } - - // Check for property assignments: obj.prop = value - if (grandParent.type === 'AssignmentExpression' && + const methodName = memberExpr.property?.value || memberExpr.property?.name; + if (PROPERTIES_THAT_MODIFY_CONTENT.includes(methodName)) { + return true; + } + } + + // Check for property assignments: obj.prop = value + if (grandParent.type === 'AssignmentExpression' && memberExpr.parentKey === 'left') { - return true; - } - } - - // Check for member expressions being assigned to: complex cases like nested.prop = value - if (ref.type === 'MemberExpression' && + return true; + } + } + + // Check for member expressions being assigned to: complex cases like nested.prop = value + if (ref.type === 'MemberExpression' && isMemberExpressionAssignedTo(ref, assignmentExpressions)) { - return true; - } - } - - return false; + return true; + } + } + + return false; } \ No newline at end of file diff --git a/src/modules/utils/createNewNode.js b/src/modules/utils/createNewNode.js index 88984d1..313f1c3 100644 --- a/src/modules/utils/createNewNode.js +++ b/src/modules/utils/createNewNode.js @@ -1,6 +1,6 @@ import {BAD_VALUE} from '../config.js'; import {getObjType} from './getObjType.js'; -import {generateCode, parseCode, logger} from 'flast'; +import {parseCode, logger} from 'flast'; /** * Creates an AST node from a JavaScript value by analyzing its type and structure. @@ -10,166 +10,166 @@ import {generateCode, parseCode, logger} from 'flast'; * @return {ASTNode|BAD_VALUE} The newly created AST node if successful; BAD_VALUE otherwise */ export function createNewNode(value) { - let newNode = BAD_VALUE; - try { - const valueType = getObjType(value); - switch (valueType) { - case 'Node': - newNode = value; - break; - case 'String': - case 'Number': - case 'Boolean': { - const valueStr = String(value); - const firstChar = valueStr[0]; - - // Handle unary expressions like -3, +5, !true (from string representations) - if (['-', '+', '!'].includes(firstChar) && valueStr.length > 1) { - const absVal = valueStr.substring(1); - // Check if the remaining part is numeric (integers only to maintain original behavior) - if (isNaN(parseInt(absVal)) && !['Infinity', 'NaN'].includes(absVal)) { - // Non-numeric string like "!hello" - treat as literal - newNode = { - type: 'Literal', - value, - raw: valueStr, - }; - } else { - // Create unary expression maintaining string representation for consistency - newNode = { - type: 'UnaryExpression', - operator: firstChar, - argument: createNewNode(absVal), - }; - } - } else if (['Infinity', 'NaN'].includes(valueStr)) { - // Special numeric identifiers - newNode = { - type: 'Identifier', - name: valueStr, - }; - } else if (Object.is(value, -0)) { - // Special case: negative zero requires unary expression - newNode = { - type: 'UnaryExpression', - operator: '-', - argument: createNewNode(0), - }; - } else { - // Regular literal values - newNode = { - type: 'Literal', - value: value, - raw: valueStr, - }; - } - break; - } - case 'Array': { - const elements = []; - // Direct iteration over array (value is already an array) - for (let i = 0; i < value.length; i++) { - const elementNode = createNewNode(value[i]); - if (elementNode === BAD_VALUE) { - // If any element fails to convert, fail the entire array - throw new Error('Array contains unconvertible element'); - } - elements.push(elementNode); - } - newNode = { - type: 'ArrayExpression', - elements, - }; - break; - } - case 'Object': { - const properties = []; - const entries = Object.entries(value); - - for (let i = 0; i < entries.length; i++) { - const [k, v] = entries[i]; - const key = createNewNode(k); - const val = createNewNode(v); - - // If any property key or value fails to convert, fail the entire object - if (key === BAD_VALUE || val === BAD_VALUE) { - throw new Error('Object contains unconvertible property'); - } - - properties.push({ - type: 'Property', - key, - value: val, - }); - } - newNode = { - type: 'ObjectExpression', - properties, - }; - break; - } - case 'Undefined': - newNode = { - type: 'Identifier', - name: 'undefined', - }; - break; - case 'Null': - newNode = { - type: 'Literal', - raw: 'null', - }; - break; - case 'BigInt': - newNode = { - type: 'Literal', - value: value, - raw: value.toString() + 'n', - bigint: value.toString(), - }; - break; - case 'Symbol': - // Symbols cannot be represented as literals in AST - // They must be created via Symbol() calls - const symbolDesc = value.description; - if (symbolDesc) { - newNode = { - type: 'CallExpression', - callee: {type: 'Identifier', name: 'Symbol'}, - arguments: [createNewNode(symbolDesc)], - }; - } else { - newNode = { - type: 'CallExpression', - callee: {type: 'Identifier', name: 'Symbol'}, - arguments: [], - }; - } - break; - case 'Function': // Covers functions and classes - try { - // Attempt to parse function source code into AST - const parsed = parseCode(value.toString()); - if (parsed?.body?.[0]) { - newNode = parsed.body[0]; - } - } catch { - // Native functions or unparseable functions return BAD_VALUE - // This is expected behavior for built-in functions like Math.max - } - break; - case 'RegExp': - newNode = { - type: 'Literal', - regex: { - pattern: value.source, - flags: value.flags, - }, - }; - break; - } - } catch (e) { - logger.debug(`[-] Unable to create a new node: ${e}`); - } - return newNode; + let newNode = BAD_VALUE; + try { + const valueType = getObjType(value); + switch (valueType) { + case 'Node': + newNode = value; + break; + case 'String': + case 'Number': + case 'Boolean': { + const valueStr = String(value); + const firstChar = valueStr[0]; + + // Handle unary expressions like -3, +5, !true (from string representations) + if (['-', '+', '!'].includes(firstChar) && valueStr.length > 1) { + const absVal = valueStr.substring(1); + // Check if the remaining part is numeric (integers only to maintain original behavior) + if (isNaN(parseInt(absVal)) && !['Infinity', 'NaN'].includes(absVal)) { + // Non-numeric string like "!hello" - treat as literal + newNode = { + type: 'Literal', + value, + raw: valueStr, + }; + } else { + // Create unary expression maintaining string representation for consistency + newNode = { + type: 'UnaryExpression', + operator: firstChar, + argument: createNewNode(absVal), + }; + } + } else if (['Infinity', 'NaN'].includes(valueStr)) { + // Special numeric identifiers + newNode = { + type: 'Identifier', + name: valueStr, + }; + } else if (Object.is(value, -0)) { + // Special case: negative zero requires unary expression + newNode = { + type: 'UnaryExpression', + operator: '-', + argument: createNewNode(0), + }; + } else { + // Regular literal values + newNode = { + type: 'Literal', + value, + raw: valueStr, + }; + } + break; + } + case 'Array': { + const elements = []; + // Direct iteration over array (value is already an array) + for (let i = 0; i < value.length; i++) { + const elementNode = createNewNode(value[i]); + if (elementNode === BAD_VALUE) { + // If any element fails to convert, fail the entire array + throw new Error('Array contains unconvertible element'); + } + elements.push(elementNode); + } + newNode = { + type: 'ArrayExpression', + elements, + }; + break; + } + case 'Object': { + const properties = []; + const entries = Object.entries(value); + + for (let i = 0; i < entries.length; i++) { + const [k, v] = entries[i]; + const key = createNewNode(k); + const val = createNewNode(v); + + // If any property key or value fails to convert, fail the entire object + if (key === BAD_VALUE || val === BAD_VALUE) { + throw new Error('Object contains unconvertible property'); + } + + properties.push({ + type: 'Property', + key, + value: val, + }); + } + newNode = { + type: 'ObjectExpression', + properties, + }; + break; + } + case 'Undefined': + newNode = { + type: 'Identifier', + name: 'undefined', + }; + break; + case 'Null': + newNode = { + type: 'Literal', + raw: 'null', + }; + break; + case 'BigInt': + newNode = { + type: 'Literal', + value, + raw: value.toString() + 'n', + bigint: value.toString(), + }; + break; + case 'Symbol': + // Symbols cannot be represented as literals in AST + // They must be created via Symbol() calls + const symbolDesc = value.description; + if (symbolDesc) { + newNode = { + type: 'CallExpression', + callee: {type: 'Identifier', name: 'Symbol'}, + arguments: [createNewNode(symbolDesc)], + }; + } else { + newNode = { + type: 'CallExpression', + callee: {type: 'Identifier', name: 'Symbol'}, + arguments: [], + }; + } + break; + case 'Function': // Covers functions and classes + try { + // Attempt to parse function source code into AST + const parsed = parseCode(value.toString()); + if (parsed?.body?.[0]) { + newNode = parsed.body[0]; + } + } catch { + // Native functions or unparseable functions return BAD_VALUE + // This is expected behavior for built-in functions like Math.max + } + break; + case 'RegExp': + newNode = { + type: 'Literal', + regex: { + pattern: value.source, + flags: value.flags, + }, + }; + break; + } + } catch (e) { + logger.debug(`[-] Unable to create a new node: ${e}`); + } + return newNode; } \ No newline at end of file diff --git a/src/modules/utils/createOrderedSrc.js b/src/modules/utils/createOrderedSrc.js index 85dec86..bd06446 100644 --- a/src/modules/utils/createOrderedSrc.js +++ b/src/modules/utils/createOrderedSrc.js @@ -8,7 +8,7 @@ const TYPES_REQUIRING_SEMICOLON = ['VariableDeclarator', 'AssignmentExpression'] /** * Comparison function for sorting nodes by their nodeId. * @param {ASTNode} a - First node to compare - * @param {ASTNode} b - Second node to compare + * @param {ASTNode} b - Second node to compare * @return {number} -1 if a comes before b, 1 if b comes before a, 0 if equal */ const sortByNodeId = (a, b) => a.nodeId > b.nodeId ? 1 : b.nodeId > a.nodeId ? -1 : 0; @@ -21,105 +21,105 @@ const sortByNodeId = (a, b) => a.nodeId > b.nodeId ? 1 : b.nodeId > a.nodeId ? - * @return {ASTNode|null} The new named function node, or null if parsing fails */ function addNameToFE(n, name) { - name = name || 'func' + n.nodeId; - const funcSrc = '(' + n.src.replace(FUNC_START_REGEXP, 'function ' + name) + ');'; - try { - const newNode = parseCode(funcSrc); - if (newNode) { - newNode.nodeId = n.nodeId; - newNode.src = funcSrc; - return newNode; - } - } catch (e) { - // Return null if parsing fails rather than undefined - return null; - } - return null; + name = name || 'func' + n.nodeId; + const funcSrc = '(' + n.src.replace(FUNC_START_REGEXP, 'function ' + name) + ');'; + try { + const newNode = parseCode(funcSrc); + if (newNode) { + newNode.nodeId = n.nodeId; + newNode.src = funcSrc; + return newNode; + } + } catch (e) { + // Return null if parsing fails rather than undefined + return null; + } + return null; } /** * Creates ordered source code from AST nodes, handling special cases for IIFEs and function expressions. * When preserveOrder is false, IIFEs are moved to the end to ensure proper execution order. * This is critical for deobfuscation where dependencies must be resolved before usage. - * + * * @param {ASTNode[]} nodes - Array of AST nodes to convert to source code * @param {boolean} [preserveOrder=false] - When false, IIFEs are pushed to the end of the code * @return {string} Combined source code of the nodes in proper execution order - * + * * @example * // Without preserveOrder: IIFEs moved to end * const nodes = [iifeNode, regularCallNode]; * createOrderedSrc(nodes); // → "regularCall();\n(function(){})();\n" - * - * // With preserveOrder: original order preserved + * + * // With preserveOrder: original order preserved * createOrderedSrc(nodes, true); // → "(function(){})();\nregularCall();\n" */ export function createOrderedSrc(nodes, preserveOrder = false) { - const seenNodes = new Set(); - const processedNodes = []; - - for (let i = 0; i < nodes.length; i++) { - let currentNode = nodes[i]; - - // Handle CallExpression nodes - if (currentNode.type === 'CallExpression') { - if (currentNode.parentNode.type === 'ExpressionStatement') { - // Use the ExpressionStatement wrapper instead of the bare CallExpression - currentNode = currentNode.parentNode; - nodes[i] = currentNode; - - // IIFE reordering: place after argument dependencies when preserveOrder is false - if (!preserveOrder && nodes[i].expression.callee.type === 'FunctionExpression') { - let maxArgNodeId = 0; - for (let j = 0; j < nodes[i].expression.arguments.length; j++) { - const arg = nodes[i].expression.arguments[j]; - if (arg?.declNode?.nodeId > maxArgNodeId) { - maxArgNodeId = arg.declNode.nodeId; - } - } - // Place IIFE after latest argument dependency, or at end if no dependencies - currentNode.nodeId = maxArgNodeId ? maxArgNodeId + 1 : currentNode.nodeId + LARGE_NUMBER; - } - } else if (nodes[i].callee.type === 'FunctionExpression') { - // Standalone function expression calls (not in ExpressionStatement) - if (!preserveOrder) { - const namedFunc = addNameToFE(nodes[i], nodes[i].parentNode?.id?.name); - if (namedFunc) { - namedFunc.nodeId = namedFunc.nodeId + LARGE_NUMBER; - currentNode = namedFunc; - nodes[i] = currentNode; - } - } - // When preserveOrder is true, keep the original node unchanged - } - } else if (currentNode.type === 'FunctionExpression' && !currentNode.id) { - // Anonymous function expressions need names for standalone declarations - const namedFunc = addNameToFE(currentNode, currentNode.parentNode?.id?.name); - if (namedFunc) { - currentNode = namedFunc; - nodes[i] = currentNode; - } - } - - // Add to processed list if not already seen - if (!seenNodes.has(currentNode)) { - seenNodes.add(currentNode); - processedNodes.push(currentNode); - } - } - - // Sort by nodeId to ensure proper execution order - processedNodes.sort(sortByNodeId); - - // Generate source code with proper formatting - let output = ''; - for (let i = 0; i < processedNodes.length; i++) { - const n = processedNodes[i]; - const needsSemicolon = TYPES_REQUIRING_SEMICOLON.includes(n.type); - const prefix = n.type === 'VariableDeclarator' ? `${n.parentNode.kind} ` : ''; - const suffix = needsSemicolon ? ';' : ''; - output += prefix + n.src + suffix + '\n'; - } - - return output; + const seenNodes = new Set(); + const processedNodes = []; + + for (let i = 0; i < nodes.length; i++) { + let currentNode = nodes[i]; + + // Handle CallExpression nodes + if (currentNode.type === 'CallExpression') { + if (currentNode.parentNode.type === 'ExpressionStatement') { + // Use the ExpressionStatement wrapper instead of the bare CallExpression + currentNode = currentNode.parentNode; + nodes[i] = currentNode; + + // IIFE reordering: place after argument dependencies when preserveOrder is false + if (!preserveOrder && nodes[i].expression.callee.type === 'FunctionExpression') { + let maxArgNodeId = 0; + for (let j = 0; j < nodes[i].expression.arguments.length; j++) { + const arg = nodes[i].expression.arguments[j]; + if (arg?.declNode?.nodeId > maxArgNodeId) { + maxArgNodeId = arg.declNode.nodeId; + } + } + // Place IIFE after latest argument dependency, or at end if no dependencies + currentNode.nodeId = maxArgNodeId ? maxArgNodeId + 1 : currentNode.nodeId + LARGE_NUMBER; + } + } else if (nodes[i].callee.type === 'FunctionExpression') { + // Standalone function expression calls (not in ExpressionStatement) + if (!preserveOrder) { + const namedFunc = addNameToFE(nodes[i], nodes[i].parentNode?.id?.name); + if (namedFunc) { + namedFunc.nodeId = namedFunc.nodeId + LARGE_NUMBER; + currentNode = namedFunc; + nodes[i] = currentNode; + } + } + // When preserveOrder is true, keep the original node unchanged + } + } else if (currentNode.type === 'FunctionExpression' && !currentNode.id) { + // Anonymous function expressions need names for standalone declarations + const namedFunc = addNameToFE(currentNode, currentNode.parentNode?.id?.name); + if (namedFunc) { + currentNode = namedFunc; + nodes[i] = currentNode; + } + } + + // Add to processed list if not already seen + if (!seenNodes.has(currentNode)) { + seenNodes.add(currentNode); + processedNodes.push(currentNode); + } + } + + // Sort by nodeId to ensure proper execution order + processedNodes.sort(sortByNodeId); + + // Generate source code with proper formatting + let output = ''; + for (let i = 0; i < processedNodes.length; i++) { + const n = processedNodes[i]; + const needsSemicolon = TYPES_REQUIRING_SEMICOLON.includes(n.type); + const prefix = n.type === 'VariableDeclarator' ? `${n.parentNode.kind} ` : ''; + const suffix = needsSemicolon ? ';' : ''; + output += prefix + n.src + suffix + '\n'; + } + + return output; } \ No newline at end of file diff --git a/src/modules/utils/doesDescendantMatchCondition.js b/src/modules/utils/doesDescendantMatchCondition.js index fc5af20..5e52b1a 100644 --- a/src/modules/utils/doesDescendantMatchCondition.js +++ b/src/modules/utils/doesDescendantMatchCondition.js @@ -14,28 +14,28 @@ * // Find ThisExpression: doesDescendantMatchCondition(node, n => n.type === 'ThisExpression', true) */ export function doesDescendantMatchCondition(targetNode, condition, returnNode = false) { - // Input validation - handle null/undefined gracefully - if (!targetNode || typeof condition !== 'function') { - return false; - } + // Input validation - handle null/undefined gracefully + if (!targetNode || typeof condition !== 'function') { + return false; + } - // Use stack-based DFS to avoid recursion depth limits - const stack = [targetNode]; - while (stack.length) { - const currentNode = stack.pop(); - - // Test current node against condition - if (condition(currentNode)) { - return returnNode ? currentNode : true; - } - - // Add children to stack for continued traversal (use traditional loop for performance) - if (currentNode.childNodes?.length) { - for (let i = currentNode.childNodes.length - 1; i >= 0; i--) { - stack.push(currentNode.childNodes[i]); - } - } - } - - return false; + // Use stack-based DFS to avoid recursion depth limits + const stack = [targetNode]; + while (stack.length) { + const currentNode = stack.pop(); + + // Test current node against condition + if (condition(currentNode)) { + return returnNode ? currentNode : true; + } + + // Add children to stack for continued traversal (use traditional loop for performance) + if (currentNode.childNodes?.length) { + for (let i = currentNode.childNodes.length - 1; i >= 0; i--) { + stack.push(currentNode.childNodes[i]); + } + } + } + + return false; } \ No newline at end of file diff --git a/src/modules/utils/evalInVm.js b/src/modules/utils/evalInVm.js index 8dc4f84..ee67a4b 100644 --- a/src/modules/utils/evalInVm.js +++ b/src/modules/utils/evalInVm.js @@ -9,24 +9,24 @@ const BAD_TYPES = ['Promise']; // Pre-computed console object key signatures for builtin object detection const MATCHING_OBJECT_KEYS = { - [Object.keys(console).sort().join('')]: {type: 'Identifier', name: 'console'}, - [Object.keys(console).sort().slice(1).join('')]: {type: 'Identifier', name: 'console'}, // Alternative console without the 'Console' object + [Object.keys(console).sort().join('')]: {type: 'Identifier', name: 'console'}, + [Object.keys(console).sort().slice(1).join('')]: {type: 'Identifier', name: 'console'}, // Alternative console without the 'Console' object }; // Anti-debugging and infinite loop trap patterns with their neutralization replacements const TRAP_STRINGS = [ - { - trap: /while\s*\(\s*(true|[1-9][0-9]*)\s*\)\s*\{\s*}/gi, - replaceWith: 'while (0) {}', - }, - { - trap: /debugger/gi, - replaceWith: '"debugge_"', - }, - { // TODO: Add as many permutations of this in an efficient manner - trap: /["']debu["']\s*\+\s*["']gger["']/gi, - replaceWith: `"debu" + "gge_"`, - }, + { + trap: /while\s*\(\s*(true|[1-9][0-9]*)\s*\)\s*\{\s*}/gi, + replaceWith: 'while (0) {}', + }, + { + trap: /debugger/gi, + replaceWith: '"debugge_"', + }, + { // TODO: Add as many permutations of this in an efficient manner + trap: /["']debu["']\s*\+\s*["']gger["']/gi, + replaceWith: '"debu" + "gge_"', + }, ]; let CACHE = {}; @@ -58,38 +58,38 @@ const MAX_CACHE_SIZE = 100; * // evalInVm('[1,2,3].length') => {type: 'Literal', value: 3, raw: '3'} */ export function evalInVm(stringToEval, sb) { - const cacheName = `eval-${generateHash(stringToEval)}`; - if (CACHE[cacheName] === undefined) { - // Simple cache eviction: clear all when hitting size limit - if (Object.keys(CACHE).length >= MAX_CACHE_SIZE) CACHE = {}; - CACHE[cacheName] = BAD_VALUE; - try { - // Neutralize anti-debugging and infinite loop traps before evaluation - for (let i = 0; i < TRAP_STRINGS.length; i++) { - const ts = TRAP_STRINGS[i]; - stringToEval = stringToEval.replace(ts.trap, ts.replaceWith); - } - let vm = sb || new Sandbox(); - let res = vm.run(stringToEval); - - // Only process valid, safe references that can be converted to AST nodes - if (vm.isReference(res) && !BAD_TYPES.includes(getObjType(res))) { - // noinspection JSUnresolvedVariable - res = res.copySync(); // Extract value from VM reference - - // Check if result matches a known builtin object (e.g., console) - const objKeys = Object.keys(res).sort().join(''); - if (MATCHING_OBJECT_KEYS[objKeys]) { - CACHE[cacheName] = MATCHING_OBJECT_KEYS[objKeys]; - } else { - CACHE[cacheName] = createNewNode(res); - } - } - } catch { - // Evaluation failed - cache entry remains BAD_VALUE - } - } - return CACHE[cacheName]; + const cacheName = `eval-${generateHash(stringToEval)}`; + if (CACHE[cacheName] === undefined) { + // Simple cache eviction: clear all when hitting size limit + if (Object.keys(CACHE).length >= MAX_CACHE_SIZE) CACHE = {}; + CACHE[cacheName] = BAD_VALUE; + try { + // Neutralize anti-debugging and infinite loop traps before evaluation + for (let i = 0; i < TRAP_STRINGS.length; i++) { + const ts = TRAP_STRINGS[i]; + stringToEval = stringToEval.replace(ts.trap, ts.replaceWith); + } + const vm = sb || new Sandbox(); + let res = vm.run(stringToEval); + + // Only process valid, safe references that can be converted to AST nodes + if (vm.isReference(res) && !BAD_TYPES.includes(getObjType(res))) { + // noinspection JSUnresolvedVariable + res = res.copySync(); // Extract value from VM reference + + // Check if result matches a known builtin object (e.g., console) + const objKeys = Object.keys(res).sort().join(''); + if (MATCHING_OBJECT_KEYS[objKeys]) { + CACHE[cacheName] = MATCHING_OBJECT_KEYS[objKeys]; + } else { + CACHE[cacheName] = createNewNode(res); + } + } + } catch { + // Evaluation failed - cache entry remains BAD_VALUE + } + } + return CACHE[cacheName]; } // Attach BAD_VALUE to evalInVm for convenient access by modules using evalInVm diff --git a/src/modules/utils/generateHash.js b/src/modules/utils/generateHash.js index fdf1bd5..11694cc 100644 --- a/src/modules/utils/generateHash.js +++ b/src/modules/utils/generateHash.js @@ -13,33 +13,33 @@ import crypto from 'node:crypto'; * // Deduplication: `context-${generateHash(node.src)}` */ export function generateHash(input) { - try { - // Input validation and normalization - let stringToHash; - - if (input === null || input === undefined) { - return 'null-undefined-hash'; - } - - // Handle AST nodes with .src property - if (typeof input === 'object' && input.src !== undefined) { - stringToHash = String(input.src); - } else { - // Convert to string (handles numbers, booleans, etc.) - stringToHash = String(input); - } - - // Generate MD5 hash for fast cache key generation - return crypto.createHash('md5').update(stringToHash).digest('hex'); - - } catch (error) { - // Fallback hash generation if crypto operations fail - // Simple string-based hash as last resort - const str = String(input?.src ?? input ?? 'error'); - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = ((hash << 5) - hash + str.charCodeAt(i)) & 0xffffffff; - } - return `fallback-${Math.abs(hash).toString(16)}`; - } + try { + // Input validation and normalization + let stringToHash; + + if (input === null || input === undefined) { + return 'null-undefined-hash'; + } + + // Handle AST nodes with .src property + if (typeof input === 'object' && input.src !== undefined) { + stringToHash = String(input.src); + } else { + // Convert to string (handles numbers, booleans, etc.) + stringToHash = String(input); + } + + // Generate MD5 hash for fast cache key generation + return crypto.createHash('md5').update(stringToHash).digest('hex'); + + } catch (error) { + // Fallback hash generation if crypto operations fail + // Simple string-based hash as last resort + const str = String(input?.src ?? input ?? 'error'); + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = ((hash << 5) - hash + str.charCodeAt(i)) & 0xffffffff; + } + return `fallback-${Math.abs(hash).toString(16)}`; + } } \ No newline at end of file diff --git a/src/modules/utils/getCache.js b/src/modules/utils/getCache.js index 34f6171..4b412e6 100644 --- a/src/modules/utils/getCache.js +++ b/src/modules/utils/getCache.js @@ -19,16 +19,16 @@ let RELEVANT_SCRIPT_HASH = null; * // cache[`eval-${generateHash(code)}`] = result; */ export function getCache(currentScriptHash) { - // Input validation - handle null/undefined gracefully - const scriptHash = currentScriptHash ?? 'no-hash'; - - // Cache invalidation: clear when script changes - if (scriptHash !== RELEVANT_SCRIPT_HASH) { - RELEVANT_SCRIPT_HASH = scriptHash; - CACHE = {}; - } - - return CACHE; + // Input validation - handle null/undefined gracefully + const scriptHash = currentScriptHash ?? 'no-hash'; + + // Cache invalidation: clear when script changes + if (scriptHash !== RELEVANT_SCRIPT_HASH) { + RELEVANT_SCRIPT_HASH = scriptHash; + CACHE = {}; + } + + return CACHE; } /** @@ -36,7 +36,7 @@ export function getCache(currentScriptHash) { * Useful for clearing memory between processing phases or for testing. */ getCache.flush = function() { - CACHE = {}; - // Note: RELEVANT_SCRIPT_HASH is intentionally preserved to avoid - // unnecessary cache misses on the next getCache call with same hash + CACHE = {}; + // Note: RELEVANT_SCRIPT_HASH is intentionally preserved to avoid + // unnecessary cache misses on the next getCache call with same hash }; \ No newline at end of file diff --git a/src/modules/utils/getCalleeName.js b/src/modules/utils/getCalleeName.js index 56b9027..1db384f 100644 --- a/src/modules/utils/getCalleeName.js +++ b/src/modules/utils/getCalleeName.js @@ -17,38 +17,38 @@ * @return {string} Function name for direct calls, variable name for method calls, empty string otherwise */ export function getCalleeName(callExpression) { - // Input validation - if (!callExpression?.callee) { - return ''; - } - - const callee = callExpression.callee; - - // Direct function call: func() - if (callee.type === 'Identifier') { - return callee.name; - } - - // Method call: traverse to base object - if (callee.type === 'MemberExpression') { - let current = callee; - - // Find the base object: obj.nested.method() -> find 'obj' - while (current.object) { - current = current.object; - } - - // Only return name for variable-based method calls - if (current.type === 'Identifier') { - return current.name; // obj.method() => 'obj' - } - - // Literal method calls return empty string to avoid collision - // 'str'.method() => '' (not counted with function calls) - return ''; - } - - // All complex expressions return empty string - // (func || fallback)(), func()(), etc. - return ''; + // Input validation + if (!callExpression?.callee) { + return ''; + } + + const callee = callExpression.callee; + + // Direct function call: func() + if (callee.type === 'Identifier') { + return callee.name; + } + + // Method call: traverse to base object + if (callee.type === 'MemberExpression') { + let current = callee; + + // Find the base object: obj.nested.method() -> find 'obj' + while (current.object) { + current = current.object; + } + + // Only return name for variable-based method calls + if (current.type === 'Identifier') { + return current.name; // obj.method() => 'obj' + } + + // Literal method calls return empty string to avoid collision + // 'str'.method() => '' (not counted with function calls) + return ''; + } + + // All complex expressions return empty string + // (func || fallback)(), func()(), etc. + return ''; } \ No newline at end of file diff --git a/src/modules/utils/getDeclarationWithContext.js b/src/modules/utils/getDeclarationWithContext.js index 31e8225..1b5b4d1 100644 --- a/src/modules/utils/getDeclarationWithContext.js +++ b/src/modules/utils/getDeclarationWithContext.js @@ -6,25 +6,25 @@ import {doesDescendantMatchCondition} from './doesDescendantMatchCondition.js'; // Node types that provide no meaningful context and should be filtered from final results const IRRELEVANT_FILTER_TYPES = [ - 'Literal', - 'Identifier', - 'MemberExpression', + 'Literal', + 'Identifier', + 'MemberExpression', ]; // Node types that provide meaningful context for code evaluation const TYPES_TO_COLLECT = [ - 'CallExpression', - 'ArrowFunctionExpression', - 'AssignmentExpression', - 'FunctionDeclaration', - 'FunctionExpression', - 'VariableDeclarator', + 'CallExpression', + 'ArrowFunctionExpression', + 'AssignmentExpression', + 'FunctionDeclaration', + 'FunctionExpression', + 'VariableDeclarator', ]; // Child node types that can be skipped during traversal as they provide no useful context const SKIP_TRAVERSAL_TYPES = [ - 'Literal', - 'ThisExpression', + 'Literal', + 'ThisExpression', ]; // IfStatement child keys for detecting conditional execution contexts @@ -41,12 +41,12 @@ const STANDALONE_WRAPPER_TYPES = ['ExpressionStatement', 'AssignmentExpression', * @return {boolean} True if the node is in an if statement branch, false otherwise */ function isConsequentOrAlternate(targetNode) { - if (!targetNode?.parentNode) return false; - - return targetNode.parentNode.type === 'IfStatement' || + if (!targetNode?.parentNode) return false; + + return targetNode.parentNode.type === 'IfStatement' || IF_STATEMENT_KEYS.includes(targetNode.parentKey) || IF_STATEMENT_KEYS.includes(targetNode.parentNode.parentKey) || - (targetNode.parentNode.parentNode?.type === 'BlockStatement' && + (targetNode.parentNode.parentNode?.type === 'BlockStatement' && IF_STATEMENT_KEYS.includes(targetNode.parentNode.parentNode.parentKey)); } @@ -57,41 +57,41 @@ function isConsequentOrAlternate(targetNode) { * * @param {ASTNode} n - The AST node to check * @return {boolean} True if the node is subject to property assignment/modification, false otherwise - * + * * Examples of detected patterns: * - obj.prop = value (assignment to property) * - obj.push(item) (mutating method call) * - obj[key] = value (computed property assignment) */ function isNodeAnAssignmentToProperty(n) { - if (!n?.parentNode || n.parentNode.type !== 'MemberExpression') { - return false; - } - - if (isConsequentOrAlternate(n.parentNode)) { - return false; - } - - // Check for assignment to property: obj.prop = value - if (n.parentNode.parentNode?.type === 'AssignmentExpression' && + if (!n?.parentNode || n.parentNode.type !== 'MemberExpression') { + return false; + } + + if (isConsequentOrAlternate(n.parentNode)) { + return false; + } + + // Check for assignment to property: obj.prop = value + if (n.parentNode.parentNode?.type === 'AssignmentExpression' && n.parentNode.parentKey === 'left') { - return true; - } - - // Check for mutating method calls: obj.push(value) - if (n.parentKey === 'object') { - const property = n.parentNode.property; - if (property?.isMarked) { - return true; // Marked references won't be collected - } - - const propertyName = property?.value || property?.name; - if (propertyName && PROPERTIES_THAT_MODIFY_CONTENT.includes(propertyName)) { - return true; - } - } - - return false; + return true; + } + + // Check for mutating method calls: obj.push(value) + if (n.parentKey === 'object') { + const property = n.parentNode.property; + if (property?.isMarked) { + return true; // Marked references won't be collected + } + + const propertyName = property?.value || property?.name; + if (propertyName && PROPERTIES_THAT_MODIFY_CONTENT.includes(propertyName)) { + return true; + } + } + + return false; } /** @@ -99,17 +99,17 @@ function isNodeAnAssignmentToProperty(n) { * @return {ASTNode[]} Nodes which aren't contained in other nodes from the array */ function removeRedundantNodes(nodes) { - /** @type {ASTNode[]} */ - const keep = []; - for (let i = 0; i < nodes.length; i++) { - const targetNode = nodes[i], - targetStart = targetNode.start, - targetEnd = targetNode.end; - if (!nodes.some(n => n !== targetNode && n.start <= targetStart && n.end >= targetEnd)) { - keep.push(targetNode); - } - } - return keep; + /** @type {ASTNode[]} */ + const keep = []; + for (let i = 0; i < nodes.length; i++) { + const targetNode = nodes[i], + targetStart = targetNode.start, + targetEnd = targetNode.end; + if (!nodes.some(n => n !== targetNode && n.start <= targetStart && n.end >= targetEnd)) { + keep.push(targetNode); + } + } + return keep; } /** @@ -120,7 +120,7 @@ function removeRedundantNodes(nodes) { * The algorithm uses caching to avoid expensive re-computation for nodes with identical content, * and includes logic to handle: * - Variable references and their declarations - * - Function scope and closure variables + * - Function scope and closure variables * - Anonymous function expressions and their contexts * - Anti-debugging function overwrites (ignoring reassigned function declarations) * - Marked nodes (scheduled for replacement/deletion) - aborts collection if found @@ -130,152 +130,152 @@ function removeRedundantNodes(nodes) { * @return {ASTNode[]} Array of context nodes (declarations, assignments, calls) relevant for evaluation */ export function getDeclarationWithContext(originNode, excludeOriginNode = false) { - // Input validation to prevent crashes - if (!originNode) { - return []; - } - /** @type {ASTNode[]} */ - const stack = [originNode]; // The working stack for nodes to be reviewed - /** @type {ASTNode[]} */ - const collected = []; // These will be our context - /** @type {Set} */ - const visitedNodes = new Set(); // Track visited nodes to prevent infinite loops - /** @type {number[][]} */ - const collectedRanges = []; // Prevent collecting overlapping nodes - - /** + // Input validation to prevent crashes + if (!originNode) { + return []; + } + /** @type {ASTNode[]} */ + const stack = [originNode]; // The working stack for nodes to be reviewed + /** @type {ASTNode[]} */ + const collected = []; // These will be our context + /** @type {Set} */ + const visitedNodes = new Set(); // Track visited nodes to prevent infinite loops + /** @type {number[][]} */ + const collectedRanges = []; // Prevent collecting overlapping nodes + + /** * Adds a node to the traversal stack if it hasn't been visited and is worth traversing. * @param {ASTNode} node - Node to potentially add to stack */ - function addToStack(node) { - if (!node || + function addToStack(node) { + if (!node || visitedNodes.has(node.nodeId) || stack.includes(node) || SKIP_TRAVERSAL_TYPES.includes(node.type)) { - return; - } - stack.push(node); - } - const cache = getCache(originNode.scriptHash); - const srcHash = generateHash(originNode.src); - const cacheNameId = `context-${originNode.nodeId}-${srcHash}`; - const cacheNameSrc = `context-${srcHash}`; - let cached = cache[cacheNameId] || cache[cacheNameSrc]; - if (!cached) { - while (stack.length) { - const node = stack.shift(); - if (visitedNodes.has(node.nodeId)) continue; - visitedNodes.add(node.nodeId); - // Do not collect any context if one of the relevant nodes is marked to be replaced or deleted - if (node.isMarked || doesDescendantMatchCondition(node, n => n.isMarked)) { - collected.length = 0; - break; - } - if (TYPES_TO_COLLECT.includes(node.type) && !isNodeInRanges(node, collectedRanges)) { - collected.push(node); - collectedRanges.push(node.range); - } + return; + } + stack.push(node); + } + const cache = getCache(originNode.scriptHash); + const srcHash = generateHash(originNode.src); + const cacheNameId = `context-${originNode.nodeId}-${srcHash}`; + const cacheNameSrc = `context-${srcHash}`; + let cached = cache[cacheNameId] || cache[cacheNameSrc]; + if (!cached) { + while (stack.length) { + const node = stack.shift(); + if (visitedNodes.has(node.nodeId)) continue; + visitedNodes.add(node.nodeId); + // Do not collect any context if one of the relevant nodes is marked to be replaced or deleted + if (node.isMarked || doesDescendantMatchCondition(node, n => n.isMarked)) { + collected.length = 0; + break; + } + if (TYPES_TO_COLLECT.includes(node.type) && !isNodeInRanges(node, collectedRanges)) { + collected.push(node); + collectedRanges.push(node.range); + } - // For each node, whether collected or not, target relevant relative nodes for further review. - /** @type {ASTNode[]} */ - const targetNodes = [node]; - switch (node.type) { - case 'Identifier': { - const refs = node.references; - // Review the declaration of an identifier - if (node.declNode && node.declNode.parentNode) { - targetNodes.push(node.declNode.parentNode); - } - else if (refs?.length && node.parentNode) targetNodes.push(node.parentNode); - for (let i = 0; i < refs?.length; i++) { - const ref = refs[i]; - // Review call expression that receive the identifier as an argument for possible augmenting functions - if ((ref.parentKey === 'arguments' && ref.parentNode.type === 'CallExpression') || + // For each node, whether collected or not, target relevant relative nodes for further review. + /** @type {ASTNode[]} */ + const targetNodes = [node]; + switch (node.type) { + case 'Identifier': { + const refs = node.references; + // Review the declaration of an identifier + if (node.declNode && node.declNode.parentNode) { + targetNodes.push(node.declNode.parentNode); + } + else if (refs?.length && node.parentNode) targetNodes.push(node.parentNode); + for (let i = 0; i < refs?.length; i++) { + const ref = refs[i]; + // Review call expression that receive the identifier as an argument for possible augmenting functions + if ((ref.parentKey === 'arguments' && ref.parentNode.type === 'CallExpression') || // Review direct assignments to the identifier (ref.parentKey === 'left' && ref.parentNode.type === 'AssignmentExpression' && node.parentNode.type !== 'FunctionDeclaration' && // Skip function reassignments !isConsequentOrAlternate(ref))) { - targetNodes.push(ref.parentNode); - // Review assignments to property - } else if (isNodeAnAssignmentToProperty(ref)) { - targetNodes.push(ref.parentNode.parentNode); - } - } - break; - } - case 'MemberExpression': - if (node.property?.declNode) targetNodes.push(node.property.declNode); - break; - case 'FunctionExpression': - // Review the parent node of anonymous functions to understand their context - if (!node.id) { - let targetParent = node; - while (targetParent.parentNode && !STANDALONE_WRAPPER_TYPES.includes(targetParent.type)) { - targetParent = targetParent.parentNode; - } - if (STANDALONE_WRAPPER_TYPES.includes(targetParent.type)) { - targetNodes.push(targetParent); - } - } - break; - } + targetNodes.push(ref.parentNode); + // Review assignments to property + } else if (isNodeAnAssignmentToProperty(ref)) { + targetNodes.push(ref.parentNode.parentNode); + } + } + break; + } + case 'MemberExpression': + if (node.property?.declNode) targetNodes.push(node.property.declNode); + break; + case 'FunctionExpression': + // Review the parent node of anonymous functions to understand their context + if (!node.id) { + let targetParent = node; + while (targetParent.parentNode && !STANDALONE_WRAPPER_TYPES.includes(targetParent.type)) { + targetParent = targetParent.parentNode; + } + if (STANDALONE_WRAPPER_TYPES.includes(targetParent.type)) { + targetNodes.push(targetParent); + } + } + break; + } + + for (let i = 0; i < targetNodes.length; i++) { + const targetNode = targetNodes[i]; + if (!visitedNodes.has(targetNode.nodeId)) stack.push(targetNode); + // noinspection JSUnresolvedVariable + if (targetNode === targetNode.scope.block) { + // Collect out-of-scope variables used inside the scope + // noinspection JSUnresolvedReference + for (let j = 0; j < targetNode.scope.through.length; j++) { + // noinspection JSUnresolvedReference + addToStack(targetNode.scope.through[j].identifier); + } + } + for (let j = 0; j < targetNode?.childNodes.length; j++) { + addToStack(targetNode.childNodes[j]); + } + } + } + // Filter and deduplicate collected nodes + /** @type {Set} */ + const filteredNodes = new Set(); + + for (let i = 0; i < collected.length; i++) { + const n = collected[i]; - for (let i = 0; i < targetNodes.length; i++) { - const targetNode = targetNodes[i]; - if (!visitedNodes.has(targetNode.nodeId)) stack.push(targetNode); - // noinspection JSUnresolvedVariable - if (targetNode === targetNode.scope.block) { - // Collect out-of-scope variables used inside the scope - // noinspection JSUnresolvedReference - for (let j = 0; j < targetNode.scope.through.length; j++) { - // noinspection JSUnresolvedReference - addToStack(targetNode.scope.through[j].identifier); - } - } - for (let j = 0; j < targetNode?.childNodes.length; j++) { - addToStack(targetNode.childNodes[j]); - } - } - } - // Filter and deduplicate collected nodes - /** @type {Set} */ - const filteredNodes = new Set(); - - for (let i = 0; i < collected.length; i++) { - const n = collected[i]; - - // Skip if already added, irrelevant type, or should be excluded - if (filteredNodes.has(n) || + // Skip if already added, irrelevant type, or should be excluded + if (filteredNodes.has(n) || IRRELEVANT_FILTER_TYPES.includes(n.type) || (excludeOriginNode && isNodeInRanges(n, [originNode.range]))) { - continue; - } - - // Handle anti-debugging function overwrites by ignoring reassigned functions - if (n.type === 'FunctionDeclaration' && n.id?.references?.length) { - let hasNonAssignmentReference = false; - const references = n.id.references; - - for (let j = 0; j < references.length; j++) { - const ref = references[j]; - if (!(ref.parentKey === 'left' && ref.parentNode?.type === 'AssignmentExpression')) { - hasNonAssignmentReference = true; - break; - } - } - - if (hasNonAssignmentReference) { - filteredNodes.add(n); - } - } else { - filteredNodes.add(n); - } - } - // Convert to array and remove redundant nodes - cached = removeRedundantNodes([...filteredNodes]); - cache[cacheNameId] = cached; // Caching context for the same node - cache[cacheNameSrc] = cached; // Caching context for a different node with similar content - } - return cached; + continue; + } + + // Handle anti-debugging function overwrites by ignoring reassigned functions + if (n.type === 'FunctionDeclaration' && n.id?.references?.length) { + let hasNonAssignmentReference = false; + const references = n.id.references; + + for (let j = 0; j < references.length; j++) { + const ref = references[j]; + if (!(ref.parentKey === 'left' && ref.parentNode?.type === 'AssignmentExpression')) { + hasNonAssignmentReference = true; + break; + } + } + + if (hasNonAssignmentReference) { + filteredNodes.add(n); + } + } else { + filteredNodes.add(n); + } + } + // Convert to array and remove redundant nodes + cached = removeRedundantNodes([...filteredNodes]); + cache[cacheNameId] = cached; // Caching context for the same node + cache[cacheNameSrc] = cached; // Caching context for a different node with similar content + } + return cached; } \ No newline at end of file diff --git a/src/modules/utils/getDescendants.js b/src/modules/utils/getDescendants.js index 872133e..e9ecf22 100644 --- a/src/modules/utils/getDescendants.js +++ b/src/modules/utils/getDescendants.js @@ -18,35 +18,35 @@ * // Returns [leftIdentifier, rightIdentifier] - all nested child nodes */ export function getDescendants(targetNode) { - // Input validation - if (!targetNode) { - return []; - } - - // Return cached result if available - if (targetNode.descendants) { - return targetNode.descendants; - } - - /** @type {Set} */ - const descendants = new Set(); - /** @type {ASTNode[]} */ - const stack = [targetNode]; - - while (stack.length) { - const currentNode = stack.pop(); - const childNodes = currentNode?.childNodes || []; - - for (let i = 0; i < childNodes.length; i++) { - const childNode = childNodes[i]; - if (!descendants.has(childNode)) { - descendants.add(childNode); - stack.push(childNode); - } - } - } - - // Cache results as array on the target node for future calls - const descendantsArray = [...descendants]; - return targetNode.descendants = descendantsArray; + // Input validation + if (!targetNode) { + return []; + } + + // Return cached result if available + if (targetNode.descendants) { + return targetNode.descendants; + } + + /** @type {Set} */ + const descendants = new Set(); + /** @type {ASTNode[]} */ + const stack = [targetNode]; + + while (stack.length) { + const currentNode = stack.pop(); + const childNodes = currentNode?.childNodes || []; + + for (let i = 0; i < childNodes.length; i++) { + const childNode = childNodes[i]; + if (!descendants.has(childNode)) { + descendants.add(childNode); + stack.push(childNode); + } + } + } + + // Cache results as array on the target node for future calls + const descendantsArray = [...descendants]; + return targetNode.descendants = descendantsArray; } \ No newline at end of file diff --git a/src/modules/utils/getMainDeclaredObjectOfMemberExpression.js b/src/modules/utils/getMainDeclaredObjectOfMemberExpression.js index b24e347..0fc4c16 100644 --- a/src/modules/utils/getMainDeclaredObjectOfMemberExpression.js +++ b/src/modules/utils/getMainDeclaredObjectOfMemberExpression.js @@ -18,24 +18,24 @@ * // computed[key].value --> returns 'computed' identifier (if it has declNode) */ export function getMainDeclaredObjectOfMemberExpression(memberExpression) { - // Input validation: only reject null/undefined, allow any valid AST node - if (!memberExpression) { - return null; - } + // Input validation: only reject null/undefined, allow any valid AST node + if (!memberExpression) { + return null; + } - let mainObject = memberExpression; - let iterationCount = 0; - const MAX_ITERATIONS = 50; // Prevent infinite loops in malformed AST + let mainObject = memberExpression; + let iterationCount = 0; + const MAX_ITERATIONS = 50; // Prevent infinite loops in malformed AST - // Traverse up the member expression chain to find the root object with a declaration - while (mainObject && - !mainObject.declNode && - mainObject.type === 'MemberExpression' && + // Traverse up the member expression chain to find the root object with a declaration + while (mainObject && + !mainObject.declNode && + mainObject.type === 'MemberExpression' && iterationCount < MAX_ITERATIONS) { - mainObject = mainObject.object; - iterationCount++; - } + mainObject = mainObject.object; + iterationCount++; + } - // Return the final object in the chain (original behavior preserved) - return mainObject; + // Return the final object in the chain (original behavior preserved) + return mainObject; } \ No newline at end of file diff --git a/src/modules/utils/getObjType.js b/src/modules/utils/getObjType.js index c7748f6..1ffe754 100644 --- a/src/modules/utils/getObjType.js +++ b/src/modules/utils/getObjType.js @@ -21,5 +21,5 @@ * // getObjType(function() {}) => 'Function' */ export function getObjType(unknownObject) { - return ({}).toString.call(unknownObject).slice(8, -1); + return ({}).toString.call(unknownObject).slice(8, -1); } \ No newline at end of file diff --git a/src/modules/utils/index.js b/src/modules/utils/index.js index 95a33b8..69f9af2 100644 --- a/src/modules/utils/index.js +++ b/src/modules/utils/index.js @@ -1,18 +1,18 @@ export default { - areReferencesModified: (await import('./areReferencesModified.js')).areReferencesModified, - createNewNode: (await import('./createNewNode.js')).createNewNode, - createOrderedSrc: (await import('./createOrderedSrc.js')).createOrderedSrc, - doesDescendantMatchCondition: (await import('./doesDescendantMatchCondition.js')).doesDescendantMatchCondition, - evalInVm: (await import('./evalInVm.js')).evalInVm, - generateHash: (await import('./generateHash.js')).generateHash, - getCache: (await import('./getCache.js')).getCache, - getCalleeName: (await import('./getCalleeName.js')).getCalleeName, - getDeclarationWithContext: (await import('./getDeclarationWithContext.js')).getDeclarationWithContext, - getDescendants: (await import('./getDescendants.js')).getDescendants, - getMainDeclaredObjectOfMemberExpression: (await import('./getMainDeclaredObjectOfMemberExpression.js')).getMainDeclaredObjectOfMemberExpression, - getObjType: (await import('./getObjType.js')).getObjType, - isNodeInRanges: (await import('./isNodeInRanges.js')).isNodeInRanges, - normalizeScript: (await import('./normalizeScript.js')).normalizeScript, - safeImplementations: (await import('./safeImplementations.js')), - sandbox: (await import('./sandbox.js')).Sandbox, + areReferencesModified: (await import('./areReferencesModified.js')).areReferencesModified, + createNewNode: (await import('./createNewNode.js')).createNewNode, + createOrderedSrc: (await import('./createOrderedSrc.js')).createOrderedSrc, + doesDescendantMatchCondition: (await import('./doesDescendantMatchCondition.js')).doesDescendantMatchCondition, + evalInVm: (await import('./evalInVm.js')).evalInVm, + generateHash: (await import('./generateHash.js')).generateHash, + getCache: (await import('./getCache.js')).getCache, + getCalleeName: (await import('./getCalleeName.js')).getCalleeName, + getDeclarationWithContext: (await import('./getDeclarationWithContext.js')).getDeclarationWithContext, + getDescendants: (await import('./getDescendants.js')).getDescendants, + getMainDeclaredObjectOfMemberExpression: (await import('./getMainDeclaredObjectOfMemberExpression.js')).getMainDeclaredObjectOfMemberExpression, + getObjType: (await import('./getObjType.js')).getObjType, + isNodeInRanges: (await import('./isNodeInRanges.js')).isNodeInRanges, + normalizeScript: (await import('./normalizeScript.js')).normalizeScript, + safeImplementations: (await import('./safeImplementations.js')), + sandbox: (await import('./sandbox.js')).Sandbox, }; \ No newline at end of file diff --git a/src/modules/utils/isNodeInRanges.js b/src/modules/utils/isNodeInRanges.js index af3792a..db894fc 100644 --- a/src/modules/utils/isNodeInRanges.js +++ b/src/modules/utils/isNodeInRanges.js @@ -23,20 +23,20 @@ * // isNodeInRanges(node, [[6, 10]]) => false (node starts before range) */ export function isNodeInRanges(targetNode, ranges) { - // Early return for empty ranges array - no ranges means node is not in any range - if (!ranges.length) { - return false; - } + // Early return for empty ranges array - no ranges means node is not in any range + if (!ranges.length) { + return false; + } - const [nodeStart, nodeEnd] = targetNode.range; + const [nodeStart, nodeEnd] = targetNode.range; - // Check if node range is completely contained within any provided range - for (let i = 0; i < ranges.length; i++) { - const [rangeStart, rangeEnd] = ranges[i]; - if (nodeStart >= rangeStart && nodeEnd <= rangeEnd) { - return true; - } - } + // Check if node range is completely contained within any provided range + for (let i = 0; i < ranges.length; i++) { + const [rangeStart, rangeEnd] = ranges[i]; + if (nodeStart >= rangeStart && nodeEnd <= rangeEnd) { + return true; + } + } - return false; + return false; } \ No newline at end of file diff --git a/src/modules/utils/normalizeScript.js b/src/modules/utils/normalizeScript.js index 374ecb0..4665098 100644 --- a/src/modules/utils/normalizeScript.js +++ b/src/modules/utils/normalizeScript.js @@ -25,9 +25,9 @@ import * as normalizeRedundantNotOperator from '../unsafe/normalizeRedundantNotO * // Output: obj.method(); true; */ export function normalizeScript(script) { - return applyIteratively(script, [ - normalizeComputed.default, - normalizeRedundantNotOperator.default, - normalizeEmptyStatements.default, - ]); + return applyIteratively(script, [ + normalizeComputed.default, + normalizeRedundantNotOperator.default, + normalizeEmptyStatements.default, + ]); } \ No newline at end of file diff --git a/src/modules/utils/safe-atob.js b/src/modules/utils/safe-atob.js index da4baa9..fb43aa1 100644 --- a/src/modules/utils/safe-atob.js +++ b/src/modules/utils/safe-atob.js @@ -14,5 +14,5 @@ * // atob('YWJjMTIz') => 'abc123' */ export function atob(val) { - return Buffer.from(val, 'base64').toString(); + return Buffer.from(val, 'base64').toString(); } \ No newline at end of file diff --git a/src/modules/utils/safe-btoa.js b/src/modules/utils/safe-btoa.js index fcc46bc..68ca217 100644 --- a/src/modules/utils/safe-btoa.js +++ b/src/modules/utils/safe-btoa.js @@ -14,5 +14,5 @@ * // btoa('abc123') => 'YWJjMTIz' */ export function btoa(val) { - return Buffer.from(val).toString('base64'); + return Buffer.from(val).toString('base64'); } \ No newline at end of file diff --git a/src/modules/utils/sandbox.js b/src/modules/utils/sandbox.js index a52d74e..f270a1e 100644 --- a/src/modules/utils/sandbox.js +++ b/src/modules/utils/sandbox.js @@ -3,14 +3,14 @@ const {Isolate, Reference} = pkg; // Security-critical APIs that must be blocked in the sandbox environment const BLOCKED_APIS = { - debugger: undefined, - WebAssembly: undefined, - fetch: undefined, - XMLHttpRequest: undefined, - WebSocket: undefined, - globalThis: undefined, - navigator: undefined, - Navigator: undefined, + debugger: undefined, + WebAssembly: undefined, + fetch: undefined, + XMLHttpRequest: undefined, + WebSocket: undefined, + globalThis: undefined, + navigator: undefined, + Navigator: undefined, }; // Default memory limit for VM instances (in MB) @@ -21,25 +21,25 @@ const DEFAULT_TIMEOUT = 1000; /** * Isolated sandbox environment for executing untrusted JavaScript code during deobfuscation. - * + * * SECURITY NOTE: This sandbox provides isolation and basic protections but is NOT truly secure. * It's better than direct eval() but should not be relied upon for security-critical applications. * The isolated-vm library provides process isolation but vulnerabilities may still exist. - * + * * This class provides an isolated VM context using the isolated-vm library to evaluate * potentially malicious JavaScript with reduced risk to the host environment. The sandbox includes: - * + * * Isolation Features: * - Separate V8 context isolated from host environment * - Blocked access to dangerous APIs (WebAssembly, fetch, WebSocket, etc.) * - Memory and execution time limits to prevent resource exhaustion * - Deterministic evaluation (Math.random and Date are deleted for consistent results) - * + * * Performance Optimizations: * - Reusable instances to avoid VM creation overhead * - Shared contexts for multiple evaluations * - Pre-configured global environment setup - * + * * Used extensively by unsafe transformation modules for: * - Evaluating binary expressions with literal operands * - Resolving member expressions on literal objects/arrays @@ -47,61 +47,61 @@ const DEFAULT_TIMEOUT = 1000; * - Local function call resolution with context */ export class Sandbox { - /** + /** * Creates a new isolated sandbox environment with security restrictions. * The sandbox is configured with memory limits, execution timeouts, and blocked APIs. */ - constructor() { - this.replacedItems = {...BLOCKED_APIS}; - this.replacedItemsNames = Object.keys(BLOCKED_APIS); - this.timeout = DEFAULT_TIMEOUT; + constructor() { + this.replacedItems = {...BLOCKED_APIS}; + this.replacedItemsNames = Object.keys(BLOCKED_APIS); + this.timeout = DEFAULT_TIMEOUT; - // Create isolated V8 context with memory limits - this.vm = new Isolate({memoryLimit: DEFAULT_MEMORY_LIMIT}); - this.context = this.vm.createContextSync(); + // Create isolated V8 context with memory limits + this.vm = new Isolate({memoryLimit: DEFAULT_MEMORY_LIMIT}); + this.context = this.vm.createContextSync(); - // Set up global reference for compatibility - this.context.global.setSync('global', this.context.global.derefInto()); + // Set up global reference for compatibility + this.context.global.setSync('global', this.context.global.derefInto()); - // Block dangerous APIs by setting them to undefined in the sandbox - for (let i = 0; i < this.replacedItemsNames.length; i++) { - const itemName = this.replacedItemsNames[i]; - this.context.global.setSync(itemName, this.replacedItems[itemName]); - } - } + // Block dangerous APIs by setting them to undefined in the sandbox + for (let i = 0; i < this.replacedItemsNames.length; i++) { + const itemName = this.replacedItemsNames[i]; + this.context.global.setSync(itemName, this.replacedItems[itemName]); + } + } - /** + /** * Executes JavaScript code in the isolated sandbox environment. - * + * * For deterministic results during deobfuscation, Math.random and Date are deleted * before execution to ensure consistent output across runs. This is critical for * reliable deobfuscation results. - * + * * @param {string} code - JavaScript code to execute in the sandbox * @return {Reference} A Reference object from isolated-vm containing the execution result - * + * * @example * // const sandbox = new Sandbox(); * // const result = sandbox.run('2 + 3'); // Returns Reference containing 5 */ - run(code) { - // Delete non-deterministic APIs to ensure consistent results across deobfuscation runs - const script = this.vm.compileScriptSync('delete Math.random; delete Date;\n\n' + code); - return script.runSync(this.context, { - timeout: this.timeout, - reference: true, - }); - } + run(code) { + // Delete non-deterministic APIs to ensure consistent results across deobfuscation runs + const script = this.vm.compileScriptSync('delete Math.random; delete Date;\n\n' + code); + return script.runSync(this.context, { + timeout: this.timeout, + reference: true, + }); + } - /** + /** * Determines if an object is a VM Reference (from isolated-vm) rather than a native JavaScript value. * This is used to distinguish between successfully evaluated results and objects that need * further processing or conversion. - * + * * @param {*} obj - Object to check * @return {boolean} True if the object is a VM Reference, false otherwise */ - isReference(obj) { - return obj != null && Object.getPrototypeOf(obj) === Reference.prototype; - } + isReference(obj) { + return obj !== null && obj !== undefined && Object.getPrototypeOf(obj) === Reference.prototype; + } } \ No newline at end of file diff --git a/src/processors/augmentedArray.js b/src/processors/augmentedArray.js index 028a72f..494f92e 100644 --- a/src/processors/augmentedArray.js +++ b/src/processors/augmentedArray.js @@ -1,6 +1,6 @@ /** * Augmented Array Replacements - * + * * Detects and resolves obfuscation patterns where arrays are shuffled by immediately-invoked * function expressions (IIFEs). This processor identifies shuffled arrays that are re-ordered * by IIFEs and replaces them with their final static state. @@ -14,7 +14,7 @@ * })(a, 1); * console[a[0]](a[1]); // Before: console['hello']('log') -> Error * // After: console['log']('hello') -> Works - * + * * Resolution Process: * 1. Identify IIFE patterns that manipulate arrays with literal shift counts * 2. Execute the IIFE in a secure VM to determine final array state @@ -47,43 +47,43 @@ const FUNCTION_DECLARATION_PATTERN = /function/i; * * @example * // Matches: (function(arr, 3) { shuffle_logic })(myArrayVar, 3) - * // Matches: ((arr, n) => { shuffle_logic })(myArrayVar, 1) + * // Matches: ((arr, n) => { shuffle_logic })(myArrayVar, 1) * // Matches: (function(fn, n) { shuffle_logic })(selfModifyingFunc, 2) [if fn reassigns itself] * // Ignores: (function() {})(), myFunc(arr), (function(fn) {})(staticFunction) */ export function augmentedArrayMatch(arb, candidateFilter = () => true) { - const matches = []; - const candidates = arb.ast[0].typeMap.CallExpression; - - for (let i = 0; i < candidates.length; i++) { - const n = candidates[i]; - if ((n.callee.type === 'FunctionExpression' || n.callee.type === 'ArrowFunctionExpression') && - n.arguments.length > 1 && + const matches = []; + const candidates = arb.ast[0].typeMap.CallExpression; + + for (let i = 0; i < candidates.length; i++) { + const n = candidates[i]; + if ((n.callee.type === 'FunctionExpression' || n.callee.type === 'ArrowFunctionExpression') && + n.arguments.length > 1 && n.arguments[0].type === 'Identifier' && - n.arguments[1].type === 'Literal' && + n.arguments[1].type === 'Literal' && !Number.isNaN(parseInt(n.arguments[1].value)) && candidateFilter(n)) { - // For function declarations, only match if they are self-modifying - if (n.arguments[0].declNode?.parentNode?.type === 'FunctionDeclaration') { - const functionBody = n.arguments[0].declNode.parentNode.body; - const functionName = n.arguments[0].name; - // Check if function reassigns itself (self-modifying pattern) - const isSelfModifying = functionBody?.body?.some(stmt => - stmt.type === 'ExpressionStatement' && + // For function declarations, only match if they are self-modifying + if (n.arguments[0].declNode?.parentNode?.type === 'FunctionDeclaration') { + const functionBody = n.arguments[0].declNode.parentNode.body; + const functionName = n.arguments[0].name; + // Check if function reassigns itself (self-modifying pattern) + const isSelfModifying = functionBody?.body?.some(stmt => + stmt.type === 'ExpressionStatement' && stmt.expression?.type === 'AssignmentExpression' && stmt.expression.left?.type === 'Identifier' && - stmt.expression.left.name === functionName - ); - if (isSelfModifying) { - matches.push(n); - } - } else if (n.arguments[0].declNode?.parentNode?.type === 'VariableDeclarator') { - // Variables are always potential candidates - matches.push(n); - } - } - } - return matches; + stmt.expression.left.name === functionName, + ); + if (isSelfModifying) { + matches.push(n); + } + } else if (n.arguments[0].declNode?.parentNode?.type === 'VariableDeclarator') { + // Variables are always potential candidates + matches.push(n); + } + } + } + return matches; } /** @@ -107,47 +107,47 @@ export function augmentedArrayMatch(arb, candidateFilter = () => true) { * // Output: const arr = [2, 1]; */ export function augmentedArrayTransform(arb, n) { - // Find the target ExpressionStatement or SequenceExpression containing this IIFE - let targetNode = n; - while (targetNode && (targetNode.type !== 'ExpressionStatement' && targetNode.parentNode.type !== 'SequenceExpression')) { - targetNode = targetNode?.parentNode; - } - - // Extract the array identifier being augmented (first argument of the IIFE) - const relevantArrayIdentifier = n.arguments.find(node => node.type === 'Identifier'); - - // Determine if the array comes from a function declaration or variable declaration - const declKind = FUNCTION_DECLARATION_PATTERN.test(relevantArrayIdentifier.declNode.parentNode.type) ? '' : 'var '; - const ref = !declKind ? `${relevantArrayIdentifier.name}()` : relevantArrayIdentifier.name; - - // Build execution context: array declaration + IIFE + array reference for final state - const contextNodes = getDeclarationWithContext(n, true); - const context = `${contextNodes.length ? createOrderedSrc(contextNodes) : ''}`; - const src = `${context};\n${createOrderedSrc([targetNode])}\n${ref};`; - - // Execute the augmentation in VM to get the final array state - const replacementNode = evalInVm(src); - if (replacementNode !== evalInVm.BAD_VALUE) { - // Mark the IIFE for removal - arb.markNode(targetNode || n); - - // Replace the array with its final augmented state - if (relevantArrayIdentifier.declNode.parentNode.type === 'FunctionDeclaration') { - // For function declarations, replace the function body with a return statement - arb.markNode(relevantArrayIdentifier.declNode.parentNode.body, { - type: 'BlockStatement', - body: [{ - type: 'ReturnStatement', - argument: replacementNode, - }], - }); - } else { - // For variable declarations, replace the initializer with the computed array - arb.markNode(relevantArrayIdentifier.declNode.parentNode.init, replacementNode); - } - } - - return arb; + // Find the target ExpressionStatement or SequenceExpression containing this IIFE + let targetNode = n; + while (targetNode && (targetNode.type !== 'ExpressionStatement' && targetNode.parentNode.type !== 'SequenceExpression')) { + targetNode = targetNode?.parentNode; + } + + // Extract the array identifier being augmented (first argument of the IIFE) + const relevantArrayIdentifier = n.arguments.find(node => node.type === 'Identifier'); + + // Determine if the array comes from a function declaration or variable declaration + const declKind = FUNCTION_DECLARATION_PATTERN.test(relevantArrayIdentifier.declNode.parentNode.type) ? '' : 'var '; + const ref = !declKind ? `${relevantArrayIdentifier.name}()` : relevantArrayIdentifier.name; + + // Build execution context: array declaration + IIFE + array reference for final state + const contextNodes = getDeclarationWithContext(n, true); + const context = `${contextNodes.length ? createOrderedSrc(contextNodes) : ''}`; + const src = `${context};\n${createOrderedSrc([targetNode])}\n${ref};`; + + // Execute the augmentation in VM to get the final array state + const replacementNode = evalInVm(src); + if (replacementNode !== evalInVm.BAD_VALUE) { + // Mark the IIFE for removal + arb.markNode(targetNode || n); + + // Replace the array with its final augmented state + if (relevantArrayIdentifier.declNode.parentNode.type === 'FunctionDeclaration') { + // For function declarations, replace the function body with a return statement + arb.markNode(relevantArrayIdentifier.declNode.parentNode.body, { + type: 'BlockStatement', + body: [{ + type: 'ReturnStatement', + argument: replacementNode, + }], + }); + } else { + // For variable declarations, replace the initializer with the computed array + arb.markNode(relevantArrayIdentifier.declNode.parentNode.init, replacementNode); + } + } + + return arb; } /** @@ -173,13 +173,13 @@ export function augmentedArrayTransform(arb, n) { * // After: const a = [2,1]; */ export function replaceArrayWithStaticAugmentedVersion(arb) { - const matches = augmentedArrayMatch(arb); - - for (let i = 0; i < matches.length; i++) { - arb = augmentedArrayTransform(arb, matches[i]); - } - - return arb; + const matches = augmentedArrayMatch(arb); + + for (let i = 0; i < matches.length; i++) { + arb = augmentedArrayTransform(arb, matches[i]); + } + + return arb; } export const preprocessors = [replaceArrayWithStaticAugmentedVersion, resolveFunctionToArray.default]; diff --git a/src/processors/caesarp.js b/src/processors/caesarp.js index 7b349df..ed64a1a 100644 --- a/src/processors/caesarp.js +++ b/src/processors/caesarp.js @@ -31,24 +31,24 @@ const VARIABLE_CONTAINING_THE_INNER_LAYER_REGEX = /\(((\w{3}\()+(\w{3})\)*)\)/gm * @return {Arborist} */ function extractInnerLayer(arb) { - // The outer layer is a lot of code moved around and concatenated, but it all comes together in the last - // couple of lines where an object's toString is being replaced with the inner layer code, and then - // run when the object is being added to a string, implicitly invoking the object's toString method. - // We can catch the variable holding the code before it's injected and output it instead. - let script = arb.script; + // The outer layer is a lot of code moved around and concatenated, but it all comes together in the last + // couple of lines where an object's toString is being replaced with the inner layer code, and then + // run when the object is being added to a string, implicitly invoking the object's toString method. + // We can catch the variable holding the code before it's injected and output it instead. + let script = arb.script; - const matches = LINE_WITH_FINAL_ASSIGNMENT_REGEX.exec(script); - if (matches?.length) { - const lineToReplace = script.substring(matches.index); - // Sometimes the first layer variable is wrapped in other functions which will decrypt it - // like OdP(qv4(dAN(RKt))) instead of just RKt, so we need output the entire chain. - const innerLayerVarMatches = VARIABLE_CONTAINING_THE_INNER_LAYER_REGEX.exec(lineToReplace); - const variableContainingTheInnerLayer = innerLayerVarMatches ? innerLayerVarMatches[0] : matches[2]; - script = script.replace(lineToReplace, `console.log(${variableContainingTheInnerLayer}.toString());})();\n`); - // script = evalWithDom(script); - if (script) arb = new Arborist(script); - } - return arb; + const matches = LINE_WITH_FINAL_ASSIGNMENT_REGEX.exec(script); + if (matches?.length) { + const lineToReplace = script.substring(matches.index); + // Sometimes the first layer variable is wrapped in other functions which will decrypt it + // like OdP(qv4(dAN(RKt))) instead of just RKt, so we need output the entire chain. + const innerLayerVarMatches = VARIABLE_CONTAINING_THE_INNER_LAYER_REGEX.exec(lineToReplace); + const variableContainingTheInnerLayer = innerLayerVarMatches ? innerLayerVarMatches[0] : matches[2]; + script = script.replace(lineToReplace, `console.log(${variableContainingTheInnerLayer}.toString());})();\n`); + // script = evalWithDom(script); + if (script) arb = new Arborist(script); + } + return arb; } export const preprocessors = [extractInnerLayer]; diff --git a/src/processors/functionToArray.js b/src/processors/functionToArray.js index 5be75d1..eea8c9c 100644 --- a/src/processors/functionToArray.js +++ b/src/processors/functionToArray.js @@ -1,27 +1,27 @@ /** * Function To Array Replacements Processor - * + * * This processor resolves obfuscation patterns where arrays are dynamically generated * by function calls and then accessed via member expressions throughout the script. - * + * * Common obfuscation pattern: * ```javascript * function getArr() { return ['a', 'b', 'c']; } * const data = getArr(); * console.log(data[0], data[1]); // Array access pattern * ``` - * + * * After processing: * ```javascript * function getArr() { return ['a', 'b', 'c']; } * const data = ['a', 'b', 'c']; // Function call replaced with literal array * console.log(data[0], data[1]); * ``` - * + * * The processor evaluates function calls in a sandbox environment to determine * their array result and replaces the call with the literal array, improving * readability and enabling further deobfuscation by other modules. - * + * * Implementation: Uses the resolveFunctionToArray module from the unsafe collection, * which provides sophisticated match/transform logic with context-aware evaluation. */ diff --git a/src/processors/index.js b/src/processors/index.js index c108251..5a0756e 100644 --- a/src/processors/index.js +++ b/src/processors/index.js @@ -2,11 +2,11 @@ * Mapping specific obfuscation type to their processors, which are lazily loaded. */ export const processors = { - 'caesar_plus': await import('./caesarp.js'), - 'obfuscator.io': await import('./obfuscator.io.js'), - 'augmented_array_replacements': await import('./augmentedArray.js'), - 'function_to_array_replacements': await import('./functionToArray.js'), - 'proxied_augmented_array_replacements': await import('./augmentedArray.js'), - 'augmented_array_function_replacements': await import('./augmentedArray.js'), - 'augmented_proxied_array_function_replacements': await import('./augmentedArray.js'), + 'caesar_plus': await import('./caesarp.js'), + 'obfuscator.io': await import('./obfuscator.io.js'), + 'augmented_array_replacements': await import('./augmentedArray.js'), + 'function_to_array_replacements': await import('./functionToArray.js'), + 'proxied_augmented_array_replacements': await import('./augmentedArray.js'), + 'augmented_array_function_replacements': await import('./augmentedArray.js'), + 'augmented_proxied_array_function_replacements': await import('./augmentedArray.js'), }; diff --git a/src/processors/obfuscator.io.js b/src/processors/obfuscator.io.js index 01b1b16..cc2b681 100644 --- a/src/processors/obfuscator.io.js +++ b/src/processors/obfuscator.io.js @@ -1,18 +1,18 @@ /** * Obfuscator.io Processor - * + * * This processor handles obfuscation patterns specific to obfuscator.io, particularly * the "debug protection" mechanism that creates infinite loops when the script detects * it has been beautified or modified. - * + * * The debug protection works by: * 1. Testing function toString() output against a regex * 2. If the test fails (indicating beautification), triggering an infinite loop * 3. Preventing the script from executing normally - * + * * This processor bypasses the protection by replacing the tested functions with * strings that pass the validation tests, effectively "freezing" their values. - * + * * Combined with augmentedArray processors for comprehensive obfuscator.io support. */ import * as augmentedArrayProcessors from './augmentedArray.js'; @@ -27,97 +27,97 @@ const FREEZE_REPLACEMENT_STRING = 'function () {return "bypassed!"}'; * Identifies Literal nodes that contain debug protection trigger values. * These literals are part of obfuscator.io's anti-debugging mechanisms that test * function stringification to detect code beautification or modification. - * + * * Matching criteria: * - Literal nodes with values 'newState' or 'removeCookie' * - Literals positioned within function expressions or property assignments * - Valid parent node structure for replacement targeting - * + * * @param {Arborist} arb - Arborist instance containing the AST * @param {Function} [candidateFilter=(() => true)] - Optional filter function for additional criteria * @return {ASTNode[]} Array of matching Literal nodes suitable for debug protection bypass - * + * * @example * // Matches: 'newState' in function context, 'removeCookie' in property assignment * // Ignores: Other literal values, literals in invalid contexts */ export function obfuscatorIoMatch(arb, candidateFilter = () => true) { - const matches = []; - const candidates = arb.ast[0].typeMap.Literal; + const matches = []; + const candidates = arb.ast[0].typeMap.Literal; - for (let i = 0; i < candidates.length; i++) { - const n = candidates[i]; - if (DEBUG_PROTECTION_TRIGGERS.includes(n.value) && candidateFilter(n)) { - matches.push(n); - } - } - return matches; + for (let i = 0; i < candidates.length; i++) { + const n = candidates[i]; + if (DEBUG_PROTECTION_TRIGGERS.includes(n.value) && candidateFilter(n)) { + matches.push(n); + } + } + return matches; } /** * Transforms a debug protection trigger literal by replacing the associated function * or value with a bypass string that satisfies obfuscator.io's validation tests. - * + * * This function handles two specific protection patterns: * 1. 'newState' - targets parent FunctionExpression nodes * 2. 'removeCookie' - targets parent property values - * + * * Algorithm: * 1. Identify the protection trigger type ('newState' or 'removeCookie') * 2. Navigate the AST structure to find the appropriate target node * 3. Replace the target with a literal containing the bypass string * 4. Mark the node for replacement in the Arborist instance - * + * * @param {Arborist} arb - Arborist instance containing the AST * @param {ASTNode} n - The Literal AST node containing the debug protection trigger * @return {Arborist} The modified Arborist instance */ export function obfuscatorIoTransform(arb, n) { - let targetNode; + let targetNode; - // Determine target node based on protection trigger type - switch (n.value) { - case 'newState': - // Navigate up to find the containing FunctionExpression - if (n.parentNode?.parentNode?.parentNode?.type === 'FunctionExpression') { - targetNode = n.parentNode.parentNode.parentNode; - } - break; - case 'removeCookie': - // Target the parent value directly - targetNode = n.parentNode?.value; - break; - } + // Determine target node based on protection trigger type + switch (n.value) { + case 'newState': + // Navigate up to find the containing FunctionExpression + if (n.parentNode?.parentNode?.parentNode?.type === 'FunctionExpression') { + targetNode = n.parentNode.parentNode.parentNode; + } + break; + case 'removeCookie': + // Target the parent value directly + targetNode = n.parentNode?.value; + break; + } - // Apply the bypass replacement if a valid target was found - if (targetNode) { - arb.markNode(targetNode, { - type: 'Literal', - value: FREEZE_REPLACEMENT_STRING, - raw: `"${FREEZE_REPLACEMENT_STRING}"`, - }); - } + // Apply the bypass replacement if a valid target was found + if (targetNode) { + arb.markNode(targetNode, { + type: 'Literal', + value: FREEZE_REPLACEMENT_STRING, + raw: `"${FREEZE_REPLACEMENT_STRING}"`, + }); + } - return arb; + return arb; } /** * Main function for obfuscator.io debug protection bypass. * Orchestrates the matching and transformation of debug protection mechanisms * to prevent infinite loops and allow deobfuscation to proceed. - * + * * @param {Arborist} arb - Arborist instance containing the AST * @param {Function} [candidateFilter=(() => true)] - Optional filter function for additional criteria * @return {Arborist} The modified Arborist instance */ function freezeUnbeautifiedValues(arb, candidateFilter = () => true) { - const matches = obfuscatorIoMatch(arb, candidateFilter); + const matches = obfuscatorIoMatch(arb, candidateFilter); - for (let i = 0; i < matches.length; i++) { - const n = matches[i]; - arb = obfuscatorIoTransform(arb, n); - } - return arb; + for (let i = 0; i < matches.length; i++) { + const n = matches[i]; + arb = obfuscatorIoTransform(arb, n); + } + return arb; } export const preprocessors = [freezeUnbeautifiedValues, ...augmentedArrayProcessors.preprocessors]; diff --git a/src/restringer.js b/src/restringer.js index a96986f..4c4fe67 100755 --- a/src/restringer.js +++ b/src/restringer.js @@ -8,123 +8,123 @@ import {readFileSync} from 'node:fs'; const __version__ = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf-8')).version; const safe = {}; for (const funcName in safeMod) { - safe[funcName] = safeMod[funcName].default || safeMod[funcName]; + safe[funcName] = safeMod[funcName].default || safeMod[funcName]; } const unsafe = {}; for (const funcName in unsafeMod) { - unsafe[funcName] = unsafeMod[funcName].default || unsafeMod[funcName]; + unsafe[funcName] = unsafeMod[funcName].default || unsafeMod[funcName]; } // Silence async errors // process.on('uncaughtException', () => {}); export class REstringer { - static __version__ = __version__; - logger = flastLogger; + static __version__ = __version__; + logger = flastLogger; - /** + /** * @param {string} script The target script to be deobfuscated * @param {boolean} [normalize] Run optional methods which will make the script more readable */ - constructor(script, normalize = true) { - this.script = script; - this.normalize = normalize; - this.modified = false; - this.obfuscationName = 'Generic'; - this._preprocessors = []; - this._postprocessors = []; - this.logger.setLogLevelLog(); - this.maxIterations = config.DEFAULT_MAX_ITERATIONS; - this.detectObfuscationType = true; - // Deobfuscation methods that don't use eval - this.safeMethods = [ - safe.rearrangeSequences, - safe.separateChainedDeclarators, - safe.rearrangeSwitches, - safe.normalizeEmptyStatements, - safe.removeRedundantBlockStatements, - safe.resolveRedundantLogicalExpressions, - safe.unwrapSimpleOperations, - safe.resolveProxyCalls, - safe.resolveProxyVariables, - safe.resolveProxyReferences, - safe.resolveMemberExpressionReferencesToArrayIndex, - safe.resolveMemberExpressionsWithDirectAssignment, - safe.parseTemplateLiteralsIntoStringLiterals, - safe.resolveDeterministicIfStatements, - safe.replaceCallExpressionsWithUnwrappedIdentifier, - safe.replaceEvalCallsWithLiteralContent, - safe.replaceIdentifierWithFixedAssignedValue, - safe.replaceIdentifierWithFixedValueNotAssignedAtDeclaration, - safe.replaceNewFuncCallsWithLiteralContent, - safe.replaceBooleanExpressionsWithIf, - safe.replaceSequencesWithExpressions, - safe.resolveFunctionConstructorCalls, - safe.replaceFunctionShellsWithWrappedValue, - safe.replaceFunctionShellsWithWrappedValueIIFE, - safe.simplifyCalls, - safe.unwrapFunctionShells, - safe.unwrapIIFEs, - safe.simplifyIfStatements, - ]; - // Deobfuscation methods that use eval - this.unsafeMethods = [ - unsafe.resolveMinimalAlphabet, - unsafe.resolveDefiniteBinaryExpressions, - unsafe.resolveAugmentedFunctionWrappedArrayReplacements, - unsafe.resolveMemberExpressionsLocalReferences, - unsafe.resolveDefiniteMemberExpressions, - unsafe.resolveBuiltinCalls, - unsafe.resolveDeterministicConditionalExpressions, - unsafe.resolveInjectedPrototypeMethodCalls, - unsafe.resolveLocalCalls, - unsafe.resolveEvalCallsOnNonLiterals, - ]; - } + constructor(script, normalize = true) { + this.script = script; + this.normalize = normalize; + this.modified = false; + this.obfuscationName = 'Generic'; + this._preprocessors = []; + this._postprocessors = []; + this.logger.setLogLevelLog(); + this.maxIterations = config.DEFAULT_MAX_ITERATIONS; + this.detectObfuscationType = true; + // Deobfuscation methods that don't use eval + this.safeMethods = [ + safe.rearrangeSequences, + safe.separateChainedDeclarators, + safe.rearrangeSwitches, + safe.normalizeEmptyStatements, + safe.removeRedundantBlockStatements, + safe.resolveRedundantLogicalExpressions, + safe.unwrapSimpleOperations, + safe.resolveProxyCalls, + safe.resolveProxyVariables, + safe.resolveProxyReferences, + safe.resolveMemberExpressionReferencesToArrayIndex, + safe.resolveMemberExpressionsWithDirectAssignment, + safe.parseTemplateLiteralsIntoStringLiterals, + safe.resolveDeterministicIfStatements, + safe.replaceCallExpressionsWithUnwrappedIdentifier, + safe.replaceEvalCallsWithLiteralContent, + safe.replaceIdentifierWithFixedAssignedValue, + safe.replaceIdentifierWithFixedValueNotAssignedAtDeclaration, + safe.replaceNewFuncCallsWithLiteralContent, + safe.replaceBooleanExpressionsWithIf, + safe.replaceSequencesWithExpressions, + safe.resolveFunctionConstructorCalls, + safe.replaceFunctionShellsWithWrappedValue, + safe.replaceFunctionShellsWithWrappedValueIIFE, + safe.simplifyCalls, + safe.unwrapFunctionShells, + safe.unwrapIIFEs, + safe.simplifyIfStatements, + ]; + // Deobfuscation methods that use eval + this.unsafeMethods = [ + unsafe.resolveMinimalAlphabet, + unsafe.resolveDefiniteBinaryExpressions, + unsafe.resolveAugmentedFunctionWrappedArrayReplacements, + unsafe.resolveMemberExpressionsLocalReferences, + unsafe.resolveDefiniteMemberExpressions, + unsafe.resolveBuiltinCalls, + unsafe.resolveDeterministicConditionalExpressions, + unsafe.resolveInjectedPrototypeMethodCalls, + unsafe.resolveLocalCalls, + unsafe.resolveEvalCallsOnNonLiterals, + ]; + } - /** + /** * Determine the type of the obfuscation, and populate the appropriate pre- and post- processors. */ - determineObfuscationType() { - const detectedObfuscationType = detectObfuscation(this.script, false).slice(-1)[0]; - if (detectedObfuscationType) { - this.obfuscationName = detectedObfuscationType; - if (processors[detectedObfuscationType]) { - ({preprocessors: this._preprocessors, postprocessors: this._postprocessors} = processors[detectedObfuscationType]); - } - } - this.logger.log(`[+] Obfuscation type is ${this.obfuscationName}`); - return this.obfuscationName; - } + determineObfuscationType() { + const detectedObfuscationType = detectObfuscation(this.script, false).slice(-1)[0]; + if (detectedObfuscationType) { + this.obfuscationName = detectedObfuscationType; + if (processors[detectedObfuscationType]) { + ({preprocessors: this._preprocessors, postprocessors: this._postprocessors} = processors[detectedObfuscationType]); + } + } + this.logger.log(`[+] Obfuscation type is ${this.obfuscationName}`); + return this.obfuscationName; + } - /** + /** * Iteratively applies safe and unsafe deobfuscation methods until no further changes occur. - * + * * Algorithm per iteration: * 1. Apply all safe methods repeatedly until they stop making changes (up to maxIterations) * 2. Apply all unsafe methods exactly once (they may be overreaching, so limited to 1 iteration) * 3. Repeat the entire process until no changes occur in either phase - * + * * This approach maximizes safe deobfuscation before using potentially risky eval-based methods, * while allowing unsafe methods to expose new opportunities for safe methods in subsequent iterations. */ - _loopSafeAndUnsafeDeobfuscationMethods() { - // Track whether any iteration made changes (vs this.modified which tracks current iteration only) - let wasEverModified, script; - do { - this.modified = false; - script = applyIteratively(this.script, this.safeMethods, this.maxIterations); - script = applyIteratively(script, this.unsafeMethods, 1); - if (this.script !== script) { - this.modified = true; - this.script = script; - } - if (this.modified) wasEverModified = true; - } while (this.modified); // Run this loop until the deobfuscation methods stop being effective. - this.modified = wasEverModified; - } + _loopSafeAndUnsafeDeobfuscationMethods() { + // Track whether any iteration made changes (vs this.modified which tracks current iteration only) + let wasEverModified, script; + do { + this.modified = false; + script = applyIteratively(this.script, this.safeMethods, this.maxIterations); + script = applyIteratively(script, this.unsafeMethods, 1); + if (this.script !== script) { + this.modified = true; + this.script = script; + } + if (this.modified) wasEverModified = true; + } while (this.modified); // Run this loop until the deobfuscation methods stop being effective. + this.modified = wasEverModified; + } - /** + /** * Entry point for this class. * Determine obfuscation type and run the pre- and post- processors accordingly. * Run the deobfuscation methods in a loop until nothing more is changed. @@ -132,25 +132,25 @@ export class REstringer { * @param {boolean} [clean] Remove dead nodes after deobfuscation. Defaults to false. * @return {boolean} true if the script was modified during deobfuscation; false otherwise. */ - deobfuscate(clean = false) { - if (this.detectObfuscationType) this.determineObfuscationType(); - this._runProcessors(this._preprocessors); - this._loopSafeAndUnsafeDeobfuscationMethods(); - this._runProcessors(this._postprocessors); - if (this.modified && this.normalize) this.script = normalizeScript(this.script); - if (clean) this.script = applyIteratively(this.script, [safe.removeDeadNodes], this.maxIterations); - return this.modified; - } + deobfuscate(clean = false) { + if (this.detectObfuscationType) this.determineObfuscationType(); + this._runProcessors(this._preprocessors); + this._loopSafeAndUnsafeDeobfuscationMethods(); + this._runProcessors(this._postprocessors); + if (this.modified && this.normalize) this.script = normalizeScript(this.script); + if (clean) this.script = applyIteratively(this.script, [safe.removeDeadNodes], this.maxIterations); + return this.modified; + } - /** + /** * Run specific deobfuscation which must run before or after the main deobfuscation loop * in order to successfully complete deobfuscation. * @param {Array} processors An array of either imported deobfuscation methods or the name of internal methods. */ - _runProcessors(processors) { - for (let i = 0; i < processors.length; i++) { - const processor = processors[i]; - this.script = applyIteratively(this.script, [processor], 1); - } - } + _runProcessors(processors) { + for (let i = 0; i < processors.length; i++) { + const processor = processors[i]; + this.script = applyIteratively(this.script, [processor], 1); + } + } } \ No newline at end of file diff --git a/src/utils/parseArgs.js b/src/utils/parseArgs.js index dbe4a49..7cb6dd3 100644 --- a/src/utils/parseArgs.js +++ b/src/utils/parseArgs.js @@ -3,33 +3,33 @@ import {Command} from 'commander'; /** * Pre-processes arguments to handle short option `=` syntax that Commander.js doesn't support. * Commander.js supports `--long-option=value` but not `-o=value`, so we only need to handle short options. - * + * * @param {string[]} args - Original command line arguments * @return {string[]} Processed arguments compatible with Commander.js */ function preprocessShortOptionsWithEquals(args) { - const processed = []; - - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - // Handle short options with = syntax: -o=value, -m=value - if (arg.startsWith('-') && !arg.startsWith('--') && arg.includes('=')) { - const equalIndex = arg.indexOf('='); - const flag = arg.substring(0, equalIndex); - const value = arg.substring(equalIndex + 1); - processed.push(flag, value); - } - // All other arguments pass through unchanged (including --long=value which Commander.js handles) - else processed.push(arg); - } - - return processed; + const processed = []; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + // Handle short options with = syntax: -o=value, -m=value + if (arg.startsWith('-') && !arg.startsWith('--') && arg.includes('=')) { + const equalIndex = arg.indexOf('='); + const flag = arg.substring(0, equalIndex); + const value = arg.substring(equalIndex + 1); + processed.push(flag, value); + } + // All other arguments pass through unchanged (including --long=value which Commander.js handles) + else processed.push(arg); + } + + return processed; } /** * Parses command line arguments into a structured options object using Commander.js. - * + * * @param {string[]} args - Array of command line arguments (typically process.argv.slice(2)) * @return {Object} Parsed options object with the following structure: * @return {string} return.inputFilename - Path to input JavaScript file @@ -42,125 +42,125 @@ function preprocessShortOptionsWithEquals(args) { * @return {string} return.outputFilename - Output filename (auto-generated or user-specified) */ export function parseArgs(args) { - // Input validation - handle edge cases gracefully - if (!args || !Array.isArray(args)) { - return createDefaultOptions(''); - } - - try { - // Pre-process to handle short option `=` syntax (e.g., -o=file.js, -m=2) - const processedArgs = preprocessShortOptionsWithEquals(args); - - const program = new Command(); - - // Configure the command with options and validation - program - .name('restringer') - .version('2.0.8', '-V, --version', 'Show version number and exit') - .description('REstringer - a JavaScript deobfuscator') - .allowUnknownOption(false) - .exitOverride() // Prevent Commander from calling process.exit() - .argument('[input_filename]', 'The obfuscated JS file') - .option('-c, --clean', 'Remove dead nodes from script after deobfuscation is complete (unsafe)') - .option('-q, --quiet', 'Suppress output to stdout. Output result only to stdout if the -o option is not set') - .option('-v, --verbose', 'Show more debug messages while deobfuscating') - .option('-o, --output [filename]', 'Write deobfuscated script to output_filename. -deob.js is used if no filename is provided') - .option('-m, --max-iterations ', 'Run at most M iterations', (value) => { - const parsed = parseInt(value, 10); - if (isNaN(parsed) || parsed <= 0) { - throw new Error('max-iterations must be a positive number'); - } - return parsed; - }); - - // Add mutually exclusive validation using preAction hook - program.hook('preAction', (thisCommand) => { - const options = thisCommand.opts(); - if (options.verbose && options.quiet) { - throw new Error('Don\'t set both -q and -v at the same time *smh*'); - } - }); - - // Check if help is requested first, then parse without help to get all options - const hasHelp = processedArgs.includes('-h') || processedArgs.includes('--help'); - - // If help is requested, parse without the help flag to get all other options - let argsToProcess = processedArgs; - if (hasHelp) { - argsToProcess = processedArgs.filter(arg => arg !== '-h' && arg !== '--help'); - } - - // Parse arguments and handle potential errors - try { - program.parse(argsToProcess, { from: 'user' }); - } catch (error) { - // Handle parsing errors (like invalid max-iterations value) - if (error.code === 'commander.helpDisplayed' || error.code === 'commander.version') { - // Help or version was displayed, return with help flag set - return { ...createDefaultOptions(''), help: true }; - } - // For other errors (like invalid max-iterations), set maxIterations to null - const opts = createDefaultOptions(''); - if (error.message.includes('max-iterations')) { - opts.maxIterations = null; - } - return opts; - } - - const options = program.opts(); - const inputFilename = program.args[0] || ''; - - // Create the return object matching the original API - const opts = createDefaultOptions(inputFilename); - - // Map Commander.js options to our expected format - opts.help = hasHelp; - opts.clean = !!options.clean; - opts.quiet = !!options.quiet; - opts.verbose = !!options.verbose; - - // Handle output option - if (options.output !== undefined) { - opts.outputToFile = true; - if (typeof options.output === 'string' && options.output.length > 0) { - opts.outputFilename = options.output; - } - } - - // Handle max-iterations option - if (options.maxIterations !== undefined) { - opts.maxIterations = options.maxIterations; - } - - // Validate required input filename (unless help is requested) - if (!hasHelp && (!opts.inputFilename || opts.inputFilename.length === 0)) { - throw new Error('missing required argument \'input_filename\''); - } - - return opts; - } catch (error) { - // Provide meaningful error context instead of silent failure - console.warn(`Warning: Error parsing arguments, using defaults. Error: ${error.message}`); - return createDefaultOptions(''); - } + // Input validation - handle edge cases gracefully + if (!args || !Array.isArray(args)) { + return createDefaultOptions(''); + } + + try { + // Pre-process to handle short option `=` syntax (e.g., -o=file.js, -m=2) + const processedArgs = preprocessShortOptionsWithEquals(args); + + const program = new Command(); + + // Configure the command with options and validation + program + .name('restringer') + .version('2.0.8', '-V, --version', 'Show version number and exit') + .description('REstringer - a JavaScript deobfuscator') + .allowUnknownOption(false) + .exitOverride() // Prevent Commander from calling process.exit() + .argument('[input_filename]', 'The obfuscated JS file') + .option('-c, --clean', 'Remove dead nodes from script after deobfuscation is complete (unsafe)') + .option('-q, --quiet', 'Suppress output to stdout. Output result only to stdout if the -o option is not set') + .option('-v, --verbose', 'Show more debug messages while deobfuscating') + .option('-o, --output [filename]', 'Write deobfuscated script to output_filename. -deob.js is used if no filename is provided') + .option('-m, --max-iterations ', 'Run at most M iterations', (value) => { + const parsed = parseInt(value, 10); + if (isNaN(parsed) || parsed <= 0) { + throw new Error('max-iterations must be a positive number'); + } + return parsed; + }); + + // Add mutually exclusive validation using preAction hook + program.hook('preAction', (thisCommand) => { + const options = thisCommand.opts(); + if (options.verbose && options.quiet) { + throw new Error('Don\'t set both -q and -v at the same time *smh*'); + } + }); + + // Check if help is requested first, then parse without help to get all options + const hasHelp = processedArgs.includes('-h') || processedArgs.includes('--help'); + + // If help is requested, parse without the help flag to get all other options + let argsToProcess = processedArgs; + if (hasHelp) { + argsToProcess = processedArgs.filter(arg => arg !== '-h' && arg !== '--help'); + } + + // Parse arguments and handle potential errors + try { + program.parse(argsToProcess, {from: 'user'}); + } catch (error) { + // Handle parsing errors (like invalid max-iterations value) + if (error.code === 'commander.helpDisplayed' || error.code === 'commander.version') { + // Help or version was displayed, return with help flag set + return {...createDefaultOptions(''), help: true}; + } + // For other errors (like invalid max-iterations), set maxIterations to null + const opts = createDefaultOptions(''); + if (error.message.includes('max-iterations')) { + opts.maxIterations = null; + } + return opts; + } + + const options = program.opts(); + const inputFilename = program.args[0] || ''; + + // Create the return object matching the original API + const opts = createDefaultOptions(inputFilename); + + // Map Commander.js options to our expected format + opts.help = hasHelp; + opts.clean = !!options.clean; + opts.quiet = !!options.quiet; + opts.verbose = !!options.verbose; + + // Handle output option + if (options.output !== undefined) { + opts.outputToFile = true; + if (typeof options.output === 'string' && options.output.length > 0) { + opts.outputFilename = options.output; + } + } + + // Handle max-iterations option + if (options.maxIterations !== undefined) { + opts.maxIterations = options.maxIterations; + } + + // Validate required input filename (unless help is requested) + if (!hasHelp && (!opts.inputFilename || opts.inputFilename.length === 0)) { + throw new Error('missing required argument \'input_filename\''); + } + + return opts; + } catch (error) { + // Provide meaningful error context instead of silent failure + console.warn(`Warning: Error parsing arguments, using defaults. Error: ${error.message}`); + return createDefaultOptions(''); + } } /** * Creates a default options object with safe fallback values. * This helper ensures consistent default behavior and reduces code duplication. - * + * * @param {string} inputFilename - The input filename to use for generating output filename * @return {Object} Default options object with all required properties */ function createDefaultOptions(inputFilename) { - return { - inputFilename, - help: false, - clean: false, - quiet: false, - verbose: false, - outputToFile: false, - maxIterations: false, - outputFilename: inputFilename ? `${inputFilename}-deob.js` : '-deob.js', - }; + return { + inputFilename, + help: false, + clean: false, + quiet: false, + verbose: false, + outputToFile: false, + maxIterations: false, + outputFilename: inputFilename ? `${inputFilename}-deob.js` : '-deob.js', + }; } diff --git a/tests/deobfuscation.test.js b/tests/deobfuscation.test.js index 169d915..21591ad 100644 --- a/tests/deobfuscation.test.js +++ b/tests/deobfuscation.test.js @@ -3,15 +3,15 @@ import assert from 'node:assert'; import {describe, it} from 'node:test'; function getDeobfuscatedCode(code) { - const restringer = new REstringer(code); - restringer.logger.setLogLevelNone(); - restringer.deobfuscate(); - return restringer.script; + const restringer = new REstringer(code); + restringer.logger.setLogLevelNone(); + restringer.deobfuscate(); + return restringer.script; } describe('Deobfuscation tests', () => { - it('Augmented Array Replacements', () => { - const code = `const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'a', 'b', 'c']; + it('Augmented Array Replacements', () => { + const code = `const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'a', 'b', 'c']; (function(targetArray, numberOfShifts) { var augmentArray = function(counter) { while (--counter) { @@ -21,7 +21,7 @@ describe('Deobfuscation tests', () => { augmentArray(++numberOfShifts); })(arr, 3); console.log(arr[7], arr[8]);`; - const expected = `const arr = [ + const expected = `const arr = [ 4, 5, 6, @@ -37,133 +37,133 @@ console.log(arr[7], arr[8]);`; 3 ]; console.log('a', 'b');`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it('Compute definite binary expressions', () => { - const code = `"2" + 3 - "5" * 0 + "1"`; - const expected = `'231';`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Don't replace modified member expressions`, () => { - const code = `var l = []; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Compute definite binary expressions', () => { + const code = '"2" + 3 - "5" * 0 + "1"'; + const expected = '\'231\';'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Don\'t replace modified member expressions', () => { + const code = `var l = []; const b = l.length * 2; const c = l.length + 1; var v = l[b]; l[b] = l[c]; l[c] = v;`; - const expected = code; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Don't replace member expressions on empty arrays`, () => { - const code = `const a = []; a.push(3); console.log(a, a.length);`; - const expected = `const a = []; a.push(3); console.log(a, a.length);`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it.skip(`TODO: Fix. Normalize Script Correctly`, () => { - const code = `"\x22" + "\x20" + "\x5c\x5c" + "\x0a" + "\b" + "\x09" + "\x0d" + "\u0000";`; - const expected = `'" \\\\\\\\\\n\\b\\t\\r\x00';`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Parse template literals into string literals`, () => { - const code = 'console.log(`https://${"url"}.${"com"}/`);'; - const expected = `console.log('https://url.com/');`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Remove nested block statements`, () => { - const code = `{{freeNested;}} {{{freeNested2}}}`; - const expected = `freeNested;\nfreeNested2;`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Remove redundant logical expressions`, () => { - const code = `if (true || 0) do_a(); else do_b(); if (false && 1) do_c(); else do_d();`; - const expected = `do_a();\ndo_d();`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Remove redundant not operators`, () => { - const code = `const a = !true; const b = !!!false;`; - const expected = `const a = false;\nconst b = true;`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace augmented function with corrected array`, () => { - const code = `(function(a, b){const myArr=a();for(let i=0;i { - const code = `if(true){a;}if(false){b}if(false||c){c}if(true&&d){d}`; - const expected = `a;\nif (c) {\n c;\n}\nif (d) {\n d;\n}`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace function calls with unwrapped identifier - arrow functions`, () => { - const code = `const x = () => String; x().fromCharCode(97);`; - const expected = `const x = () => String;\n'a';`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace function calls with unwrapped identifier - function declarations`, () => { - const code = `function x() {return String}\nx().fromCharCode(97);`; - const expected = `function x() { + const expected = code; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Don\'t replace member expressions on empty arrays', () => { + const code = 'const a = []; a.push(3); console.log(a, a.length);'; + const expected = 'const a = []; a.push(3); console.log(a, a.length);'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it.skip('TODO: Fix. Normalize Script Correctly', () => { + const code = '"\x22" + "\x20" + "\x5c\x5c" + "\x0a" + "\b" + "\x09" + "\x0d" + "\u0000";'; + const expected = '\'" \\\\\\\\\\n\\b\\t\\r\x00\';'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Parse template literals into string literals', () => { + const code = 'console.log(`https://${"url"}.${"com"}/`);'; + const expected = 'console.log(\'https://url.com/\');'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Remove nested block statements', () => { + const code = '{{freeNested;}} {{{freeNested2}}}'; + const expected = 'freeNested;\nfreeNested2;'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Remove redundant logical expressions', () => { + const code = 'if (true || 0) do_a(); else do_b(); if (false && 1) do_c(); else do_d();'; + const expected = 'do_a();\ndo_d();'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Remove redundant not operators', () => { + const code = 'const a = !true; const b = !!!false;'; + const expected = 'const a = false;\nconst b = true;'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace augmented function with corrected array', () => { + const code = '(function(a, b){const myArr=a();for(let i=0;i { + const code = 'if(true){a;}if(false){b}if(false||c){c}if(true&&d){d}'; + const expected = 'a;\nif (c) {\n c;\n}\nif (d) {\n d;\n}'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace function calls with unwrapped identifier - arrow functions', () => { + const code = 'const x = () => String; x().fromCharCode(97);'; + const expected = 'const x = () => String;\n\'a\';'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace function calls with unwrapped identifier - function declarations', () => { + const code = 'function x() {return String}\nx().fromCharCode(97);'; + const expected = `function x() { return String; } 'a';`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace function evals - eval(string)`, () => { - const code = `eval("console.log");`; - const expected = `console.log;`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace function evals in call expressions - eval(string)(args)`, () => { - const code = `eval("atob")("c3VjY2Vzcw==");`; - const expected = `'success';`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace identifier with fixed assigned value`, () => { - const code = `const a = 'value'; function v(arg) {console.log(a, a[0], a.indexOf('e'));}`; - const expected = `const a = 'value'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace function evals - eval(string)', () => { + const code = 'eval("console.log");'; + const expected = 'console.log;'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace function evals in call expressions - eval(string)(args)', () => { + const code = 'eval("atob")("c3VjY2Vzcw==");'; + const expected = '\'success\';'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace identifier with fixed assigned value', () => { + const code = 'const a = \'value\'; function v(arg) {console.log(a, a[0], a.indexOf(\'e\'));}'; + const expected = `const a = 'value'; function v(arg) { console.log('value', 'v', 4); }`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace literal proxies`, () => { - const code = `const b='hello'; console.log(b + ' world');`; - const expected = `const b = 'hello';\nconsole.log('hello world');`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace local calls proxy - arrow functions`, () => { - const code = `const a = n => ['hello', 'world'][n]; function c() {const b = a; return b(0) + ' ' + b(1);}`; - const expected = `const a = n => [ + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace literal proxies', () => { + const code = 'const b=\'hello\'; console.log(b + \' world\');'; + const expected = 'const b = \'hello\';\nconsole.log(\'hello world\');'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace local calls proxy - arrow functions', () => { + const code = 'const a = n => [\'hello\', \'world\'][n]; function c() {const b = a; return b(0) + \' \' + b(1);}'; + const expected = `const a = n => [ 'hello', 'world' ][n]; function c() { return 'hello world'; }`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it.skip(`TODO: FIX Replace local calls proxy - function declarations`, () => { - // TODO: For some reason running this test sometimes breaks isolated-vm with the error: - // Assertion failed: (environment != nullptr), function GetCurrent, file environment.h, line 202. - const code = `function a(n) { return ['hello', 'world'][n]; } function c() {const b = a; return b(0) + ' ' + b(1);}`; - const expected = `function a(n) { + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it.skip('TODO: FIX Replace local calls proxy - function declarations', () => { + // TODO: For some reason running this test sometimes breaks isolated-vm with the error: + // Assertion failed: (environment != nullptr), function GetCurrent, file environment.h, line 202. + const code = 'function a(n) { return [\'hello\', \'world\'][n]; } function c() {const b = a; return b(0) + \' \' + b(1);}'; + const expected = `function a(n) { return [ 'hello', 'world' @@ -172,26 +172,26 @@ function c() { function c() { return 'hello world'; }`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace local calls with values - immediate`, () => { - const code = `function localCall() {return 'value'} localCall()`; - const expected = `function localCall() { + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace local calls with values - immediate', () => { + const code = 'function localCall() {return \'value\'} localCall()'; + const expected = `function localCall() { return 'value'; } 'value';`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace local calls with values - nested`, () => { - const code = `const three = 3; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace local calls with values - nested', () => { + const code = `const three = 3; const one = 1; function a() {return three;} function b() {return one;} function c(a1, b1) {return a1 + b1} c(a(), b());`; - const expected = `const three = 3; + const expected = `const three = 3; const one = 1; function a() { return 3; @@ -203,90 +203,90 @@ function c(a1, b1) { return a1 + b1; } 4;`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace local member expressions property reference with value`, () => { - const code = `const a = {a: "hello "}; a.b = "world"; console.log(a.a + a.b);`; - const expected = `const a = { a: 'hello ' }; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace local member expressions property reference with value', () => { + const code = 'const a = {a: "hello "}; a.b = "world"; console.log(a.a + a.b);'; + const expected = `const a = { a: 'hello ' }; a.b = 'world'; console.log('hello world');`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace local member expressions proxy - chained proxies`, () => { - const code = `const a = ["hello"], b = a[0], c = b; console.log(c);`; - const expected = `const a = ['hello'];\nconst b = 'hello';\nconst c = 'hello';\nconsole.log('hello');`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace local member expressions proxy - member assignment`, () => { - const code = `const a = ["hello"], b = a[0];`; - const expected = `const a = ['hello'];\nconst b = 'hello';`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace member expression references with values`, () => { - const code = `const a = ["hello", " ", "world"]; console.log(a[0] + a[1] + a[2]);`; - const expected = `const a = [ + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace local member expressions proxy - chained proxies', () => { + const code = 'const a = ["hello"], b = a[0], c = b; console.log(c);'; + const expected = 'const a = [\'hello\'];\nconst b = \'hello\';\nconst c = \'hello\';\nconsole.log(\'hello\');'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace local member expressions proxy - member assignment', () => { + const code = 'const a = ["hello"], b = a[0];'; + const expected = 'const a = [\'hello\'];\nconst b = \'hello\';'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace member expression references with values', () => { + const code = 'const a = ["hello", " ", "world"]; console.log(a[0] + a[1] + a[2]);'; + const expected = `const a = [ 'hello', ' ', 'world' ]; console.log('hello world');`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace reference proxy`, () => { - const code = `const a = ['hello', ' world'], b = a[0], c = a; console.log(b + c[1]);`; - const expected = `const a = [ + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace reference proxy', () => { + const code = 'const a = [\'hello\', \' world\'], b = a[0], c = a; console.log(b + c[1]);'; + const expected = `const a = [ 'hello', ' world' ]; const b = 'hello'; console.log('hello world');`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Replace wrapped functions with return statement`, () => { - const code = `function A(a,b){return function() {return a+b;}.apply(this, arguments);}`; - const expected = `function A(a, b) { + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Replace wrapped functions with return statement', () => { + const code = 'function A(a,b){return function() {return a+b;}.apply(this, arguments);}'; + const expected = `function A(a, b) { return a + b; }`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Resolve builtin call expressions: btoa & atob`, () => { - const code = `atob('dGVzdA=='); btoa('test');`; - const expected = `'test';\n'dGVzdA==';`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Resolve definite member expressions`, () => { - const code = `'1234567890'[3]`; - const expected = `'4';`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Resolve deterministic conditional expressions`, () => { - const code = `(true ? 'o' : 'x') + (false ? 'X' : 'k');`; - const expected = `'ok';`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Resolve directly assigned member expressions`, () => { - const code = `function a() {} a.b = 3; a.c = '5'; console.log(a.b + a.c);`; - const expected = `function a() { + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Resolve builtin call expressions: btoa & atob', () => { + const code = 'atob(\'dGVzdA==\'); btoa(\'test\');'; + const expected = '\'test\';\n\'dGVzdA==\';'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Resolve definite member expressions', () => { + const code = '\'1234567890\'[3]'; + const expected = '\'4\';'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Resolve deterministic conditional expressions', () => { + const code = '(true ? \'o\' : \'x\') + (false ? \'X\' : \'k\');'; + const expected = '\'ok\';'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Resolve directly assigned member expressions', () => { + const code = 'function a() {} a.b = 3; a.c = \'5\'; console.log(a.b + a.c);'; + const expected = `function a() { } a.b = 3; a.c = '5'; console.log('35');`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Resolve external references with context`, () => { - const code = `const a = [1, 2, 3]; (function(arr) {arr.forEach((x, i, arr) => arr[i] = x * 10)})(a); function b() {const c = [...a]; return c[0] + 3;}`; - const expected = `const a = [ + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Resolve external references with context', () => { + const code = 'const a = [1, 2, 3]; (function(arr) {arr.forEach((x, i, arr) => arr[i] = x * 10)})(a); function b() {const c = [...a]; return c[0] + 3;}'; + const expected = `const a = [ 1, 2, 3 @@ -298,12 +298,12 @@ function b() { const c = [...a]; return 13; }`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Resolve function constructor calls`, () => { - const code = `function a() {} const b = a.constructor('', "return a()"); const c = b.constructor('a', 'b', 'return a + b');`; - const expected = `function a() { + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Resolve function constructor calls', () => { + const code = 'function a() {} const b = a.constructor(\'\', "return a()"); const c = b.constructor(\'a\', \'b\', \'return a + b\');'; + const expected = `function a() { } const b = function () { return a(); @@ -311,27 +311,27 @@ const b = function () { const c = function (a, b) { return a + b; };`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Resolve injected prototype method calls`, () => { - const code = `String.prototype.secret = function() {return 'secret ' + this}; 'hello'.secret();`; - const expected = `String.prototype.secret = function () { + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Resolve injected prototype method calls', () => { + const code = 'String.prototype.secret = function() {return \'secret \' + this}; \'hello\'.secret();'; + const expected = `String.prototype.secret = function () { return 'secret ' + this; }; 'secret hello';`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Resolve member expression local references with unary expressions correctly`, () => { - const code = `const a = ['-example', '-3', '-Infinity']; a[0]; a[1]; a[2];`; - const expected = `const a = [\n '-example',\n '-3',\n '-Infinity'\n];\n'-example';\n-'3';\n-Infinity;`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Resolve member expression references with context`, () => { - const code = `const a = [1, 2, 3]; (function(arr) {arr.forEach((x, i, arr) => arr[i] = x * 3)})(a); const b = a[0];`; - const expected = `const a = [ + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Resolve member expression local references with unary expressions correctly', () => { + const code = 'const a = [\'-example\', \'-3\', \'-Infinity\']; a[0]; a[1]; a[2];'; + const expected = 'const a = [\n \'-example\',\n \'-3\',\n \'-Infinity\'\n];\n\'-example\';\n-\'3\';\n-Infinity;'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Resolve member expression references with context', () => { + const code = 'const a = [1, 2, 3]; (function(arr) {arr.forEach((x, i, arr) => arr[i] = x * 3)})(a); const b = a[0];'; + const expected = `const a = [ 1, 2, 3 @@ -340,50 +340,50 @@ const c = function (a, b) { arr.forEach((x, i, arr) => arr[i] = x * 3); }(a)); const b = 3;`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Unwrap function shells`, () => { - const code = `function O() {return function () {return clearInterval;}.apply(this, arguments);}`; - const expected = `function O() { + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Unwrap function shells', () => { + const code = 'function O() {return function () {return clearInterval;}.apply(this, arguments);}'; + const expected = `function O() { return clearInterval; }`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Verify correct context for function declaration`, () => { - const code = `function a(v) {return v + '4'}; if (a(0)) {console.log(a(18));}`; - const expected = `function a(v) {\n return v + '4';\n}\nconsole.log('184');`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Verify correct context for function variable`, () => { - const code = `let a = function (v) {return v + '4'}; if (a(0)) {console.log(a(18));}`; - const expected = `let a = function (v) {\n return v + '4';\n};\nconsole.log('184');`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Verify correct replacement of member expressions with literals`, () => { - const code = `const n = 3, b = 'B'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Verify correct context for function declaration', () => { + const code = 'function a(v) {return v + \'4\'}; if (a(0)) {console.log(a(18));}'; + const expected = 'function a(v) {\n return v + \'4\';\n}\nconsole.log(\'184\');'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Verify correct context for function variable', () => { + const code = 'let a = function (v) {return v + \'4\'}; if (a(0)) {console.log(a(18));}'; + const expected = 'let a = function (v) {\n return v + \'4\';\n};\nconsole.log(\'184\');'; + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Verify correct replacement of member expressions with literals', () => { + const code = `const n = 3, b = 'B'; const a = {b: 'hello'}; a.n = 15; console.log(a.n, a.b);`; - const expected = `const n = 3; + const expected = `const n = 3; const b = 'B'; const a = { b: 'hello' }; a.n = 15; console.log(15, 'hello');`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it(`Verify random values remain untouched`, () => { - const code = `const a = new Date(); const b = Date.now(); const c = Math.random(); const d = 4; console.log(a + b + c + d);`; - const expected = `const a = new Date(); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Verify random values remain untouched', () => { + const code = 'const a = new Date(); const b = Date.now(); const c = Math.random(); const d = 4; console.log(a + b + c + d);'; + const expected = `const a = new Date(); const b = Date.now(); const c = Math.random(); const d = 4; console.log(a + b + c + 4);`; - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); }); diff --git a/tests/functionality.test.js b/tests/functionality.test.js index 0203107..da7e2bf 100644 --- a/tests/functionality.test.js +++ b/tests/functionality.test.js @@ -2,17 +2,16 @@ import assert from 'node:assert'; import {describe, it} from 'node:test'; import {REstringer} from '../src/restringer.js'; - describe('Functionality tests', () => { - it('Set max iterations', () => { - const code = `eval('eval("eval(3)")')`; - const restringer = new REstringer(code); - restringer.logger.setLogLevelNone(); - restringer.maxIterations.value = 3; - restringer.deobfuscate(); - assert.strictEqual(restringer.script, 'eval(3);'); - }); - it('REstringer.__version__ is populated', () => { - assert.ok(REstringer.__version__); - }); + it('Set max iterations', () => { + const code = 'eval(\'eval("eval(3)")\')'; + const restringer = new REstringer(code); + restringer.logger.setLogLevelNone(); + restringer.maxIterations.value = 3; + restringer.deobfuscate(); + assert.strictEqual(restringer.script, 'eval(3);'); + }); + it('REstringer.__version__ is populated', () => { + assert.ok(REstringer.__version__); + }); }); diff --git a/tests/modules.unsafe.test.js b/tests/modules.unsafe.test.js index 42715ab..bd77ec6 100644 --- a/tests/modules.unsafe.test.js +++ b/tests/modules.unsafe.test.js @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars */ + import assert from 'node:assert'; import {describe, it} from 'node:test'; import {Arborist, applyIteratively, generateFlatAST} from 'flast'; @@ -11,158 +11,158 @@ import {Arborist, applyIteratively, generateFlatAST} from 'flast'; * @return {string} The result of the operation */ function applyModuleToCode(code, func, looped = false) { - let result; - if (looped) { - result = applyIteratively(code, [func]); - } else { - const arb = new Arborist(code); - result = func(arb); - result.applyChanges(); - result = result.script; - } - return result; + let result; + if (looped) { + result = applyIteratively(code, [func]); + } else { + const arb = new Arborist(code); + result = func(arb); + result.applyChanges(); + result = result.script; + } + return result; } describe('UNSAFE: normalizeRedundantNotOperator', async () => { - const targetModule = (await import('../src/modules/unsafe/normalizeRedundantNotOperator.js')).default; - it('TP-1: Mixed literals and expressions', () => { - const code = `!true || !false || !0 || !1 || !a || !'a' || ![] || !{} || !-1 || !!true || !!!true`; - const expected = `false || true || true || false || !a || false || false || false || false || true || false;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: String literals', () => { - const code = `!'' || !'hello' || !'0' || !' '`; - const expected = `true || false || false || false;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Number literals', () => { - const code = `!42 || !-42 || !0.5 || !-0.5`; - const expected = `false || false || false || false;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Null literal', () => { - const code = `!null`; - const expected = `true;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-5: Empty array and object literals', () => { - const code = `!{} || ![]`; - const expected = `false || false;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-6: Simple nested NOT operations', () => { - const code = `!!false || !!true`; - const expected = `false || true;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-7: Normalize complex literals that can be safely evaluated', () => { - const code = `!undefined || ![1,2,3] || !{a:1}`; - const expected = `true || false || false;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Do not normalize NOT on variables', () => { - const code = `!variable || !obj.prop || !func()`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Do not normalize NOT on complex expressions', () => { - const code = `!(a + b) || !(x > y) || !(z && w)`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Do not normalize NOT on function calls', () => { - const code = `!getValue() || !Math.random() || !Array.isArray(arr)`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Do not normalize NOT on computed properties', () => { - const code = `!obj[key] || !arr[0] || !matrix[i][j]`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Do not normalize literals with unpredictable values', () => { - const code = `!Infinity || !-Infinity`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/unsafe/normalizeRedundantNotOperator.js')).default; + it('TP-1: Mixed literals and expressions', () => { + const code = '!true || !false || !0 || !1 || !a || !\'a\' || ![] || !{} || !-1 || !!true || !!!true'; + const expected = 'false || true || true || false || !a || false || false || false || false || true || false;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: String literals', () => { + const code = '!\'\' || !\'hello\' || !\'0\' || !\' \''; + const expected = 'true || false || false || false;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Number literals', () => { + const code = '!42 || !-42 || !0.5 || !-0.5'; + const expected = 'false || false || false || false;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Null literal', () => { + const code = '!null'; + const expected = 'true;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-5: Empty array and object literals', () => { + const code = '!{} || ![]'; + const expected = 'false || false;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-6: Simple nested NOT operations', () => { + const code = '!!false || !!true'; + const expected = 'false || true;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-7: Normalize complex literals that can be safely evaluated', () => { + const code = '!undefined || ![1,2,3] || !{a:1}'; + const expected = 'true || false || false;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Do not normalize NOT on variables', () => { + const code = '!variable || !obj.prop || !func()'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Do not normalize NOT on complex expressions', () => { + const code = '!(a + b) || !(x > y) || !(z && w)'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Do not normalize NOT on function calls', () => { + const code = '!getValue() || !Math.random() || !Array.isArray(arr)'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Do not normalize NOT on computed properties', () => { + const code = '!obj[key] || !arr[0] || !matrix[i][j]'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Do not normalize literals with unpredictable values', () => { + const code = '!Infinity || !-Infinity'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); }); describe('UNSAFE: resolveAugmentedFunctionWrappedArrayReplacements', async () => { - const targetModule = (await import('../src/modules/unsafe/resolveAugmentedFunctionWrappedArrayReplacements.js')).default; - - it.todo('Add Missing True Positive Test Cases'); - - it('TN-1: Do not transform functions without augmentation', () => { - const code = `function simpleFunc() { return 'test'; } + const targetModule = (await import('../src/modules/unsafe/resolveAugmentedFunctionWrappedArrayReplacements.js')).default; + + it.todo('Add Missing True Positive Test Cases'); + + it('TN-1: Do not transform functions without augmentation', () => { + const code = `function simpleFunc() { return 'test'; } simpleFunc();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - - it('TN-2: Do not transform functions without array operations', () => { - const code = `function myFunc() { myFunc = 'modified'; return 'value'; } + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + + it('TN-2: Do not transform functions without array operations', () => { + const code = `function myFunc() { myFunc = 'modified'; return 'value'; } myFunc();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - - it('TN-3: Do not transform when no matching expression statements', () => { - const code = `var arr = ['a', 'b']; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + + it('TN-3: Do not transform when no matching expression statements', () => { + const code = `var arr = ['a', 'b']; function decrypt(i) { return arr[i]; } decrypt.modified = true; decrypt(0);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - - it('TN-4: Do not transform anonymous functions', () => { - const code = `var func = function() { func = 'modified'; return arr[0]; }; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + + it('TN-4: Do not transform anonymous functions', () => { + const code = `var func = function() { func = 'modified'; return arr[0]; }; func();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - - it('TN-5: Do not transform when array candidate has no declNode', () => { - const code = `function decrypt() { + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + + it('TN-5: Do not transform when array candidate has no declNode', () => { + const code = `function decrypt() { decrypt = 'modified'; return undeclaredArr[0]; } decrypt();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - - it('TN-6: Do not transform when expression statement pattern is wrong', () => { - const code = `var arr = ['a', 'b']; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + + it('TN-6: Do not transform when expression statement pattern is wrong', () => { + const code = `var arr = ['a', 'b']; function decrypt(i) { decrypt = 'modified'; return arr[i]; } (function() { return arr; })(); // Wrong pattern - not matching decrypt(0);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); - - it('TN-7: Do not transform when no replacement candidates found', () => { - const code = `var arr = ['a', 'b']; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); + + it('TN-7: Do not transform when no replacement candidates found', () => { + const code = `var arr = ['a', 'b']; function decrypt(i) { decrypt = 'modified'; return arr[i]; @@ -170,881 +170,880 @@ describe('UNSAFE: resolveAugmentedFunctionWrappedArrayReplacements', async () => (function(arr) { return arr; })(arr); // No calls to decrypt function to replace console.log('test');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.strictEqual(result, expected); - }); + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.strictEqual(result, expected); + }); }); describe('UNSAFE: resolveBuiltinCalls', async () => { - const targetModule = (await import('../src/modules/unsafe/resolveBuiltinCalls.js')).default; - it('TP-1: atob', () => { - const code = `atob('c29sdmVkIQ==');`; - const expected = `'solved!';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: btoa', () => { - const code = `btoa('solved!');`; - const expected = `'c29sdmVkIQ==';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: split', () => { - const code = `'ok'.split('');`; - const expected = `[\n 'o',\n 'k'\n];`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Member expression with literal arguments', () => { - const code = `String.fromCharCode(72, 101, 108, 108, 111);`; - const expected = `'Hello';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-5: Multiple builtin calls', () => { - const code = `btoa('test') + atob('dGVzdA==');`; - const expected = `'dGVzdA==' + 'test';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-6: String method with multiple arguments', () => { - const code = `'hello world'.replace('world', 'universe');`; - const expected = `'hello universe';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: querySelector', () => { - const code = `document.querySelector('div');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Unknown variable', () => { - const code = `atob(x)`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Overwritten builtin', () => { - const code = `function atob() {return 1;} atob('test');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Skip builtin function call', () => { - const code = `Array(5);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Skip member expression with restricted property', () => { - const code = `'test'.length;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-6: Function call with this expression', () => { - const code = `this.btoa('test');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-7: Constructor property access', () => { - const code = `String.constructor('return 1');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-8: Member expression with computed property using variable', () => { - const code = `String[methodName]('test');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/unsafe/resolveBuiltinCalls.js')).default; + it('TP-1: atob', () => { + const code = 'atob(\'c29sdmVkIQ==\');'; + const expected = '\'solved!\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: btoa', () => { + const code = 'btoa(\'solved!\');'; + const expected = '\'c29sdmVkIQ==\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: split', () => { + const code = '\'ok\'.split(\'\');'; + const expected = '[\n \'o\',\n \'k\'\n];'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Member expression with literal arguments', () => { + const code = 'String.fromCharCode(72, 101, 108, 108, 111);'; + const expected = '\'Hello\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-5: Multiple builtin calls', () => { + const code = 'btoa(\'test\') + atob(\'dGVzdA==\');'; + const expected = '\'dGVzdA==\' + \'test\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-6: String method with multiple arguments', () => { + const code = '\'hello world\'.replace(\'world\', \'universe\');'; + const expected = '\'hello universe\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: querySelector', () => { + const code = 'document.querySelector(\'div\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Unknown variable', () => { + const code = 'atob(x)'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Overwritten builtin', () => { + const code = 'function atob() {return 1;} atob(\'test\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Skip builtin function call', () => { + const code = 'Array(5);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Skip member expression with restricted property', () => { + const code = '\'test\'.length;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-6: Function call with this expression', () => { + const code = 'this.btoa(\'test\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-7: Constructor property access', () => { + const code = 'String.constructor(\'return 1\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-8: Member expression with computed property using variable', () => { + const code = 'String[methodName](\'test\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); }); describe('UNSAFE: resolveDefiniteBinaryExpressions', async () => { - const targetModule = (await import('../src/modules/unsafe/resolveDefiniteBinaryExpressions.js')).default; - it('TP-1: Mixed arithmetic and string operations', () => { - const code = `5 * 3; '2' + 2; '10' - 1; 'o' + 'k'; 'o' - 'k'; 3 - -1;`; - const expected = `15;\n'22';\n9;\n'ok';\nNaN;\n4;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Division and modulo operations', () => { - const code = `10 / 2; 7 % 3; 15 / 3;`; - const expected = `5;\n1;\n5;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Bitwise operations', () => { - const code = `5 & 3; 5 | 3; 5 ^ 3;`; - const expected = `1;\n7;\n6;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Comparison operations', () => { - const code = `5 > 3; 2 < 1; 5 === 5; 'a' !== 'b';`; - const expected = `true;\nfalse;\ntrue;\ntrue;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-5: Negative number edge case handling', () => { - const code = `10 - 15; 3 - 8;`; - const expected = `-5;\n-5;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-6: Null operations and string concatenation', () => { - const code = `null + 5; 'test' + 'ing';`; - const expected = `5;\n'testing';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Do not resolve expressions with variables', () => { - const code = `x + 5; a * b;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Do not resolve expressions with function calls', () => { - const code = `foo() + 5; Math.max(1, 2) * 3;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Do not resolve member expressions', () => { - const code = `obj.prop + 5; arr[0] * 2;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Do not resolve complex nested expressions', () => { - const code = `(x + y) * z; foo(a) + bar(b);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Do not resolve logical expressions (not BinaryExpressions)', () => { - const code = `true && false; true || false; !true;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-6: Do not resolve expressions with undefined identifier', () => { - const code = `undefined + 3; x + undefined;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - - // Test the inlined helper function - const {doesBinaryExpressionContainOnlyLiterals} = await import('../src/modules/unsafe/resolveDefiniteBinaryExpressions.js'); - - it('Helper TP-1: Literal node', () => { - const ast = generateFlatAST(`'a'`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'Literal')); - assert.ok(result); - }); - it('Helper TP-2: Binary expression with literals', () => { - const ast = generateFlatAST(`1 + 2`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'BinaryExpression')); - assert.ok(result); - }); - it('Helper TP-3: Unary expression with literal', () => { - const ast = generateFlatAST(`-'a'`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'UnaryExpression')); - assert.ok(result); - }); - it('Helper TP-4: Complex nested binary expressions', () => { - const ast = generateFlatAST(`1 + 2 + 3 + 4`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'BinaryExpression')); - assert.ok(result); - }); - it('Helper TP-5: Logical expression with literals', () => { - const ast = generateFlatAST(`true && false`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'LogicalExpression')); - assert.ok(result); - }); - it('Helper TP-6: Conditional expression with literals', () => { - const ast = generateFlatAST(`true ? 1 : 2`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'ConditionalExpression')); - assert.ok(result); - }); - it('Helper TP-7: Sequence expression with literals', () => { - const ast = generateFlatAST(`(1, 2, 3)`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'SequenceExpression')); - assert.ok(result); - }); - it('Helper TN-7: Update expression with identifier', () => { - const ast = generateFlatAST(`let x = 5; ++x;`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'UpdateExpression')); - assert.strictEqual(result, false); // ++x contains an identifier, not a literal - }); - it('Helper TN-1: Identifier is rejected', () => { - const ast = generateFlatAST(`a`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'Identifier')); - assert.strictEqual(result, false); - }); - it('Helper TN-2: Unary expression with identifier', () => { - const ast = generateFlatAST(`!a`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'UnaryExpression')); - assert.strictEqual(result, false); - }); - it('Helper TN-3: Binary expression with identifier', () => { - const ast = generateFlatAST(`1 + b`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'BinaryExpression')); - assert.strictEqual(result, false); - }); - it('Helper TN-4: Complex non-literal expressions are rejected', () => { - const ast = generateFlatAST(`true && x`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'LogicalExpression')); - assert.strictEqual(result, false); - }); - it('Helper TN-5: Function calls and member expressions', () => { - const ast = generateFlatAST(`func()`); - const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'CallExpression')); - assert.strictEqual(result, false); - - const ast2 = generateFlatAST(`obj.prop`); - const result2 = doesBinaryExpressionContainOnlyLiterals(ast2.find(n => n.type === 'MemberExpression')); - assert.strictEqual(result2, false); - }); - it('Helper TN-6: Null and undefined handling', () => { - assert.strictEqual(doesBinaryExpressionContainOnlyLiterals(null), false); - assert.strictEqual(doesBinaryExpressionContainOnlyLiterals(undefined), false); - assert.strictEqual(doesBinaryExpressionContainOnlyLiterals({}), false); - }); + const targetModule = (await import('../src/modules/unsafe/resolveDefiniteBinaryExpressions.js')).default; + it('TP-1: Mixed arithmetic and string operations', () => { + const code = '5 * 3; \'2\' + 2; \'10\' - 1; \'o\' + \'k\'; \'o\' - \'k\'; 3 - -1;'; + const expected = '15;\n\'22\';\n9;\n\'ok\';\nNaN;\n4;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Division and modulo operations', () => { + const code = '10 / 2; 7 % 3; 15 / 3;'; + const expected = '5;\n1;\n5;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Bitwise operations', () => { + const code = '5 & 3; 5 | 3; 5 ^ 3;'; + const expected = '1;\n7;\n6;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Comparison operations', () => { + const code = '5 > 3; 2 < 1; 5 === 5; \'a\' !== \'b\';'; + const expected = 'true;\nfalse;\ntrue;\ntrue;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-5: Negative number edge case handling', () => { + const code = '10 - 15; 3 - 8;'; + const expected = '-5;\n-5;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-6: Null operations and string concatenation', () => { + const code = 'null + 5; \'test\' + \'ing\';'; + const expected = '5;\n\'testing\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Do not resolve expressions with variables', () => { + const code = 'x + 5; a * b;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Do not resolve expressions with function calls', () => { + const code = 'foo() + 5; Math.max(1, 2) * 3;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Do not resolve member expressions', () => { + const code = 'obj.prop + 5; arr[0] * 2;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Do not resolve complex nested expressions', () => { + const code = '(x + y) * z; foo(a) + bar(b);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Do not resolve logical expressions (not BinaryExpressions)', () => { + const code = 'true && false; true || false; !true;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-6: Do not resolve expressions with undefined identifier', () => { + const code = 'undefined + 3; x + undefined;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + + // Test the inlined helper function + const {doesBinaryExpressionContainOnlyLiterals} = await import('../src/modules/unsafe/resolveDefiniteBinaryExpressions.js'); + + it('Helper TP-1: Literal node', () => { + const ast = generateFlatAST('\'a\''); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'Literal')); + assert.ok(result); + }); + it('Helper TP-2: Binary expression with literals', () => { + const ast = generateFlatAST('1 + 2'); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'BinaryExpression')); + assert.ok(result); + }); + it('Helper TP-3: Unary expression with literal', () => { + const ast = generateFlatAST('-\'a\''); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'UnaryExpression')); + assert.ok(result); + }); + it('Helper TP-4: Complex nested binary expressions', () => { + const ast = generateFlatAST('1 + 2 + 3 + 4'); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'BinaryExpression')); + assert.ok(result); + }); + it('Helper TP-5: Logical expression with literals', () => { + const ast = generateFlatAST('true && false'); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'LogicalExpression')); + assert.ok(result); + }); + it('Helper TP-6: Conditional expression with literals', () => { + const ast = generateFlatAST('true ? 1 : 2'); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'ConditionalExpression')); + assert.ok(result); + }); + it('Helper TP-7: Sequence expression with literals', () => { + const ast = generateFlatAST('(1, 2, 3)'); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'SequenceExpression')); + assert.ok(result); + }); + it('Helper TN-7: Update expression with identifier', () => { + const ast = generateFlatAST('let x = 5; ++x;'); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'UpdateExpression')); + assert.strictEqual(result, false); // ++x contains an identifier, not a literal + }); + it('Helper TN-1: Identifier is rejected', () => { + const ast = generateFlatAST('a'); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'Identifier')); + assert.strictEqual(result, false); + }); + it('Helper TN-2: Unary expression with identifier', () => { + const ast = generateFlatAST('!a'); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'UnaryExpression')); + assert.strictEqual(result, false); + }); + it('Helper TN-3: Binary expression with identifier', () => { + const ast = generateFlatAST('1 + b'); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'BinaryExpression')); + assert.strictEqual(result, false); + }); + it('Helper TN-4: Complex non-literal expressions are rejected', () => { + const ast = generateFlatAST('true && x'); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'LogicalExpression')); + assert.strictEqual(result, false); + }); + it('Helper TN-5: Function calls and member expressions', () => { + const ast = generateFlatAST('func()'); + const result = doesBinaryExpressionContainOnlyLiterals(ast.find(n => n.type === 'CallExpression')); + assert.strictEqual(result, false); + + const ast2 = generateFlatAST('obj.prop'); + const result2 = doesBinaryExpressionContainOnlyLiterals(ast2.find(n => n.type === 'MemberExpression')); + assert.strictEqual(result2, false); + }); + it('Helper TN-6: Null and undefined handling', () => { + assert.strictEqual(doesBinaryExpressionContainOnlyLiterals(null), false); + assert.strictEqual(doesBinaryExpressionContainOnlyLiterals(undefined), false); + assert.strictEqual(doesBinaryExpressionContainOnlyLiterals({}), false); + }); }); describe('UNSAFE: resolveDefiniteMemberExpressions', async () => { - const targetModule = (await import('../src/modules/unsafe/resolveDefiniteMemberExpressions.js')).default; - it('TP-1: String and array indexing with properties', () => { - const code = `'123'[0]; 'hello'.length;`; - const expected = `'1';\n5;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Array literal indexing', () => { - const code = `[1, 2, 3][0]; [4, 5, 6][2];`; - const expected = `1;\n6;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: String indexing with different positions', () => { - const code = `'test'[1]; 'world'[4];`; - const expected = `'e';\n'd';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Array length property', () => { - const code = `[1, 2, 3, 4].length; ['a', 'b'].length;`; - const expected = `4;\n2;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-5: Mixed literal types in arrays', () => { - const code = `['hello', 42, true][0]; [null, undefined, 'test'][2];`; - const expected = `'hello';\n'test';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-6: Non-computed property access with identifier', () => { - const code = `'testing'.length; [1, 2, 3].length;`; - const expected = `7;\n3;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Do not transform update expressions', () => { - const code = `++[[]][0];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Do not transform method calls (callee position)', () => { - const code = `'test'.split(''); [1, 2, 3].join(',');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Do not transform computed properties with variables', () => { - const code = `'hello'[index]; arr[i];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Do not transform non-literal objects', () => { - const code = `obj.property; variable[0];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Do not transform empty literals', () => { - const code = `''[0]; [].length;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-6: Do not transform complex property expressions', () => { - const code = `'test'[getValue()]; obj[prop + 'name'];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-7: Do not transform out-of-bounds access (handled by sandbox)', () => { - const code = `'abc'[10]; [1, 2][5];`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/unsafe/resolveDefiniteMemberExpressions.js')).default; + it('TP-1: String and array indexing with properties', () => { + const code = '\'123\'[0]; \'hello\'.length;'; + const expected = '\'1\';\n5;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Array literal indexing', () => { + const code = '[1, 2, 3][0]; [4, 5, 6][2];'; + const expected = '1;\n6;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: String indexing with different positions', () => { + const code = '\'test\'[1]; \'world\'[4];'; + const expected = '\'e\';\n\'d\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Array length property', () => { + const code = '[1, 2, 3, 4].length; [\'a\', \'b\'].length;'; + const expected = '4;\n2;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-5: Mixed literal types in arrays', () => { + const code = '[\'hello\', 42, true][0]; [null, undefined, \'test\'][2];'; + const expected = '\'hello\';\n\'test\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-6: Non-computed property access with identifier', () => { + const code = '\'testing\'.length; [1, 2, 3].length;'; + const expected = '7;\n3;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Do not transform update expressions', () => { + const code = '++[[]][0];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Do not transform method calls (callee position)', () => { + const code = '\'test\'.split(\'\'); [1, 2, 3].join(\',\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Do not transform computed properties with variables', () => { + const code = '\'hello\'[index]; arr[i];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Do not transform non-literal objects', () => { + const code = 'obj.property; variable[0];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Do not transform empty literals', () => { + const code = '\'\'[0]; [].length;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-6: Do not transform complex property expressions', () => { + const code = '\'test\'[getValue()]; obj[prop + \'name\'];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-7: Do not transform out-of-bounds access (handled by sandbox)', () => { + const code = '\'abc\'[10]; [1, 2][5];'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); }); describe('UNSAFE: resolveDeterministicConditionalExpressions', async () => { - const targetModule = (await import('../src/modules/unsafe/resolveDeterministicConditionalExpressions.js')).default; - it('TP-1: Boolean literals (true/false)', () => { - const code = `(true ? 1 : 2); (false ? 3 : 4);`; - const expected = `1;\n4;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Truthy string literals', () => { - const code = `('hello' ? 'yes' : 'no'); ('a' ? 42 : 0);`; - const expected = `'yes';\n42;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Falsy string literal (empty string)', () => { - const code = `('' ? 'yes' : 'no'); ('' ? 42 : 0);`; - const expected = `'no';\n0;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Truthy number literals', () => { - const code = `(1 ? 'one' : 'zero'); (42 ? 'yes' : 'no'); (123 ? 'positive' : 'zero');`; - const expected = `'one';\n'yes';\n'positive';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-5: Falsy number literal (zero)', () => { - const code = `(0 ? 'yes' : 'no'); (0 ? 42 : 'zero');`; - const expected = `'no';\n'zero';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-6: Null literal', () => { - const code = `(null ? 'yes' : 'no'); (null ? 'defined' : 'null');`; - const expected = `'no';\n'null';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-7: Nested conditional expressions (single pass)', () => { - const code = `(true ? (false ? 'inner1' : 'inner2') : 'outer');`; - const expected = `false ? 'inner1' : 'inner2';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-8: Complex expressions as branches', () => { - const code = `(1 ? console.log('truthy') : console.log('falsy'));`; - const expected = `console.log('truthy');`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Non-literal test expressions', () => { - const code = `({} ? 1 : 2); ([].length ? 3 : 4);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Variable test expressions', () => { - const code = `(x ? 'yes' : 'no'); (condition ? true : false);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Function call test expressions', () => { - const code = `(getValue() ? 'yes' : 'no'); (check() ? 1 : 0);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Binary expression test expressions', () => { - const code = `(a + b ? 'yes' : 'no'); (x > 5 ? 'big' : 'small');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Member expression test expressions', () => { - const code = `(obj.prop ? 'yes' : 'no'); (arr[0] ? 'first' : 'empty');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-6: Unary expressions (not literals)', () => { - const code = `(-1 ? 'negative' : 'zero'); (!true ? 'no' : 'yes');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-7: Undefined identifier (not literal)', () => { - const code = `(undefined ? 'defined' : 'undefined');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/unsafe/resolveDeterministicConditionalExpressions.js')).default; + it('TP-1: Boolean literals (true/false)', () => { + const code = '(true ? 1 : 2); (false ? 3 : 4);'; + const expected = '1;\n4;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Truthy string literals', () => { + const code = '(\'hello\' ? \'yes\' : \'no\'); (\'a\' ? 42 : 0);'; + const expected = '\'yes\';\n42;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Falsy string literal (empty string)', () => { + const code = '(\'\' ? \'yes\' : \'no\'); (\'\' ? 42 : 0);'; + const expected = '\'no\';\n0;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Truthy number literals', () => { + const code = '(1 ? \'one\' : \'zero\'); (42 ? \'yes\' : \'no\'); (123 ? \'positive\' : \'zero\');'; + const expected = '\'one\';\n\'yes\';\n\'positive\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-5: Falsy number literal (zero)', () => { + const code = '(0 ? \'yes\' : \'no\'); (0 ? 42 : \'zero\');'; + const expected = '\'no\';\n\'zero\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-6: Null literal', () => { + const code = '(null ? \'yes\' : \'no\'); (null ? \'defined\' : \'null\');'; + const expected = '\'no\';\n\'null\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-7: Nested conditional expressions (single pass)', () => { + const code = '(true ? (false ? \'inner1\' : \'inner2\') : \'outer\');'; + const expected = 'false ? \'inner1\' : \'inner2\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-8: Complex expressions as branches', () => { + const code = '(1 ? console.log(\'truthy\') : console.log(\'falsy\'));'; + const expected = 'console.log(\'truthy\');'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Non-literal test expressions', () => { + const code = '({} ? 1 : 2); ([].length ? 3 : 4);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Variable test expressions', () => { + const code = '(x ? \'yes\' : \'no\'); (condition ? true : false);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Function call test expressions', () => { + const code = '(getValue() ? \'yes\' : \'no\'); (check() ? 1 : 0);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Binary expression test expressions', () => { + const code = '(a + b ? \'yes\' : \'no\'); (x > 5 ? \'big\' : \'small\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Member expression test expressions', () => { + const code = '(obj.prop ? \'yes\' : \'no\'); (arr[0] ? \'first\' : \'empty\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-6: Unary expressions (not literals)', () => { + const code = '(-1 ? \'negative\' : \'zero\'); (!true ? \'no\' : \'yes\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-7: Undefined identifier (not literal)', () => { + const code = '(undefined ? \'defined\' : \'undefined\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); }); describe('UNSAFE: resolveEvalCallsOnNonLiterals', async () => { - const targetModule = (await import('../src/modules/unsafe/resolveEvalCallsOnNonLiterals.js')).default; - it('TP-1: Function call that returns string', () => { - const code = `eval(function(a) {return a}('atob'));`; - const expected = `atob;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Array access returning empty string', () => { - const code = `eval([''][0]);`; - const expected = `''`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Variable reference resolution', () => { - const code = `var x = 'console.log("test")'; eval(x);`; - const expected = `var x = 'console.log("test")';\nconsole.log('test');`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Function expression IIFE', () => { - const code = `eval((function() { return 'var a = 5;'; })());`; - const expected = `var a = 5;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-5: Member expression property access', () => { - const code = `var obj = {code: 'var y = 10;'}; eval(obj.code);`; - const expected = `var obj = { code: 'var y = 10;' };\nvar y = 10;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-6: Array index with complex expression', () => { - const code = `var arr = ['if (true) { x = 1; }']; eval(arr[0]);`; - const expected = `var arr = ['if (true) { x = 1; }'];\nif (true) {\n x = 1;\n}`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Eval with literal string (already handled by another module)', () => { - const code = `eval('console.log("literal")');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Non-eval function calls', () => { - const code = `execute(function() { return 'code'; }());`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Eval with multiple arguments', () => { - const code = `eval('code', extra);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Eval with no arguments', () => { - const code = `eval();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Computed member expression for eval', () => { - const code = `obj['eval'](dynamicCode);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-6: Eval with non-evaluable expression', () => { - const code = `eval(undefined);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/unsafe/resolveEvalCallsOnNonLiterals.js')).default; + it('TP-1: Function call that returns string', () => { + const code = 'eval(function(a) {return a}(\'atob\'));'; + const expected = 'atob;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Array access returning empty string', () => { + const code = 'eval([\'\'][0]);'; + const expected = '\'\''; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Variable reference resolution', () => { + const code = 'var x = \'console.log("test")\'; eval(x);'; + const expected = 'var x = \'console.log("test")\';\nconsole.log(\'test\');'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Function expression IIFE', () => { + const code = 'eval((function() { return \'var a = 5;\'; })());'; + const expected = 'var a = 5;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-5: Member expression property access', () => { + const code = 'var obj = {code: \'var y = 10;\'}; eval(obj.code);'; + const expected = 'var obj = { code: \'var y = 10;\' };\nvar y = 10;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-6: Array index with complex expression', () => { + const code = 'var arr = [\'if (true) { x = 1; }\']; eval(arr[0]);'; + const expected = 'var arr = [\'if (true) { x = 1; }\'];\nif (true) {\n x = 1;\n}'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Eval with literal string (already handled by another module)', () => { + const code = 'eval(\'console.log("literal")\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Non-eval function calls', () => { + const code = 'execute(function() { return \'code\'; }());'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Eval with multiple arguments', () => { + const code = 'eval(\'code\', extra);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Eval with no arguments', () => { + const code = 'eval();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Computed member expression for eval', () => { + const code = 'obj[\'eval\'](dynamicCode);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-6: Eval with non-evaluable expression', () => { + const code = 'eval(undefined);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); }); describe('UNSAFE: resolveFunctionToArray', async () => { - const targetModule = (await import('../src/modules/unsafe/resolveFunctionToArray.js')).default; - it('TP-1: Simple function returning array', () => { - const code = `function a() {return [1];}\nconst b = a();`; - const expected = `function a() {\n return [1];\n}\nconst b = [1];`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Function with multiple elements', () => { - const code = `function getArr() { return ['one', 'two', 'three']; }\nlet arr = getArr();`; - const expected = `function getArr() {\n return [\n 'one',\n 'two',\n 'three'\n ];\n}\nlet arr = [\n 'one',\n 'two',\n 'three'\n];`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Arrow function returning array', () => { - const code = `const makeArray = () => [1, 2, 3];\nconst data = makeArray();`; - const expected = `const makeArray = () => [\n 1,\n 2,\n 3\n];\nconst data = [\n 1,\n 2,\n 3\n];`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Function with parameters (ignored)', () => { - const code = `function createArray(x) { return [x, x + 1]; }\nconst nums = createArray();`; - const expected = `function createArray(x) {\n return [\n x,\n x + 1\n ];\n}\nconst nums = [\n undefined,\n NaN\n];`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-5: Multiple variables with array access only', () => { - const code = `function getColors() { return ['red', 'blue']; }\nconst colors = getColors();\nconst first = colors[0];`; - const expected = `function getColors() {\n return [\n 'red',\n 'blue'\n ];\n}\nconst colors = [\n 'red',\n 'blue'\n];\nconst first = colors[0];`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Function call with non-array-access usage', () => { - const code = `function getValue() { return 'test'; }\nconst val = getValue();\nconsole.log(val);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-6: Variable with empty references array (should transform)', () => { - const code = `function getArray() { return [1, 2]; }\nconst unused = getArray();`; - const expected = `function getArray() {\n return [\n 1,\n 2\n ];\n}\nconst unused = [\n 1,\n 2\n];`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Variable not assigned function call', () => { - const code = `const arr = [1, 2, 3];\nconsole.log(arr[0]);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Mixed usage (array access and other)', () => { - const code = `function getData() { return [1, 2]; }\nconst data = getData();\nconsole.log(data[0], data);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-7: Function with property access (length is MemberExpression)', () => { - const code = `function getArray() { return [1, 2, 3]; }\nconst arr = getArray();\nconst len = arr.length;`; - const expected = `function getArray() {\n return [\n 1,\n 2,\n 3\n ];\n}\nconst arr = [\n 1,\n 2,\n 3\n];\nconst len = arr.length;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Function with method calls (not just property access)', () => { - const code = `function getArray() { return [1, 2, 3]; }\nconst arr = getArray();\narr.push(4);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-6: Non-literal init expression', () => { - const code = `const arr = someFunction();\nconsole.log(arr[0]);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/unsafe/resolveFunctionToArray.js')).default; + it('TP-1: Simple function returning array', () => { + const code = 'function a() {return [1];}\nconst b = a();'; + const expected = 'function a() {\n return [1];\n}\nconst b = [1];'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Function with multiple elements', () => { + const code = 'function getArr() { return [\'one\', \'two\', \'three\']; }\nlet arr = getArr();'; + const expected = 'function getArr() {\n return [\n \'one\',\n \'two\',\n \'three\'\n ];\n}\nlet arr = [\n \'one\',\n \'two\',\n \'three\'\n];'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Arrow function returning array', () => { + const code = 'const makeArray = () => [1, 2, 3];\nconst data = makeArray();'; + const expected = 'const makeArray = () => [\n 1,\n 2,\n 3\n];\nconst data = [\n 1,\n 2,\n 3\n];'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Function with parameters (ignored)', () => { + const code = 'function createArray(x) { return [x, x + 1]; }\nconst nums = createArray();'; + const expected = 'function createArray(x) {\n return [\n x,\n x + 1\n ];\n}\nconst nums = [\n undefined,\n NaN\n];'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-5: Multiple variables with array access only', () => { + const code = 'function getColors() { return [\'red\', \'blue\']; }\nconst colors = getColors();\nconst first = colors[0];'; + const expected = 'function getColors() {\n return [\n \'red\',\n \'blue\'\n ];\n}\nconst colors = [\n \'red\',\n \'blue\'\n];\nconst first = colors[0];'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Function call with non-array-access usage', () => { + const code = 'function getValue() { return \'test\'; }\nconst val = getValue();\nconsole.log(val);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-6: Variable with empty references array (should transform)', () => { + const code = 'function getArray() { return [1, 2]; }\nconst unused = getArray();'; + const expected = 'function getArray() {\n return [\n 1,\n 2\n ];\n}\nconst unused = [\n 1,\n 2\n];'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Variable not assigned function call', () => { + const code = 'const arr = [1, 2, 3];\nconsole.log(arr[0]);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Mixed usage (array access and other)', () => { + const code = 'function getData() { return [1, 2]; }\nconst data = getData();\nconsole.log(data[0], data);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-7: Function with property access (length is MemberExpression)', () => { + const code = 'function getArray() { return [1, 2, 3]; }\nconst arr = getArray();\nconst len = arr.length;'; + const expected = 'function getArray() {\n return [\n 1,\n 2,\n 3\n ];\n}\nconst arr = [\n 1,\n 2,\n 3\n];\nconst len = arr.length;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Function with method calls (not just property access)', () => { + const code = 'function getArray() { return [1, 2, 3]; }\nconst arr = getArray();\narr.push(4);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-6: Non-literal init expression', () => { + const code = 'const arr = someFunction();\nconsole.log(arr[0]);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); }); describe('UNSAFE: resolveInjectedPrototypeMethodCalls', async () => { - const targetModule = (await import('../src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js')).default; - it('TP-1: String prototype method injection', () => { - const code = `String.prototype.secret = function () {return 'secret ' + this;}; 'hello'.secret();`; - const expected = `String.prototype.secret = function () {\n return 'secret ' + this;\n};\n'secret hello';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Number prototype method injection', () => { - const code = `Number.prototype.double = function () {return this * 2;}; (5).double();`; - const expected = `Number.prototype.double = function () {\n return this * 2;\n};\n10;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Array prototype method injection', () => { - const code = `Array.prototype.first = function () {return this[0];}; [1, 2, 3].first();`; - const expected = `Array.prototype.first = function () {\n return this[0];\n};\n1;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Method with parameters', () => { - const code = `String.prototype.multiply = function (n) {return this + this;}; 'hi'.multiply(2);`; - const expected = `String.prototype.multiply = function (n) {\n return this + this;\n};\n'hihi';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-5: Multiple calls to same injected method', () => { - const code = `String.prototype.shout = function () {return this.toUpperCase() + '!';}; 'hello'.shout(); 'world'.shout();`; - const expected = `String.prototype.shout = function () {\n return this.toUpperCase() + '!';\n};\n'HELLO!';\n'WORLD!';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-6: Identifier assignment to prototype method', () => { - const code = `function helper() {return 'helped';} String.prototype.help = helper; 'test'.help();`; - const expected = `function helper() {\n return 'helped';\n}\nString.prototype.help = helper;\n'helped';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-7: Method call with missing arguments resolves to expected result', () => { - const code = `String.prototype.test = function (a, b) {return a + b;}; 'hello'.test();`; - const expected = `String.prototype.test = function (a, b) {\n return a + b;\n};\nNaN;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-8: Arrow function prototype method injection', () => { - const code = `String.prototype.reverse = () => 'reversed'; 'hello'.reverse();`; - const expected = `String.prototype.reverse = () => 'reversed';\n'reversed';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-9: Arrow function with parameters', () => { - const code = `String.prototype.repeat = (n) => 'repeated'; 'test'.repeat(3);`; - const expected = `String.prototype.repeat = n => 'repeated';\n'repeated';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-10: Arrow function using closure variable', () => { - const code = `const value = 'closure'; String.prototype.getClosure = () => value; 'hello'.getClosure();`; - const expected = `const value = 'closure';\nString.prototype.getClosure = () => value;\n'closure';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Non-prototype property assignment', () => { - const code = `String.custom = function () {return 'custom';}; String.custom();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Non-function assignment to prototype', () => { - const code = `String.prototype.value = 'static'; 'test'.value;`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Call to non-injected method', () => { - const code = `String.prototype.custom = function () {return 'custom';}; 'test'.other();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Assignment with non-assignment operator', () => { - const code = `String.prototype.test += function () {return 'test';}; 'hello'.test();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Complex expression assignment to prototype', () => { - const code = `String.prototype.complex = getValue() + 'suffix'; 'test'.complex();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-6: Arrow function returning this (may not evaluate safely)', () => { - const code = `String.prototype.getThis = () => this; 'hello'.getThis();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js')).default; + it('TP-1: String prototype method injection', () => { + const code = 'String.prototype.secret = function () {return \'secret \' + this;}; \'hello\'.secret();'; + const expected = 'String.prototype.secret = function () {\n return \'secret \' + this;\n};\n\'secret hello\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Number prototype method injection', () => { + const code = 'Number.prototype.double = function () {return this * 2;}; (5).double();'; + const expected = 'Number.prototype.double = function () {\n return this * 2;\n};\n10;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Array prototype method injection', () => { + const code = 'Array.prototype.first = function () {return this[0];}; [1, 2, 3].first();'; + const expected = 'Array.prototype.first = function () {\n return this[0];\n};\n1;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Method with parameters', () => { + const code = 'String.prototype.multiply = function (n) {return this + this;}; \'hi\'.multiply(2);'; + const expected = 'String.prototype.multiply = function (n) {\n return this + this;\n};\n\'hihi\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-5: Multiple calls to same injected method', () => { + const code = 'String.prototype.shout = function () {return this.toUpperCase() + \'!\';}; \'hello\'.shout(); \'world\'.shout();'; + const expected = 'String.prototype.shout = function () {\n return this.toUpperCase() + \'!\';\n};\n\'HELLO!\';\n\'WORLD!\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-6: Identifier assignment to prototype method', () => { + const code = 'function helper() {return \'helped\';} String.prototype.help = helper; \'test\'.help();'; + const expected = 'function helper() {\n return \'helped\';\n}\nString.prototype.help = helper;\n\'helped\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-7: Method call with missing arguments resolves to expected result', () => { + const code = 'String.prototype.test = function (a, b) {return a + b;}; \'hello\'.test();'; + const expected = 'String.prototype.test = function (a, b) {\n return a + b;\n};\nNaN;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-8: Arrow function prototype method injection', () => { + const code = 'String.prototype.reverse = () => \'reversed\'; \'hello\'.reverse();'; + const expected = 'String.prototype.reverse = () => \'reversed\';\n\'reversed\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-9: Arrow function with parameters', () => { + const code = 'String.prototype.repeat = (n) => \'repeated\'; \'test\'.repeat(3);'; + const expected = 'String.prototype.repeat = n => \'repeated\';\n\'repeated\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-10: Arrow function using closure variable', () => { + const code = 'const value = \'closure\'; String.prototype.getClosure = () => value; \'hello\'.getClosure();'; + const expected = 'const value = \'closure\';\nString.prototype.getClosure = () => value;\n\'closure\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Non-prototype property assignment', () => { + const code = 'String.custom = function () {return \'custom\';}; String.custom();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Non-function assignment to prototype', () => { + const code = 'String.prototype.value = \'static\'; \'test\'.value;'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Call to non-injected method', () => { + const code = 'String.prototype.custom = function () {return \'custom\';}; \'test\'.other();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Assignment with non-assignment operator', () => { + const code = 'String.prototype.test += function () {return \'test\';}; \'hello\'.test();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Complex expression assignment to prototype', () => { + const code = 'String.prototype.complex = getValue() + \'suffix\'; \'test\'.complex();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-6: Arrow function returning this (may not evaluate safely)', () => { + const code = 'String.prototype.getThis = () => this; \'hello\'.getThis();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); }); describe('UNSAFE: resolveLocalCalls', async () => { - const targetModule = (await import('../src/modules/unsafe/resolveLocalCalls.js')).default; - it('TP-1: Function declaration', () => { - const code = `function add(a, b) {return a + b;} add(1, 2);`; - const expected = `function add(a, b) {\n return a + b;\n}\n3;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Arrow function', () => { - const code = `const add = (a, b) => a + b; add(1, 2);`; - const expected = `const add = (a, b) => a + b;\n3;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Overwritten builtin', () => { - const code = `const atob = (a, b) => a + b; atob('got-');`; - const expected = `const atob = (a, b) => a + b;\n'got-undefined';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Function expression', () => { - const code = `const multiply = function(a, b) {return a * b;}; multiply(3, 4);`; - const expected = `const multiply = function (a, b) {\n return a * b;\n};\n12;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-5: Multiple calls to same function', () => { - const code = `function double(x) {return x * 2;} double(5); double(10);`; - const expected = `function double(x) {\n return x * 2;\n}\n10;\n20;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-6: Function returning string', () => { - const code = `function greet(name) {return 'Hello ' + name;} greet('World');`; - const expected = `function greet(name) {\n return 'Hello ' + name;\n}\n'Hello World';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Missing declaration', () => { - const code = `add(1, 2);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Skipped builtin', () => { - const code = `btoa('a');`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: No replacement with undefined', () => { - const code = `function a() {} a();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Complex member expression property access', () => { - const code = `const obj = {value: 'test'}; const fn = (o) => o.value; fn(obj);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-7: Function call argument with FunctionExpression', () => { - const code = `function test(fn) {return fn();} test(function(){return 'call';});`; - const expected = `function test(fn) {\n return fn();\n}\n'call';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Function toString (anti-debugging protection)', () => { - const code = `function test() {return 'test';} test.toString();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-6: Simple wrapper function (handled by safe modules)', () => { - const code = `function wrapper() {return 'literal';} wrapper();`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-7: Call with ThisExpression argument', () => { - const code = `function test(ctx) {return ctx;} test(this);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-8: Member expression call on empty array', () => { - const code = `const arr = []; const fn = a => a.length; fn(arr);`; - const expected = code; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/unsafe/resolveLocalCalls.js')).default; + it('TP-1: Function declaration', () => { + const code = 'function add(a, b) {return a + b;} add(1, 2);'; + const expected = 'function add(a, b) {\n return a + b;\n}\n3;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Arrow function', () => { + const code = 'const add = (a, b) => a + b; add(1, 2);'; + const expected = 'const add = (a, b) => a + b;\n3;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Overwritten builtin', () => { + const code = 'const atob = (a, b) => a + b; atob(\'got-\');'; + const expected = 'const atob = (a, b) => a + b;\n\'got-undefined\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Function expression', () => { + const code = 'const multiply = function(a, b) {return a * b;}; multiply(3, 4);'; + const expected = 'const multiply = function (a, b) {\n return a * b;\n};\n12;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-5: Multiple calls to same function', () => { + const code = 'function double(x) {return x * 2;} double(5); double(10);'; + const expected = 'function double(x) {\n return x * 2;\n}\n10;\n20;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-6: Function returning string', () => { + const code = 'function greet(name) {return \'Hello \' + name;} greet(\'World\');'; + const expected = 'function greet(name) {\n return \'Hello \' + name;\n}\n\'Hello World\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Missing declaration', () => { + const code = 'add(1, 2);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Skipped builtin', () => { + const code = 'btoa(\'a\');'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: No replacement with undefined', () => { + const code = 'function a() {} a();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Complex member expression property access', () => { + const code = 'const obj = {value: \'test\'}; const fn = (o) => o.value; fn(obj);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-7: Function call argument with FunctionExpression', () => { + const code = 'function test(fn) {return fn();} test(function(){return \'call\';});'; + const expected = 'function test(fn) {\n return fn();\n}\n\'call\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Function toString (anti-debugging protection)', () => { + const code = 'function test() {return \'test\';} test.toString();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-6: Simple wrapper function (handled by safe modules)', () => { + const code = 'function wrapper() {return \'literal\';} wrapper();'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-7: Call with ThisExpression argument', () => { + const code = 'function test(ctx) {return ctx;} test(this);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-8: Member expression call on empty array', () => { + const code = 'const arr = []; const fn = a => a.length; fn(arr);'; + const expected = code; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); }); describe('UNSAFE: resolveMinimalAlphabet', async () => { - const targetModule = (await import('../src/modules/unsafe/resolveMinimalAlphabet.js')).default; - it('TP-1: Unary expressions on literals and arrays', () => { - const code = `+true; -true; +false; -false; +[]; ~true; ~false; ~[]; +[3]; +['']; -[4]; ![]; +[[]];`; - const expected = `1;\n-'1';\n0;\n-0;\n0;\n-'2';\n-'1';\n-'1';\n3;\n0;\n-'4';\nfalse;\n0;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Binary expressions with arrays (JSFuck patterns)', () => { - const code = `[] + []; [+[]]; (![]+[]); +[!+[]+!+[]];`; - const expected = `'';\n[0];\n'false';\n2;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Unary expressions on null literal', () => { - const code = `+null; -null; !null;`; - const expected = `0;\n-0;\ntrue;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Binary expressions with string concatenation', () => { - const code = `true + []; false + ''; null + 'test';`; - const expected = `'true';\n'false';\n'nulltest';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Expressions containing ThisExpression should be skipped', () => { - const code = `-false; -[]; +{}; -{}; -'a'; ~{}; -['']; +[1, 2]; +this; +[this];`; - const expected = `-0;\n-0;\n+{};\n-{};\nNaN;\n~{};\n-0;\nNaN;\n+this;\n+[this];`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Binary expressions with non-plus operators', () => { - const code = `true - false; true * false; true / false;`; - const expected = `true - false; true * false; true / false;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Unary expressions on numeric literals', () => { - const code = `+42; -42; ~42; !42;`; - const expected = `+42; -42; ~42; !42;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Unary expressions on undefined identifier', () => { - const code = `+undefined; -undefined;`; - const expected = `+undefined; -undefined;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/unsafe/resolveMinimalAlphabet.js')).default; + it('TP-1: Unary expressions on literals and arrays', () => { + const code = '+true; -true; +false; -false; +[]; ~true; ~false; ~[]; +[3]; +[\'\']; -[4]; ![]; +[[]];'; + const expected = '1;\n-\'1\';\n0;\n-0;\n0;\n-\'2\';\n-\'1\';\n-\'1\';\n3;\n0;\n-\'4\';\nfalse;\n0;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Binary expressions with arrays (JSFuck patterns)', () => { + const code = '[] + []; [+[]]; (![]+[]); +[!+[]+!+[]];'; + const expected = '\'\';\n[0];\n\'false\';\n2;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Unary expressions on null literal', () => { + const code = '+null; -null; !null;'; + const expected = '0;\n-0;\ntrue;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Binary expressions with string concatenation', () => { + const code = 'true + []; false + \'\'; null + \'test\';'; + const expected = '\'true\';\n\'false\';\n\'nulltest\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Expressions containing ThisExpression should be skipped', () => { + const code = '-false; -[]; +{}; -{}; -\'a\'; ~{}; -[\'\']; +[1, 2]; +this; +[this];'; + const expected = '-0;\n-0;\n+{};\n-{};\nNaN;\n~{};\n-0;\nNaN;\n+this;\n+[this];'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Binary expressions with non-plus operators', () => { + const code = 'true - false; true * false; true / false;'; + const expected = 'true - false; true * false; true / false;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Unary expressions on numeric literals', () => { + const code = '+42; -42; ~42; !42;'; + const expected = '+42; -42; ~42; !42;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Unary expressions on undefined identifier', () => { + const code = '+undefined; -undefined;'; + const expected = '+undefined; -undefined;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); }); describe('resolveMemberExpressionsLocalReferences (resolveMemberExpressionsLocalReferences.js)', async () => { - const targetModule = (await import('../src/modules/unsafe/resolveMemberExpressionsLocalReferences.js')).default; - it('TP-1: Array index access with literal', () => { - const code = `const a = [1, 2, 3]; const b = a[1];`; - const expected = `const a = [\n 1,\n 2,\n 3\n];\nconst b = 2;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Object property access with dot notation', () => { - const code = `const obj = {hello: 'world'}; const val = obj.hello;`; - const expected = `const obj = { hello: 'world' };\nconst val = 'world';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Object property access with string literal', () => { - const code = `const obj = {hello: 'world'}; const val = obj['hello'];`; - const expected = `const obj = { hello: 'world' };\nconst val = 'world';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Constructor property access', () => { - const code = `const obj = {constructor: 'test'}; const val = obj.constructor;`; - const expected = `const obj = { constructor: 'test' };\nconst val = 'test';`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Object computed property with identifier variable', () => { - const code = `const obj = {key: 'value'}; const prop = 'key'; const val = obj[prop];`; - const expected = `const obj = {key: 'value'}; const prop = 'key'; const val = obj[prop];`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Array index with identifier variable', () => { - const code = `const a = [10, 20, 30]; const idx = 0; const b = a[idx];`; - const expected = `const a = [10, 20, 30]; const idx = 0; const b = a[idx];`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Function parameter reference', () => { - const code = `function test(param) { const arr = [1, 2, 3]; return arr[param]; }`; - const expected = `function test(param) { const arr = [1, 2, 3]; return arr[param]; }`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Member expression on left side of assignment', () => { - const code = `const obj = {prop: 1}; obj.prop = 2;`; - const expected = `const obj = {prop: 1}; obj.prop = 2;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Member expression used as call expression callee', () => { - const code = `const obj = {fn: function() { return 42; }}; obj.fn();`; - const expected = `const obj = {fn: function() { return 42; }}; obj.fn();`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); - it('TN-6: Property with skipped name (length)', () => { - const code = `const arr = [1, 2, 3]; const val = arr.length;`; - const expected = `const arr = [1, 2, 3]; const val = arr.length;`; - const result = applyModuleToCode(code, targetModule); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/unsafe/resolveMemberExpressionsLocalReferences.js')).default; + it('TP-1: Array index access with literal', () => { + const code = 'const a = [1, 2, 3]; const b = a[1];'; + const expected = 'const a = [\n 1,\n 2,\n 3\n];\nconst b = 2;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Object property access with dot notation', () => { + const code = 'const obj = {hello: \'world\'}; const val = obj.hello;'; + const expected = 'const obj = { hello: \'world\' };\nconst val = \'world\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Object property access with string literal', () => { + const code = 'const obj = {hello: \'world\'}; const val = obj[\'hello\'];'; + const expected = 'const obj = { hello: \'world\' };\nconst val = \'world\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Constructor property access', () => { + const code = 'const obj = {constructor: \'test\'}; const val = obj.constructor;'; + const expected = 'const obj = { constructor: \'test\' };\nconst val = \'test\';'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Object computed property with identifier variable', () => { + const code = 'const obj = {key: \'value\'}; const prop = \'key\'; const val = obj[prop];'; + const expected = 'const obj = {key: \'value\'}; const prop = \'key\'; const val = obj[prop];'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Array index with identifier variable', () => { + const code = 'const a = [10, 20, 30]; const idx = 0; const b = a[idx];'; + const expected = 'const a = [10, 20, 30]; const idx = 0; const b = a[idx];'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Function parameter reference', () => { + const code = 'function test(param) { const arr = [1, 2, 3]; return arr[param]; }'; + const expected = 'function test(param) { const arr = [1, 2, 3]; return arr[param]; }'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Member expression on left side of assignment', () => { + const code = 'const obj = {prop: 1}; obj.prop = 2;'; + const expected = 'const obj = {prop: 1}; obj.prop = 2;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Member expression used as call expression callee', () => { + const code = 'const obj = {fn: function() { return 42; }}; obj.fn();'; + const expected = 'const obj = {fn: function() { return 42; }}; obj.fn();'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); + it('TN-6: Property with skipped name (length)', () => { + const code = 'const arr = [1, 2, 3]; const val = arr.length;'; + const expected = 'const arr = [1, 2, 3]; const val = arr.length;'; + const result = applyModuleToCode(code, targetModule); + assert.deepStrictEqual(result, expected); + }); }); - diff --git a/tests/modules.utils.test.js b/tests/modules.utils.test.js index 4f2e50f..621f5a9 100644 --- a/tests/modules.utils.test.js +++ b/tests/modules.utils.test.js @@ -1,1397 +1,1397 @@ -/* eslint-disable no-unused-vars */ + import assert from 'node:assert'; import {generateFlatAST} from 'flast'; import {describe, it, beforeEach} from 'node:test'; import {BAD_VALUE} from '../src/modules/config.js'; describe('UTILS: evalInVm', async () => { - const targetModule = (await import('../src/modules/utils/evalInVm.js')).evalInVm; - it('TP-1: String concatenation', () => { - const code = `'hello ' + 'there';`; - const expected = {type: 'Literal', value: 'hello there', raw: 'hello there'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Arithmetic operations', () => { - const code = `5 + 3 * 2`; - const expected = {type: 'Literal', value: 11, raw: '11'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Array literal evaluation', () => { - const code = `[1, 2, 3]`; - const result = targetModule(code); - assert.strictEqual(result.type, 'ArrayExpression'); - assert.strictEqual(result.elements.length, 3); - }); - it('TP-4: Object literal evaluation', () => { - const code = `({a: 1, b: 2})`; - const result = targetModule(code); - assert.strictEqual(result.type, 'ObjectExpression'); - assert.strictEqual(result.properties.length, 2); - }); - it('TP-5: Boolean operations', () => { - const code = `true && false`; - const expected = {type: 'Literal', value: false, raw: 'false'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TP-6: Array length property', () => { - const code = `[1, 2, 3].length`; - const expected = {type: 'Literal', value: 3, raw: '3'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TP-7: String method calls', () => { - const code = `'test'.toUpperCase()`; - const expected = {type: 'Literal', value: 'TEST', raw: 'TEST'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TP-8: Caching behavior - identical code returns same result', () => { - const code = `2 + 2`; - const result1 = targetModule(code); - const result2 = targetModule(code); - assert.deepStrictEqual(result1, result2); - }); - it('TP-9: Sandbox reuse', async () => { - const {Sandbox} = await import('../src/modules/utils/sandbox.js'); - const sandbox = new Sandbox(); - const code = `5 * 5`; - const expected = {type: 'Literal', value: 25, raw: '25'}; - const result = targetModule(code, sandbox); - assert.deepStrictEqual(result, expected); - }); - it('TP-10: Multi-statement code with valid operations', () => { - const code = `var x = 5; x * 2`; - const expected = {type: 'Literal', value: 10, raw: '10'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TP-11: Trap neutralization - infinite while loop', () => { - const code = `while(true) {}; 'safe'`; - const expected = {type: 'Literal', value: 'safe', raw: 'safe'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TP-12: Complex expression evaluation', () => { - const code = `Math.pow(2, 3) + 2`; - const expected = {type: 'Literal', value: 10, raw: '10'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TP-14: Debugger statement (neutralized and evaluates successfully)', () => { - const code = `debugger; 42`; - const expected = {type: 'Literal', value: 42, raw: '42'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TP-13: Split debugger string neutralization works', () => { - const code = `'debu' + 'gger'; 123`; - const expected = {type: 'Literal', value: 123, raw: '123'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Non-deterministic function calls', () => { - const code = `Math.random();`; - const expected = BAD_VALUE; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Console object evaluation', () => { - const code = `function a() {return console;} a();`; - const expected = BAD_VALUE; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Promise objects (bad type)', () => { - const code = `Promise.resolve(42)`; - const expected = BAD_VALUE; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Invalid syntax', () => { - const code = `invalid syntax {{{`; - const expected = BAD_VALUE; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Function calls with side effects', () => { - const code = `alert('test')`; - const expected = BAD_VALUE; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TN-6: Variable references (undefined)', () => { - const code = `unknownVariable`; - const expected = BAD_VALUE; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('TN-7: Complex expressions with timing dependencies', () => { - const code = `Date.now()`; - const expected = BAD_VALUE; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/utils/evalInVm.js')).evalInVm; + it('TP-1: String concatenation', () => { + const code = '\'hello \' + \'there\';'; + const expected = {type: 'Literal', value: 'hello there', raw: 'hello there'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Arithmetic operations', () => { + const code = '5 + 3 * 2'; + const expected = {type: 'Literal', value: 11, raw: '11'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Array literal evaluation', () => { + const code = '[1, 2, 3]'; + const result = targetModule(code); + assert.strictEqual(result.type, 'ArrayExpression'); + assert.strictEqual(result.elements.length, 3); + }); + it('TP-4: Object literal evaluation', () => { + const code = '({a: 1, b: 2})'; + const result = targetModule(code); + assert.strictEqual(result.type, 'ObjectExpression'); + assert.strictEqual(result.properties.length, 2); + }); + it('TP-5: Boolean operations', () => { + const code = 'true && false'; + const expected = {type: 'Literal', value: false, raw: 'false'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TP-6: Array length property', () => { + const code = '[1, 2, 3].length'; + const expected = {type: 'Literal', value: 3, raw: '3'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TP-7: String method calls', () => { + const code = '\'test\'.toUpperCase()'; + const expected = {type: 'Literal', value: 'TEST', raw: 'TEST'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TP-8: Caching behavior - identical code returns same result', () => { + const code = '2 + 2'; + const result1 = targetModule(code); + const result2 = targetModule(code); + assert.deepStrictEqual(result1, result2); + }); + it('TP-9: Sandbox reuse', async () => { + const {Sandbox} = await import('../src/modules/utils/sandbox.js'); + const sandbox = new Sandbox(); + const code = '5 * 5'; + const expected = {type: 'Literal', value: 25, raw: '25'}; + const result = targetModule(code, sandbox); + assert.deepStrictEqual(result, expected); + }); + it('TP-10: Multi-statement code with valid operations', () => { + const code = 'var x = 5; x * 2'; + const expected = {type: 'Literal', value: 10, raw: '10'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TP-11: Trap neutralization - infinite while loop', () => { + const code = 'while(true) {}; \'safe\''; + const expected = {type: 'Literal', value: 'safe', raw: 'safe'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TP-12: Complex expression evaluation', () => { + const code = 'Math.pow(2, 3) + 2'; + const expected = {type: 'Literal', value: 10, raw: '10'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TP-14: Debugger statement (neutralized and evaluates successfully)', () => { + const code = 'debugger; 42'; + const expected = {type: 'Literal', value: 42, raw: '42'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TP-13: Split debugger string neutralization works', () => { + const code = '\'debu\' + \'gger\'; 123'; + const expected = {type: 'Literal', value: 123, raw: '123'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Non-deterministic function calls', () => { + const code = 'Math.random();'; + const expected = BAD_VALUE; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Console object evaluation', () => { + const code = 'function a() {return console;} a();'; + const expected = BAD_VALUE; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Promise objects (bad type)', () => { + const code = 'Promise.resolve(42)'; + const expected = BAD_VALUE; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Invalid syntax', () => { + const code = 'invalid syntax {{{'; + const expected = BAD_VALUE; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Function calls with side effects', () => { + const code = 'alert(\'test\')'; + const expected = BAD_VALUE; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TN-6: Variable references (undefined)', () => { + const code = 'unknownVariable'; + const expected = BAD_VALUE; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('TN-7: Complex expressions with timing dependencies', () => { + const code = 'Date.now()'; + const expected = BAD_VALUE; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); }); describe('UTILS: areReferencesModified', async () => { - const targetModule = (await import('../src/modules/utils/areReferencesModified.js')).areReferencesModified; - it('TP-1: Update expression', () => { - const code = `let a = 1; let b = 2 + a, c = a + 3; a++;`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = 1').id.references); - assert.ok(result); - }); - it('TP-2: Direct assignment', () => { - const code = `let a = 1; let b = 2 + a, c = (a += 2) + 3;`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = 1').id.references); - assert.ok(result); - }); - it('TP-3: Assignment to property', () => { - const code = `const a = {b: 2}; a.b = 1;`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = {b: 2}').id.references); - assert.ok(result); - }); - it('TP-4: Re-assignment to property', () => { - const code = `const a = {b: 2}; a.b = 1; a.c = a.b; a.b = 3;`; - const ast = generateFlatAST(code); - const result = targetModule(ast, [ast.find(n => n.src === `a.c = a.b`)?.right]); - assert.ok(result); - }); - it('TP-5: Delete operation on object property', () => { - const code = `const a = {b: 1, c: 2}; delete a.b;`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = {b: 1, c: 2}').id.references); - assert.ok(result); - }); - it('TP-6: Delete operation on array element', () => { - const code = `const a = [1, 2, 3]; delete a[1];`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = [1, 2, 3]').id.references); - assert.ok(result); - }); - it('TP-7: For-in loop variable modification', () => { - const code = `const a = {x: 1}; for (a.prop in {y: 2}) {}`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = {x: 1}').id.references); - assert.ok(result); - }); - it('TP-8: For-of loop variable modification', () => { - const code = `let a = []; for (a.item of [1, 2, 3]) {}`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = []').id.references); - assert.ok(result); - }); - it('TP-9: Array mutating method call', () => { - const code = `const a = [1, 2]; a.push(3);`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = [1, 2]').id.references); - assert.ok(result); - }); - it('TP-10: Array sort method call', () => { - const code = `const a = [3, 1, 2]; a.sort();`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = [3, 1, 2]').id.references); - assert.ok(result); - }); - it('TP-11: Object destructuring assignment', () => { - const code = `let a = {x: 1}; ({x: a.y} = {x: 2});`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = {x: 1}').id.references); - assert.ok(result); - }); - it('TP-12: Array destructuring assignment', () => { - const code = `let a = [1]; [a.item] = [2];`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = [1]').id.references); - assert.ok(result); - }); - it('TP-13: Update expression on member expression', () => { - const code = `const a = {count: 0}; a.count++;`; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = {count: 0}').id.references); - assert.ok(result); - }); - it('TN-1: No assignment', () => { - const code = `const a = 1; let b = 2 + a, c = a + 3;`; - const expected = false; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = 1').id.references); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Read-only property access', () => { - const code = `const a = {b: 1}; const c = a.b;`; - const expected = false; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = {b: 1}').id.references); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Read-only array access', () => { - const code = `const a = [1, 2, 3]; const b = a[1];`; - const expected = false; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = [1, 2, 3]').id.references); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Non-mutating method calls', () => { - const code = `const a = [1, 2, 3]; const b = a.slice(1);`; - const expected = false; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = [1, 2, 3]').id.references); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: For-in loop with different variable', () => { - const code = `const a = {x: 1}; for (let key in a) {}`; - const expected = false; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = {x: 1}').id.references); - assert.deepStrictEqual(result, expected); - }); - it('TN-6: Safe destructuring (different variable)', () => { - const code = `const a = {x: 1}; const {x} = a;`; - const expected = false; - const ast = generateFlatAST(code); - const result = targetModule(ast, ast.find(n => n.src === 'a = {x: 1}').id.references); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/utils/areReferencesModified.js')).areReferencesModified; + it('TP-1: Update expression', () => { + const code = 'let a = 1; let b = 2 + a, c = a + 3; a++;'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = 1').id.references); + assert.ok(result); + }); + it('TP-2: Direct assignment', () => { + const code = 'let a = 1; let b = 2 + a, c = (a += 2) + 3;'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = 1').id.references); + assert.ok(result); + }); + it('TP-3: Assignment to property', () => { + const code = 'const a = {b: 2}; a.b = 1;'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = {b: 2}').id.references); + assert.ok(result); + }); + it('TP-4: Re-assignment to property', () => { + const code = 'const a = {b: 2}; a.b = 1; a.c = a.b; a.b = 3;'; + const ast = generateFlatAST(code); + const result = targetModule(ast, [ast.find(n => n.src === 'a.c = a.b')?.right]); + assert.ok(result); + }); + it('TP-5: Delete operation on object property', () => { + const code = 'const a = {b: 1, c: 2}; delete a.b;'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = {b: 1, c: 2}').id.references); + assert.ok(result); + }); + it('TP-6: Delete operation on array element', () => { + const code = 'const a = [1, 2, 3]; delete a[1];'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = [1, 2, 3]').id.references); + assert.ok(result); + }); + it('TP-7: For-in loop variable modification', () => { + const code = 'const a = {x: 1}; for (a.prop in {y: 2}) {}'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = {x: 1}').id.references); + assert.ok(result); + }); + it('TP-8: For-of loop variable modification', () => { + const code = 'let a = []; for (a.item of [1, 2, 3]) {}'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = []').id.references); + assert.ok(result); + }); + it('TP-9: Array mutating method call', () => { + const code = 'const a = [1, 2]; a.push(3);'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = [1, 2]').id.references); + assert.ok(result); + }); + it('TP-10: Array sort method call', () => { + const code = 'const a = [3, 1, 2]; a.sort();'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = [3, 1, 2]').id.references); + assert.ok(result); + }); + it('TP-11: Object destructuring assignment', () => { + const code = 'let a = {x: 1}; ({x: a.y} = {x: 2});'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = {x: 1}').id.references); + assert.ok(result); + }); + it('TP-12: Array destructuring assignment', () => { + const code = 'let a = [1]; [a.item] = [2];'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = [1]').id.references); + assert.ok(result); + }); + it('TP-13: Update expression on member expression', () => { + const code = 'const a = {count: 0}; a.count++;'; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = {count: 0}').id.references); + assert.ok(result); + }); + it('TN-1: No assignment', () => { + const code = 'const a = 1; let b = 2 + a, c = a + 3;'; + const expected = false; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = 1').id.references); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Read-only property access', () => { + const code = 'const a = {b: 1}; const c = a.b;'; + const expected = false; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = {b: 1}').id.references); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Read-only array access', () => { + const code = 'const a = [1, 2, 3]; const b = a[1];'; + const expected = false; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = [1, 2, 3]').id.references); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Non-mutating method calls', () => { + const code = 'const a = [1, 2, 3]; const b = a.slice(1);'; + const expected = false; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = [1, 2, 3]').id.references); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: For-in loop with different variable', () => { + const code = 'const a = {x: 1}; for (let key in a) {}'; + const expected = false; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = {x: 1}').id.references); + assert.deepStrictEqual(result, expected); + }); + it('TN-6: Safe destructuring (different variable)', () => { + const code = 'const a = {x: 1}; const {x} = a;'; + const expected = false; + const ast = generateFlatAST(code); + const result = targetModule(ast, ast.find(n => n.src === 'a = {x: 1}').id.references); + assert.deepStrictEqual(result, expected); + }); }); describe('UTILS: createNewNode', async () => { - const targetModule = (await import('../src/modules/utils/createNewNode.js')).createNewNode; - it('Literan: String', () => { - const code = 'Baryo'; - const expected = {type: 'Literal', value: 'Baryo', raw: 'Baryo'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Literan: String that starts with !', () => { - const code = '!Baryo'; - const expected = {type: 'Literal', value: '!Baryo', raw: '!Baryo'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Literal: Number - positive number', () => { - const code = 3; - const expected = {type: 'Literal', value: 3, raw: '3'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Literal: Number - negative number', () => { - const code = -3; - const expected = {type: 'UnaryExpression', operator: '-', argument: {type: 'Literal', value: '3', raw: '3'}}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Literal: Number - negative infinity', () => { - const code = -Infinity; - const expected = {type: 'UnaryExpression', operator: '-', argument: {type: 'Identifier', name: 'Infinity'}}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Literal: Number - negative zero', () => { - const code = -0; - const expected = {type: 'UnaryExpression', operator: '-', argument: {type: 'Literal', value: 0, raw: '0'}}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Literal: Number - NOT operator', () => { - const code = '!3'; - const expected = {type: 'UnaryExpression', operator: '!', argument: {type: 'Literal', value: '3', raw: '3'}}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Literal: Number - Identifier', () => { - const code1 = Infinity; - const expected1 = {type: 'Identifier', name: 'Infinity'}; - const result1 = targetModule(code1); - assert.deepStrictEqual(result1, expected1); - const code2 = NaN; - const expected2 = {type: 'Identifier', name: 'NaN'}; - const result2 = targetModule(code2); - assert.deepStrictEqual(result2, expected2); - }); - it('Literal: Boolean', () => { - const code = true; - const expected = {type: 'Literal', value: true, 'raw': 'true'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Array: empty', () => { - const code = []; - const expected = {type: 'ArrayExpression', elements: []}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Array: populated', () => { - const code = [1, 'a']; - const expected = {type: 'ArrayExpression', elements: [ - {type: 'Literal', value: 1, raw: '1'}, - {type: 'Literal', value: 'a', raw: 'a'} - ]}; - const result = targetModule(code); - assert.deepEqual(result, expected); - }); - it('Object: empty', () => { - const code = {}; - const expected = {type: 'ObjectExpression', properties: []}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Object: populated', () => { - const code = {a: 1}; - const expected = {type: 'ObjectExpression', properties: [{ - type: 'Property', - key: {type: 'Literal', value: 'a', raw: 'a'}, - value: {type: 'Literal', value: 1, raw: '1'} - }]}; - const result = targetModule(code); - assert.deepEqual(result, expected); - }); - it('Object: populated with BAD_VALUE', () => { - const code = {a() {}}; - const expected = BAD_VALUE; - const result = targetModule(code); - assert.deepEqual(result, expected); - }); - it('Undefined', () => { - const code = undefined; - const expected = {type: 'Identifier', name: 'undefined'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Null', () => { - const code = null; - const expected = {type: 'Literal', raw: 'null'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it.todo('TODO: Implement Function', () => { - }); - it('RegExp', () => { - const code = /regexp/gi; - const expected = {type: 'Literal', regex: {flags: 'gi', pattern: 'regexp'}}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('BigInt', () => { - const code = 123n; - const expected = {type: 'Literal', value: 123n, raw: '123n', bigint: '123'}; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Symbol with description', () => { - const code = Symbol('test'); - const expected = { - type: 'CallExpression', - callee: {type: 'Identifier', name: 'Symbol'}, - arguments: [{type: 'Literal', value: 'test', raw: 'test'}] - }; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); - it('Symbol without description', () => { - const code = Symbol(); - const expected = { - type: 'CallExpression', - callee: {type: 'Identifier', name: 'Symbol'}, - arguments: [] - }; - const result = targetModule(code); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/utils/createNewNode.js')).createNewNode; + it('Literan: String', () => { + const code = 'Baryo'; + const expected = {type: 'Literal', value: 'Baryo', raw: 'Baryo'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Literan: String that starts with !', () => { + const code = '!Baryo'; + const expected = {type: 'Literal', value: '!Baryo', raw: '!Baryo'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Literal: Number - positive number', () => { + const code = 3; + const expected = {type: 'Literal', value: 3, raw: '3'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Literal: Number - negative number', () => { + const code = -3; + const expected = {type: 'UnaryExpression', operator: '-', argument: {type: 'Literal', value: '3', raw: '3'}}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Literal: Number - negative infinity', () => { + const code = -Infinity; + const expected = {type: 'UnaryExpression', operator: '-', argument: {type: 'Identifier', name: 'Infinity'}}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Literal: Number - negative zero', () => { + const code = -0; + const expected = {type: 'UnaryExpression', operator: '-', argument: {type: 'Literal', value: 0, raw: '0'}}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Literal: Number - NOT operator', () => { + const code = '!3'; + const expected = {type: 'UnaryExpression', operator: '!', argument: {type: 'Literal', value: '3', raw: '3'}}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Literal: Number - Identifier', () => { + const code1 = Infinity; + const expected1 = {type: 'Identifier', name: 'Infinity'}; + const result1 = targetModule(code1); + assert.deepStrictEqual(result1, expected1); + const code2 = NaN; + const expected2 = {type: 'Identifier', name: 'NaN'}; + const result2 = targetModule(code2); + assert.deepStrictEqual(result2, expected2); + }); + it('Literal: Boolean', () => { + const code = true; + const expected = {type: 'Literal', value: true, 'raw': 'true'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Array: empty', () => { + const code = []; + const expected = {type: 'ArrayExpression', elements: []}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Array: populated', () => { + const code = [1, 'a']; + const expected = {type: 'ArrayExpression', elements: [ + {type: 'Literal', value: 1, raw: '1'}, + {type: 'Literal', value: 'a', raw: 'a'}, + ]}; + const result = targetModule(code); + assert.deepEqual(result, expected); + }); + it('Object: empty', () => { + const code = {}; + const expected = {type: 'ObjectExpression', properties: []}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Object: populated', () => { + const code = {a: 1}; + const expected = {type: 'ObjectExpression', properties: [{ + type: 'Property', + key: {type: 'Literal', value: 'a', raw: 'a'}, + value: {type: 'Literal', value: 1, raw: '1'}, + }]}; + const result = targetModule(code); + assert.deepEqual(result, expected); + }); + it('Object: populated with BAD_VALUE', () => { + const code = {a() {}}; + const expected = BAD_VALUE; + const result = targetModule(code); + assert.deepEqual(result, expected); + }); + it('Undefined', () => { + const code = undefined; + const expected = {type: 'Identifier', name: 'undefined'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Null', () => { + const code = null; + const expected = {type: 'Literal', raw: 'null'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it.todo('TODO: Implement Function', () => { + }); + it('RegExp', () => { + const code = /regexp/gi; + const expected = {type: 'Literal', regex: {flags: 'gi', pattern: 'regexp'}}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('BigInt', () => { + const code = 123n; + const expected = {type: 'Literal', value: 123n, raw: '123n', bigint: '123'}; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Symbol with description', () => { + const code = Symbol('test'); + const expected = { + type: 'CallExpression', + callee: {type: 'Identifier', name: 'Symbol'}, + arguments: [{type: 'Literal', value: 'test', raw: 'test'}], + }; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); + it('Symbol without description', () => { + const code = Symbol(); + const expected = { + type: 'CallExpression', + callee: {type: 'Identifier', name: 'Symbol'}, + arguments: [], + }; + const result = targetModule(code); + assert.deepStrictEqual(result, expected); + }); }); describe('UTILS: doesDescendantMatchCondition', async () => { - const targetModule = (await import('../src/modules/utils/doesDescendantMatchCondition.js')).doesDescendantMatchCondition; - - it('TP-1: Find descendant by type (boolean return)', () => { - const code = `function test() { return this.prop; }`; - const ast = generateFlatAST(code); - const functionNode = ast.find(n => n.type === 'FunctionDeclaration'); - const result = targetModule(functionNode, n => n.type === 'ThisExpression'); - assert.ok(result); - }); - it('TP-2: Find descendant by type (node return)', () => { - const code = `function test() { return this.prop; }`; - const ast = generateFlatAST(code); - const functionNode = ast.find(n => n.type === 'FunctionDeclaration'); - const result = targetModule(functionNode, n => n.type === 'ThisExpression', true); - assert.strictEqual(result.type, 'ThisExpression'); - }); - it('TP-3: Find marked descendant (simulating isMarked property)', () => { - const code = `const a = 1 + 2;`; - const ast = generateFlatAST(code); - const varDecl = ast.find(n => n.type === 'VariableDeclaration'); - // Simulate marking a descendant node - const binaryExpr = ast.find(n => n.type === 'BinaryExpression'); - binaryExpr.isMarked = true; - const result = targetModule(varDecl, n => n.isMarked); - assert.ok(result); - }); - it('TP-4: Multiple nested descendants', () => { - const code = `function outer() { function inner() { return this.value; } }`; - const ast = generateFlatAST(code); - const outerFunc = ast.find(n => n.type === 'FunctionDeclaration' && n.id.name === 'outer'); - const result = targetModule(outerFunc, n => n.type === 'ThisExpression'); - assert.ok(result); - }); - it('TP-5: Find specific assignment pattern', () => { - const code = `const obj = {prop: value}; obj.prop = newValue;`; - const ast = generateFlatAST(code); - const program = ast[0]; - const result = targetModule(program, n => - n.type === 'AssignmentExpression' && - n.left?.property?.name === 'prop' - ); - assert.ok(result); - }); - it('TN-1: No matching descendants', () => { - const code = `const a = 1 + 2;`; - const ast = generateFlatAST(code); - const varDecl = ast.find(n => n.type === 'VariableDeclaration'); - const result = targetModule(varDecl, n => n.type === 'ThisExpression'); - assert.strictEqual(result, false); - }); - it('TN-2: Node itself matches condition', () => { - const code = `const a = 1;`; - const ast = generateFlatAST(code); - const literal = ast.find(n => n.type === 'Literal'); - const result = targetModule(literal, n => n.type === 'Literal'); - assert.ok(result); // Should find the node itself - }); - it('TN-3: Null/undefined input handling', () => { - const result1 = targetModule(null, n => n.type === 'Literal'); - const result2 = targetModule(undefined, n => n.type === 'Literal'); - const result3 = targetModule({}, null); - const result4 = targetModule({}, undefined); - assert.strictEqual(result1, false); - assert.strictEqual(result2, false); - assert.strictEqual(result3, false); - assert.strictEqual(result4, false); - }); - it('TN-4: Node with no children', () => { - const code = `const name = 'test';`; - const ast = generateFlatAST(code); - const literal = ast.find(n => n.type === 'Literal'); - const result = targetModule(literal, n => n.type === 'ThisExpression'); - assert.strictEqual(result, false); - }); - it('TN-5: Empty childNodes array', () => { - const mockNode = { type: 'MockNode', childNodes: [] }; - const result = targetModule(mockNode, n => n.type === 'ThisExpression'); - assert.strictEqual(result, false); - }); + const targetModule = (await import('../src/modules/utils/doesDescendantMatchCondition.js')).doesDescendantMatchCondition; + + it('TP-1: Find descendant by type (boolean return)', () => { + const code = 'function test() { return this.prop; }'; + const ast = generateFlatAST(code); + const functionNode = ast.find(n => n.type === 'FunctionDeclaration'); + const result = targetModule(functionNode, n => n.type === 'ThisExpression'); + assert.ok(result); + }); + it('TP-2: Find descendant by type (node return)', () => { + const code = 'function test() { return this.prop; }'; + const ast = generateFlatAST(code); + const functionNode = ast.find(n => n.type === 'FunctionDeclaration'); + const result = targetModule(functionNode, n => n.type === 'ThisExpression', true); + assert.strictEqual(result.type, 'ThisExpression'); + }); + it('TP-3: Find marked descendant (simulating isMarked property)', () => { + const code = 'const a = 1 + 2;'; + const ast = generateFlatAST(code); + const varDecl = ast.find(n => n.type === 'VariableDeclaration'); + // Simulate marking a descendant node + const binaryExpr = ast.find(n => n.type === 'BinaryExpression'); + binaryExpr.isMarked = true; + const result = targetModule(varDecl, n => n.isMarked); + assert.ok(result); + }); + it('TP-4: Multiple nested descendants', () => { + const code = 'function outer() { function inner() { return this.value; } }'; + const ast = generateFlatAST(code); + const outerFunc = ast.find(n => n.type === 'FunctionDeclaration' && n.id.name === 'outer'); + const result = targetModule(outerFunc, n => n.type === 'ThisExpression'); + assert.ok(result); + }); + it('TP-5: Find specific assignment pattern', () => { + const code = 'const obj = {prop: value}; obj.prop = newValue;'; + const ast = generateFlatAST(code); + const program = ast[0]; + const result = targetModule(program, n => + n.type === 'AssignmentExpression' && + n.left?.property?.name === 'prop', + ); + assert.ok(result); + }); + it('TN-1: No matching descendants', () => { + const code = 'const a = 1 + 2;'; + const ast = generateFlatAST(code); + const varDecl = ast.find(n => n.type === 'VariableDeclaration'); + const result = targetModule(varDecl, n => n.type === 'ThisExpression'); + assert.strictEqual(result, false); + }); + it('TN-2: Node itself matches condition', () => { + const code = 'const a = 1;'; + const ast = generateFlatAST(code); + const literal = ast.find(n => n.type === 'Literal'); + const result = targetModule(literal, n => n.type === 'Literal'); + assert.ok(result); // Should find the node itself + }); + it('TN-3: Null/undefined input handling', () => { + const result1 = targetModule(null, n => n.type === 'Literal'); + const result2 = targetModule(undefined, n => n.type === 'Literal'); + const result3 = targetModule({}, null); + const result4 = targetModule({}, undefined); + assert.strictEqual(result1, false); + assert.strictEqual(result2, false); + assert.strictEqual(result3, false); + assert.strictEqual(result4, false); + }); + it('TN-4: Node with no children', () => { + const code = 'const name = \'test\';'; + const ast = generateFlatAST(code); + const literal = ast.find(n => n.type === 'Literal'); + const result = targetModule(literal, n => n.type === 'ThisExpression'); + assert.strictEqual(result, false); + }); + it('TN-5: Empty childNodes array', () => { + const mockNode = {type: 'MockNode', childNodes: []}; + const result = targetModule(mockNode, n => n.type === 'ThisExpression'); + assert.strictEqual(result, false); + }); }); describe('UTILS: generateHash', async () => { - const targetModule = (await import('../src/modules/utils/generateHash.js')).generateHash; - - it('TP-1: Generate hash for normal string', () => { - const input = 'const a = 1;'; - const result = targetModule(input); - assert.strictEqual(typeof result, 'string'); - assert.strictEqual(result.length, 32); // MD5 produces 32-char hex - assert.match(result, /^[a-f0-9]{32}$/); // Valid hex string - }); - it('TP-2: Generate hash for AST node with .src property', () => { - const mockNode = { src: 'const b = 2;', type: 'VariableDeclaration' }; - const result = targetModule(mockNode); - assert.strictEqual(typeof result, 'string'); - assert.strictEqual(result.length, 32); - assert.match(result, /^[a-f0-9]{32}$/); - }); - it('TP-3: Generate hash for number input', () => { - const result = targetModule(42); - assert.strictEqual(typeof result, 'string'); - assert.strictEqual(result.length, 32); - assert.match(result, /^[a-f0-9]{32}$/); - }); - it('TP-4: Generate hash for boolean input', () => { - const result = targetModule(true); - assert.strictEqual(typeof result, 'string'); - assert.strictEqual(result.length, 32); - assert.match(result, /^[a-f0-9]{32}$/); - }); - it('TP-5: Generate hash for empty string', () => { - const result = targetModule(''); - assert.strictEqual(typeof result, 'string'); - assert.strictEqual(result.length, 32); - assert.match(result, /^[a-f0-9]{32}$/); - }); - it('TP-6: Consistent hashes for identical inputs', () => { - const input = 'function test() {}'; - const hash1 = targetModule(input); - const hash2 = targetModule(input); - assert.strictEqual(hash1, hash2); - }); - it('TP-7: Different hashes for different inputs', () => { - const hash1 = targetModule('const a = 1;'); - const hash2 = targetModule('const a = 2;'); - assert.notStrictEqual(hash1, hash2); - }); - it('TN-1: Handle null input gracefully', () => { - const result = targetModule(null); - assert.strictEqual(result, 'null-undefined-hash'); - }); - it('TN-2: Handle undefined input gracefully', () => { - const result = targetModule(undefined); - assert.strictEqual(result, 'null-undefined-hash'); - }); - it('TN-3: Handle object without .src property', () => { - const mockObj = { type: 'SomeNode', value: 42 }; - const result = targetModule(mockObj); - assert.strictEqual(typeof result, 'string'); - // Should convert object to string representation - assert.match(result, /^[a-f0-9]{32}$/); - }); + const targetModule = (await import('../src/modules/utils/generateHash.js')).generateHash; + + it('TP-1: Generate hash for normal string', () => { + const input = 'const a = 1;'; + const result = targetModule(input); + assert.strictEqual(typeof result, 'string'); + assert.strictEqual(result.length, 32); // MD5 produces 32-char hex + assert.match(result, /^[a-f0-9]{32}$/); // Valid hex string + }); + it('TP-2: Generate hash for AST node with .src property', () => { + const mockNode = {src: 'const b = 2;', type: 'VariableDeclaration'}; + const result = targetModule(mockNode); + assert.strictEqual(typeof result, 'string'); + assert.strictEqual(result.length, 32); + assert.match(result, /^[a-f0-9]{32}$/); + }); + it('TP-3: Generate hash for number input', () => { + const result = targetModule(42); + assert.strictEqual(typeof result, 'string'); + assert.strictEqual(result.length, 32); + assert.match(result, /^[a-f0-9]{32}$/); + }); + it('TP-4: Generate hash for boolean input', () => { + const result = targetModule(true); + assert.strictEqual(typeof result, 'string'); + assert.strictEqual(result.length, 32); + assert.match(result, /^[a-f0-9]{32}$/); + }); + it('TP-5: Generate hash for empty string', () => { + const result = targetModule(''); + assert.strictEqual(typeof result, 'string'); + assert.strictEqual(result.length, 32); + assert.match(result, /^[a-f0-9]{32}$/); + }); + it('TP-6: Consistent hashes for identical inputs', () => { + const input = 'function test() {}'; + const hash1 = targetModule(input); + const hash2 = targetModule(input); + assert.strictEqual(hash1, hash2); + }); + it('TP-7: Different hashes for different inputs', () => { + const hash1 = targetModule('const a = 1;'); + const hash2 = targetModule('const a = 2;'); + assert.notStrictEqual(hash1, hash2); + }); + it('TN-1: Handle null input gracefully', () => { + const result = targetModule(null); + assert.strictEqual(result, 'null-undefined-hash'); + }); + it('TN-2: Handle undefined input gracefully', () => { + const result = targetModule(undefined); + assert.strictEqual(result, 'null-undefined-hash'); + }); + it('TN-3: Handle object without .src property', () => { + const mockObj = {type: 'SomeNode', value: 42}; + const result = targetModule(mockObj); + assert.strictEqual(typeof result, 'string'); + // Should convert object to string representation + assert.match(result, /^[a-f0-9]{32}$/); + }); }); describe('UTILS: createOrderedSrc', async () => { - const targetModule = (await import('../src/modules/utils/createOrderedSrc.js')).createOrderedSrc; - it('TP-1: Re-order nodes', () => { - const code = 'a; b;'; - const expected = `a\nb\n`; - const ast = generateFlatAST(code); - const targetNodes = [ - 4, // b() - 2, // a() - ]; - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Wrap calls in expressions', () => { - const code = 'a();'; - const expected = `a();\n`; - const ast = generateFlatAST(code);const targetNodes = [ - 2, // a() - ]; - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Push IIFEs to the end in order', () => { - const code = '(function(a){})(); a(); (function(b){})(); b();'; - const expected = `a();\nb();\n(function(a){})();\n(function(b){})();\n`; - const ast = generateFlatAST(code); - const targetNodes = [ - 10, // (function(b){})() - 15, // b() - 7, // a() - 2, // (function(a){})() - ]; - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Add dynamic name to IIFEs', () => { - const code = '!function(a){}(); a();'; - const expected = `a();\n(function func3(a){}());\n`; - const ast = generateFlatAST(code);const targetNodes = [ - 3, // function(a){}() - 8, // a() - ]; - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it('TP-5: Add variable name to IIFEs', () => { - const code = 'const b = function(a){}(); a();'; - const expected = `a();\n(function b(a){}());\n`; - const ast = generateFlatAST(code);const targetNodes = [ - 4, // function(a){}() - 9, // a() - ]; - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it(`TP-6: Preserve node order`, () => { - const code = '(function(a){})(); a(); (function(b){})(); b();'; - const expected = `(function(a){})();\na();\n(function(b){})();\nb();\n`; - const ast = generateFlatAST(code); - const targetNodes = [ - 10, // (function(b){})() - 7, // a() - 15, // b() - 2, // (function(a){})() - ]; - const result = targetModule(targetNodes.map(n => ast[n]), true); - assert.deepStrictEqual(result, expected); - }); - it(`TP-7: Standalone FEs`, () => { - const code = '~function(iife1){}();~function(iife2){}();'; - const expected = `(function func4(iife1){});\n(function func10(iife2){});\n`; - const ast = generateFlatAST(code); - const targetNodes = [ - 10, // function(iife2){} - 4, // function(iife1){} - ]; - const result = targetModule(targetNodes.map(n => ast[n]), true); - assert.deepStrictEqual(result, expected); - }); - it('TP-8: Variable declarations with semicolons', () => { - const code = 'const a = 1; let b = 2;'; - const expected = `const a = 1;\nlet b = 2;\n`; - const ast = generateFlatAST(code); - const targetNodes = [ - 2, // a = 1 - 5, // b = 2 - ]; - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it('TP-9: Assignment expressions with semicolons', () => { - const code = 'let a; a = 1; a = 2;'; - const expected = `a = 1;\na = 2;\n`; - const ast = generateFlatAST(code); - const targetNodes = [ - 8, // a = 2 (ExpressionStatement) - 4, // a = 1 (ExpressionStatement) - ]; - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it('TP-10: Duplicate node elimination', () => { - const code = 'a(); b();'; - const expected = `a();\nb();\n`; - const ast = generateFlatAST(code); - const duplicatedNodes = [ - 2, // a() - 5, // b() - 2, // a() again (duplicate) - ]; - const result = targetModule(duplicatedNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it('TP-11: IIFE dependency ordering with arguments', () => { - const code = 'const x = 1; (function(a){return a;})(x);'; - const expected = `const x = 1;\n(function(a){return a;})(x);\n`; - const ast = generateFlatAST(code); - const targetNodes = [ - 5, // (function(a){return a;})(x) - 2, // x = 1 - ]; - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Empty node array', () => { - const expected = ''; - const result = targetModule([]); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Single node without reordering', () => { - const code = 'a();'; - const expected = `a();\n`; - const ast = generateFlatAST(code); - const targetNodes = [2]; // a() - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Non-CallExpression and non-FunctionExpression nodes', () => { - const code = 'const a = 1; const b = "hello";'; - const expected = `const a = 1;\nconst b = "hello";\n`; - const ast = generateFlatAST(code); - const targetNodes = [ - 5, // b = "hello" - 2, // a = 1 - ]; - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it('TN-4: CallExpression without ExpressionStatement parent', () => { - const code = 'const result = a();'; - const expected = `const result = a();\n`; - const ast = generateFlatAST(code); - const targetNodes = [2]; // result = a() - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Named function expressions (no renaming needed)', () => { - const code = 'const f = function named() {};'; - const expected = `const f = function named() {};\n`; - const ast = generateFlatAST(code); - const targetNodes = [2]; // f = function named() {} - const result = targetModule(targetNodes.map(n => ast[n])); - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/utils/createOrderedSrc.js')).createOrderedSrc; + it('TP-1: Re-order nodes', () => { + const code = 'a; b;'; + const expected = 'a\nb\n'; + const ast = generateFlatAST(code); + const targetNodes = [ + 4, // b() + 2, // a() + ]; + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Wrap calls in expressions', () => { + const code = 'a();'; + const expected = 'a();\n'; + const ast = generateFlatAST(code);const targetNodes = [ + 2, // a() + ]; + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Push IIFEs to the end in order', () => { + const code = '(function(a){})(); a(); (function(b){})(); b();'; + const expected = 'a();\nb();\n(function(a){})();\n(function(b){})();\n'; + const ast = generateFlatAST(code); + const targetNodes = [ + 10, // (function(b){})() + 15, // b() + 7, // a() + 2, // (function(a){})() + ]; + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Add dynamic name to IIFEs', () => { + const code = '!function(a){}(); a();'; + const expected = 'a();\n(function func3(a){}());\n'; + const ast = generateFlatAST(code);const targetNodes = [ + 3, // function(a){}() + 8, // a() + ]; + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TP-5: Add variable name to IIFEs', () => { + const code = 'const b = function(a){}(); a();'; + const expected = 'a();\n(function b(a){}());\n'; + const ast = generateFlatAST(code);const targetNodes = [ + 4, // function(a){}() + 9, // a() + ]; + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TP-6: Preserve node order', () => { + const code = '(function(a){})(); a(); (function(b){})(); b();'; + const expected = '(function(a){})();\na();\n(function(b){})();\nb();\n'; + const ast = generateFlatAST(code); + const targetNodes = [ + 10, // (function(b){})() + 7, // a() + 15, // b() + 2, // (function(a){})() + ]; + const result = targetModule(targetNodes.map(n => ast[n]), true); + assert.deepStrictEqual(result, expected); + }); + it('TP-7: Standalone FEs', () => { + const code = '~function(iife1){}();~function(iife2){}();'; + const expected = '(function func4(iife1){});\n(function func10(iife2){});\n'; + const ast = generateFlatAST(code); + const targetNodes = [ + 10, // function(iife2){} + 4, // function(iife1){} + ]; + const result = targetModule(targetNodes.map(n => ast[n]), true); + assert.deepStrictEqual(result, expected); + }); + it('TP-8: Variable declarations with semicolons', () => { + const code = 'const a = 1; let b = 2;'; + const expected = 'const a = 1;\nlet b = 2;\n'; + const ast = generateFlatAST(code); + const targetNodes = [ + 2, // a = 1 + 5, // b = 2 + ]; + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TP-9: Assignment expressions with semicolons', () => { + const code = 'let a; a = 1; a = 2;'; + const expected = 'a = 1;\na = 2;\n'; + const ast = generateFlatAST(code); + const targetNodes = [ + 8, // a = 2 (ExpressionStatement) + 4, // a = 1 (ExpressionStatement) + ]; + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TP-10: Duplicate node elimination', () => { + const code = 'a(); b();'; + const expected = 'a();\nb();\n'; + const ast = generateFlatAST(code); + const duplicatedNodes = [ + 2, // a() + 5, // b() + 2, // a() again (duplicate) + ]; + const result = targetModule(duplicatedNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TP-11: IIFE dependency ordering with arguments', () => { + const code = 'const x = 1; (function(a){return a;})(x);'; + const expected = 'const x = 1;\n(function(a){return a;})(x);\n'; + const ast = generateFlatAST(code); + const targetNodes = [ + 5, // (function(a){return a;})(x) + 2, // x = 1 + ]; + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Empty node array', () => { + const expected = ''; + const result = targetModule([]); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Single node without reordering', () => { + const code = 'a();'; + const expected = 'a();\n'; + const ast = generateFlatAST(code); + const targetNodes = [2]; // a() + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Non-CallExpression and non-FunctionExpression nodes', () => { + const code = 'const a = 1; const b = "hello";'; + const expected = 'const a = 1;\nconst b = "hello";\n'; + const ast = generateFlatAST(code); + const targetNodes = [ + 5, // b = "hello" + 2, // a = 1 + ]; + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TN-4: CallExpression without ExpressionStatement parent', () => { + const code = 'const result = a();'; + const expected = 'const result = a();\n'; + const ast = generateFlatAST(code); + const targetNodes = [2]; // result = a() + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Named function expressions (no renaming needed)', () => { + const code = 'const f = function named() {};'; + const expected = 'const f = function named() {};\n'; + const ast = generateFlatAST(code); + const targetNodes = [2]; // f = function named() {} + const result = targetModule(targetNodes.map(n => ast[n])); + assert.deepStrictEqual(result, expected); + }); }); describe('UTILS: getCache', async () => { - const getCache = (await import('../src/modules/utils/getCache.js')).getCache; - - // Reset cache before each test to ensure isolation - beforeEach(() => { - getCache.flush(); - }); - - it('TP-1: Retain values for same script hash', () => { - const hash1 = 'script-hash-1'; - const cache = getCache(hash1); - assert.deepStrictEqual(cache, {}); - - cache['eval-result'] = 'cached-value'; - const cache2 = getCache(hash1); // Same hash should return same cache - assert.deepStrictEqual(cache2, {['eval-result']: 'cached-value'}); - assert.strictEqual(cache, cache2); // Should be same object reference - }); - it('TP-2: Cache invalidation on script hash change', () => { - const hash1 = 'script-hash-1'; - const hash2 = 'script-hash-2'; - - const cache1 = getCache(hash1); - cache1['data'] = 'first-script'; - - // Different hash should get fresh cache - const cache2 = getCache(hash2); - assert.deepStrictEqual(cache2, {}); - assert.notStrictEqual(cache1, cache2); // Different object references - - // Original cache data should be lost - const cache1Again = getCache(hash1); - assert.deepStrictEqual(cache1Again, {}); // Fresh cache for hash1 - }); - it('TP-3: Manual flush preserves script hash', () => { - const hash = 'preserve-hash'; - const cache = getCache(hash); - cache['before-flush'] = 'data'; - - getCache.flush(); - - // Should get empty cache but same hash should not trigger invalidation - const cacheAfterFlush = getCache(hash); - assert.deepStrictEqual(cacheAfterFlush, {}); - }); - it('TP-4: Multiple script hash switches', () => { - const hashes = ['hash-a', 'hash-b', 'hash-c']; - - // Fill cache for each hash - for (let i = 0; i < hashes.length; i++) { - const cache = getCache(hashes[i]); - cache[`data-${i}`] = `value-${i}`; - } - - // Only the last hash should have preserved cache - const finalCache = getCache('hash-c'); - assert.deepStrictEqual(finalCache, {'data-2': 'value-2'}); - - // Previous hashes should get fresh caches - for (const hash of ['hash-a', 'hash-b']) { - const cache = getCache(hash); - assert.deepStrictEqual(cache, {}); - } - }); - it('TP-5: Cache object mutation persistence', () => { - const hash = 'mutation-test'; - const cache1 = getCache(hash); - const cache2 = getCache(hash); - - // Both should reference the same object - cache1['shared'] = 'value'; - assert.strictEqual(cache2['shared'], 'value'); - - cache2['another'] = 'different'; - assert.strictEqual(cache1['another'], 'different'); - }); - it('TN-1: Handle null script hash gracefully', () => { - const cache = getCache(null); - assert.deepStrictEqual(cache, {}); - cache['null-test'] = 'handled'; - - // Should maintain cache for 'no-hash' key - const cache2 = getCache(null); - assert.deepStrictEqual(cache2, {'null-test': 'handled'}); - }); - it('TN-2: Handle undefined script hash gracefully', () => { - const cache = getCache(undefined); - assert.deepStrictEqual(cache, {}); - cache['undefined-test'] = 'handled'; - - // Should maintain cache for 'no-hash' key - const cache2 = getCache(undefined); - assert.deepStrictEqual(cache2, {'undefined-test': 'handled'}); - }); - it('TN-3: Null and undefined should share same fallback cache', () => { - const cache1 = getCache(null); - const cache2 = getCache(undefined); - - cache1['shared-fallback'] = 'test'; - assert.strictEqual(cache2['shared-fallback'], 'test'); - assert.strictEqual(cache1, cache2); // Same object reference - }); - it('TN-4: Empty string script hash', () => { - const cache = getCache(''); - assert.deepStrictEqual(cache, {}); - cache['empty-string'] = 'value'; - - const cache2 = getCache(''); - assert.deepStrictEqual(cache2, {'empty-string': 'value'}); - }); - it('TN-5: Flush after multiple hash changes', () => { - const hash1 = 'multi-1'; - const hash2 = 'multi-2'; - - getCache(hash1)['data1'] = 'value1'; - getCache(hash2)['data2'] = 'value2'; // This invalidates hash1's cache - - getCache.flush(); // Should clear current (hash2) cache - - // Both should now be empty - assert.deepStrictEqual(getCache(hash1), {}); - assert.deepStrictEqual(getCache(hash2), {}); - }); + const getCache = (await import('../src/modules/utils/getCache.js')).getCache; + + // Reset cache before each test to ensure isolation + beforeEach(() => { + getCache.flush(); + }); + + it('TP-1: Retain values for same script hash', () => { + const hash1 = 'script-hash-1'; + const cache = getCache(hash1); + assert.deepStrictEqual(cache, {}); + + cache['eval-result'] = 'cached-value'; + const cache2 = getCache(hash1); // Same hash should return same cache + assert.deepStrictEqual(cache2, {['eval-result']: 'cached-value'}); + assert.strictEqual(cache, cache2); // Should be same object reference + }); + it('TP-2: Cache invalidation on script hash change', () => { + const hash1 = 'script-hash-1'; + const hash2 = 'script-hash-2'; + + const cache1 = getCache(hash1); + cache1.data = 'first-script'; + + // Different hash should get fresh cache + const cache2 = getCache(hash2); + assert.deepStrictEqual(cache2, {}); + assert.notStrictEqual(cache1, cache2); // Different object references + + // Original cache data should be lost + const cache1Again = getCache(hash1); + assert.deepStrictEqual(cache1Again, {}); // Fresh cache for hash1 + }); + it('TP-3: Manual flush preserves script hash', () => { + const hash = 'preserve-hash'; + const cache = getCache(hash); + cache['before-flush'] = 'data'; + + getCache.flush(); + + // Should get empty cache but same hash should not trigger invalidation + const cacheAfterFlush = getCache(hash); + assert.deepStrictEqual(cacheAfterFlush, {}); + }); + it('TP-4: Multiple script hash switches', () => { + const hashes = ['hash-a', 'hash-b', 'hash-c']; + + // Fill cache for each hash + for (let i = 0; i < hashes.length; i++) { + const cache = getCache(hashes[i]); + cache[`data-${i}`] = `value-${i}`; + } + + // Only the last hash should have preserved cache + const finalCache = getCache('hash-c'); + assert.deepStrictEqual(finalCache, {'data-2': 'value-2'}); + + // Previous hashes should get fresh caches + for (const hash of ['hash-a', 'hash-b']) { + const cache = getCache(hash); + assert.deepStrictEqual(cache, {}); + } + }); + it('TP-5: Cache object mutation persistence', () => { + const hash = 'mutation-test'; + const cache1 = getCache(hash); + const cache2 = getCache(hash); + + // Both should reference the same object + cache1.shared = 'value'; + assert.strictEqual(cache2.shared, 'value'); + + cache2.another = 'different'; + assert.strictEqual(cache1.another, 'different'); + }); + it('TN-1: Handle null script hash gracefully', () => { + const cache = getCache(null); + assert.deepStrictEqual(cache, {}); + cache['null-test'] = 'handled'; + + // Should maintain cache for 'no-hash' key + const cache2 = getCache(null); + assert.deepStrictEqual(cache2, {'null-test': 'handled'}); + }); + it('TN-2: Handle undefined script hash gracefully', () => { + const cache = getCache(undefined); + assert.deepStrictEqual(cache, {}); + cache['undefined-test'] = 'handled'; + + // Should maintain cache for 'no-hash' key + const cache2 = getCache(undefined); + assert.deepStrictEqual(cache2, {'undefined-test': 'handled'}); + }); + it('TN-3: Null and undefined should share same fallback cache', () => { + const cache1 = getCache(null); + const cache2 = getCache(undefined); + + cache1['shared-fallback'] = 'test'; + assert.strictEqual(cache2['shared-fallback'], 'test'); + assert.strictEqual(cache1, cache2); // Same object reference + }); + it('TN-4: Empty string script hash', () => { + const cache = getCache(''); + assert.deepStrictEqual(cache, {}); + cache['empty-string'] = 'value'; + + const cache2 = getCache(''); + assert.deepStrictEqual(cache2, {'empty-string': 'value'}); + }); + it('TN-5: Flush after multiple hash changes', () => { + const hash1 = 'multi-1'; + const hash2 = 'multi-2'; + + getCache(hash1).data1 = 'value1'; + getCache(hash2).data2 = 'value2'; // This invalidates hash1's cache + + getCache.flush(); // Should clear current (hash2) cache + + // Both should now be empty + assert.deepStrictEqual(getCache(hash1), {}); + assert.deepStrictEqual(getCache(hash2), {}); + }); }); describe('UTILS: getCalleeName', async () => { - const targetModule = (await import('../src/modules/utils/getCalleeName.js')).getCalleeName; - it('TP-1: Simple identifier callee', () => { - const code = `func();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - assert.strictEqual(result, 'func'); - }); - it('TP-2: Member expression callee (single level)', () => { - const code = `obj.method();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - assert.strictEqual(result, 'obj'); - }); - it('TP-3: Nested member expression callee', () => { - const code = `obj.nested.method();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - assert.strictEqual(result, 'obj'); - }); - it('TP-4: Deeply nested member expression', () => { - const code = `obj.a.b.c.d();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - assert.strictEqual(result, 'obj'); - }); - it('TP-5: Avoid counting collision between function and literal calls', () => { - // This test demonstrates the collision avoidance - const code = `function t1() { return 1; } t1(); 't1'.toString();`; - const ast = generateFlatAST(code); - const calls = ast.filter(n => n.type === 'CallExpression'); - - const functionCall = calls[0]; // t1() - const literalMethodCall = calls[1]; // 't1'.toString() - - assert.strictEqual(targetModule(functionCall), 't1'); // Function call counted - assert.strictEqual(targetModule(literalMethodCall), ''); // Literal method not counted - }); - it('TN-1: Literal string method calls return empty', () => { - const code = `'test'.split('');`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - assert.strictEqual(result, ''); // Don't count literal methods - }); - it('TN-2: Literal number method calls return empty', () => { - const code = `1..toString();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - assert.strictEqual(result, ''); // Don't count literal methods - }); - it('TN-3: ThisExpression method calls return empty', () => { - const code = `this.method();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - assert.strictEqual(result, ''); // Don't count 'this' methods - }); - it('TN-4: Boolean literal method calls return empty', () => { - const code = `true.valueOf();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - assert.strictEqual(result, ''); // Don't count literal methods - }); - it('TN-5: Logical expression callee returns empty', () => { - const code = `(func || fallback)();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - assert.strictEqual(result, ''); // Don't count complex expressions - }); - it('TN-6: CallExpression as base object returns empty', () => { - const code = `func()[0]();`; - const ast = generateFlatAST(code); - const outerCall = ast.filter(n => n.type === 'CallExpression')[0]; // First = outer call func()[0]() - const result = targetModule(outerCall); - assert.strictEqual(result, ''); // Don't count chained calls - }); - it('TN-7: Null/undefined input handling', () => { - const result1 = targetModule(null); - const result2 = targetModule(undefined); - const result3 = targetModule({}); - const result4 = targetModule({callee: null}); - assert.strictEqual(result1, ''); - assert.strictEqual(result2, ''); - assert.strictEqual(result3, ''); - assert.strictEqual(result4, ''); - }); - it('TN-8: Computed member expression with identifier', () => { - const code = `obj[key]();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - assert.strictEqual(result, 'obj'); // Variable method call, return base variable - }); - it('TN-9: Complex callee without name returns empty', () => { - // Create mock node with no name/value - const mockCall = { - callee: { - type: 'SomeComplexExpression', - // No name, value, or object properties - } - }; - const result = targetModule(mockCall); - assert.strictEqual(result, ''); // Complex expressions return empty - }); + const targetModule = (await import('../src/modules/utils/getCalleeName.js')).getCalleeName; + it('TP-1: Simple identifier callee', () => { + const code = 'func();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + assert.strictEqual(result, 'func'); + }); + it('TP-2: Member expression callee (single level)', () => { + const code = 'obj.method();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + assert.strictEqual(result, 'obj'); + }); + it('TP-3: Nested member expression callee', () => { + const code = 'obj.nested.method();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + assert.strictEqual(result, 'obj'); + }); + it('TP-4: Deeply nested member expression', () => { + const code = 'obj.a.b.c.d();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + assert.strictEqual(result, 'obj'); + }); + it('TP-5: Avoid counting collision between function and literal calls', () => { + // This test demonstrates the collision avoidance + const code = 'function t1() { return 1; } t1(); \'t1\'.toString();'; + const ast = generateFlatAST(code); + const calls = ast.filter(n => n.type === 'CallExpression'); + + const functionCall = calls[0]; // t1() + const literalMethodCall = calls[1]; // 't1'.toString() + + assert.strictEqual(targetModule(functionCall), 't1'); // Function call counted + assert.strictEqual(targetModule(literalMethodCall), ''); // Literal method not counted + }); + it('TN-1: Literal string method calls return empty', () => { + const code = '\'test\'.split(\'\');'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + assert.strictEqual(result, ''); // Don't count literal methods + }); + it('TN-2: Literal number method calls return empty', () => { + const code = '1..toString();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + assert.strictEqual(result, ''); // Don't count literal methods + }); + it('TN-3: ThisExpression method calls return empty', () => { + const code = 'this.method();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + assert.strictEqual(result, ''); // Don't count 'this' methods + }); + it('TN-4: Boolean literal method calls return empty', () => { + const code = 'true.valueOf();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + assert.strictEqual(result, ''); // Don't count literal methods + }); + it('TN-5: Logical expression callee returns empty', () => { + const code = '(func || fallback)();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + assert.strictEqual(result, ''); // Don't count complex expressions + }); + it('TN-6: CallExpression as base object returns empty', () => { + const code = 'func()[0]();'; + const ast = generateFlatAST(code); + const outerCall = ast.filter(n => n.type === 'CallExpression')[0]; // First = outer call func()[0]() + const result = targetModule(outerCall); + assert.strictEqual(result, ''); // Don't count chained calls + }); + it('TN-7: Null/undefined input handling', () => { + const result1 = targetModule(null); + const result2 = targetModule(undefined); + const result3 = targetModule({}); + const result4 = targetModule({callee: null}); + assert.strictEqual(result1, ''); + assert.strictEqual(result2, ''); + assert.strictEqual(result3, ''); + assert.strictEqual(result4, ''); + }); + it('TN-8: Computed member expression with identifier', () => { + const code = 'obj[key]();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + assert.strictEqual(result, 'obj'); // Variable method call, return base variable + }); + it('TN-9: Complex callee without name returns empty', () => { + // Create mock node with no name/value + const mockCall = { + callee: { + type: 'SomeComplexExpression', + // No name, value, or object properties + }, + }; + const result = targetModule(mockCall); + assert.strictEqual(result, ''); // Complex expressions return empty + }); }); describe('UTILS: getDeclarationWithContext', async () => { - const targetModule = (await import('../src/modules/utils/getDeclarationWithContext.js')).getDeclarationWithContext; - const getCache = (await import('../src/modules/utils/getCache.js')).getCache; - beforeEach(() => { - getCache.flush(); - }); - it(`TP-1: Call expression with function declaration`, () => { - const code = `function a() {return 1;}\na();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - const expected = [ast[7], ast[1]]; - assert.deepStrictEqual(result, expected); - }); - it(`TP-2: Call expression with function expression`, () => { - const code = `const a = () => 2;\na();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - const expected = [ast[7], ast[2]]; - assert.deepStrictEqual(result, expected); - }); - it(`TP-3: Nested call with FE`, () => { - const code = `const b = 3;\nconst a = () => b;\na();`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'CallExpression')); - const expected = [ast[11], ast[6], ast[2]]; - assert.deepStrictEqual(result, expected); - }); - it(`TP-4: Anti-debugging function overwrite`, () => { - const code = `function a() {}\na = {};\na.b = 2;\na = {};\na(a.b);`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'FunctionDeclaration')); - const expected = [ast[1], ast[9]]; - assert.deepStrictEqual(result, expected); - }); - it(`TP-5: Collect assignments on references`, () => { - const code = `let a = 1; function b(arg) {arg = 3;} b(a);`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'Identifier' && n.name === 'a')); - const expected = [ast[2], ast[14], ast[5]]; - assert.deepStrictEqual(result, expected); - }); - it(`TP-6: Collect relevant parents for anonymous FE`, () => { - const code = `(function() {})()`; - const ast = generateFlatAST(code); - const result = targetModule(ast.find(n => n.type === 'FunctionExpression')); - const expected = [ast[2]]; - assert.deepStrictEqual(result, expected); - }); - it(`TP-7: Node without scriptHash should still work` , () => { - const code = `function test() { return 42; } test();`; - const ast = generateFlatAST(code); - const callNode = ast.find(n => n.type === 'CallExpression'); - delete callNode.scriptHash; // Remove scriptHash property - const result = targetModule(callNode); - const expected = [ast.find(n => n.type === 'CallExpression'), ast.find(n => n.type === 'FunctionDeclaration')]; - assert.deepStrictEqual(result, expected); - }); - it(`TP-8: Node without nodeId should still work` , () => { - const code = `const x = 1; console.log(x);`; - const ast = generateFlatAST(code); - const callNode = ast.find(n => n.type === 'CallExpression'); - delete callNode.nodeId; // Remove nodeId property - const result = targetModule(callNode); - assert.ok(Array.isArray(result)); - assert.ok(result.length > 0); - }); - it(`TN-1: Prevent collection before changes are applied` , () => { - const code = `function a() {}\na = {};\na.b = 2;\na = a.b;\na(a.b);`; - const ast = generateFlatAST(code); - ast[9].isMarked = true; - const result = targetModule(ast.find(n => n.src === 'a = a.b'), true); - const expected = []; - assert.deepStrictEqual(result, expected); - }); - it(`TN-2: Handle null input gracefully` , () => { - const result = targetModule(null); - const expected = []; - assert.deepStrictEqual(result, expected); - }); - it(`TN-3: Handle undefined input gracefully` , () => { - const result = targetModule(undefined); - const expected = []; - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/utils/getDeclarationWithContext.js')).getDeclarationWithContext; + const getCache = (await import('../src/modules/utils/getCache.js')).getCache; + beforeEach(() => { + getCache.flush(); + }); + it('TP-1: Call expression with function declaration', () => { + const code = 'function a() {return 1;}\na();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + const expected = [ast[7], ast[1]]; + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Call expression with function expression', () => { + const code = 'const a = () => 2;\na();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + const expected = [ast[7], ast[2]]; + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Nested call with FE', () => { + const code = 'const b = 3;\nconst a = () => b;\na();'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'CallExpression')); + const expected = [ast[11], ast[6], ast[2]]; + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Anti-debugging function overwrite', () => { + const code = 'function a() {}\na = {};\na.b = 2;\na = {};\na(a.b);'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'FunctionDeclaration')); + const expected = [ast[1], ast[9]]; + assert.deepStrictEqual(result, expected); + }); + it('TP-5: Collect assignments on references', () => { + const code = 'let a = 1; function b(arg) {arg = 3;} b(a);'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'Identifier' && n.name === 'a')); + const expected = [ast[2], ast[14], ast[5]]; + assert.deepStrictEqual(result, expected); + }); + it('TP-6: Collect relevant parents for anonymous FE', () => { + const code = '(function() {})()'; + const ast = generateFlatAST(code); + const result = targetModule(ast.find(n => n.type === 'FunctionExpression')); + const expected = [ast[2]]; + assert.deepStrictEqual(result, expected); + }); + it('TP-7: Node without scriptHash should still work' , () => { + const code = 'function test() { return 42; } test();'; + const ast = generateFlatAST(code); + const callNode = ast.find(n => n.type === 'CallExpression'); + delete callNode.scriptHash; // Remove scriptHash property + const result = targetModule(callNode); + const expected = [ast.find(n => n.type === 'CallExpression'), ast.find(n => n.type === 'FunctionDeclaration')]; + assert.deepStrictEqual(result, expected); + }); + it('TP-8: Node without nodeId should still work' , () => { + const code = 'const x = 1; console.log(x);'; + const ast = generateFlatAST(code); + const callNode = ast.find(n => n.type === 'CallExpression'); + delete callNode.nodeId; // Remove nodeId property + const result = targetModule(callNode); + assert.ok(Array.isArray(result)); + assert.ok(result.length > 0); + }); + it('TN-1: Prevent collection before changes are applied' , () => { + const code = 'function a() {}\na = {};\na.b = 2;\na = a.b;\na(a.b);'; + const ast = generateFlatAST(code); + ast[9].isMarked = true; + const result = targetModule(ast.find(n => n.src === 'a = a.b'), true); + const expected = []; + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Handle null input gracefully' , () => { + const result = targetModule(null); + const expected = []; + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Handle undefined input gracefully' , () => { + const result = targetModule(undefined); + const expected = []; + assert.deepStrictEqual(result, expected); + }); }); describe('UTILS: getDescendants', async () => { - const targetModule = (await import('../src/modules/utils/getDescendants.js')).getDescendants; - it('TP-1', () => { - const code = `a + b;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'BinaryExpression'); - const expected = ast.slice(targetNode.nodeId + 1); - const result = targetModule(targetNode); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Limited scope', () => { - const code = `a + -b; c + d;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'BinaryExpression'); - const expected = ast.slice(targetNode.nodeId + 1, targetNode.nodeId + 4); - const result = targetModule(targetNode); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Nested function with complex descendants', () => { - const code = `function test(a) { return a + (b * c); }`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'FunctionDeclaration'); - const result = targetModule(targetNode); - // Should include all nested nodes: parameters, body, expressions, identifiers - assert.ok(Array.isArray(result)); - assert.ok(result.length > 8); // Should have many nested descendants - assert.ok(result.some(n => n.type === 'Identifier' && n.name === 'a')); - assert.ok(result.some(n => n.type === 'BinaryExpression')); - }); - it('TP-4: Object expression with properties', () => { - const code = `const obj = { prop1: value1, prop2: value2 };`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'ObjectExpression'); - const result = targetModule(targetNode); - assert.ok(Array.isArray(result)); - assert.ok(result.length > 4); // Properties and their values - assert.ok(result.some(n => n.type === 'Property')); - assert.ok(result.some(n => n.type === 'Identifier' && n.name === 'value1')); - }); - it('TP-5: Array expression with elements', () => { - const code = `const arr = [a, b + c, func()];`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'ArrayExpression'); - const result = targetModule(targetNode); - assert.ok(Array.isArray(result)); - assert.ok(result.length > 5); // Elements and their nested parts - assert.ok(result.some(n => n.type === 'Identifier' && n.name === 'a')); - assert.ok(result.some(n => n.type === 'BinaryExpression')); - assert.ok(result.some(n => n.type === 'CallExpression')); - }); - it('TP-6: Caching behavior - same node returns cached result', () => { - const code = `a + b;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'BinaryExpression'); - - const result1 = targetModule(targetNode); - const result2 = targetModule(targetNode); - - // Should return same cached array reference - assert.strictEqual(result1, result2); - assert.ok(targetNode.descendants); // Cache property should exist - assert.strictEqual(targetNode.descendants, result1); - }); - it('TN-1: No descendants for leaf nodes', () => { - const code = `a; b; c;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'Identifier'); - const expected = []; - const result = targetModule(targetNode); - assert.deepStrictEqual(result, expected); - }); - it('TN-2: Null input returns empty array', () => { - const result = targetModule(null); - const expected = []; - assert.deepStrictEqual(result, expected); - }); - it('TN-3: Undefined input returns empty array', () => { - const result = targetModule(undefined); - const expected = []; - assert.deepStrictEqual(result, expected); - }); - it('TN-4: Node with no childNodes property', () => { - const mockNode = { type: 'MockNode' }; // No childNodes - const result = targetModule(mockNode); - const expected = []; - assert.deepStrictEqual(result, expected); - }); - it('TN-5: Node with empty childNodes array', () => { - const mockNode = { type: 'MockNode', childNodes: [] }; - const result = targetModule(mockNode); - const expected = []; - assert.deepStrictEqual(result, expected); - }); + const targetModule = (await import('../src/modules/utils/getDescendants.js')).getDescendants; + it('TP-1', () => { + const code = 'a + b;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'BinaryExpression'); + const expected = ast.slice(targetNode.nodeId + 1); + const result = targetModule(targetNode); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Limited scope', () => { + const code = 'a + -b; c + d;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'BinaryExpression'); + const expected = ast.slice(targetNode.nodeId + 1, targetNode.nodeId + 4); + const result = targetModule(targetNode); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Nested function with complex descendants', () => { + const code = 'function test(a) { return a + (b * c); }'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'FunctionDeclaration'); + const result = targetModule(targetNode); + // Should include all nested nodes: parameters, body, expressions, identifiers + assert.ok(Array.isArray(result)); + assert.ok(result.length > 8); // Should have many nested descendants + assert.ok(result.some(n => n.type === 'Identifier' && n.name === 'a')); + assert.ok(result.some(n => n.type === 'BinaryExpression')); + }); + it('TP-4: Object expression with properties', () => { + const code = 'const obj = { prop1: value1, prop2: value2 };'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'ObjectExpression'); + const result = targetModule(targetNode); + assert.ok(Array.isArray(result)); + assert.ok(result.length > 4); // Properties and their values + assert.ok(result.some(n => n.type === 'Property')); + assert.ok(result.some(n => n.type === 'Identifier' && n.name === 'value1')); + }); + it('TP-5: Array expression with elements', () => { + const code = 'const arr = [a, b + c, func()];'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'ArrayExpression'); + const result = targetModule(targetNode); + assert.ok(Array.isArray(result)); + assert.ok(result.length > 5); // Elements and their nested parts + assert.ok(result.some(n => n.type === 'Identifier' && n.name === 'a')); + assert.ok(result.some(n => n.type === 'BinaryExpression')); + assert.ok(result.some(n => n.type === 'CallExpression')); + }); + it('TP-6: Caching behavior - same node returns cached result', () => { + const code = 'a + b;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'BinaryExpression'); + + const result1 = targetModule(targetNode); + const result2 = targetModule(targetNode); + + // Should return same cached array reference + assert.strictEqual(result1, result2); + assert.ok(targetNode.descendants); // Cache property should exist + assert.strictEqual(targetNode.descendants, result1); + }); + it('TN-1: No descendants for leaf nodes', () => { + const code = 'a; b; c;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'Identifier'); + const expected = []; + const result = targetModule(targetNode); + assert.deepStrictEqual(result, expected); + }); + it('TN-2: Null input returns empty array', () => { + const result = targetModule(null); + const expected = []; + assert.deepStrictEqual(result, expected); + }); + it('TN-3: Undefined input returns empty array', () => { + const result = targetModule(undefined); + const expected = []; + assert.deepStrictEqual(result, expected); + }); + it('TN-4: Node with no childNodes property', () => { + const mockNode = {type: 'MockNode'}; // No childNodes + const result = targetModule(mockNode); + const expected = []; + assert.deepStrictEqual(result, expected); + }); + it('TN-5: Node with empty childNodes array', () => { + const mockNode = {type: 'MockNode', childNodes: []}; + const result = targetModule(mockNode); + const expected = []; + assert.deepStrictEqual(result, expected); + }); }); describe('UTILS: getMainDeclaredObjectOfMemberExpression', async () => { - const targetModule = (await import('../src/modules/utils/getMainDeclaredObjectOfMemberExpression.js')).getMainDeclaredObjectOfMemberExpression; - it('TP-1: Simple member expression with declared object', () => { - const code = `a.b;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'MemberExpression'); - const expected = targetNode.object; - const result = targetModule(targetNode); - assert.deepStrictEqual(result, expected); - }); - it('TP-2: Nested member expression finds root identifier', () => { - const code = `a.b.c.d;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'MemberExpression'); - const expected = ast.find(n => n.type === 'Identifier' && n.src === 'a'); - const result = targetModule(targetNode); - assert.deepStrictEqual(result, expected); - }); - it('TP-3: Computed member expression with declared base', () => { - const code = `obj[key].prop;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'MemberExpression' && n.property?.name === 'prop'); - const expected = ast.find(n => n.type === 'Identifier' && n.name === 'obj'); - const result = targetModule(targetNode); - assert.deepStrictEqual(result, expected); - }); - it('TP-4: Deep nesting finds correct root', () => { - const code = `root.level1.level2.level3.level4;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'MemberExpression' && n.property?.name === 'level4'); - const expected = ast.find(n => n.type === 'Identifier' && n.name === 'root'); - const result = targetModule(targetNode); - assert.deepStrictEqual(result, expected); - }); - it('TN-1: Non-MemberExpression input returns the input unchanged', () => { - const code = `const x = 42;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'Identifier' && n.name === 'x'); - const result = targetModule(targetNode); - assert.deepStrictEqual(result, targetNode); // Original behavior: return input unchanged - }); - it('TN-2: Null input returns null', () => { - const result = targetModule(null); - assert.strictEqual(result, null); - }); - it('TN-3: Undefined input returns null', () => { - const result = targetModule(undefined); - assert.strictEqual(result, null); - }); - it('TN-4: Member expression with no declNode still returns the object', () => { - const code = `undeclared.prop;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'MemberExpression'); - // Remove declNode from the identifier to simulate undeclared variable - const identifier = targetNode.object; - delete identifier.declNode; - const result = targetModule(targetNode); - assert.deepStrictEqual(result, identifier); // Should return the identifier even without declNode - }); - it('TN-5: Non-MemberExpression with declNode returns itself', () => { - const code = `const x = 42; x;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'Identifier' && n.name === 'x' && n.declNode); - const result = targetModule(targetNode); - assert.deepStrictEqual(result, targetNode); - }); + const targetModule = (await import('../src/modules/utils/getMainDeclaredObjectOfMemberExpression.js')).getMainDeclaredObjectOfMemberExpression; + it('TP-1: Simple member expression with declared object', () => { + const code = 'a.b;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'MemberExpression'); + const expected = targetNode.object; + const result = targetModule(targetNode); + assert.deepStrictEqual(result, expected); + }); + it('TP-2: Nested member expression finds root identifier', () => { + const code = 'a.b.c.d;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'MemberExpression'); + const expected = ast.find(n => n.type === 'Identifier' && n.src === 'a'); + const result = targetModule(targetNode); + assert.deepStrictEqual(result, expected); + }); + it('TP-3: Computed member expression with declared base', () => { + const code = 'obj[key].prop;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'MemberExpression' && n.property?.name === 'prop'); + const expected = ast.find(n => n.type === 'Identifier' && n.name === 'obj'); + const result = targetModule(targetNode); + assert.deepStrictEqual(result, expected); + }); + it('TP-4: Deep nesting finds correct root', () => { + const code = 'root.level1.level2.level3.level4;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'MemberExpression' && n.property?.name === 'level4'); + const expected = ast.find(n => n.type === 'Identifier' && n.name === 'root'); + const result = targetModule(targetNode); + assert.deepStrictEqual(result, expected); + }); + it('TN-1: Non-MemberExpression input returns the input unchanged', () => { + const code = 'const x = 42;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'Identifier' && n.name === 'x'); + const result = targetModule(targetNode); + assert.deepStrictEqual(result, targetNode); // Original behavior: return input unchanged + }); + it('TN-2: Null input returns null', () => { + const result = targetModule(null); + assert.strictEqual(result, null); + }); + it('TN-3: Undefined input returns null', () => { + const result = targetModule(undefined); + assert.strictEqual(result, null); + }); + it('TN-4: Member expression with no declNode still returns the object', () => { + const code = 'undeclared.prop;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'MemberExpression'); + // Remove declNode from the identifier to simulate undeclared variable + const identifier = targetNode.object; + delete identifier.declNode; + const result = targetModule(targetNode); + assert.deepStrictEqual(result, identifier); // Should return the identifier even without declNode + }); + it('TN-5: Non-MemberExpression with declNode returns itself', () => { + const code = 'const x = 42; x;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'Identifier' && n.name === 'x' && n.declNode); + const result = targetModule(targetNode); + assert.deepStrictEqual(result, targetNode); + }); }); describe('UTILS: getObjType', async () => { - const targetModule = (await import('../src/modules/utils/getObjType.js')).getObjType; - it('TP-1: Detect Array type', () => { - const result = targetModule([1, 2, 3]); - assert.strictEqual(result, 'Array'); - }); - it('TP-2: Detect Object type', () => { - const result = targetModule({key: 'value'}); - assert.strictEqual(result, 'Object'); - }); - it('TP-3: Detect String type', () => { - const result = targetModule('hello'); - assert.strictEqual(result, 'String'); - }); - it('TP-4: Detect Number type', () => { - const result = targetModule(42); - assert.strictEqual(result, 'Number'); - }); - it('TP-5: Detect Boolean type', () => { - const result = targetModule(true); - assert.strictEqual(result, 'Boolean'); - }); - it('TP-6: Detect Null type', () => { - const result = targetModule(null); - assert.strictEqual(result, 'Null'); - }); - it('TP-7: Detect Undefined type', () => { - const result = targetModule(undefined); - assert.strictEqual(result, 'Undefined'); - }); - it('TP-8: Detect Date type', () => { - const result = targetModule(new Date()); - assert.strictEqual(result, 'Date'); - }); - it('TP-9: Detect RegExp type', () => { - const result = targetModule(/pattern/); - assert.strictEqual(result, 'RegExp'); - }); - it('TP-10: Detect Function type', () => { - const result = targetModule(function() {}); - assert.strictEqual(result, 'Function'); - }); - it('TP-11: Detect Arrow Function type', () => { - const result = targetModule(() => {}); - assert.strictEqual(result, 'Function'); - }); - it('TP-12: Detect Error type', () => { - const result = targetModule(new Error('test')); - assert.strictEqual(result, 'Error'); - }); - it('TP-13: Detect empty array', () => { - const result = targetModule([]); - assert.strictEqual(result, 'Array'); - }); - it('TP-14: Detect empty object', () => { - const result = targetModule({}); - assert.strictEqual(result, 'Object'); - }); - it('TP-15: Detect Symbol type', () => { - const result = targetModule(Symbol('test')); - assert.strictEqual(result, 'Symbol'); - }); - it('TP-16: Detect BigInt type', () => { - const result = targetModule(BigInt(123)); - assert.strictEqual(result, 'BigInt'); - }); + const targetModule = (await import('../src/modules/utils/getObjType.js')).getObjType; + it('TP-1: Detect Array type', () => { + const result = targetModule([1, 2, 3]); + assert.strictEqual(result, 'Array'); + }); + it('TP-2: Detect Object type', () => { + const result = targetModule({key: 'value'}); + assert.strictEqual(result, 'Object'); + }); + it('TP-3: Detect String type', () => { + const result = targetModule('hello'); + assert.strictEqual(result, 'String'); + }); + it('TP-4: Detect Number type', () => { + const result = targetModule(42); + assert.strictEqual(result, 'Number'); + }); + it('TP-5: Detect Boolean type', () => { + const result = targetModule(true); + assert.strictEqual(result, 'Boolean'); + }); + it('TP-6: Detect Null type', () => { + const result = targetModule(null); + assert.strictEqual(result, 'Null'); + }); + it('TP-7: Detect Undefined type', () => { + const result = targetModule(undefined); + assert.strictEqual(result, 'Undefined'); + }); + it('TP-8: Detect Date type', () => { + const result = targetModule(new Date()); + assert.strictEqual(result, 'Date'); + }); + it('TP-9: Detect RegExp type', () => { + const result = targetModule(/pattern/); + assert.strictEqual(result, 'RegExp'); + }); + it('TP-10: Detect Function type', () => { + const result = targetModule(() => {}); + assert.strictEqual(result, 'Function'); + }); + it('TP-11: Detect Arrow Function type', () => { + const result = targetModule(() => {}); + assert.strictEqual(result, 'Function'); + }); + it('TP-12: Detect Error type', () => { + const result = targetModule(new Error('test')); + assert.strictEqual(result, 'Error'); + }); + it('TP-13: Detect empty array', () => { + const result = targetModule([]); + assert.strictEqual(result, 'Array'); + }); + it('TP-14: Detect empty object', () => { + const result = targetModule({}); + assert.strictEqual(result, 'Object'); + }); + it('TP-15: Detect Symbol type', () => { + const result = targetModule(Symbol('test')); + assert.strictEqual(result, 'Symbol'); + }); + it('TP-16: Detect BigInt type', () => { + const result = targetModule(BigInt(123)); + assert.strictEqual(result, 'BigInt'); + }); }); describe('UTILS: isNodeInRanges', async () => { - const targetModule = (await import('../src/modules/utils/isNodeInRanges.js')).isNodeInRanges; - it('TP-1: Node completely within single range', () => { - const code = `a.b;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.src === 'b'); - const result = targetModule(targetNode, [[2, 3]]); - assert.ok(result); - }); - it('TP-2: Node within multiple ranges (first match)', () => { - const code = `a.b;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.src === 'b'); - const result = targetModule(targetNode, [[0, 5], [10, 15]]); - assert.ok(result); - }); - it('TP-3: Node within multiple ranges (second match)', () => { - const code = `a.b;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.src === 'b'); - const result = targetModule(targetNode, [[0, 1], [2, 4]]); - assert.ok(result); - }); - it('TP-4: Node exactly matching range boundaries', () => { - const code = `a.b;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.src === 'b'); - const result = targetModule(targetNode, [[2, 3]]); - assert.ok(result); - }); - it('TP-5: Large range containing small node', () => { - const code = `function test() { return x; }`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.src === 'x'); - const result = targetModule(targetNode, [[0, 100]]); - assert.ok(result); - }); - it('TN-1: Node extends beyond range end', () => { - const code = `a.b;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.src === 'b'); - const result = targetModule(targetNode, [[1, 2]]); - assert.strictEqual(result, false); - }); - it('TN-2: Node starts before range start', () => { - const code = `a.b;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.src === 'a'); - const result = targetModule(targetNode, [[1, 5]]); - assert.strictEqual(result, false); - }); - it('TN-3: Empty ranges array', () => { - const code = `a.b;`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.src === 'b'); - const result = targetModule(targetNode, []); - assert.strictEqual(result, false); - }); - it('TN-4: Node range partially overlapping but not contained', () => { - const code = `function test() {}`; - const ast = generateFlatAST(code); - const targetNode = ast.find(n => n.type === 'FunctionDeclaration'); - const result = targetModule(targetNode, [[5, 10]]); - assert.strictEqual(result, false); - }); + const targetModule = (await import('../src/modules/utils/isNodeInRanges.js')).isNodeInRanges; + it('TP-1: Node completely within single range', () => { + const code = 'a.b;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.src === 'b'); + const result = targetModule(targetNode, [[2, 3]]); + assert.ok(result); + }); + it('TP-2: Node within multiple ranges (first match)', () => { + const code = 'a.b;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.src === 'b'); + const result = targetModule(targetNode, [[0, 5], [10, 15]]); + assert.ok(result); + }); + it('TP-3: Node within multiple ranges (second match)', () => { + const code = 'a.b;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.src === 'b'); + const result = targetModule(targetNode, [[0, 1], [2, 4]]); + assert.ok(result); + }); + it('TP-4: Node exactly matching range boundaries', () => { + const code = 'a.b;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.src === 'b'); + const result = targetModule(targetNode, [[2, 3]]); + assert.ok(result); + }); + it('TP-5: Large range containing small node', () => { + const code = 'function test() { return x; }'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.src === 'x'); + const result = targetModule(targetNode, [[0, 100]]); + assert.ok(result); + }); + it('TN-1: Node extends beyond range end', () => { + const code = 'a.b;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.src === 'b'); + const result = targetModule(targetNode, [[1, 2]]); + assert.strictEqual(result, false); + }); + it('TN-2: Node starts before range start', () => { + const code = 'a.b;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.src === 'a'); + const result = targetModule(targetNode, [[1, 5]]); + assert.strictEqual(result, false); + }); + it('TN-3: Empty ranges array', () => { + const code = 'a.b;'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.src === 'b'); + const result = targetModule(targetNode, []); + assert.strictEqual(result, false); + }); + it('TN-4: Node range partially overlapping but not contained', () => { + const code = 'function test() {}'; + const ast = generateFlatAST(code); + const targetNode = ast.find(n => n.type === 'FunctionDeclaration'); + const result = targetModule(targetNode, [[5, 10]]); + assert.strictEqual(result, false); + }); }); describe('UTILS: Sandbox', async () => { - const {Sandbox} = await import('../src/modules/utils/sandbox.js'); - it('TP-1: Basic code execution', () => { - const sandbox = new Sandbox(); - const result = sandbox.run('2 + 3'); - assert.ok(sandbox.isReference(result)); - assert.strictEqual(result.copySync(), 5); - }); - it('TP-2: String operations', () => { - const sandbox = new Sandbox(); - const result = sandbox.run('"hello" + " world"'); - assert.ok(sandbox.isReference(result)); - assert.strictEqual(result.copySync(), 'hello world'); - }); - it('TP-3: Array operations', () => { - const sandbox = new Sandbox(); - const result = sandbox.run('[1, 2, 3].length'); - assert.ok(sandbox.isReference(result)); - assert.strictEqual(result.copySync(), 3); - }); - it('TP-4: Object operations', () => { - const sandbox = new Sandbox(); - const result = sandbox.run('({a: 1, b: 2}).a'); - assert.ok(sandbox.isReference(result)); - assert.strictEqual(result.copySync(), 1); - }); - it('TP-5: Multiple executions on same sandbox', () => { - const sandbox = new Sandbox(); - const result1 = sandbox.run('var x = 10; x'); - const result2 = sandbox.run('x * 2'); - assert.strictEqual(result1.copySync(), 10); - assert.strictEqual(result2.copySync(), 20); - }); - it('TP-6: Deterministic behavior - Math.random is deleted', () => { - const sandbox = new Sandbox(); - const result = sandbox.run('typeof Math.random'); - assert.strictEqual(result.copySync(), 'undefined'); - }); - it('TP-7: Deterministic behavior - Date is deleted', () => { - const sandbox = new Sandbox(); - const result = sandbox.run('typeof Date'); - assert.strictEqual(result.copySync(), 'undefined'); - }); - it('TP-8: Blocked API - WebAssembly is undefined', () => { - const sandbox = new Sandbox(); - const result = sandbox.run('typeof WebAssembly'); - assert.strictEqual(result.copySync(), 'undefined'); - }); - it('TP-9: Blocked API - fetch is undefined', () => { - const sandbox = new Sandbox(); - const result = sandbox.run('typeof fetch'); - assert.strictEqual(result.copySync(), 'undefined'); - }); - it('TP-10: isReference method correctly identifies VM References', () => { - const sandbox = new Sandbox(); - const vmRef = sandbox.run('42'); - const nativeValue = 42; - assert.ok(sandbox.isReference(vmRef)); - assert.ok(!sandbox.isReference(nativeValue)); - }); - it('TN-1: isReference returns false for null', () => { - const sandbox = new Sandbox(); - assert.ok(!sandbox.isReference(null)); - }); - it('TN-2: isReference returns false for undefined', () => { - const sandbox = new Sandbox(); - assert.ok(!sandbox.isReference(undefined)); - }); - it('TN-3: isReference returns false for regular objects', () => { - const sandbox = new Sandbox(); - assert.ok(!sandbox.isReference({})); - assert.ok(!sandbox.isReference([])); - assert.ok(!sandbox.isReference('string')); - }); + const {Sandbox} = await import('../src/modules/utils/sandbox.js'); + it('TP-1: Basic code execution', () => { + const sandbox = new Sandbox(); + const result = sandbox.run('2 + 3'); + assert.ok(sandbox.isReference(result)); + assert.strictEqual(result.copySync(), 5); + }); + it('TP-2: String operations', () => { + const sandbox = new Sandbox(); + const result = sandbox.run('"hello" + " world"'); + assert.ok(sandbox.isReference(result)); + assert.strictEqual(result.copySync(), 'hello world'); + }); + it('TP-3: Array operations', () => { + const sandbox = new Sandbox(); + const result = sandbox.run('[1, 2, 3].length'); + assert.ok(sandbox.isReference(result)); + assert.strictEqual(result.copySync(), 3); + }); + it('TP-4: Object operations', () => { + const sandbox = new Sandbox(); + const result = sandbox.run('({a: 1, b: 2}).a'); + assert.ok(sandbox.isReference(result)); + assert.strictEqual(result.copySync(), 1); + }); + it('TP-5: Multiple executions on same sandbox', () => { + const sandbox = new Sandbox(); + const result1 = sandbox.run('var x = 10; x'); + const result2 = sandbox.run('x * 2'); + assert.strictEqual(result1.copySync(), 10); + assert.strictEqual(result2.copySync(), 20); + }); + it('TP-6: Deterministic behavior - Math.random is deleted', () => { + const sandbox = new Sandbox(); + const result = sandbox.run('typeof Math.random'); + assert.strictEqual(result.copySync(), 'undefined'); + }); + it('TP-7: Deterministic behavior - Date is deleted', () => { + const sandbox = new Sandbox(); + const result = sandbox.run('typeof Date'); + assert.strictEqual(result.copySync(), 'undefined'); + }); + it('TP-8: Blocked API - WebAssembly is undefined', () => { + const sandbox = new Sandbox(); + const result = sandbox.run('typeof WebAssembly'); + assert.strictEqual(result.copySync(), 'undefined'); + }); + it('TP-9: Blocked API - fetch is undefined', () => { + const sandbox = new Sandbox(); + const result = sandbox.run('typeof fetch'); + assert.strictEqual(result.copySync(), 'undefined'); + }); + it('TP-10: isReference method correctly identifies VM References', () => { + const sandbox = new Sandbox(); + const vmRef = sandbox.run('42'); + const nativeValue = 42; + assert.ok(sandbox.isReference(vmRef)); + assert.ok(!sandbox.isReference(nativeValue)); + }); + it('TN-1: isReference returns false for null', () => { + const sandbox = new Sandbox(); + assert.ok(!sandbox.isReference(null)); + }); + it('TN-2: isReference returns false for undefined', () => { + const sandbox = new Sandbox(); + assert.ok(!sandbox.isReference(undefined)); + }); + it('TN-3: isReference returns false for regular objects', () => { + const sandbox = new Sandbox(); + assert.ok(!sandbox.isReference({})); + assert.ok(!sandbox.isReference([])); + assert.ok(!sandbox.isReference('string')); + }); }); \ No newline at end of file diff --git a/tests/processors.test.js b/tests/processors.test.js index 705a103..6207a8f 100644 --- a/tests/processors.test.js +++ b/tests/processors.test.js @@ -6,12 +6,12 @@ import {describe, it} from 'node:test'; * @param {Arborist} arb */ function applyEachProcessor(arb) { - return proc => { - if (typeof proc === 'function') { - arb = proc(arb); - arb.applyChanges(); - } - }; + return proc => { + if (typeof proc === 'function') { + arb = proc(arb); + arb.applyChanges(); + } + }; } /** @@ -20,15 +20,15 @@ function applyEachProcessor(arb) { * @return {Arborist} */ function applyProcessors(arb, processors) { - processors.preprocessors.forEach(applyEachProcessor(arb)); - processors.postprocessors.forEach(applyEachProcessor(arb)); - return arb; + processors.preprocessors.forEach(applyEachProcessor(arb)); + processors.postprocessors.forEach(applyEachProcessor(arb)); + return arb; } describe('Processors tests: Augmented Array', async () => { - const targetProcessors = (await import('../src/processors/augmentedArray.js')); - it('TP-1: Complex IIFE with mixed array elements', () => { - const code = `const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'a', 'b', 'c']; + const targetProcessors = (await import('../src/processors/augmentedArray.js')); + it('TP-1: Complex IIFE with mixed array elements', () => { + const code = `const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'a', 'b', 'c']; (function (targetArray, numberOfShifts) { var augmentArray = function (counter) { while (--counter) { @@ -37,108 +37,108 @@ describe('Processors tests: Augmented Array', async () => { }; augmentArray(++numberOfShifts); }(arr, 3));`; - const expected = `const arr = [\n 4,\n 5,\n 6,\n 7,\n 8,\n 9,\n 10,\n 'a',\n 'b',\n 'c',\n 1,\n 2,\n 3\n];`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TP-2: Simple array with single shift', () => { - const code = `const data = ['first', 'second', 'third']; + const expected = 'const arr = [\n 4,\n 5,\n 6,\n 7,\n 8,\n 9,\n 10,\n \'a\',\n \'b\',\n \'c\',\n 1,\n 2,\n 3\n];'; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TP-2: Simple array with single shift', () => { + const code = `const data = ['first', 'second', 'third']; (function(arr, shifts) { for (let i = 0; i < shifts; i++) { arr.push(arr.shift()); } })(data, 1);`; - const expected = `const data = [\n 'second',\n 'third',\n 'first'\n];`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TP-3: Array with zero shifts (no change)', () => { - const code = `const unchanged = [1, 2, 3]; + const expected = 'const data = [\n \'second\',\n \'third\',\n \'first\'\n];'; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TP-3: Array with zero shifts (no change)', () => { + const code = `const unchanged = [1, 2, 3]; (function(arr, n) { for (let i = 0; i < n; i++) { arr.push(arr.shift()); } })(unchanged, 0);`; - const expected = `const unchanged = [\n 1,\n 2,\n 3\n];`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TP-4: Array with larger shift count', () => { - const code = `const numbers = [10, 20, 30, 40, 50]; + const expected = 'const unchanged = [\n 1,\n 2,\n 3\n];'; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TP-4: Array with larger shift count', () => { + const code = `const numbers = [10, 20, 30, 40, 50]; (function(arr, count) { for (let i = 0; i < count; i++) { arr.push(arr.shift()); } })(numbers, 3);`; - const expected = `const numbers = [\n 40,\n 50,\n 10,\n 20,\n 30\n];`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TN-1: IIFE with non-literal shift count', () => { - const code = `const arr = [1, 2, 3]; + const expected = 'const numbers = [\n 40,\n 50,\n 10,\n 20,\n 30\n];'; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TN-1: IIFE with non-literal shift count', () => { + const code = `const arr = [1, 2, 3]; let shifts = 2; (function(array, n) { for (let i = 0; i < n; i++) { array.push(array.shift()); } })(arr, shifts);`; - let arb = new Arborist(code); - const originalScript = arb.script; - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, originalScript); - }); - it('TN-2: IIFE with insufficient arguments', () => { - const code = `const arr = [1, 2, 3]; + let arb = new Arborist(code); + const originalScript = arb.script; + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, originalScript); + }); + it('TN-2: IIFE with insufficient arguments', () => { + const code = `const arr = [1, 2, 3]; (function(array) { array.push(array.shift()); })(arr);`; - let arb = new Arborist(code); - const originalScript = arb.script; - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, originalScript); - }); - it('TN-3: IIFE with non-identifier array argument', () => { - const code = `(function(array, shifts) { + let arb = new Arborist(code); + const originalScript = arb.script; + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, originalScript); + }); + it('TN-3: IIFE with non-identifier array argument', () => { + const code = `(function(array, shifts) { for (let i = 0; i < shifts; i++) { array.push(array.shift()); } })([1, 2, 3], 1);`; - let arb = new Arborist(code); - const originalScript = arb.script; - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, originalScript); - }); - it('TN-4: Non-IIFE function call', () => { - const code = `const arr = [1, 2, 3]; + let arb = new Arborist(code); + const originalScript = arb.script; + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, originalScript); + }); + it('TN-4: Non-IIFE function call', () => { + const code = `const arr = [1, 2, 3]; function shuffle(array, shifts) { for (let i = 0; i < shifts; i++) { array.push(array.shift()); } } shuffle(arr, 2);`; - let arb = new Arborist(code); - const originalScript = arb.script; - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, originalScript); - }); - it('TN-5: Invalid shift count (NaN)', () => { - const code = `const arr = [1, 2, 3]; + let arb = new Arborist(code); + const originalScript = arb.script; + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, originalScript); + }); + it('TN-5: Invalid shift count (NaN)', () => { + const code = `const arr = [1, 2, 3]; (function(array, shifts) { for (let i = 0; i < shifts; i++) { array.push(array.shift()); } })(arr, "invalid");`; - let arb = new Arborist(code); - const originalScript = arb.script; - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, originalScript); - }); - it('TN-9: Function passed to IIFE (function not self-modifying)', () => { - const code = `function getArray() { + let arb = new Arborist(code); + const originalScript = arb.script; + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, originalScript); + }); + it('TN-9: Function passed to IIFE (function not self-modifying)', () => { + const code = `function getArray() { return ['a', 'b', 'c']; } (function(fn, shifts) { @@ -147,83 +147,83 @@ shuffle(arr, 2);`; arr.push(arr.shift()); } })(getArray, 2);`; - // The IIFE modifies a local copy, but the function itself is not self-modifying - // so no transformation should occur - let arb = new Arborist(code); - const originalScript = arb.script; - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, originalScript); - }); - it('TP-5: Arrow function IIFE', () => { - const code = `const items = ['x', 'y', 'z']; + // The IIFE modifies a local copy, but the function itself is not self-modifying + // so no transformation should occur + let arb = new Arborist(code); + const originalScript = arb.script; + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, originalScript); + }); + it('TP-5: Arrow function IIFE', () => { + const code = `const items = ['x', 'y', 'z']; ((arr, n) => { for (let i = 0; i < n; i++) { arr.push(arr.shift()); } })(items, 1);`; - const expected = `const items = [ + const expected = `const items = [ 'y', 'z', 'x' ];`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TP-6: Shift count larger than array length', () => { - const code = `const small = ['a', 'b']; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TP-6: Shift count larger than array length', () => { + const code = `const small = ['a', 'b']; (function(arr, shifts) { for (let i = 0; i < shifts; i++) { arr.push(arr.shift()); } })(small, 5);`; - // 5 shifts on 2-element array: a,b -> b,a -> a,b -> b,a -> a,b -> b,a - const expected = `const small = [ + // 5 shifts on 2-element array: a,b -> b,a -> a,b -> b,a -> a,b -> b,a + const expected = `const small = [ 'b', 'a' ];`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TN-10: Arrow function without parentheses around parameters', () => { - const code = `const arr = [1, 2, 3]; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TN-10: Arrow function without parentheses around parameters', () => { + const code = `const arr = [1, 2, 3]; (arr => { arr.push(arr.shift()); })(arr);`; - let arb = new Arborist(code); - const originalScript = arb.script; - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, originalScript); - }); - it('TN-11: Negative shift count', () => { - const code = `const arr = [1, 2, 3]; + let arb = new Arborist(code); + const originalScript = arb.script; + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, originalScript); + }); + it('TN-11: Negative shift count', () => { + const code = `const arr = [1, 2, 3]; (function(array, shifts) { for (let i = 0; i < shifts; i++) { array.push(array.shift()); } })(arr, -1);`; - let arb = new Arborist(code); - const originalScript = arb.script; - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, originalScript); - }); - it('TN-12: IIFE with complex array manipulation that cannot be resolved', () => { - const code = `const arr = [1, 2, 3]; + let arb = new Arborist(code); + const originalScript = arb.script; + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, originalScript); + }); + it('TN-12: IIFE with complex array manipulation that cannot be resolved', () => { + const code = `const arr = [1, 2, 3]; (function(array, shifts) { Math.random() > 0.5 ? array.push(array.shift()) : array.unshift(array.pop()); })(arr, 1);`; - let arb = new Arborist(code); - const originalScript = arb.script; - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, originalScript); - }); + let arb = new Arborist(code); + const originalScript = arb.script; + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, originalScript); + }); }); describe('Processors tests: Caesar Plus', async () => { - const targetProcessors = (await import('../src/processors/caesarp.js')); - // TODO: Fix test - it.skip('TP-1: FIX ME', () => { - const code = `(function() { + const targetProcessors = (await import('../src/processors/caesarp.js')); + // TODO: Fix test + it.skip('TP-1: FIX ME', () => { + const code = `(function() { const a = document.createElement('div'); const b = 'Y29uc29sZS5sb2co'; const c = 'IlJFc3RyaW5nZXIiKQ=='; @@ -234,88 +234,88 @@ describe('Processors tests: Caesar Plus', async () => { dbt['toString'] = ''.constructor.constructor(atb(abc)); dbt = dbt + "this will execute dbt's toString method"; })();`; - const expected = `console.log("REstringer")`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); + const expected = 'console.log("REstringer")'; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); }); describe('Processors tests: Function to Array', async () => { - const targetProcessors = (await import('../src/processors/functionToArray.js')); - it('TP-1: Independent call', () => { - const code = `function getArr() {return ['One', 'Two', 'Three']} const a = getArr(); console.log(a[0] + ' + ' + a[1] + ' = ' + a[2]);`; - const expected = `function getArr() {\n return [\n 'One',\n 'Two',\n 'Three'\n ];\n}\nconst a = [\n 'One',\n 'Two',\n 'Three'\n];\nconsole.log(a[0] + ' + ' + a[1] + ' = ' + a[2]);`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TP-2: IIFE', () => { - const code = `const a = (function(){return ['One', 'Two', 'Three']})(); console.log(a[0] + ' + ' + a[1] + ' = ' + a[2]);`; - const expected = `const a = [\n 'One',\n 'Two',\n 'Three'\n];\nconsole.log(a[0] + ' + ' + a[1] + ' = ' + a[2]);`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TP-3: Arrow function returning array', () => { - const code = `const getItems = () => ['x', 'y', 'z']; const items = getItems(); console.log(items[0]);`; - const expected = `const getItems = () => [\n 'x',\n 'y',\n 'z'\n];\nconst items = [\n 'x',\n 'y',\n 'z'\n];\nconsole.log(items[0]);`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TP-4: Multiple variables with array access only', () => { - const code = `function getData() {return [1, 2, 3]} const x = getData(); const y = getData(); console.log(x[0], y[1]);`; - const expected = `function getData() {\n return [\n 1,\n 2,\n 3\n ];\n}\nconst x = [\n 1,\n 2,\n 3\n];\nconst y = [\n 1,\n 2,\n 3\n];\nconsole.log(x[0], y[1]);`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TN-1: Function called multiple times without assignment', () => { - const code = `function getArr() {return ['One', 'Two', 'Three']} console.log(getArr()[0] + ' + ' + getArr()[1] + ' = ' + getArr()[2]);`; - const expected = code; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TN-2: Mixed usage (array access and other)', () => { - const code = `function getArr() {return ['a', 'b', 'c']} const data = getArr(); console.log(data[0], data.length, data.slice(1));`; - const expected = code; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TN-3: Variable not assigned function call', () => { - const code = `const arr = ['static', 'array']; console.log(arr[0], arr[1]);`; - const expected = code; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); + const targetProcessors = (await import('../src/processors/functionToArray.js')); + it('TP-1: Independent call', () => { + const code = 'function getArr() {return [\'One\', \'Two\', \'Three\']} const a = getArr(); console.log(a[0] + \' + \' + a[1] + \' = \' + a[2]);'; + const expected = 'function getArr() {\n return [\n \'One\',\n \'Two\',\n \'Three\'\n ];\n}\nconst a = [\n \'One\',\n \'Two\',\n \'Three\'\n];\nconsole.log(a[0] + \' + \' + a[1] + \' = \' + a[2]);'; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TP-2: IIFE', () => { + const code = 'const a = (function(){return [\'One\', \'Two\', \'Three\']})(); console.log(a[0] + \' + \' + a[1] + \' = \' + a[2]);'; + const expected = 'const a = [\n \'One\',\n \'Two\',\n \'Three\'\n];\nconsole.log(a[0] + \' + \' + a[1] + \' = \' + a[2]);'; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TP-3: Arrow function returning array', () => { + const code = 'const getItems = () => [\'x\', \'y\', \'z\']; const items = getItems(); console.log(items[0]);'; + const expected = 'const getItems = () => [\n \'x\',\n \'y\',\n \'z\'\n];\nconst items = [\n \'x\',\n \'y\',\n \'z\'\n];\nconsole.log(items[0]);'; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TP-4: Multiple variables with array access only', () => { + const code = 'function getData() {return [1, 2, 3]} const x = getData(); const y = getData(); console.log(x[0], y[1]);'; + const expected = 'function getData() {\n return [\n 1,\n 2,\n 3\n ];\n}\nconst x = [\n 1,\n 2,\n 3\n];\nconst y = [\n 1,\n 2,\n 3\n];\nconsole.log(x[0], y[1]);'; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TN-1: Function called multiple times without assignment', () => { + const code = 'function getArr() {return [\'One\', \'Two\', \'Three\']} console.log(getArr()[0] + \' + \' + getArr()[1] + \' = \' + getArr()[2]);'; + const expected = code; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TN-2: Mixed usage (array access and other)', () => { + const code = 'function getArr() {return [\'a\', \'b\', \'c\']} const data = getArr(); console.log(data[0], data.length, data.slice(1));'; + const expected = code; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TN-3: Variable not assigned function call', () => { + const code = 'const arr = [\'static\', \'array\']; console.log(arr[0], arr[1]);'; + const expected = code; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); }); describe('Processors tests: Obfuscator.io', async () => { - const targetProcessors = (await import('../src/processors/obfuscator.io.js')); - it('TP-1', () => { - const code = `var a = { + const targetProcessors = (await import('../src/processors/obfuscator.io.js')); + it('TP-1', () => { + const code = `var a = { 'removeCookie': function () { return 'dev'; } }`; - const expected = `var a = { 'removeCookie': 'function () {return "bypassed!"}' };`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); - it('TP-2', () => { - const code = `var a = function (f) { + const expected = 'var a = { \'removeCookie\': \'function () {return "bypassed!"}\' };'; + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); + it('TP-2', () => { + const code = `var a = function (f) { this['JoJo'] = function () { return 'newState'; } }`; - const expected = `var a = function (f) { + const expected = `var a = function (f) { this['JoJo'] = 'function () {return "bypassed!"}'; };`; - let arb = new Arborist(code); - arb = applyProcessors(arb, targetProcessors); - assert.strictEqual(arb.script, expected); - }); + let arb = new Arborist(code); + arb = applyProcessors(arb, targetProcessors); + assert.strictEqual(arb.script, expected); + }); }); diff --git a/tests/samples.test.js b/tests/samples.test.js index 8056d3d..065c209 100644 --- a/tests/samples.test.js +++ b/tests/samples.test.js @@ -6,101 +6,101 @@ import {join} from 'node:path'; import {REstringer} from '../src/restringer.js'; function getDeobfuscatedCode(code) { - const restringer = new REstringer(code); - restringer.logger.setLogLevel(restringer.logger.logLevels.NONE); - restringer.deobfuscate(); - return restringer.script; + const restringer = new REstringer(code); + restringer.logger.setLogLevel(restringer.logger.logLevels.NONE); + restringer.deobfuscate(); + return restringer.script; } describe('Samples tests', () => { - const resourcePath = './resources'; - const cwd = fileURLToPath(import.meta.url).split('/').slice(0, -1).join('/'); - it('Deobfuscate sample: JSFuck', () => { - const sampleFilename = join(cwd, resourcePath, 'jsfuck.js'); - const expectedSolutionFilename = sampleFilename + '-deob.js'; - const code = readFileSync(sampleFilename, 'utf-8'); - const expected = readFileSync(expectedSolutionFilename, 'utf-8'); - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it('Deobfuscate sample: Ant & Cockroach', () => { - const sampleFilename = join(cwd, resourcePath, 'ant.js'); - const expectedSolutionFilename = sampleFilename + '-deob.js'; - const code = readFileSync(sampleFilename, 'utf-8'); - const expected = readFileSync(expectedSolutionFilename, 'utf-8'); - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it('Deobfuscate sample: New Function IIFE', () => { - const sampleFilename = join(cwd, resourcePath, 'newFunc.js'); - const expectedSolutionFilename = sampleFilename + '-deob.js'; - const code = readFileSync(sampleFilename, 'utf-8'); - const expected = readFileSync(expectedSolutionFilename, 'utf-8'); - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it('Deobfuscate sample: Hunter', () => { - const sampleFilename = join(cwd, resourcePath, 'hunter.js'); - const expectedSolutionFilename = sampleFilename + '-deob.js'; - const code = readFileSync(sampleFilename, 'utf-8'); - const expected = readFileSync(expectedSolutionFilename, 'utf-8'); - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it('Deobfuscate sample: _$_', () => { - const sampleFilename = join(cwd, resourcePath, 'udu.js'); - const expectedSolutionFilename = sampleFilename + '-deob.js'; - const code = readFileSync(sampleFilename, 'utf-8'); - const expected = readFileSync(expectedSolutionFilename, 'utf-8'); - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it('Deobfuscate sample: Prototype Calls', () => { - const sampleFilename = join(cwd, resourcePath, 'prototypeCalls.js'); - const expectedSolutionFilename = sampleFilename + '-deob.js'; - const code = readFileSync(sampleFilename, 'utf-8'); - const expected = readFileSync(expectedSolutionFilename, 'utf-8'); - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it.skip('TODO: FIX Deobfuscate sample: Caesar+', () => { - const sampleFilename = join(cwd, resourcePath, 'caesar.js'); - const expectedSolutionFilename = sampleFilename + '-deob.js'; - const code = readFileSync(sampleFilename, 'utf-8'); - const expected = readFileSync(expectedSolutionFilename, 'utf-8'); - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it('Deobfuscate sample: eval(Ox$', () => { - const sampleFilename = join(cwd, resourcePath, 'evalOxd.js'); - const expectedSolutionFilename = sampleFilename + '-deob.js'; - const code = readFileSync(sampleFilename, 'utf-8'); - const expected = readFileSync(expectedSolutionFilename, 'utf-8'); - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it('Deobfuscate sample: Obfuscator.io', () => { - const sampleFilename = join(cwd, resourcePath, 'obfuscator.io.js'); - const expectedSolutionFilename = sampleFilename + '-deob.js'; - const code = readFileSync(sampleFilename, 'utf-8'); - const expected = readFileSync(expectedSolutionFilename, 'utf-8'); - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it('Deobfuscate sample: $s', () => { - const sampleFilename = join(cwd, resourcePath, 'ds.js'); - const expectedSolutionFilename = sampleFilename + '-deob.js'; - const code = readFileSync(sampleFilename, 'utf-8'); - const expected = readFileSync(expectedSolutionFilename, 'utf-8'); - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); - it('Deobfuscate sample: Local Proxies', () => { - const sampleFilename = join(cwd, resourcePath, 'localProxies.js'); - const expectedSolutionFilename = sampleFilename + '-deob.js'; - const code = readFileSync(sampleFilename, 'utf-8'); - const expected = readFileSync(expectedSolutionFilename, 'utf-8'); - const result = getDeobfuscatedCode(code); - assert.strictEqual(result, expected); - }); + const resourcePath = './resources'; + const cwd = fileURLToPath(import.meta.url).split('/').slice(0, -1).join('/'); + it('Deobfuscate sample: JSFuck', () => { + const sampleFilename = join(cwd, resourcePath, 'jsfuck.js'); + const expectedSolutionFilename = sampleFilename + '-deob.js'; + const code = readFileSync(sampleFilename, 'utf-8'); + const expected = readFileSync(expectedSolutionFilename, 'utf-8'); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Deobfuscate sample: Ant & Cockroach', () => { + const sampleFilename = join(cwd, resourcePath, 'ant.js'); + const expectedSolutionFilename = sampleFilename + '-deob.js'; + const code = readFileSync(sampleFilename, 'utf-8'); + const expected = readFileSync(expectedSolutionFilename, 'utf-8'); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Deobfuscate sample: New Function IIFE', () => { + const sampleFilename = join(cwd, resourcePath, 'newFunc.js'); + const expectedSolutionFilename = sampleFilename + '-deob.js'; + const code = readFileSync(sampleFilename, 'utf-8'); + const expected = readFileSync(expectedSolutionFilename, 'utf-8'); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Deobfuscate sample: Hunter', () => { + const sampleFilename = join(cwd, resourcePath, 'hunter.js'); + const expectedSolutionFilename = sampleFilename + '-deob.js'; + const code = readFileSync(sampleFilename, 'utf-8'); + const expected = readFileSync(expectedSolutionFilename, 'utf-8'); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Deobfuscate sample: _$_', () => { + const sampleFilename = join(cwd, resourcePath, 'udu.js'); + const expectedSolutionFilename = sampleFilename + '-deob.js'; + const code = readFileSync(sampleFilename, 'utf-8'); + const expected = readFileSync(expectedSolutionFilename, 'utf-8'); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Deobfuscate sample: Prototype Calls', () => { + const sampleFilename = join(cwd, resourcePath, 'prototypeCalls.js'); + const expectedSolutionFilename = sampleFilename + '-deob.js'; + const code = readFileSync(sampleFilename, 'utf-8'); + const expected = readFileSync(expectedSolutionFilename, 'utf-8'); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it.skip('TODO: FIX Deobfuscate sample: Caesar+', () => { + const sampleFilename = join(cwd, resourcePath, 'caesar.js'); + const expectedSolutionFilename = sampleFilename + '-deob.js'; + const code = readFileSync(sampleFilename, 'utf-8'); + const expected = readFileSync(expectedSolutionFilename, 'utf-8'); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Deobfuscate sample: eval(Ox$', () => { + const sampleFilename = join(cwd, resourcePath, 'evalOxd.js'); + const expectedSolutionFilename = sampleFilename + '-deob.js'; + const code = readFileSync(sampleFilename, 'utf-8'); + const expected = readFileSync(expectedSolutionFilename, 'utf-8'); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Deobfuscate sample: Obfuscator.io', () => { + const sampleFilename = join(cwd, resourcePath, 'obfuscator.io.js'); + const expectedSolutionFilename = sampleFilename + '-deob.js'; + const code = readFileSync(sampleFilename, 'utf-8'); + const expected = readFileSync(expectedSolutionFilename, 'utf-8'); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Deobfuscate sample: $s', () => { + const sampleFilename = join(cwd, resourcePath, 'ds.js'); + const expectedSolutionFilename = sampleFilename + '-deob.js'; + const code = readFileSync(sampleFilename, 'utf-8'); + const expected = readFileSync(expectedSolutionFilename, 'utf-8'); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); + it('Deobfuscate sample: Local Proxies', () => { + const sampleFilename = join(cwd, resourcePath, 'localProxies.js'); + const expectedSolutionFilename = sampleFilename + '-deob.js'; + const code = readFileSync(sampleFilename, 'utf-8'); + const expected = readFileSync(expectedSolutionFilename, 'utf-8'); + const result = getDeobfuscatedCode(code); + assert.strictEqual(result, expected); + }); }); \ No newline at end of file diff --git a/tests/utils.test.js b/tests/utils.test.js index 0890de9..ae9f40a 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -4,124 +4,124 @@ import {parseArgs} from '../src/utils/parseArgs.js'; const consolelog = console.log; describe('parseArgs tests', () => { - it('TP-1: Defaults', () => { - assert.deepEqual(parseArgs(['input.js']), { - inputFilename: 'input.js', - help: false, - clean: false, - quiet: false, - verbose: false, - outputToFile: false, - maxIterations: false, - outputFilename: 'input.js-deob.js' - }); - }); - it('TP-2: All on - short', () => { - assert.deepEqual(parseArgs(['input.js', '-h', '-c', '-q', '-v', '-o', '-m', '1']), { - inputFilename: 'input.js', - help: true, - clean: true, - quiet: true, - verbose: true, - outputToFile: true, - maxIterations: 1, - outputFilename: 'input.js-deob.js' - }); - }); - it('TP-3: All on - full', () => { - assert.deepEqual(parseArgs(['input.js', '--help', '--clean', '--quiet', '--verbose', '--output', '--max-iterations=1']), { - inputFilename: 'input.js', - help: true, - clean: true, - quiet: true, - verbose: true, - outputToFile: true, - maxIterations: 1, - outputFilename: 'input.js-deob.js' - }); - }); - it('TP-4: Custom outputFilename split', () => { - assert.deepEqual(parseArgs(['input.js', '-o', 'customName.js']), { - inputFilename: 'input.js', - help: false, - clean: false, - quiet: false, - verbose: false, - outputToFile: true, - maxIterations: false, - outputFilename: 'customName.js' - }); - }); - it('TP-5: Custom outputFilename equals', () => { - assert.deepEqual(parseArgs(['input.js', '-o=customName.js']), { - inputFilename: 'input.js', - help: false, - clean: false, - quiet: false, - verbose: false, - outputToFile: true, - maxIterations: false, - outputFilename: 'customName.js' - }); - }); - it('TP-6: Custom outputFilename full', () => { - assert.deepEqual(parseArgs(['input.js', '--output=customName.js']), { - inputFilename: 'input.js', - help: false, - clean: false, - quiet: false, - verbose: false, - outputToFile: true, - maxIterations: false, - outputFilename: 'customName.js' - }); - }); - it('TP-7: Max iterations short equals', () => { - assert.deepEqual(parseArgs(['input.js', '-m=2']), { - inputFilename: 'input.js', - help: false, - clean: false, - quiet: false, - verbose: false, - outputToFile: false, - maxIterations: 2, - outputFilename: 'input.js-deob.js' - }); - }); - it('TP-8: Max iterations short split', () => { - assert.deepEqual(parseArgs(['input.js', '-m', '2']), { - inputFilename: 'input.js', - help: false, - clean: false, - quiet: false, - verbose: false, - outputToFile: false, - maxIterations: 2, - outputFilename: 'input.js-deob.js' - }); - }); - it('TP-9: Max iterations long equals', () => { - assert.deepEqual(parseArgs(['input.js', '--max-iterations=2']), { - inputFilename: 'input.js', - help: false, - clean: false, - quiet: false, - verbose: false, - outputToFile: false, - maxIterations: 2, - outputFilename: 'input.js-deob.js' - }); - }); - it('TP-10: Max iterations long split', () => { - assert.deepEqual(parseArgs(['input.js', '--max-iterations', '2']), { - inputFilename: 'input.js', - help: false, - clean: false, - quiet: false, - verbose: false, - outputToFile: false, - maxIterations: 2, - outputFilename: 'input.js-deob.js' - }); - }); + it('TP-1: Defaults', () => { + assert.deepEqual(parseArgs(['input.js']), { + inputFilename: 'input.js', + help: false, + clean: false, + quiet: false, + verbose: false, + outputToFile: false, + maxIterations: false, + outputFilename: 'input.js-deob.js', + }); + }); + it('TP-2: All on - short', () => { + assert.deepEqual(parseArgs(['input.js', '-h', '-c', '-q', '-v', '-o', '-m', '1']), { + inputFilename: 'input.js', + help: true, + clean: true, + quiet: true, + verbose: true, + outputToFile: true, + maxIterations: 1, + outputFilename: 'input.js-deob.js', + }); + }); + it('TP-3: All on - full', () => { + assert.deepEqual(parseArgs(['input.js', '--help', '--clean', '--quiet', '--verbose', '--output', '--max-iterations=1']), { + inputFilename: 'input.js', + help: true, + clean: true, + quiet: true, + verbose: true, + outputToFile: true, + maxIterations: 1, + outputFilename: 'input.js-deob.js', + }); + }); + it('TP-4: Custom outputFilename split', () => { + assert.deepEqual(parseArgs(['input.js', '-o', 'customName.js']), { + inputFilename: 'input.js', + help: false, + clean: false, + quiet: false, + verbose: false, + outputToFile: true, + maxIterations: false, + outputFilename: 'customName.js', + }); + }); + it('TP-5: Custom outputFilename equals', () => { + assert.deepEqual(parseArgs(['input.js', '-o=customName.js']), { + inputFilename: 'input.js', + help: false, + clean: false, + quiet: false, + verbose: false, + outputToFile: true, + maxIterations: false, + outputFilename: 'customName.js', + }); + }); + it('TP-6: Custom outputFilename full', () => { + assert.deepEqual(parseArgs(['input.js', '--output=customName.js']), { + inputFilename: 'input.js', + help: false, + clean: false, + quiet: false, + verbose: false, + outputToFile: true, + maxIterations: false, + outputFilename: 'customName.js', + }); + }); + it('TP-7: Max iterations short equals', () => { + assert.deepEqual(parseArgs(['input.js', '-m=2']), { + inputFilename: 'input.js', + help: false, + clean: false, + quiet: false, + verbose: false, + outputToFile: false, + maxIterations: 2, + outputFilename: 'input.js-deob.js', + }); + }); + it('TP-8: Max iterations short split', () => { + assert.deepEqual(parseArgs(['input.js', '-m', '2']), { + inputFilename: 'input.js', + help: false, + clean: false, + quiet: false, + verbose: false, + outputToFile: false, + maxIterations: 2, + outputFilename: 'input.js-deob.js', + }); + }); + it('TP-9: Max iterations long equals', () => { + assert.deepEqual(parseArgs(['input.js', '--max-iterations=2']), { + inputFilename: 'input.js', + help: false, + clean: false, + quiet: false, + verbose: false, + outputToFile: false, + maxIterations: 2, + outputFilename: 'input.js-deob.js', + }); + }); + it('TP-10: Max iterations long split', () => { + assert.deepEqual(parseArgs(['input.js', '--max-iterations', '2']), { + inputFilename: 'input.js', + help: false, + clean: false, + quiet: false, + verbose: false, + outputToFile: false, + maxIterations: 2, + outputFilename: 'input.js-deob.js', + }); + }); }); \ No newline at end of file From aacfd858fe08925955b9ff0724e6535a3b608b98 Mon Sep 17 00:00:00 2001 From: Ben Baryo <19845603+ctrl-escp@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:47:33 +0200 Subject: [PATCH 4/4] Update Node.js version requirements in documentation and workflows - Changed Node.js version requirement in README.md and CONTRIBUTING.md from v20+ to v22+ for clarity and consistency. - Updated GitHub Actions workflow to support Node.js versions 22.x and 24.x, ensuring compatibility with the latest releases. --- .github/workflows/node.js.yml | 2 +- README.md | 4 ++-- docs/CONTRIBUTING.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index a5c625c..0a2c3c9 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/README.md b/README.md index d886c96..c00d533 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ REstringer automatically detects obfuscation patterns and applies targeted deobf ## Installation ### Requirements -- **Node.js v20+** (v22+ recommended) +- **Node.js v22+** ### Global Installation (CLI) ```bash @@ -345,4 +345,4 @@ This project is licensed under the [MIT License](LICENSE). **Made with ❤️ by [HUMAN Security](https://www.HumanSecurity.com/)** - \ No newline at end of file + diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index cc5c5d7..583c636 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -42,7 +42,7 @@ Thank you for your interest in contributing to REstringer! This guide covers eve ### Prerequisites -- **Node.js v20+** (v22+ recommended) +- **Node.js v22+** - **npm** (latest stable version) - **Git** for version control