From 8be923d41b601ffeb9631caa011a4fe3a929ef65 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Thu, 9 Jul 2026 07:30:31 +0200 Subject: [PATCH 1/4] Optimize scope identifiers transformer performance (#1417) --- .../ScopeIdentifiersTransformer.ts | 74 ++++++++----------- 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/src/node-transformers/rename-identifiers-transformers/ScopeIdentifiersTransformer.ts b/src/node-transformers/rename-identifiers-transformers/ScopeIdentifiersTransformer.ts index 689d8ec0e..eb0f6cefa 100644 --- a/src/node-transformers/rename-identifiers-transformers/ScopeIdentifiersTransformer.ts +++ b/src/node-transformers/rename-identifiers-transformers/ScopeIdentifiersTransformer.ts @@ -32,9 +32,9 @@ export class ScopeIdentifiersTransformer extends AbstractNodeTransformer { private readonly identifierReplacer: IIdentifierReplacer; /** - * @type {WeakMap} + * @type {WeakMap | false>} */ - private readonly lexicalScopesWithObjectPatternWithoutDeclarationMap: WeakMap = + private readonly lexicalScopesWithObjectPatternWithoutDeclarationMap: WeakMap | false> = new WeakMap(); /** @@ -336,14 +336,6 @@ export class ScopeIdentifiersTransformer extends AbstractNodeTransformer { identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope ): boolean { - let isLexicalScopeHasObjectPatternWithoutDeclaration: boolean | undefined = - this.lexicalScopesWithObjectPatternWithoutDeclarationMap.get(lexicalScopeNode); - - // lexical scope was traversed before and object pattern without declaration was not found - if (isLexicalScopeHasObjectPatternWithoutDeclaration === false) { - return false; - } - const hasVarDefinitions: boolean = variable.defs.some( (definition: eslintScope.Definition) => (definition).kind === 'var' ); @@ -352,42 +344,40 @@ export class ScopeIdentifiersTransformer extends AbstractNodeTransformer { return false; } - let isProhibitedVariableDeclaration: boolean = false; - - estraverse.traverse(lexicalScopeNode, { - enter: (node: ESTree.Node, parentNode: ESTree.Node | null): void | estraverse.VisitorOption => { - if ( - NodeGuards.isObjectPatternNode(node) && - parentNode && - NodeGuards.isAssignmentExpressionNode(parentNode) - ) { - isLexicalScopeHasObjectPatternWithoutDeclaration = true; - - const properties: (ESTree.Property | ESTree.RestElement)[] = node.properties; - - for (const property of properties) { - isProhibitedVariableDeclaration = - NodeGuards.isPropertyNode(property) && - !property.computed && - property.shorthand && - NodeGuards.isIdentifierNode(property.key) && - identifierNode.name === property.key.name; - - if (!isProhibitedVariableDeclaration) { - continue; - } + let prohibitedVariableNames: Set | false | undefined = + this.lexicalScopesWithObjectPatternWithoutDeclarationMap.get(lexicalScopeNode); - return estraverse.VisitorOption.Break; + if (prohibitedVariableNames === undefined) { + const foundVariableNames: Set = new Set(); + + estraverse.traverse(lexicalScopeNode, { + enter: (node: ESTree.Node, parentNode: ESTree.Node | null): void => { + if ( + NodeGuards.isObjectPatternNode(node) && + parentNode && + NodeGuards.isAssignmentExpressionNode(parentNode) + ) { + const properties: (ESTree.Property | ESTree.RestElement)[] = node.properties; + + for (const property of properties) { + if ( + NodeGuards.isPropertyNode(property) && + !property.computed && + property.shorthand && + NodeGuards.isIdentifierNode(property.key) + ) { + foundVariableNames.add(property.key.name); + } + } } } - } - }); + }); - this.lexicalScopesWithObjectPatternWithoutDeclarationMap.set( - lexicalScopeNode, - isLexicalScopeHasObjectPatternWithoutDeclaration ?? false - ); + prohibitedVariableNames = foundVariableNames.size ? foundVariableNames : false; + + this.lexicalScopesWithObjectPatternWithoutDeclarationMap.set(lexicalScopeNode, prohibitedVariableNames); + } - return isProhibitedVariableDeclaration; + return prohibitedVariableNames !== false && prohibitedVariableNames.has(identifierNode.name); } } From 004433466509a1eae3e00da0dac7fa7ce299f684 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Thu, 9 Jul 2026 07:30:46 +0200 Subject: [PATCH 2/4] Reuse scope analysis between identifier transformers (#1418) --- src/analyzers/scope-analyzer/ScopeAnalyzer.ts | 24 ++++- .../scope-analyzer/IScopeAnalyzer.ts | 6 ++ .../node-transformers/INodeTransformer.ts | 5 + .../node/IScopeIdentifiersTraverser.ts | 12 +-- .../AbstractNodeTransformer.ts | 5 + .../NodeTransformersRunner.ts | 101 ++++++++++++++---- ...DeadCodeInjectionIdentifiersTransformer.ts | 6 +- .../VariablePreserveTransformer.ts | 41 +++++-- .../ScopeIdentifiersTransformer.ts | 20 +++- .../ScopeThroughIdentifiersTransformer.ts | 20 +++- src/node/ScopeIdentifiersTraverser.ts | 31 +++--- 11 files changed, 209 insertions(+), 62 deletions(-) diff --git a/src/analyzers/scope-analyzer/ScopeAnalyzer.ts b/src/analyzers/scope-analyzer/ScopeAnalyzer.ts index 1c001e376..a976f5f16 100644 --- a/src/analyzers/scope-analyzer/ScopeAnalyzer.ts +++ b/src/analyzers/scope-analyzer/ScopeAnalyzer.ts @@ -40,6 +40,11 @@ export class ScopeAnalyzer implements IScopeAnalyzer { */ private scopeManager: eslintScope.ScopeManager | null = null; + /** + * @type {eslintScope.ScopeManager | null} + */ + private sanitizedScopeManager: eslintScope.ScopeManager | null = null; + /** * `eslint-scope` reads `ranges` property of a nodes * Should attach that property to the some custom nodes @@ -75,6 +80,9 @@ export class ScopeAnalyzer implements IScopeAnalyzer { public analyze(astTree: ESTree.Node): void { const sourceTypeLength: number = ScopeAnalyzer.sourceTypes.length; + this.scopeManager = null; + this.sanitizedScopeManager = null; + ScopeAnalyzer.attachMissingRanges(astTree); for (let i: number = 0; i < sourceTypeLength; i++) { @@ -117,11 +125,25 @@ export class ScopeAnalyzer implements IScopeAnalyzer { throw new Error('Cannot acquire scope for node'); } - this.sanitizeScopes(scope); + if (this.sanitizedScopeManager !== this.scopeManager) { + this.sanitizeScopes(scope); + this.sanitizedScopeManager = this.scopeManager; + } return scope; } + /** + * Checks whether a scope for the given node is already available in the current + * scope manager (i.e. `analyze` has been run for the tree this node belongs to). + * + * @param {Node} node + * @returns {boolean} + */ + public isAnalyzed(node: ESTree.Node): boolean { + return !!this.scopeManager?.acquire(node, ScopeAnalyzer.isRootNode(node)); + } + /** * Fix Annex B function hoisting references. * diff --git a/src/interfaces/analyzers/scope-analyzer/IScopeAnalyzer.ts b/src/interfaces/analyzers/scope-analyzer/IScopeAnalyzer.ts index a4d80b071..bd6aa1119 100644 --- a/src/interfaces/analyzers/scope-analyzer/IScopeAnalyzer.ts +++ b/src/interfaces/analyzers/scope-analyzer/IScopeAnalyzer.ts @@ -14,4 +14,10 @@ export interface IScopeAnalyzer extends IAnalyzer<[ESTree.Node], void> { * @returns {Scope} */ acquireScope(node: ESTree.Node): eslintScope.Scope; + + /** + * @param {Node} node + * @returns {boolean} + */ + isAnalyzed(node: ESTree.Node): boolean; } diff --git a/src/interfaces/node-transformers/INodeTransformer.ts b/src/interfaces/node-transformers/INodeTransformer.ts index 9d67274df..26a54100c 100644 --- a/src/interfaces/node-transformers/INodeTransformer.ts +++ b/src/interfaces/node-transformers/INodeTransformer.ts @@ -8,6 +8,11 @@ import { NodeTransformer } from '../../enums/node-transformers/NodeTransformer'; import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage'; export interface INodeTransformer extends ITransformer { + /** + * @type {boolean} + */ + runOnProgramNodeOnly?: boolean; + /** * @param {NodeTransformationStage} nodeTransformationStage * @returns {IVisitor | null} diff --git a/src/interfaces/node/IScopeIdentifiersTraverser.ts b/src/interfaces/node/IScopeIdentifiersTraverser.ts index 870b838e9..973ec4582 100644 --- a/src/interfaces/node/IScopeIdentifiersTraverser.ts +++ b/src/interfaces/node/IScopeIdentifiersTraverser.ts @@ -7,23 +7,23 @@ import { IScopeThroughIdentifiersTraverserCallbackData } from './IScopeThroughId export interface IScopeIdentifiersTraverser { /** * @param {Program} programNode - * @param {Node | null} parentNode * @param {TScopeIdentifiersTraverserCallback} callback + * @param {boolean} analyzeScope */ traverseScopeIdentifiers( programNode: ESTree.Program, - parentNode: ESTree.Node | null, - callback: TScopeIdentifiersTraverserCallback + callback: TScopeIdentifiersTraverserCallback, + analyzeScope?: boolean ): void; /** * @param {Node} node - * @param {Node | null} parentNode * @param {TScopeIdentifiersTraverserCallback} callback + * @param {boolean} analyzeScope */ traverseScopeThroughIdentifiers( node: ESTree.Node, - parentNode: ESTree.Node | null, - callback: TScopeIdentifiersTraverserCallback + callback: TScopeIdentifiersTraverserCallback, + analyzeScope?: boolean ): void; } diff --git a/src/node-transformers/AbstractNodeTransformer.ts b/src/node-transformers/AbstractNodeTransformer.ts index ebaa85e4c..2d97957cc 100644 --- a/src/node-transformers/AbstractNodeTransformer.ts +++ b/src/node-transformers/AbstractNodeTransformer.ts @@ -19,6 +19,11 @@ export abstract class AbstractNodeTransformer implements INodeTransformer { */ public readonly runAfter: NodeTransformer[] | undefined; + /** + * @type {boolean} + */ + public readonly runOnProgramNodeOnly: boolean | undefined; + /** * @type {IOptions} */ diff --git a/src/node-transformers/NodeTransformersRunner.ts b/src/node-transformers/NodeTransformersRunner.ts index 360566caf..bcbed0ecd 100644 --- a/src/node-transformers/NodeTransformersRunner.ts +++ b/src/node-transformers/NodeTransformersRunner.ts @@ -23,6 +23,12 @@ import { VisitorDirection } from '../enums/node-transformers/VisitorDirection'; import { NodeGuards } from '../node/NodeGuards'; import { NodeMetadata } from '../node/NodeMetadata'; +interface IVisitorsData { + enterVisitors: IVisitor[]; + leaveVisitors: IVisitor[]; + runOnProgramNodeOnly: boolean; +} + @injectable() export class NodeTransformersRunner implements INodeTransformersRunner { /** @@ -75,27 +81,20 @@ export class NodeTransformersRunner implements INodeTransformersRunner { this.nodeTransformerNamesGroupsBuilder.build(normalizedNodeTransformers); for (const nodeTransformerNamesGroup of nodeTransformerNamesGroups) { - const enterVisitors: IVisitor[] = []; - const leaveVisitors: IVisitor[] = []; - - for (const nodeTransformerName of nodeTransformerNamesGroup) { - const nodeTransformer: INodeTransformer = normalizedNodeTransformers[nodeTransformerName]; - const visitor: IVisitor | null = nodeTransformer.getVisitor(nodeTransformationStage); - - if (!visitor) { - continue; - } + const visitorsData: IVisitorsData = this.buildVisitorsData( + nodeTransformerNamesGroup, + normalizedNodeTransformers, + nodeTransformationStage + ); + const { enterVisitors, leaveVisitors } = visitorsData; - if (visitor.enter) { - enterVisitors.push({ enter: visitor.enter }); - } - - if (visitor.leave) { - leaveVisitors.push({ leave: visitor.leave }); - } + if (!enterVisitors.length && !leaveVisitors.length) { + continue; } - if (!enterVisitors.length && !leaveVisitors.length) { + if (this.canRunOnProgramNodeOnly(visitorsData, astTree)) { + astTree = this.runOnProgramNodeOnly(astTree, enterVisitors); + continue; } @@ -108,6 +107,72 @@ export class NodeTransformersRunner implements INodeTransformersRunner { return astTree; } + /** + * @param {NodeTransformer[]} nodeTransformerNamesGroup + * @param {TDictionary} normalizedNodeTransformers + * @param {NodeTransformationStage} nodeTransformationStage + * @returns {IVisitorsData} + */ + private buildVisitorsData( + nodeTransformerNamesGroup: NodeTransformer[], + normalizedNodeTransformers: TDictionary, + nodeTransformationStage: NodeTransformationStage + ): IVisitorsData { + const enterVisitors: IVisitor[] = []; + const leaveVisitors: IVisitor[] = []; + let runOnProgramNodeOnly: boolean = true; + + for (const nodeTransformerName of nodeTransformerNamesGroup) { + const nodeTransformer: INodeTransformer = normalizedNodeTransformers[nodeTransformerName]; + const visitor: IVisitor | null = nodeTransformer.getVisitor(nodeTransformationStage); + + if (!visitor) { + continue; + } + + if (!nodeTransformer.runOnProgramNodeOnly) { + runOnProgramNodeOnly = false; + } + + if (visitor.enter) { + enterVisitors.push({ enter: visitor.enter }); + } + + if (visitor.leave) { + leaveVisitors.push({ leave: visitor.leave }); + } + } + + return { enterVisitors, leaveVisitors, runOnProgramNodeOnly }; + } + + /** + * @param {IVisitorsData} visitorsData + * @param {Node} astTree + * @returns {boolean} + */ + private canRunOnProgramNodeOnly(visitorsData: IVisitorsData, astTree: ESTree.Node): boolean { + return ( + visitorsData.runOnProgramNodeOnly && + !visitorsData.leaveVisitors.length && + NodeGuards.isProgramNode(astTree) + ); + } + + /** + * @param {T} astTree + * @param {IVisitor[]} enterVisitors + * @returns {T} + */ + private runOnProgramNodeOnly(astTree: T, enterVisitors: IVisitor[]): T { + const visitorResult: TVisitorResult = this.mergeVisitorsForDirection( + enterVisitors, + VisitorDirection.Enter + )(astTree, astTree.parentNode ?? null); + + return visitorResult && NodeGuards.isNode(visitorResult) ? visitorResult : astTree; + } + /** * @param {NodeTransformer[]} nodeTransformerNames * @param {NodeTransformationStage} nodeTransformationStage diff --git a/src/node-transformers/dead-code-injection-transformers/DeadCodeInjectionIdentifiersTransformer.ts b/src/node-transformers/dead-code-injection-transformers/DeadCodeInjectionIdentifiersTransformer.ts index 9f253f346..1aa8c01ea 100644 --- a/src/node-transformers/dead-code-injection-transformers/DeadCodeInjectionIdentifiersTransformer.ts +++ b/src/node-transformers/dead-code-injection-transformers/DeadCodeInjectionIdentifiersTransformer.ts @@ -62,7 +62,7 @@ export class DeadCodeInjectionIdentifiersTransformer extends AbstractNodeTransfo return { enter: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => { if (parentNode && NodeGuards.isProgramNode(node)) { - return this.transformNode(node, parentNode); + return this.transformNode(node); } } }; @@ -74,13 +74,11 @@ export class DeadCodeInjectionIdentifiersTransformer extends AbstractNodeTransfo /** * @param {VariableDeclaration} programNode - * @param {NodeGuards} parentNode * @returns {NodeGuards} */ - public transformNode(programNode: ESTree.Program, parentNode: ESTree.Node): ESTree.Node { + public transformNode(programNode: ESTree.Program): ESTree.Node { this.scopeIdentifiersTraverser.traverseScopeThroughIdentifiers( programNode, - parentNode, (data: IScopeThroughIdentifiersTraverserCallbackData) => { const { reference, variableLexicalScopeNode } = data; diff --git a/src/node-transformers/preparing-transformers/VariablePreserveTransformer.ts b/src/node-transformers/preparing-transformers/VariablePreserveTransformer.ts index 2d6f8b3a5..8f192c918 100644 --- a/src/node-transformers/preparing-transformers/VariablePreserveTransformer.ts +++ b/src/node-transformers/preparing-transformers/VariablePreserveTransformer.ts @@ -29,6 +29,11 @@ export class VariablePreserveTransformer extends AbstractNodeTransformer { */ public override readonly runAfter: NodeTransformer[] = [NodeTransformer.ParentificationTransformer]; + /** + * @type {boolean} + */ + public override readonly runOnProgramNodeOnly: boolean = true; + /** * @type {IIdentifierReplacer} */ @@ -64,17 +69,21 @@ export class VariablePreserveTransformer extends AbstractNodeTransformer { * @returns {IVisitor | null} */ public getVisitor(nodeTransformationStage: NodeTransformationStage): IVisitor | null { + const visitor: IVisitor = { + enter: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => { + if (parentNode && NodeGuards.isProgramNode(node)) { + return this.transformNode(node); + } + } + }; + switch (nodeTransformationStage) { case NodeTransformationStage.Preparing: - case NodeTransformationStage.Converting: case NodeTransformationStage.RenameIdentifiers: - return { - enter: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => { - if (parentNode && NodeGuards.isProgramNode(node)) { - return this.transformNode(node, parentNode); - } - } - }; + return visitor; + + case NodeTransformationStage.Converting: + return this.shouldPreserveConvertingStageIdentifiers() ? visitor : null; default: return null; @@ -83,13 +92,11 @@ export class VariablePreserveTransformer extends AbstractNodeTransformer { /** * @param {VariableDeclaration} programNode - * @param {NodeGuards} parentNode * @returns {NodeGuards} */ - public transformNode(programNode: ESTree.Program, parentNode: ESTree.Node): ESTree.Node { + public transformNode(programNode: ESTree.Program): ESTree.Node { this.scopeIdentifiersTraverser.traverseScopeIdentifiers( programNode, - parentNode, this.preserveScopeVariableIdentifiers ); @@ -136,4 +143,16 @@ export class VariablePreserveTransformer extends AbstractNodeTransformer { this.identifierReplacer.preserveNameForLexicalScope(identifierNode, lexicalScopeNode); } + + /** + * @returns {boolean} + */ + private shouldPreserveConvertingStageIdentifiers(): boolean { + return ( + this.options.controlFlowFlattening || + this.options.deadCodeInjection || + this.options.stringArray || + this.options.transformObjectKeys + ); + } } diff --git a/src/node-transformers/rename-identifiers-transformers/ScopeIdentifiersTransformer.ts b/src/node-transformers/rename-identifiers-transformers/ScopeIdentifiersTransformer.ts index eb0f6cefa..4ae7e37ee 100644 --- a/src/node-transformers/rename-identifiers-transformers/ScopeIdentifiersTransformer.ts +++ b/src/node-transformers/rename-identifiers-transformers/ScopeIdentifiersTransformer.ts @@ -15,6 +15,7 @@ import { IScopeIdentifiersTraverserCallbackData } from '../../interfaces/node/IS import { IVisitor } from '../../interfaces/node-transformers/IVisitor'; import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage'; +import { NodeTransformer } from '../../enums/node-transformers/NodeTransformer'; import { AbstractNodeTransformer } from '../AbstractNodeTransformer'; import { NodeGuards } from '../../node/NodeGuards'; @@ -26,6 +27,16 @@ import { NodeMetadata } from '../../node/NodeMetadata'; @injectFromBase() @injectable() export class ScopeIdentifiersTransformer extends AbstractNodeTransformer { + /** + * @type {NodeTransformer[]} + */ + public override readonly runAfter: NodeTransformer[] = [NodeTransformer.VariablePreserveTransformer]; + + /** + * @type {boolean} + */ + public override readonly runOnProgramNodeOnly: boolean = true; + /** * @type {IIdentifierReplacer} */ @@ -70,7 +81,7 @@ export class ScopeIdentifiersTransformer extends AbstractNodeTransformer { return { enter: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => { if (parentNode && NodeGuards.isProgramNode(node)) { - return this.transformNode(node, parentNode); + return this.transformNode(node); } } }; @@ -82,13 +93,11 @@ export class ScopeIdentifiersTransformer extends AbstractNodeTransformer { /** * @param {VariableDeclaration} programNode - * @param {NodeGuards} parentNode * @returns {NodeGuards} */ - public transformNode(programNode: ESTree.Program, parentNode: ESTree.Node): ESTree.Node { + public transformNode(programNode: ESTree.Program): ESTree.Node { this.scopeIdentifiersTraverser.traverseScopeIdentifiers( programNode, - parentNode, (data: IScopeIdentifiersTraverserCallbackData) => { const { isGlobalDeclaration, variable, variableLexicalScopeNode } = data; @@ -105,7 +114,8 @@ export class ScopeIdentifiersTransformer extends AbstractNodeTransformer { } this.transformScopeVariableIdentifiers(variable, variableLexicalScopeNode, isGlobalDeclaration); - } + }, + false ); return programNode; diff --git a/src/node-transformers/rename-identifiers-transformers/ScopeThroughIdentifiersTransformer.ts b/src/node-transformers/rename-identifiers-transformers/ScopeThroughIdentifiersTransformer.ts index ff2c93d88..bb051b703 100644 --- a/src/node-transformers/rename-identifiers-transformers/ScopeThroughIdentifiersTransformer.ts +++ b/src/node-transformers/rename-identifiers-transformers/ScopeThroughIdentifiersTransformer.ts @@ -14,6 +14,7 @@ import { IThroughIdentifierReplacer } from '../../interfaces/node-transformers/r import { IVisitor } from '../../interfaces/node-transformers/IVisitor'; import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage'; +import { NodeTransformer } from '../../enums/node-transformers/NodeTransformer'; import { AbstractNodeTransformer } from '../AbstractNodeTransformer'; import { NodeGuards } from '../../node/NodeGuards'; @@ -24,6 +25,16 @@ import { NodeGuards } from '../../node/NodeGuards'; @injectFromBase() @injectable() export class ScopeThroughIdentifiersTransformer extends AbstractNodeTransformer { + /** + * @type {NodeTransformer[]} + */ + public override readonly runAfter: NodeTransformer[] = [NodeTransformer.ScopeIdentifiersTransformer]; + + /** + * @type {boolean} + */ + public override readonly runOnProgramNodeOnly: boolean = true; + /** * @type {IScopeIdentifiersTraverser} */ @@ -62,7 +73,7 @@ export class ScopeThroughIdentifiersTransformer extends AbstractNodeTransformer return { enter: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => { if (parentNode && NodeGuards.isProgramNode(node)) { - return this.transformNode(node, parentNode); + return this.transformNode(node); } } }; @@ -74,18 +85,17 @@ export class ScopeThroughIdentifiersTransformer extends AbstractNodeTransformer /** * @param {VariableDeclaration} programNode - * @param {NodeGuards} parentNode * @returns {NodeGuards} */ - public transformNode(programNode: ESTree.Program, parentNode: ESTree.Node): ESTree.Node { + public transformNode(programNode: ESTree.Program): ESTree.Node { this.scopeIdentifiersTraverser.traverseScopeThroughIdentifiers( programNode, - parentNode, (data: IScopeThroughIdentifiersTraverserCallbackData) => { const { reference, variableLexicalScopeNode } = data; this.transformScopeThroughIdentifiers(reference, variableLexicalScopeNode); - } + }, + false ); return programNode; diff --git a/src/node/ScopeIdentifiersTraverser.ts b/src/node/ScopeIdentifiersTraverser.ts index 7261bfa91..b14eb3047 100644 --- a/src/node/ScopeIdentifiersTraverser.ts +++ b/src/node/ScopeIdentifiersTraverser.ts @@ -43,34 +43,28 @@ export class ScopeIdentifiersTraverser implements IScopeIdentifiersTraverser { /** * @param {Program} programNode - * @param {Node | null} parentNode * @param {TScopeIdentifiersTraverserCallback} callback */ public traverseScopeIdentifiers( programNode: ESTree.Program, - parentNode: ESTree.Node | null, - callback: TScopeIdentifiersTraverserCallback + callback: TScopeIdentifiersTraverserCallback, + analyzeScope: boolean = true ): void { - this.scopeAnalyzer.analyze(programNode); - - const globalScope: eslintScope.Scope = this.scopeAnalyzer.acquireScope(programNode); + const globalScope: eslintScope.Scope = this.acquireGlobalScope(programNode, analyzeScope); this.traverseScopeIdentifiersRecursive(globalScope, globalScope, callback); } /** * @param {Program} programNode - * @param {Node | null} parentNode * @param {TScopeIdentifiersTraverserCallback} callback */ public traverseScopeThroughIdentifiers( programNode: ESTree.Program, - parentNode: ESTree.Node | null, - callback: TScopeIdentifiersTraverserCallback + callback: TScopeIdentifiersTraverserCallback, + analyzeScope: boolean = true ): void { - this.scopeAnalyzer.analyze(programNode); - - const globalScope: eslintScope.Scope = this.scopeAnalyzer.acquireScope(programNode); + const globalScope: eslintScope.Scope = this.acquireGlobalScope(programNode, analyzeScope); this.traverseScopeThroughIdentifiersRecursive(globalScope, globalScope, callback); } @@ -158,4 +152,17 @@ export class ScopeIdentifiersTraverser implements IScopeIdentifiersTraverser { this.traverseScopeThroughIdentifiersRecursive(rootScope, childScope, callback); } } + + /** + * @param {Program} programNode + * @param {boolean} analyzeScope + * @returns {Scope} + */ + private acquireGlobalScope(programNode: ESTree.Program, analyzeScope: boolean): eslintScope.Scope { + if (analyzeScope || !this.scopeAnalyzer.isAnalyzed(programNode)) { + this.scopeAnalyzer.analyze(programNode); + } + + return this.scopeAnalyzer.acquireScope(programNode); + } } From fecc14506a7167dcb796887f61c9d5e412e2146c Mon Sep 17 00:00:00 2001 From: Sebastian Cao Date: Thu, 9 Jul 2026 13:33:44 +0800 Subject: [PATCH 3/4] Fixed `SyntaxError` when obfuscating a class that extends a boolean literal (#1420) --- CHANGELOG.md | 4 ++++ .../BooleanLiteralTransformer.ts | 9 +++++++++ .../BooleanLiteralTransformer.spec.ts | 18 ++++++++++++++++++ .../fixtures/super-class-value.js | 1 + 4 files changed, 32 insertions(+) create mode 100644 test/functional-tests/node-transformers/converting-transformers/boolean-literal-transformer/fixtures/super-class-value.js diff --git a/CHANGELOG.md b/CHANGELOG.md index eb4ce7892..f4ae63703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.4.5 +--- +* Fixed `SyntaxError` when obfuscating a class that extends a boolean literal (e.g. `class C extends true {}`). Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1131 + v5.4.4 --- * Fixed `Invalid regular expression` error when obfuscating code that uses ES2025 RegExp pattern modifiers (e.g. `/(?i:abc)/`). Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1410 diff --git a/src/node-transformers/converting-transformers/BooleanLiteralTransformer.ts b/src/node-transformers/converting-transformers/BooleanLiteralTransformer.ts index d3a6672ce..cb8bbe162 100644 --- a/src/node-transformers/converting-transformers/BooleanLiteralTransformer.ts +++ b/src/node-transformers/converting-transformers/BooleanLiteralTransformer.ts @@ -66,6 +66,15 @@ export class BooleanLiteralTransformer extends AbstractNodeTransformer { return literalNode; } + /** + * A class heritage (`extends`) must be a `LeftHandSideExpression`, so replacing a + * boolean literal there with a unary expression (`class Foo extends ![] {}`) would + * produce a `SyntaxError`. Keep the original literal in that position. + */ + if ('superClass' in parentNode && parentNode.superClass === literalNode) { + return literalNode; + } + const literalValue: ESTree.SimpleLiteral['value'] = literalNode.value; const unaryExpressionNode: ESTree.UnaryExpression = literalValue diff --git a/test/functional-tests/node-transformers/converting-transformers/boolean-literal-transformer/BooleanLiteralTransformer.spec.ts b/test/functional-tests/node-transformers/converting-transformers/boolean-literal-transformer/BooleanLiteralTransformer.spec.ts index de8f025bf..863ab61a6 100644 --- a/test/functional-tests/node-transformers/converting-transformers/boolean-literal-transformer/BooleanLiteralTransformer.spec.ts +++ b/test/functional-tests/node-transformers/converting-transformers/boolean-literal-transformer/BooleanLiteralTransformer.spec.ts @@ -46,4 +46,22 @@ describe('BooleanLiteralTransformer', () => { assert.match(obfuscatedCode, regExp); }); }); + + describe('boolean literal as a class super class', () => { + const regExp: RegExp = /^class Foo extends true *{}$/; + + let obfuscatedCode: string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/super-class-value.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should not transform a boolean literal used as a class heritage to avoid a `SyntaxError`', () => { + assert.match(obfuscatedCode, regExp); + }); + }); }); diff --git a/test/functional-tests/node-transformers/converting-transformers/boolean-literal-transformer/fixtures/super-class-value.js b/test/functional-tests/node-transformers/converting-transformers/boolean-literal-transformer/fixtures/super-class-value.js new file mode 100644 index 000000000..7e5c9c82d --- /dev/null +++ b/test/functional-tests/node-transformers/converting-transformers/boolean-literal-transformer/fixtures/super-class-value.js @@ -0,0 +1 @@ +class Foo extends true {} From c6d0872a631ed290d29fce6551fbfceb6c31577d Mon Sep 17 00:00:00 2001 From: sanex3339 Date: Thu, 9 Jul 2026 09:35:51 +0400 Subject: [PATCH 4/4] Update changelog --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4ae63703..0fa607161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,11 @@ Change Log -v5.4.5 ---- -* Fixed `SyntaxError` when obfuscating a class that extends a boolean literal (e.g. `class C extends true {}`). Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1131 - v5.4.4 --- +* Optimized scope identifiers transformer performance +* Optimized identifier renaming performance by reusing scope analysis between transformers * Fixed `Invalid regular expression` error when obfuscating code that uses ES2025 RegExp pattern modifiers (e.g. `/(?i:abc)/`). Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1410 +* Fixed `SyntaxError` when obfuscating a class that extends a boolean literal (e.g. `class C extends true {}`). Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1131 v5.4.3 ---