Fixed class expression name references inside class body being incorrectly resolved to an import binding with the same name, causing broken code at runtime (#1387)

This commit is contained in:
Timofey Kachalov
2026-03-21 20:55:38 +04:00
committed by GitHub
parent 0a06d29d68
commit 7792fb0be3
5 changed files with 87 additions and 2 deletions
+4
View File
@@ -1,5 +1,9 @@
Change Log
v5.3.1
---
* Fixed class expression name references inside class body being incorrectly resolved to an import binding with the same name, causing broken code at runtime. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1386
v5.3.0
---
* Add Pro API support to CLI
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "javascript-obfuscator",
"version": "5.3.0",
"version": "5.3.1",
"description": "JavaScript obfuscator",
"keywords": [
"obfuscator",
@@ -238,7 +238,11 @@ export class ScopeAnalyzer implements IScopeAnalyzer {
(definition: eslintScope.Definition) => definition.type === 'ClassName'
);
return isValidClassNameVariable && variable.name === classNameVariable.name;
const isImportBinding: boolean = variable.defs.some(
(definition: eslintScope.Definition) => definition.type === 'ImportBinding'
);
return isValidClassNameVariable && variable.name === classNameVariable.name && !isImportBinding;
}
);
@@ -6,6 +6,7 @@ import { evalLocal } from '../../../helpers/evalLocal';
import { readFileAsString } from '../../../helpers/readFileAsString';
import { JavaScriptObfuscator } from '../../../../src/JavaScriptObfuscatorFacade';
import { NO_ADDITIONAL_NODES_PRESET } from '../../../../src/options/presets/NoCustomNodes';
describe('ScopeAnalyzer', () => {
describe('analyze', () => {
@@ -121,5 +122,64 @@ describe('ScopeAnalyzer', () => {
});
});
});
describe('Variant #3: class expression name shadowing import binding', () => {
const samplesCount: number = 50;
let obfuscatedCode: string;
let importBindingName: string | null;
let classExpressionName: string | null;
beforeEach(() => {
const code: string = readFileAsString(
__dirname + '/fixtures/class-expression-name-shadowing-import.js'
);
importBindingName = null;
classExpressionName = null;
for (let i = 0; i < samplesCount; i++) {
obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
...NO_ADDITIONAL_NODES_PRESET,
compact: false,
stringArray: false,
seed: i
}).getObfuscatedCode();
const importMatch = obfuscatedCode.match(/import\s*\{\s*i\s+as\s+(\w+)\s*\}/);
const classMatch = obfuscatedCode.match(/=\s*class\s+(\w+)\s*\{/);
if (importMatch && classMatch) {
importBindingName = importMatch[1];
classExpressionName = classMatch[1];
if (importBindingName !== classExpressionName) {
break;
}
}
}
});
it('should not rename class expression self-references to the import binding', () => {
assert.isNotNull(importBindingName, 'should find import binding name');
assert.isNotNull(classExpressionName, 'should find class expression name');
const getInstanceMatch = obfuscatedCode.match(/getInstance[^}]*\{([^}]*)\}/s);
assert.isNotNull(getInstanceMatch, 'should find getInstance body');
const getInstanceBody: string = getInstanceMatch![1];
assert.include(
getInstanceBody,
`new ${classExpressionName!}()`,
'new <class>() inside getInstance should reference the class expression name'
);
assert.notInclude(
getInstanceBody,
`new ${importBindingName!}()`,
'new <import>() inside getInstance should NOT reference the import binding'
);
});
});
});
});
@@ -0,0 +1,17 @@
import { i as e } from './runtime.js';
function factory() {
return 'factory-result';
}
var P = class e {
static instance;
static getInstance() {
return ((e.instance ||= new e()), e.instance);
}
};
var x = e(factory(), 1);
export { P, x };