浏览代码

Merge pull request #742 from javascript-obfuscator/string-array-wrappers-visited-scopes-storage

Refactoring of StringArrayTransformer.ts: moved logic to StringArrayW…
Timofey Kachalov 4 年之前
父节点
当前提交
b40b580560
共有 27 个文件被更改,包括 1176 次插入929 次删除
  1. 0 0
      dist/index.browser.js
  2. 0 0
      dist/index.cli.js
  3. 0 0
      dist/index.js
  4. 1 0
      src/JavaScriptObfuscator.ts
  5. 3 1
      src/container/ServiceIdentifiers.ts
  6. 5 0
      src/container/modules/node-transformers/StringArrayTransformersModule.ts
  7. 12 0
      src/container/modules/storages/StoragesModule.ts
  8. 1 0
      src/enums/node-transformers/NodeTransformer.ts
  9. 10 0
      src/interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperDataStorage.ts
  10. 21 0
      src/interfaces/storages/string-array-transformers/IVisitedLexicalScopeNodesStackStorage.ts
  11. 2 2
      src/interfaces/utils/IArrayUtils.ts
  12. 211 0
      src/node-transformers/string-array-transformers/StringArrayScopeCallsWrapperTransformer.ts
  13. 35 149
      src/node-transformers/string-array-transformers/StringArrayTransformer.ts
  14. 28 0
      src/storages/string-array-transformers/StringArrayScopeCallsWrapperDataStorage.ts
  15. 55 0
      src/storages/string-array-transformers/VisitedLexicalScopeNodesStackStorage.ts
  16. 3 3
      src/utils/ArrayUtils.ts
  17. 766 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/StringArrayScopeCallsWrapperTransformer.spec.ts
  18. 0 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/chained-calls-1.js
  19. 0 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/chained-calls-2.js
  20. 0 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/prohibited-scope-1.js
  21. 0 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/prohibited-scope-2.js
  22. 0 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/wrappers-count-const.js
  23. 0 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/wrappers-count-eval.js
  24. 0 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/wrappers-count-var.js
  25. 17 769
      test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/StringArrayTransformer.spec.ts
  26. 1 0
      test/index.spec.ts
  27. 5 5
      test/unit-tests/utils/ArrayUtils.spec.ts

文件差异内容过多而无法显示
+ 0 - 0
dist/index.browser.js


文件差异内容过多而无法显示
+ 0 - 0
dist/index.cli.js


文件差异内容过多而无法显示
+ 0 - 0
dist/index.js


+ 1 - 0
src/JavaScriptObfuscator.ts

@@ -86,6 +86,7 @@ export class JavaScriptObfuscator implements IJavaScriptObfuscator {
         NodeTransformer.ParentificationTransformer,
         NodeTransformer.ScopeIdentifiersTransformer,
         NodeTransformer.SplitStringTransformer,
+        NodeTransformer.StringArrayScopeCallsWrapperTransformer,
         NodeTransformer.StringArrayTransformer,
         NodeTransformer.TemplateLiteralTransformer,
         NodeTransformer.VariableDeclarationsMergeTransformer,

+ 3 - 1
src/container/ServiceIdentifiers.ts

@@ -37,6 +37,7 @@ export enum ServiceIdentifiers {
     INodeGuard = 'INodeGuard',
     INodeTransformer = 'INodeTransformer',
     INodeTransformerNamesGroupsBuilder = 'INodeTransformerNamesGroupsBuilder',
+    INodeTransformersRunner = 'INodeTransformersRunner',
     INumberNumericalExpressionAnalyzer = 'INumberNumericalExpressionAnalyzer',
     IObfuscationEventEmitter = 'IObfuscationEventEmitter',
     IObfuscatedCode = 'IObfuscatedCode',
@@ -49,9 +50,10 @@ export enum ServiceIdentifiers {
     IScopeIdentifiersTraverser = 'IScopeIdentifiersTraverser',
     ISourceCode = 'ISourceCode',
     IScopeAnalyzer = 'IScopeAnalyzer',
+    IStringArrayScopeCallsWrapperDataStorage = 'IStringArrayScopeCallsWrapperDataStorage',
     IStringArrayStorage = 'IStringArrayStorage',
     IStringArrayStorageAnalyzer = 'IStringArrayStorageAnalyzer',
-    INodeTransformersRunner = 'INodeTransformersRunner',
+    IVisitedLexicalScopeNodesStackStorage = 'IVisitedLexicalScopeNodesStackStorage',
     Newable__ICustomNode = 'Newable<ICustomNode>',
     Newable__TControlFlowStorage = 'Newable<TControlFlowStorage>',
     TCustomNodeGroupStorage = 'TCustomNodeGroupStorage',

+ 5 - 0
src/container/modules/node-transformers/StringArrayTransformersModule.ts

@@ -5,10 +5,15 @@ import { INodeTransformer } from '../../../interfaces/node-transformers/INodeTra
 
 import { NodeTransformer } from '../../../enums/node-transformers/NodeTransformer';
 
+import { StringArrayScopeCallsWrapperTransformer } from '../../../node-transformers/string-array-transformers/StringArrayScopeCallsWrapperTransformer';
 import { StringArrayTransformer } from '../../../node-transformers/string-array-transformers/StringArrayTransformer';
 
 export const stringArrayTransformersModule: interfaces.ContainerModule = new ContainerModule((bind: interfaces.Bind) => {
     // string array transformers
+    bind<INodeTransformer>(ServiceIdentifiers.INodeTransformer)
+        .to(StringArrayScopeCallsWrapperTransformer)
+        .whenTargetNamed(NodeTransformer.StringArrayScopeCallsWrapperTransformer);
+
     bind<INodeTransformer>(ServiceIdentifiers.INodeTransformer)
         .to(StringArrayTransformer)
         .whenTargetNamed(NodeTransformer.StringArrayTransformer);

+ 12 - 0
src/container/modules/storages/StoragesModule.ts

@@ -7,12 +7,16 @@ import { TCustomCodeHelperGroupStorage } from '../../../types/storages/TCustomCo
 import { ILiteralNodesCacheStorage } from '../../../interfaces/storages/string-array-transformers/ILiteralNodesCacheStorage';
 import { IOptions } from '../../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../../interfaces/utils/IRandomGenerator';
+import { IStringArrayScopeCallsWrapperDataStorage } from '../../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperDataStorage';
 import { IStringArrayStorage } from '../../../interfaces/storages/string-array-transformers/IStringArrayStorage';
+import { IVisitedLexicalScopeNodesStackStorage } from '../../../interfaces/storages/string-array-transformers/IVisitedLexicalScopeNodesStackStorage';
 
 import { ControlFlowStorage } from '../../../storages/custom-nodes/ControlFlowStorage';
 import { CustomCodeHelperGroupStorage } from '../../../storages/custom-code-helpers/CustomCodeHelperGroupStorage';
 import { LiteralNodesCacheStorage } from '../../../storages/string-array-transformers/LiteralNodesCacheStorage';
+import { StringArrayScopeCallsWrapperDataStorage } from '../../../storages/string-array-transformers/StringArrayScopeCallsWrapperDataStorage';
 import { StringArrayStorage } from '../../../storages/string-array-transformers/StringArrayStorage';
+import { VisitedLexicalScopeNodesStackStorage } from '../../../storages/string-array-transformers/VisitedLexicalScopeNodesStackStorage';
 
 export const storagesModule: interfaces.ContainerModule = new ContainerModule((bind: interfaces.Bind) => {
     // storages
@@ -28,6 +32,14 @@ export const storagesModule: interfaces.ContainerModule = new ContainerModule((b
         .to(StringArrayStorage)
         .inSingletonScope();
 
+    bind<IStringArrayScopeCallsWrapperDataStorage>(ServiceIdentifiers.IStringArrayScopeCallsWrapperDataStorage)
+        .to(StringArrayScopeCallsWrapperDataStorage)
+        .inSingletonScope();
+
+    bind<IVisitedLexicalScopeNodesStackStorage>(ServiceIdentifiers.IVisitedLexicalScopeNodesStackStorage)
+        .to(VisitedLexicalScopeNodesStackStorage)
+        .inSingletonScope();
+
     bind<interfaces.Newable<TControlFlowStorage>>(ServiceIdentifiers.Newable__TControlFlowStorage)
         .toConstructor(ControlFlowStorage);
 

+ 1 - 0
src/enums/node-transformers/NodeTransformer.ts

@@ -25,6 +25,7 @@ export enum NodeTransformer {
     ScopeThroughIdentifiersTransformer = 'ScopeThroughIdentifiersTransformer',
     SplitStringTransformer = 'SplitStringTransformer',
     StringArrayTransformer = 'StringArrayTransformer',
+    StringArrayScopeCallsWrapperTransformer = 'StringArrayScopeCallsWrapperTransformer',
     TemplateLiteralTransformer = 'TemplateLiteralTransformer',
     VariableDeclarationsMergeTransformer = 'VariableDeclarationsMergeTransformer',
     VariablePreserveTransformer = 'VariablePreserveTransformer'

+ 10 - 0
src/interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperDataStorage.ts

@@ -0,0 +1,10 @@
+import { TNodeWithLexicalScope } from '../../../types/node/TNodeWithLexicalScope';
+import { TStringArrayScopeCallsWrapperDataByEncoding } from '../../../types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperDataByEncoding';
+
+import { IMapStorage } from '../IMapStorage';
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface IStringArrayScopeCallsWrapperDataStorage extends IMapStorage<
+    TNodeWithLexicalScope,
+    TStringArrayScopeCallsWrapperDataByEncoding
+> {}

+ 21 - 0
src/interfaces/storages/string-array-transformers/IVisitedLexicalScopeNodesStackStorage.ts

@@ -0,0 +1,21 @@
+import { TNodeWithLexicalScopeAndStatements } from '../../../types/node/TNodeWithLexicalScopeAndStatements';
+
+import { IArrayStorage } from '../IArrayStorage';
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface IVisitedLexicalScopeNodesStackStorage extends IArrayStorage<TNodeWithLexicalScopeAndStatements> {
+    /**
+     * @returns {TNodeWithLexicalScopeAndStatements | undefined}
+     */
+    getLastElement (): TNodeWithLexicalScopeAndStatements | undefined;
+
+    /**
+     * @returns {TNodeWithLexicalScopeAndStatements | undefined}
+     */
+    pop (): TNodeWithLexicalScopeAndStatements | undefined;
+
+    /**
+     * @param {TNodeWithLexicalScopeAndStatements} lexicalScopeNode
+     */
+    push (lexicalScopeNode: TNodeWithLexicalScopeAndStatements): void;
+}

+ 2 - 2
src/interfaces/utils/IArrayUtils.ts

@@ -13,9 +13,9 @@ export interface IArrayUtils {
 
     /**
      * @param {T[]} array
-     * @returns {T | null}
+     * @returns {T | undefined}
      */
-    getLastElement <T> (array: T[]): T | null;
+    getLastElement <T> (array: T[]): T | undefined;
 
     /**
      * @param array

+ 211 - 0
src/node-transformers/string-array-transformers/StringArrayScopeCallsWrapperTransformer.ts

@@ -0,0 +1,211 @@
+import { inject, injectable, } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import * as ESTree from 'estree';
+
+import { TInitialData } from '../../types/TInitialData';
+import { TNodeWithLexicalScope } from '../../types/node/TNodeWithLexicalScope';
+import { TNodeWithLexicalScopeAndStatements } from '../../types/node/TNodeWithLexicalScopeAndStatements';
+import { TStringArrayEncoding } from '../../types/options/TStringArrayEncoding';
+import { TStringArrayScopeCallsWrapperDataByEncoding } from '../../types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperDataByEncoding';
+import { TStringArrayTransformerCustomNodeFactory } from '../../types/container/custom-nodes/TStringArrayTransformerCustomNodeFactory';
+
+import { ICustomNode } from '../../interfaces/custom-nodes/ICustomNode';
+import { IOptions } from '../../interfaces/options/IOptions';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+import { IStringArrayScopeCallsWrapperData } from '../../interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperData';
+import { IStringArrayScopeCallsWrapperDataStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperDataStorage';
+import { IStringArrayStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayStorage';
+import { IVisitedLexicalScopeNodesStackStorage } from '../../interfaces/storages/string-array-transformers/IVisitedLexicalScopeNodesStackStorage';
+import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
+
+import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage';
+import { StringArrayTransformerCustomNode } from '../../enums/custom-nodes/StringArrayTransformerCustomNode';
+
+import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
+import { NodeAppender } from '../../node/NodeAppender';
+import { NodeGuards } from '../../node/NodeGuards';
+import { StringArrayScopeCallsWrapperNode } from '../../custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperNode';
+
+@injectable()
+export class StringArrayScopeCallsWrapperTransformer extends AbstractNodeTransformer {
+    /**
+     * @type {IStringArrayStorage}
+     */
+    private readonly stringArrayStorage: IStringArrayStorage;
+
+    /**
+     * @type {IStringArrayScopeCallsWrapperDataStorage}
+     */
+    private readonly stringArrayScopeCallsWrapperDataStorage: IStringArrayScopeCallsWrapperDataStorage;
+
+    /**
+     * @type {TStringArrayTransformerCustomNodeFactory}
+     */
+    private readonly stringArrayTransformerCustomNodeFactory: TStringArrayTransformerCustomNodeFactory;
+
+    /**
+     * @type {IVisitedLexicalScopeNodesStackStorage}
+     */
+    private readonly visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage;
+
+    /**
+     * @param {IRandomGenerator} randomGenerator
+     * @param {IOptions} options
+     * @param {IVisitedLexicalScopeNodesStackStorage} visitedLexicalScopeNodesStackStorage
+     * @param {IStringArrayStorage} stringArrayStorage
+     * @param {IStringArrayScopeCallsWrapperDataStorage} stringArrayScopeCallsWrapperDataStorage
+     * @param {TStringArrayTransformerCustomNodeFactory} stringArrayTransformerCustomNodeFactory
+     */
+    public constructor (
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
+        @inject(ServiceIdentifiers.IOptions) options: IOptions,
+        @inject(ServiceIdentifiers.IVisitedLexicalScopeNodesStackStorage) visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage,
+        @inject(ServiceIdentifiers.IStringArrayStorage) stringArrayStorage: IStringArrayStorage,
+        @inject(ServiceIdentifiers.IStringArrayScopeCallsWrapperDataStorage) stringArrayScopeCallsWrapperDataStorage: IStringArrayScopeCallsWrapperDataStorage,
+        @inject(ServiceIdentifiers.Factory__IStringArrayTransformerCustomNode)
+            stringArrayTransformerCustomNodeFactory: TStringArrayTransformerCustomNodeFactory
+    ) {
+        super(randomGenerator, options);
+
+        this.visitedLexicalScopeNodesStackStorage = visitedLexicalScopeNodesStackStorage;
+        this.stringArrayStorage = stringArrayStorage;
+        this.stringArrayScopeCallsWrapperDataStorage = stringArrayScopeCallsWrapperDataStorage;
+        this.stringArrayTransformerCustomNodeFactory = stringArrayTransformerCustomNodeFactory;
+    }
+
+    /**
+     * @param {NodeTransformationStage} nodeTransformationStage
+     * @returns {IVisitor | null}
+     */
+    public getVisitor (nodeTransformationStage: NodeTransformationStage): IVisitor | null {
+        switch (nodeTransformationStage) {
+            case NodeTransformationStage.StringArray:
+                return {
+                    enter: (node: ESTree.Node): void => {
+                        if (NodeGuards.isNodeWithLexicalScopeAndStatements(node)) {
+                            this.onLexicalScopeNodeEnter(node);
+                        }
+                    },
+                    leave: (node: ESTree.Node): ESTree.Node | undefined => {
+                        if (NodeGuards.isNodeWithLexicalScopeAndStatements(node)) {
+                            this.onLexicalScopeNodeLeave();
+
+                            return this.transformNode(node);
+                        }
+                    }
+                };
+
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * @param {TNodeWithLexicalScopeAndStatements} lexicalScopeNode
+     * @returns {TNodeWithLexicalScopeAndStatements}
+     */
+    public transformNode (lexicalScopeNode: TNodeWithLexicalScopeAndStatements): TNodeWithLexicalScopeAndStatements {
+        if (!this.options.stringArrayWrappersCount) {
+            return lexicalScopeNode;
+        }
+
+        const lexicalScopeBodyNode: ESTree.Program | ESTree.BlockStatement =
+            NodeGuards.isProgramNode(lexicalScopeNode)
+                ? lexicalScopeNode
+                : lexicalScopeNode.body;
+
+        const stringArrayScopeCallsWrapperDataByEncoding: TStringArrayScopeCallsWrapperDataByEncoding | null =
+            this.stringArrayScopeCallsWrapperDataStorage.get(lexicalScopeNode) ?? null;
+
+        if (!stringArrayScopeCallsWrapperDataByEncoding) {
+            return lexicalScopeNode;
+        }
+
+        const stringArrayScopeCallsWrapperDataList: (IStringArrayScopeCallsWrapperData | undefined)[] =
+            Object.values(stringArrayScopeCallsWrapperDataByEncoding);
+
+        // iterates over data for each encoding type
+        for (const stringArrayScopeCallsWrapperData of stringArrayScopeCallsWrapperDataList) {
+            if (!stringArrayScopeCallsWrapperData) {
+                continue;
+            }
+
+            const {encoding, names} = stringArrayScopeCallsWrapperData;
+            const namesLength: number = names.length;
+
+            /**
+             * Iterates over each name of scope wrapper name
+             * Reverse iteration appends wrappers at index `0` at the correct order
+             */
+            for (let i = namesLength - 1; i >= 0; i--) {
+                const stringArrayScopeCallsWrapperName: string = names[i];
+                const upperStringArrayCallsWrapperName: string = this.getUpperStringArrayCallsWrapperName(encoding);
+
+                const stringArrayScopeCallsWrapperNode: ICustomNode<TInitialData<StringArrayScopeCallsWrapperNode>> =
+                    this.stringArrayTransformerCustomNodeFactory(
+                        StringArrayTransformerCustomNode.StringArrayScopeCallsWrapperNode
+                    );
+
+                stringArrayScopeCallsWrapperNode.initialize(
+                    stringArrayScopeCallsWrapperName,
+                    upperStringArrayCallsWrapperName
+                );
+
+                NodeAppender.prepend(
+                    lexicalScopeBodyNode,
+                    stringArrayScopeCallsWrapperNode.getNode()
+                );
+            }
+        }
+
+        return lexicalScopeNode;
+    }
+
+    /**
+     * @param {TStringArrayEncoding} encoding
+     * @returns {string}
+     */
+    private getRootStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
+        return this.stringArrayStorage.getStorageCallsWrapperName(encoding);
+    }
+
+    /**
+     * @param {TStringArrayEncoding} encoding
+     * @returns {string}
+     */
+    private getUpperStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
+        const rootStringArrayCallsWrapperName: string = this.getRootStringArrayCallsWrapperName(encoding);
+
+        if (!this.options.stringArrayWrappersChainedCalls) {
+            return rootStringArrayCallsWrapperName;
+        }
+
+        const parentLexicalScope: TNodeWithLexicalScope | undefined = this.visitedLexicalScopeNodesStackStorage.getLastElement();
+
+        if (!parentLexicalScope) {
+            return rootStringArrayCallsWrapperName;
+        }
+
+        const parentLexicalScopeDataByEncoding = this.stringArrayScopeCallsWrapperDataStorage
+            .get(parentLexicalScope) ?? null;
+        const parentLexicalScopeNames: string[] | null = parentLexicalScopeDataByEncoding?.[encoding]?.names ?? null;
+
+        return parentLexicalScopeNames?.length
+            ? this.randomGenerator
+                .getRandomGenerator()
+                .pickone(parentLexicalScopeNames)
+            : rootStringArrayCallsWrapperName;
+    }
+
+    /**
+     * @param {TNodeWithLexicalScopeAndStatements} lexicalScopeNode
+     */
+    private onLexicalScopeNodeEnter (lexicalScopeNode: TNodeWithLexicalScopeAndStatements): void {
+        this.visitedLexicalScopeNodesStackStorage.push(lexicalScopeNode);
+    }
+
+    private onLexicalScopeNodeLeave (): void {
+        this.visitedLexicalScopeNodesStackStorage.pop();
+    }
+}

+ 35 - 149
src/node-transformers/string-array-transformers/StringArrayTransformer.ts

@@ -5,13 +5,11 @@ import * as ESTree from 'estree';
 
 import { TInitialData } from '../../types/TInitialData';
 import { TNodeWithLexicalScope } from '../../types/node/TNodeWithLexicalScope';
-import { TNodeWithLexicalScopeAndStatements } from '../../types/node/TNodeWithLexicalScopeAndStatements';
 import { TStatement } from '../../types/node/TStatement';
 import { TStringArrayEncoding } from '../../types/options/TStringArrayEncoding';
 import { TStringArrayScopeCallsWrapperDataByEncoding } from '../../types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperDataByEncoding';
 import { TStringArrayTransformerCustomNodeFactory } from '../../types/container/custom-nodes/TStringArrayTransformerCustomNodeFactory';
 
-import { IArrayUtils } from '../../interfaces/utils/IArrayUtils';
 import { ICustomNode } from '../../interfaces/custom-nodes/ICustomNode';
 import { IEscapeSequenceEncoder } from '../../interfaces/utils/IEscapeSequenceEncoder';
 import { IIdentifierNamesGenerator } from '../../interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator';
@@ -19,32 +17,26 @@ import { TIdentifierNamesGeneratorFactory } from '../../types/container/generato
 import { ILiteralNodesCacheStorage } from '../../interfaces/storages/string-array-transformers/ILiteralNodesCacheStorage';
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
-import { IStringArrayScopeCallsWrapperData } from '../../interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperData';
+import { IStringArrayScopeCallsWrapperDataStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperDataStorage';
 import { IStringArrayStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayStorage';
 import { IStringArrayStorageAnalyzer } from '../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
 import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-transformers/IStringArrayStorageItem';
+import { IVisitedLexicalScopeNodesStackStorage } from '../../interfaces/storages/string-array-transformers/IVisitedLexicalScopeNodesStackStorage';
 import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
 
 import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage';
 import { StringArrayTransformerCustomNode } from '../../enums/custom-nodes/StringArrayTransformerCustomNode';
 
 import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
-import { NodeAppender } from '../../node/NodeAppender';
 import { NodeFactory } from '../../node/NodeFactory';
 import { NodeGuards } from '../../node/NodeGuards';
 import { NodeLiteralUtils } from '../../node/NodeLiteralUtils';
 import { NodeMetadata } from '../../node/NodeMetadata';
 import { NodeUtils } from '../../node/NodeUtils';
 import { StringArrayCallNode } from '../../custom-nodes/string-array-nodes/StringArrayCallNode';
-import { StringArrayScopeCallsWrapperNode } from '../../custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperNode';
 
 @injectable()
 export class StringArrayTransformer extends AbstractNodeTransformer {
-    /**
-     * @type {IArrayUtils}
-     */
-    private readonly arrayUtils: IArrayUtils;
-
     /**
      * @type {IEscapeSequenceEncoder}
      */
@@ -60,14 +52,6 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
      */
     private readonly literalNodesCacheStorage: ILiteralNodesCacheStorage;
 
-    /**
-     * @type {Map<TNodeWithLexicalScope, TStringArrayScopeCallsWrapperDataByEncoding>}
-     */
-    private readonly stringArrayScopeCallsWrapperDataByEncodingMap: Map<
-        TNodeWithLexicalScope,
-        TStringArrayScopeCallsWrapperDataByEncoding
-    > = new Map();
-
     /**
      * @type {IStringArrayStorage}
      */
@@ -78,23 +62,29 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
      */
     private readonly stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer;
 
+    /**
+     * @type {IStringArrayScopeCallsWrapperDataStorage}
+     */
+    private readonly stringArrayScopeCallsWrapperDataStorage: IStringArrayScopeCallsWrapperDataStorage;
+
     /**
      * @type {TStringArrayTransformerCustomNodeFactory}
      */
     private readonly stringArrayTransformerCustomNodeFactory: TStringArrayTransformerCustomNodeFactory;
 
     /**
-     * @type {TNodeWithLexicalScope[]}
+     * @type {IVisitedLexicalScopeNodesStackStorage}
      */
-    private readonly visitedLexicalScopeNodesStack: TNodeWithLexicalScope[] = [];
+    private readonly visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage;
 
     /**
      * @param {IRandomGenerator} randomGenerator
      * @param {IOptions} options
-     * @param {IArrayUtils} arrayUtils
      * @param {IEscapeSequenceEncoder} escapeSequenceEncoder
      * @param {ILiteralNodesCacheStorage} literalNodesCacheStorage
+     * @param {IVisitedLexicalScopeNodesStackStorage} visitedLexicalScopeNodesStackStorage
      * @param {IStringArrayStorage} stringArrayStorage
+     * @param {IStringArrayScopeCallsWrapperDataStorage} stringArrayScopeCallsWrapperDataStorage
      * @param {IStringArrayStorageAnalyzer} stringArrayStorageAnalyzer
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {TStringArrayTransformerCustomNodeFactory} stringArrayTransformerCustomNodeFactory
@@ -102,10 +92,11 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
     public constructor (
         @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
         @inject(ServiceIdentifiers.IOptions) options: IOptions,
-        @inject(ServiceIdentifiers.IArrayUtils) arrayUtils: IArrayUtils,
         @inject(ServiceIdentifiers.IEscapeSequenceEncoder) escapeSequenceEncoder: IEscapeSequenceEncoder,
         @inject(ServiceIdentifiers.ILiteralNodesCacheStorage) literalNodesCacheStorage: ILiteralNodesCacheStorage,
+        @inject(ServiceIdentifiers.IVisitedLexicalScopeNodesStackStorage) visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage,
         @inject(ServiceIdentifiers.IStringArrayStorage) stringArrayStorage: IStringArrayStorage,
+        @inject(ServiceIdentifiers.IStringArrayScopeCallsWrapperDataStorage) stringArrayScopeCallsWrapperDataStorage: IStringArrayScopeCallsWrapperDataStorage,
         @inject(ServiceIdentifiers.IStringArrayStorageAnalyzer) stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer,
         @inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
             identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
@@ -114,10 +105,11 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
     ) {
         super(randomGenerator, options);
 
-        this.arrayUtils = arrayUtils;
         this.escapeSequenceEncoder = escapeSequenceEncoder;
         this.literalNodesCacheStorage = literalNodesCacheStorage;
+        this.visitedLexicalScopeNodesStackStorage = visitedLexicalScopeNodesStackStorage;
         this.stringArrayStorage = stringArrayStorage;
+        this.stringArrayScopeCallsWrapperDataStorage = stringArrayScopeCallsWrapperDataStorage;
         this.stringArrayStorageAnalyzer = stringArrayStorageAnalyzer;
         this.identifierNamesGenerator = identifierNamesGeneratorFactory(options);
         this.stringArrayTransformerCustomNodeFactory = stringArrayTransformerCustomNodeFactory;
@@ -136,20 +128,9 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
                             this.prepareNode(node);
                         }
 
-                        if (NodeGuards.isNodeWithLexicalScopeAndStatements(node)) {
-                            this.onLexicalScopeNodeEnter(node);
-                        }
-
                         if (parentNode && NodeGuards.isLiteralNode(node) && !NodeMetadata.isReplacedLiteral(node)) {
                             return this.transformNode(node, parentNode);
                         }
-                    },
-                    leave: (node: ESTree.Node): ESTree.Node | undefined => {
-                        if (NodeGuards.isNodeWithLexicalScopeAndStatements(node)) {
-                            this.onLexicalScopeNodeLeave();
-
-                            return this.transformLexicalScopeNode(node);
-                        }
                     }
                 };
 
@@ -251,20 +232,33 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
     private getStringArrayCallsWrapperName (stringArrayStorageItemData: IStringArrayStorageItemData): string {
         const {encoding} = stringArrayStorageItemData;
 
-        const stringArrayCallsWrapperName: string = this.stringArrayStorage.getStorageCallsWrapperName(encoding);
+        return !this.options.stringArrayWrappersCount
+            ? this.getRootStringArrayCallsWrapperName(encoding)
+            : this.getUpperStringArrayCallsWrapperName(encoding);
 
-        if (!this.options.stringArrayWrappersCount) {
-            return stringArrayCallsWrapperName;
-        }
+    }
+
+    /**
+     * @param {TStringArrayEncoding} encoding
+     * @returns {string}
+     */
+    private getRootStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
+        return this.stringArrayStorage.getStorageCallsWrapperName(encoding);
+    }
 
-        const currentLexicalScopeNode: TNodeWithLexicalScope | null = this.arrayUtils.getLastElement(this.visitedLexicalScopeNodesStack);
+    /**
+     * @param {TStringArrayEncoding} encoding
+     * @returns {string}
+     */
+    private getUpperStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
+        const currentLexicalScopeNode: TNodeWithLexicalScope | undefined = this.visitedLexicalScopeNodesStackStorage.getLastElement();
 
         if (!currentLexicalScopeNode) {
             throw new Error('Cannot find current lexical scope node');
         }
 
         const stringArrayScopeCallsWrapperDataByEncoding: TStringArrayScopeCallsWrapperDataByEncoding =
-            this.stringArrayScopeCallsWrapperDataByEncodingMap.get(currentLexicalScopeNode) ?? {};
+            this.stringArrayScopeCallsWrapperDataStorage.get(currentLexicalScopeNode) ?? {};
         const stringArrayScopeCallsWrapperNames: string[] = stringArrayScopeCallsWrapperDataByEncoding[encoding]?.names ?? [];
         const isFilledScopeCallsWrapperNamesList: boolean = stringArrayScopeCallsWrapperNames.length === this.options.stringArrayWrappersCount;
 
@@ -277,7 +271,7 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
                 names: stringArrayScopeCallsWrapperNames
             };
 
-            this.stringArrayScopeCallsWrapperDataByEncodingMap.set(
+            this.stringArrayScopeCallsWrapperDataStorage.set(
                 currentLexicalScopeNode,
                 stringArrayScopeCallsWrapperDataByEncoding
             );
@@ -286,114 +280,6 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
         return this.randomGenerator.getRandomGenerator().pickone(stringArrayScopeCallsWrapperNames);
     }
 
-    /**
-     * @param {TNodeWithLexicalScope} lexicalScopeNode
-     */
-    private onLexicalScopeNodeEnter (lexicalScopeNode: TNodeWithLexicalScope): void {
-        this.visitedLexicalScopeNodesStack.push(lexicalScopeNode);
-    }
-
-    private onLexicalScopeNodeLeave (): void {
-        this.visitedLexicalScopeNodesStack.pop();
-    }
-
-    /**
-     * @param {TNodeWithLexicalScopeAndStatements} lexicalScopeNode
-     * @returns {TNodeWithLexicalScopeAndStatements}
-     */
-    private transformLexicalScopeNode (lexicalScopeNode: TNodeWithLexicalScopeAndStatements): TNodeWithLexicalScopeAndStatements {
-        if (!this.options.stringArrayWrappersCount) {
-            return lexicalScopeNode;
-        }
-
-        const lexicalScopeBodyNode: ESTree.Program | ESTree.BlockStatement =
-            NodeGuards.isProgramNode(lexicalScopeNode)
-                ? lexicalScopeNode
-                : lexicalScopeNode.body;
-
-        const stringArrayScopeCallsWrapperDataByEncoding: TStringArrayScopeCallsWrapperDataByEncoding | null =
-            this.stringArrayScopeCallsWrapperDataByEncodingMap.get(lexicalScopeNode) ?? null;
-
-        if (!stringArrayScopeCallsWrapperDataByEncoding) {
-            return lexicalScopeNode;
-        }
-
-        const stringArrayScopeCallsWrapperDataList: (IStringArrayScopeCallsWrapperData | undefined)[] =
-            Object.values(stringArrayScopeCallsWrapperDataByEncoding);
-
-        // iterates over data for each encoding type
-        for (const stringArrayScopeCallsWrapperData of stringArrayScopeCallsWrapperDataList) {
-            if (!stringArrayScopeCallsWrapperData) {
-                continue;
-            }
-
-            const {encoding, names} = stringArrayScopeCallsWrapperData;
-            const namesLength: number = names.length;
-
-            /**
-             * Iterates over each name of scope wrapper name
-             * Reverse iteration appends wrappers at index `0` at the correct order
-             */
-            for (let i = namesLength - 1; i >= 0; i--) {
-                const stringArrayScopeCallsWrapperName: string = names[i];
-                const upperStringArrayCallsWrapperName: string = this.getUpperStringArrayCallsWrapperName(encoding);
-
-                const stringArrayScopeCallsWrapperNode: ICustomNode<TInitialData<StringArrayScopeCallsWrapperNode>> =
-                    this.stringArrayTransformerCustomNodeFactory(
-                        StringArrayTransformerCustomNode.StringArrayScopeCallsWrapperNode
-                    );
-
-                stringArrayScopeCallsWrapperNode.initialize(
-                    stringArrayScopeCallsWrapperName,
-                    upperStringArrayCallsWrapperName
-                );
-
-                NodeAppender.prepend(
-                    lexicalScopeBodyNode,
-                    stringArrayScopeCallsWrapperNode.getNode()
-                );
-            }
-        }
-
-        return lexicalScopeNode;
-    }
-
-    /**
-     * @param {TStringArrayEncoding} encoding
-     * @returns {string}
-     */
-    private getRootStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
-        return this.stringArrayStorage.getStorageCallsWrapperName(encoding);
-    }
-
-    /**
-     * @param {TStringArrayEncoding} encoding
-     * @returns {string}
-     */
-    private getUpperStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
-        const rootStringArrayCallsWrapperName: string = this.getRootStringArrayCallsWrapperName(encoding);
-
-        if (!this.options.stringArrayWrappersChainedCalls) {
-            return rootStringArrayCallsWrapperName;
-        }
-
-        const parentLexicalScope: TNodeWithLexicalScope | null = this.arrayUtils.getLastElement(this.visitedLexicalScopeNodesStack);
-
-        if (!parentLexicalScope) {
-            return rootStringArrayCallsWrapperName;
-        }
-
-        const parentLexicalScopeDataByEncoding = this.stringArrayScopeCallsWrapperDataByEncodingMap
-            .get(parentLexicalScope) ?? null;
-        const parentLexicalScopeNames: string[] | null = parentLexicalScopeDataByEncoding?.[encoding]?.names ?? null;
-
-        return parentLexicalScopeNames?.length
-            ? this.randomGenerator
-                .getRandomGenerator()
-                .pickone(parentLexicalScopeNames)
-            : rootStringArrayCallsWrapperName;
-    }
-
     /**
      * @param {Literal} literalNode
      * @param {Node} parentNode

+ 28 - 0
src/storages/string-array-transformers/StringArrayScopeCallsWrapperDataStorage.ts

@@ -0,0 +1,28 @@
+import { inject, injectable } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import { TNodeWithLexicalScope } from '../../types/node/TNodeWithLexicalScope';
+import { TStringArrayScopeCallsWrapperDataByEncoding } from '../../types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperDataByEncoding';
+
+import { IOptions } from '../../interfaces/options/IOptions';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+import { IStringArrayScopeCallsWrapperDataStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperDataStorage';
+
+import { MapStorage } from '../MapStorage';
+
+@injectable()
+export class StringArrayScopeCallsWrapperDataStorage extends MapStorage <
+    TNodeWithLexicalScope,
+    TStringArrayScopeCallsWrapperDataByEncoding
+> implements IStringArrayScopeCallsWrapperDataStorage {
+    /**
+     * @param {IRandomGenerator} randomGenerator
+     * @param {IOptions} options
+     */
+    public constructor (
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
+        @inject(ServiceIdentifiers.IOptions) options: IOptions
+    ) {
+        super(randomGenerator, options);
+    }
+}

+ 55 - 0
src/storages/string-array-transformers/VisitedLexicalScopeNodesStackStorage.ts

@@ -0,0 +1,55 @@
+import { inject, injectable } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import { TNodeWithLexicalScopeAndStatements } from '../../types/node/TNodeWithLexicalScopeAndStatements';
+
+import { IArrayUtils } from '../../interfaces/utils/IArrayUtils';
+import { IOptions } from '../../interfaces/options/IOptions';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+import { IVisitedLexicalScopeNodesStackStorage } from '../../interfaces/storages/string-array-transformers/IVisitedLexicalScopeNodesStackStorage';
+
+import { ArrayStorage } from '../ArrayStorage';
+
+@injectable()
+export class VisitedLexicalScopeNodesStackStorage extends ArrayStorage <TNodeWithLexicalScopeAndStatements> implements IVisitedLexicalScopeNodesStackStorage {
+    /**
+     * @type {IArrayUtils}
+     */
+    private readonly arrayUtils: IArrayUtils;
+
+    /**
+     * @param {IRandomGenerator} randomGenerator
+     * @param {IOptions} options
+     * @param {IArrayUtils} arrayUtils
+     */
+    public constructor (
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
+        @inject(ServiceIdentifiers.IOptions) options: IOptions,
+        @inject(ServiceIdentifiers.IArrayUtils) arrayUtils: IArrayUtils,
+    ) {
+        super(randomGenerator, options);
+
+        this.arrayUtils = arrayUtils;
+    }
+
+    /**
+     * @returns {TNodeWithLexicalScopeAndStatements | undefined}
+     */
+    public getLastElement (): TNodeWithLexicalScopeAndStatements | undefined {
+        return this.arrayUtils.getLastElement(this.getStorage());
+    }
+
+    /**
+     * @param {TNodeWithLexicalScopeAndStatements} lexicalScopeNode
+     */
+    public push (lexicalScopeNode: TNodeWithLexicalScopeAndStatements): void {
+        this.storage.push(lexicalScopeNode);
+    }
+
+    /**
+     * @returns {TNodeWithLexicalScopeAndStatements | undefined}
+     */
+    public pop (): TNodeWithLexicalScopeAndStatements | undefined {
+        return this.storage.pop();
+    }
+}

+ 3 - 3
src/utils/ArrayUtils.ts

@@ -67,12 +67,12 @@ export class ArrayUtils implements IArrayUtils {
 
     /**
      * @param {T[]} array
-     * @returns {T | null}
+     * @returns {T | undefined}
      */
-    public getLastElement <T> (array: T[]): T | null {
+    public getLastElement <T> (array: T[]): T | undefined {
         const arrayLength: number = array.length;
 
-        return array[arrayLength - 1] ?? null;
+        return array[arrayLength - 1] ?? undefined;
     }
 
     /**

+ 766 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/StringArrayScopeCallsWrapperTransformer.spec.ts

@@ -0,0 +1,766 @@
+import { assert } from 'chai';
+
+import { IdentifierNamesGenerator } from '../../../../../src/enums/generators/identifier-names-generators/IdentifierNamesGenerator';
+import { StringArrayEncoding } from '../../../../../src/enums/StringArrayEncoding';
+
+import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
+
+import { readFileAsString } from '../../../../helpers/readFileAsString';
+
+import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade';
+
+describe('StringArrayScopeCallsWrapperTransformer', function () {
+    this.timeout(120000);
+
+    describe('Variant #1: base', () => {
+        describe('Variant #1: root scope', () => {
+            describe('Variant #1: option value value is lower then count `literal` nodes in the scope', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                    'return _0x([a-f0-9]){4,6};' +
+                    '};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
+                    'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
+                    'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 2
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+
+            describe('Variant #2: option value is bigger then count `literal` nodes in the scope', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                    'return _0x([a-f0-9]){4,6};' +
+                    '};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
+                    'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
+                    'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 5
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+
+            describe('Variant #3: correct wrappers order', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                    'const f *= *b;' +
+                    'const g *= *b;' +
+                    'const foo *= *[f|g]\\(\'0x0\'\\);' +
+                    'const bar *= *[f|g]\\(\'0x1\'\\);' +
+                    'const baz *= *[f|g]\\(\'0x2\'\\);'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 2
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+        });
+
+        describe('Variant #2: function scope', () => {
+            describe('Variant #1: option value is lower then count `literal` nodes in the scope', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                    'function test *\\( *\\) *{' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
+                    '}'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 2
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+
+            describe('Variant #2: option value is bigger then count `literal` nodes in the scope', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                    'function test *\\(\\) *{' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
+                    '}'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 5
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+
+            describe('Variant #3: correct wrappers order', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                    'function test *\\( *\\) *{' +
+                    'const h *= *b;' +
+                    'const i *= *b;' +
+                    'const c *= *[h|i]\\(\'0x3\'\\);' +
+                    'const d *= *[h|i]\\(\'0x4\'\\);' +
+                    'const e *= *[h|i]\\(\'0x5\'\\);' +
+                    '}'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 2
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+        });
+
+        describe('Variant #3: prohibited scopes', () => {
+            describe('Variant #1: if statement scope', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                    'var c *= *b;' +
+                    'if *\\(!!\\[]\\) *{' +
+                    'var foo *= *c\\(\'0x0\'\\);' +
+                    '}'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/prohibited-scope-1.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 1
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should not add scope calls wrappers to a prohibited scope', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+
+            describe('Variant #2: arrow function scope without statements', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                    'var c *= *b;' +
+                    '\\[]\\[c\\(\'0x0\'\\)]\\(\\(\\) *=> *c\\(\'0x1\'\\)\\);'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/prohibited-scope-2.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 1
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should not add scope calls wrappers to a prohibited scope', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+        });
+
+        describe('Variant #4: prevailing kind of variables', () => {
+            const stringArrayCallRegExp: RegExp = new RegExp(
+                'return _0x([a-f0-9]){4,6};' +
+                '};' +
+                'var _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                'var _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                'var foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
+                'var bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
+                'var baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
+            );
+
+            let obfuscatedCode: string;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-var.js');
+
+                obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                    code,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        stringArray: true,
+                        stringArrayThreshold: 1,
+                        stringArrayWrappersCount: 2
+                    }
+                ).getObfuscatedCode();
+            });
+
+            it('should add scope calls wrappers with a correct variables kind', () => {
+                assert.match(obfuscatedCode, stringArrayCallRegExp);
+            });
+        });
+
+        describe('Variant #5: correct evaluation of the scope calls wrappers', () => {
+            const expectedEvaluationResult: string = 'aaabbbcccdddeee';
+            let evaluationResult: string;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-eval.js');
+
+                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                    code,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        stringArray: true,
+                        stringArrayThreshold: 1,
+                        stringArrayWrappersCount: 5
+                    }
+                ).getObfuscatedCode();
+
+                evaluationResult = eval(obfuscatedCode);
+            });
+
+            it('should correctly evaluate scope calls wrappers', () => {
+                assert.equal(evaluationResult, expectedEvaluationResult);
+            });
+        });
+
+        describe('Variant #6: `stringArrayWrappersChainedCalls` option is enabled', () => {
+            describe('Variant #1: correct chained calls', () => {
+                describe('Variant #1: `Mangled` identifier names generator', () => {
+                    const stringArrayCallRegExp: RegExp = new RegExp(
+                        'const q *= *b;' +
+                        'const foo *= *q\\(\'0x0\'\\);' +
+                        'function test\\(c, *d\\) *{' +
+                        'const r *= *q;' +
+                        'const e *= *r\\(\'0x1\'\\);' +
+                        'const f *= *r\\(\'0x2\'\\);' +
+                        'function g\\(h, *i\\) *{' +
+                        'const s *= *r;' +
+                        'const j *= *s\\(\'0x3\'\\);' +
+                        'const k *= *s\\(\'0x4\'\\);' +
+                        'function l\\(m, *n *\\) *{' +
+                        'const t *= *s;' +
+                        'const o *= *t\\(\'0x3\'\\);' +
+                        'const p *= *t\\(\'0x4\'\\);' +
+                        'return o *\\+ *p;' +
+                        '}' +
+                        'return j *\\+ *k;' +
+                        '}' +
+                        'return e *\\+ *f *\\+ *g\\(\\);' +
+                        '}'
+                    );
+
+                    let obfuscatedCode: string;
+
+                    before(() => {
+                        const code: string = readFileAsString(__dirname + '/fixtures/chained-calls-1.js');
+
+                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                            code,
+                            {
+                                ...NO_ADDITIONAL_NODES_PRESET,
+                                identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
+                                stringArray: true,
+                                stringArrayThreshold: 1,
+                                stringArrayWrappersChainedCalls: true,
+                                stringArrayWrappersCount: 1
+                            }
+                        ).getObfuscatedCode();
+                    });
+
+                    it('should add correct scope calls wrappers', () => {
+                        assert.match(obfuscatedCode, stringArrayCallRegExp);
+                    });
+                });
+            });
+
+            describe('Variant #2: correct evaluation of the string array wrappers chained calls', () => {
+                describe('Variant #1: base', () => {
+                    describe('Variant #1: `Hexadecimal` identifier names generator', () => {
+                        const samplesCount: number = 50;
+                        const expectedEvaluationResult: string = 'aaabbbcccdddeee';
+                        let isEvaluationSuccessful: boolean = true;
+
+                        before(() => {
+                            const code: string = readFileAsString(__dirname + '/fixtures/chained-calls-1.js');
+
+                            for (let i = 0; i < samplesCount; i++) {
+                                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                                    code,
+                                    {
+                                        ...NO_ADDITIONAL_NODES_PRESET,
+                                        identifierNamesGenerator: IdentifierNamesGenerator.HexadecimalIdentifierNamesGenerator,
+                                        stringArray: true,
+                                        stringArrayThreshold: 1,
+                                        stringArrayEncoding: [
+                                            StringArrayEncoding.None,
+                                            StringArrayEncoding.Rc4
+                                        ],
+                                        stringArrayWrappersChainedCalls: true,
+                                        stringArrayWrappersCount: 5
+                                    }
+                                ).getObfuscatedCode();
+
+                                const evaluationResult: string = eval(obfuscatedCode);
+
+                                if (evaluationResult !== expectedEvaluationResult) {
+                                    isEvaluationSuccessful = false;
+                                    break;
+                                }
+                            }
+                        });
+
+                        it('should correctly evaluate string array wrappers chained calls', () => {
+                            assert.equal(isEvaluationSuccessful, true);
+                        });
+                    });
+
+                    describe('Variant #2: `Mangled` identifier names generator', () => {
+                        const samplesCount: number = 50;
+                        const expectedEvaluationResult: string = 'aaabbbcccdddeee';
+                        let isEvaluationSuccessful: boolean = true;
+
+                        before(() => {
+                            const code: string = readFileAsString(__dirname + '/fixtures/chained-calls-1.js');
+
+                            for (let i = 0; i < samplesCount; i++) {
+                                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                                    code,
+                                    {
+                                        ...NO_ADDITIONAL_NODES_PRESET,
+                                        identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
+                                        stringArray: true,
+                                        stringArrayThreshold: 1,
+                                        stringArrayEncoding: [
+                                            StringArrayEncoding.None,
+                                            StringArrayEncoding.Rc4
+                                        ],
+                                        stringArrayWrappersChainedCalls: true,
+                                        stringArrayWrappersCount: 5
+                                    }
+                                ).getObfuscatedCode();
+
+                                const evaluationResult: string = eval(obfuscatedCode);
+
+                                if (evaluationResult !== expectedEvaluationResult) {
+                                    isEvaluationSuccessful = false;
+                                    break;
+                                }
+                            }
+                        });
+
+                        it('should correctly evaluate string array wrappers chained calls', () => {
+                            assert.equal(isEvaluationSuccessful, true);
+                        });
+                    });
+                });
+
+                describe('Variant #2: advanced', () => {
+                    describe('Variant #1: `Hexadecimal` identifier names generator', () => {
+                        const samplesCount: number = 50;
+                        const expectedEvaluationResult: string = 'aaabbbcccdddeee';
+                        let isEvaluationSuccessful: boolean = true;
+
+                        before(() => {
+                            const code: string = readFileAsString(__dirname + '/fixtures/chained-calls-2.js');
+
+                            for (let i = 0; i < samplesCount; i++) {
+                                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                                    code,
+                                    {
+                                        ...NO_ADDITIONAL_NODES_PRESET,
+                                        identifierNamesGenerator: IdentifierNamesGenerator.HexadecimalIdentifierNamesGenerator,
+                                        stringArray: true,
+                                        stringArrayThreshold: 1,
+                                        stringArrayEncoding: [
+                                            StringArrayEncoding.None,
+                                            StringArrayEncoding.Rc4
+                                        ],
+                                        stringArrayWrappersChainedCalls: true,
+                                        stringArrayWrappersCount: 5
+                                    }
+                                ).getObfuscatedCode();
+
+                                const evaluationResult: string = eval(obfuscatedCode);
+
+                                if (evaluationResult !== expectedEvaluationResult) {
+                                    isEvaluationSuccessful = false;
+                                    break;
+                                }
+                            }
+                        });
+
+                        it('should correctly evaluate string array wrappers chained calls', () => {
+                            assert.equal(isEvaluationSuccessful, true);
+                        });
+                    });
+
+                    describe('Variant #2: `Mangled` identifier names generator', () => {
+                        const samplesCount: number = 50;
+                        const expectedEvaluationResult: string = 'aaabbbcccdddeee';
+                        let isEvaluationSuccessful: boolean = true;
+
+                        before(() => {
+                            const code: string = readFileAsString(__dirname + '/fixtures/chained-calls-2.js');
+
+                            for (let i = 0; i < samplesCount; i++) {
+                                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                                    code,
+                                    {
+                                        ...NO_ADDITIONAL_NODES_PRESET,
+                                        identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
+                                        stringArray: true,
+                                        stringArrayThreshold: 1,
+                                        stringArrayEncoding: [
+                                            StringArrayEncoding.None,
+                                            StringArrayEncoding.Rc4
+                                        ],
+                                        stringArrayWrappersChainedCalls: true,
+                                        stringArrayWrappersCount: 5
+                                    }
+                                ).getObfuscatedCode();
+
+                                const evaluationResult: string = eval(obfuscatedCode);
+
+                                if (evaluationResult !== expectedEvaluationResult) {
+                                    isEvaluationSuccessful = false;
+                                    break;
+                                }
+                            }
+                        });
+
+                        it('should correctly evaluate string array wrappers chained calls', () => {
+                            assert.equal(isEvaluationSuccessful, true);
+                        });
+                    });
+                });
+            });
+        });
+    });
+
+    describe('Variant #2: none and base64 encoding', () => {
+        describe('Variant #1: root scope', () => {
+            describe('Variant #1: `1` scope calls wrapper for each encoding type', () => {
+                const stringArrayWrappersRegExp: RegExp = new RegExp(
+                    'return _0x([a-f0-9]){4,6};' +
+                    '};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    // this one may be added or not depends on:
+                    // if all literal values encoded with a single encoding or not
+                    '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
+                    'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
+                    'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
+                    'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayEncoding: [
+                                StringArrayEncoding.None,
+                                StringArrayEncoding.Base64
+                            ],
+                            stringArrayWrappersCount: 1,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayWrappersRegExp);
+                });
+            });
+
+            describe('Variant #2: `2` scope calls wrappers for each encoding type', () => {
+                const stringArrayWrappersRegExp: RegExp = new RegExp(
+                    'return _0x([a-f0-9]){4,6};' +
+                    '};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    // this one may be added or not depends on:
+                    // if all literal values encoded with a single encoding or not
+                    '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
+                    'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
+                    'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
+                    'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayEncoding: [
+                                StringArrayEncoding.None,
+                                StringArrayEncoding.Base64
+                            ],
+                            stringArrayWrappersCount: 2,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayWrappersRegExp);
+                });
+            });
+        });
+
+        describe('Variant #2: function scope', () => {
+            describe('Variant #1: `1` scope calls wrapper for each encoding type', () => {
+                const stringArrayWrappersRegExp: RegExp = new RegExp(
+                    'function test *\\( *\\) *{' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    // this one may be added or not depends on:
+                    // if all literal values encoded with a single encoding or not
+                    '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
+                    '}'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayEncoding: [
+                                StringArrayEncoding.None,
+                                StringArrayEncoding.Base64
+                            ],
+                            stringArrayWrappersCount: 1,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayWrappersRegExp);
+                });
+            });
+
+            describe('Variant #2: `2` scope calls wrappers for each encoding type', () => {
+                const stringArrayWrappersRegExp: RegExp = new RegExp(
+                    'function test *\\( *\\) *{' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    // this one may be added or not depends on:
+                    // if all literal values encoded with a single encoding or not
+                    '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
+                    '}'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayEncoding: [
+                                StringArrayEncoding.None,
+                                StringArrayEncoding.Base64
+                            ],
+                            stringArrayWrappersCount: 2,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayWrappersRegExp);
+                });
+            });
+        });
+    });
+
+    describe('Variant #3: none and rc4 encoding', () => {
+        describe('Variant #1: correct evaluation of the scope calls wrappers', () => {
+            const expectedEvaluationResult: string = 'aaabbbcccdddeee';
+            let evaluationResult: string;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-eval.js');
+
+                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                    code,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        stringArray: true,
+                        stringArrayThreshold: 1,
+                        stringArrayEncoding: [
+                            StringArrayEncoding.None,
+                            StringArrayEncoding.Rc4
+                        ],
+                        stringArrayWrappersCount: 5
+                    }
+                ).getObfuscatedCode();
+
+                evaluationResult = eval(obfuscatedCode);
+            });
+
+            it('should correctly evaluate scope calls wrappers', () => {
+                assert.equal(evaluationResult, expectedEvaluationResult);
+            });
+        });
+    });
+
+    describe('Variant #4: base64 and rc4 encoding', () => {
+        describe('Variant #1: correct evaluation of the scope calls wrappers', () => {
+            const expectedEvaluationResult: string = 'aaabbbcccdddeee';
+            let evaluationResult: string;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-eval.js');
+
+                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                    code,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        stringArray: true,
+                        stringArrayThreshold: 1,
+                        stringArrayEncoding: [
+                            StringArrayEncoding.Base64,
+                            StringArrayEncoding.Rc4
+                        ],
+                        stringArrayWrappersCount: 5
+                    }
+                ).getObfuscatedCode();
+
+                evaluationResult = eval(obfuscatedCode);
+            });
+
+            it('should correctly evaluate scope calls wrappers', () => {
+                assert.equal(evaluationResult, expectedEvaluationResult);
+            });
+        });
+    });
+});

+ 0 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-chained-calls-1.js → test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/chained-calls-1.js


+ 0 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-chained-calls-2.js → test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/chained-calls-2.js


+ 0 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-count-prohibited-scope-1.js → test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/prohibited-scope-1.js


+ 0 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-count-prohibited-scope-2.js → test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/prohibited-scope-2.js


+ 0 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-count-const.js → test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/wrappers-count-const.js


+ 0 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-count-eval.js → test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/wrappers-count-eval.js


+ 0 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-count-var.js → test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/wrappers-count-var.js


+ 17 - 769
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/StringArrayTransformer.spec.ts

@@ -63,537 +63,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #3: `stringArrayWrappersCount` option is enabled', () => {
-        describe('Variant #1: root scope', () => {
-            describe('Variant #1: option value value is lower then count `literal` nodes in the scope', () => {
-                const stringArrayCallRegExp: RegExp = new RegExp(
-                        'return _0x([a-f0-9]){4,6};' +
-                    '};' +
-                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                    'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
-                    'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
-                    'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
-                );
-
-                let obfuscatedCode: string;
-
-                before(() => {
-                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
-
-                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                        code,
-                        {
-                            ...NO_ADDITIONAL_NODES_PRESET,
-                            stringArray: true,
-                            stringArrayThreshold: 1,
-                            stringArrayWrappersCount: 2
-                        }
-                    ).getObfuscatedCode();
-                });
-
-                it('should add scope calls wrappers', () => {
-                    assert.match(obfuscatedCode, stringArrayCallRegExp);
-                });
-            });
-
-            describe('Variant #2: option value is bigger then count `literal` nodes in the scope', () => {
-                const stringArrayCallRegExp: RegExp = new RegExp(
-                        'return _0x([a-f0-9]){4,6};' +
-                    '};' +
-                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                    'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
-                    'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
-                    'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
-                );
-
-                let obfuscatedCode: string;
-
-                before(() => {
-                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
-
-                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                        code,
-                        {
-                            ...NO_ADDITIONAL_NODES_PRESET,
-                            stringArray: true,
-                            stringArrayThreshold: 1,
-                            stringArrayWrappersCount: 5
-                        }
-                    ).getObfuscatedCode();
-                });
-
-                it('should add scope calls wrappers', () => {
-                    assert.match(obfuscatedCode, stringArrayCallRegExp);
-                });
-            });
-
-            describe('Variant #3: correct wrappers order', () => {
-                const stringArrayCallRegExp: RegExp = new RegExp(
-                    'const f *= *b;' +
-                    'const g *= *b;' +
-                    'const foo *= *[f|g]\\(\'0x0\'\\);' +
-                    'const bar *= *[f|g]\\(\'0x1\'\\);' +
-                    'const baz *= *[f|g]\\(\'0x2\'\\);'
-                );
-
-                let obfuscatedCode: string;
-
-                before(() => {
-                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
-
-                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                        code,
-                        {
-                            ...NO_ADDITIONAL_NODES_PRESET,
-                            identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
-                            stringArray: true,
-                            stringArrayThreshold: 1,
-                            stringArrayWrappersCount: 2
-                        }
-                    ).getObfuscatedCode();
-                });
-
-                it('should add scope calls wrappers', () => {
-                    assert.match(obfuscatedCode, stringArrayCallRegExp);
-                });
-            });
-        });
-
-        describe('Variant #2: function scope', () => {
-            describe('Variant #1: option value is lower then count `literal` nodes in the scope', () => {
-                const stringArrayCallRegExp: RegExp = new RegExp(
-                    'function test *\\( *\\) *{' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
-                    '}'
-                );
-
-                let obfuscatedCode: string;
-
-                before(() => {
-                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
-
-                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                        code,
-                        {
-                            ...NO_ADDITIONAL_NODES_PRESET,
-                            stringArray: true,
-                            stringArrayThreshold: 1,
-                            stringArrayWrappersCount: 2
-                        }
-                    ).getObfuscatedCode();
-                });
-
-                it('should add scope calls wrappers', () => {
-                    assert.match(obfuscatedCode, stringArrayCallRegExp);
-                });
-            });
-
-            describe('Variant #2: option value is bigger then count `literal` nodes in the scope', () => {
-                const stringArrayCallRegExp: RegExp = new RegExp(
-                    'function test *\\(\\) *{' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
-                    '}'
-                );
-
-                let obfuscatedCode: string;
-
-                before(() => {
-                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
-
-                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                        code,
-                        {
-                            ...NO_ADDITIONAL_NODES_PRESET,
-                            stringArray: true,
-                            stringArrayThreshold: 1,
-                            stringArrayWrappersCount: 5
-                        }
-                    ).getObfuscatedCode();
-                });
-
-                it('should add scope calls wrappers', () => {
-                    assert.match(obfuscatedCode, stringArrayCallRegExp);
-                });
-            });
-
-            describe('Variant #3: correct wrappers order', () => {
-                const stringArrayCallRegExp: RegExp = new RegExp(
-                    'function test *\\( *\\) *{' +
-                        'const h *= *b;' +
-                        'const i *= *b;' +
-                        'const c *= *[h|i]\\(\'0x3\'\\);' +
-                        'const d *= *[h|i]\\(\'0x4\'\\);' +
-                        'const e *= *[h|i]\\(\'0x5\'\\);' +
-                    '}'
-                );
-
-                let obfuscatedCode: string;
-
-                before(() => {
-                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
-
-                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                        code,
-                        {
-                            ...NO_ADDITIONAL_NODES_PRESET,
-                            identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
-                            stringArray: true,
-                            stringArrayThreshold: 1,
-                            stringArrayWrappersCount: 2
-                        }
-                    ).getObfuscatedCode();
-                });
-
-                it('should add scope calls wrappers', () => {
-                    assert.match(obfuscatedCode, stringArrayCallRegExp);
-                });
-            });
-        });
-
-        describe('Variant #3: prohibited scopes', () => {
-            describe('Variant #1: if statement scope', () => {
-                const stringArrayCallRegExp: RegExp = new RegExp(
-                    'var c *= *b;' +
-                    'if *\\(!!\\[]\\) *{' +
-                        'var foo *= *c\\(\'0x0\'\\);' +
-                    '}'
-                );
-
-                let obfuscatedCode: string;
-
-                before(() => {
-                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-prohibited-scope-1.js');
-
-                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                        code,
-                        {
-                            ...NO_ADDITIONAL_NODES_PRESET,
-                            identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
-                            stringArray: true,
-                            stringArrayThreshold: 1,
-                            stringArrayWrappersCount: 1
-                        }
-                    ).getObfuscatedCode();
-                });
-
-                it('should not add scope calls wrappers to a prohibited scope', () => {
-                    assert.match(obfuscatedCode, stringArrayCallRegExp);
-                });
-            });
-
-            describe('Variant #2: arrow function scope without statements', () => {
-                const stringArrayCallRegExp: RegExp = new RegExp(
-                    'var c *= *b;' +
-                    '\\[]\\[c\\(\'0x0\'\\)]\\(\\(\\) *=> *c\\(\'0x1\'\\)\\);'
-                );
-
-                let obfuscatedCode: string;
-
-                before(() => {
-                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-prohibited-scope-2.js');
-
-                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                        code,
-                        {
-                            ...NO_ADDITIONAL_NODES_PRESET,
-                            identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
-                            stringArray: true,
-                            stringArrayThreshold: 1,
-                            stringArrayWrappersCount: 1
-                        }
-                    ).getObfuscatedCode();
-                });
-
-                it('should not add scope calls wrappers to a prohibited scope', () => {
-                    assert.match(obfuscatedCode, stringArrayCallRegExp);
-                });
-            });
-        });
-
-        describe('Variant #4: prevailing kind of variables', () => {
-            const stringArrayCallRegExp: RegExp = new RegExp(
-                    'return _0x([a-f0-9]){4,6};' +
-                '};' +
-                'var _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                'var _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                'var foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
-                'var bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
-                'var baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
-            );
-
-            let obfuscatedCode: string;
-
-            before(() => {
-                const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-var.js');
-
-                obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                    code,
-                    {
-                        ...NO_ADDITIONAL_NODES_PRESET,
-                        stringArray: true,
-                        stringArrayThreshold: 1,
-                        stringArrayWrappersCount: 2
-                    }
-                ).getObfuscatedCode();
-            });
-
-            it('should add scope calls wrappers with a correct variables kind', () => {
-                assert.match(obfuscatedCode, stringArrayCallRegExp);
-            });
-        });
-
-        describe('Variant #5: correct evaluation of the scope calls wrappers', () => {
-            const expectedEvaluationResult: string = 'aaabbbcccdddeee';
-            let evaluationResult: string;
-
-            before(() => {
-                const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-eval.js');
-
-                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
-                    code,
-                    {
-                        ...NO_ADDITIONAL_NODES_PRESET,
-                        stringArray: true,
-                        stringArrayThreshold: 1,
-                        stringArrayWrappersCount: 5
-                    }
-                ).getObfuscatedCode();
-
-                evaluationResult = eval(obfuscatedCode);
-            });
-
-            it('should correctly evaluate scope calls wrappers', () => {
-                assert.equal(evaluationResult, expectedEvaluationResult);
-            });
-        });
-
-        describe('Variant #6: `stringArrayWrappersChainedCalls` option is enabled', () => {
-            describe('Variant #1: correct chained calls', () => {
-                describe('Variant #1: `Mangled` identifier names generator', () => {
-                    const stringArrayCallRegExp: RegExp = new RegExp(
-                        'const q *= *b;' +
-                        'const foo *= *q\\(\'0x0\'\\);' +
-                        'function test\\(c, *d\\) *{' +
-                            'const r *= *q;' +
-                            'const e *= *r\\(\'0x1\'\\);' +
-                            'const f *= *r\\(\'0x2\'\\);' +
-                            'function g\\(h, *i\\) *{' +
-                                'const s *= *r;' +
-                                'const j *= *s\\(\'0x3\'\\);' +
-                                'const k *= *s\\(\'0x4\'\\);' +
-                                'function l\\(m, *n *\\) *{' +
-                                    'const t *= *s;' +
-                                    'const o *= *t\\(\'0x3\'\\);' +
-                                    'const p *= *t\\(\'0x4\'\\);' +
-                                    'return o *\\+ *p;' +
-                                '}' +
-                                'return j *\\+ *k;' +
-                            '}' +
-                            'return e *\\+ *f *\\+ *g\\(\\);' +
-                        '}'
-                    );
-
-                    let obfuscatedCode: string;
-
-                    before(() => {
-                        const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-chained-calls-1.js');
-
-                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                            code,
-                            {
-                                ...NO_ADDITIONAL_NODES_PRESET,
-                                identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
-                                stringArray: true,
-                                stringArrayThreshold: 1,
-                                stringArrayWrappersChainedCalls: true,
-                                stringArrayWrappersCount: 1
-                            }
-                        ).getObfuscatedCode();
-                    });
-
-                    it('should add correct scope calls wrappers', () => {
-                        assert.match(obfuscatedCode, stringArrayCallRegExp);
-                    });
-                });
-            });
-
-            describe('Variant #2: correct evaluation of the string array wrappers chained calls', () => {
-                describe('Variant #1: base', () => {
-                    describe('Variant #1: `Hexadecimal` identifier names generator', () => {
-                        const samplesCount: number = 50;
-                        const expectedEvaluationResult: string = 'aaabbbcccdddeee';
-                        let isEvaluationSuccessful: boolean = true;
-
-                        before(() => {
-                            const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-chained-calls-1.js');
-
-                            for (let i = 0; i < samplesCount; i++) {
-                                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
-                                    code,
-                                    {
-                                        ...NO_ADDITIONAL_NODES_PRESET,
-                                        identifierNamesGenerator: IdentifierNamesGenerator.HexadecimalIdentifierNamesGenerator,
-                                        stringArray: true,
-                                        stringArrayThreshold: 1,
-                                        stringArrayEncoding: [
-                                            StringArrayEncoding.None,
-                                            StringArrayEncoding.Rc4
-                                        ],
-                                        stringArrayWrappersChainedCalls: true,
-                                        stringArrayWrappersCount: 5
-                                    }
-                                ).getObfuscatedCode();
-
-                                const evaluationResult: string = eval(obfuscatedCode);
-
-                                if (evaluationResult !== expectedEvaluationResult) {
-                                    isEvaluationSuccessful = false;
-                                    break;
-                                }
-                            }
-                        });
-
-                        it('should correctly evaluate string array wrappers chained calls', () => {
-                            assert.equal(isEvaluationSuccessful, true);
-                        });
-                    });
-
-                    describe('Variant #2: `Mangled` identifier names generator', () => {
-                        const samplesCount: number = 50;
-                        const expectedEvaluationResult: string = 'aaabbbcccdddeee';
-                        let isEvaluationSuccessful: boolean = true;
-
-                        before(() => {
-                            const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-chained-calls-1.js');
-
-                            for (let i = 0; i < samplesCount; i++) {
-                                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
-                                    code,
-                                    {
-                                        ...NO_ADDITIONAL_NODES_PRESET,
-                                        identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
-                                        stringArray: true,
-                                        stringArrayThreshold: 1,
-                                        stringArrayEncoding: [
-                                            StringArrayEncoding.None,
-                                            StringArrayEncoding.Rc4
-                                        ],
-                                        stringArrayWrappersChainedCalls: true,
-                                        stringArrayWrappersCount: 5
-                                    }
-                                ).getObfuscatedCode();
-
-                                const evaluationResult: string = eval(obfuscatedCode);
-
-                                if (evaluationResult !== expectedEvaluationResult) {
-                                    isEvaluationSuccessful = false;
-                                    break;
-                                }
-                            }
-                        });
-
-                        it('should correctly evaluate string array wrappers chained calls', () => {
-                            assert.equal(isEvaluationSuccessful, true);
-                        });
-                    });
-                });
-
-                describe('Variant #2: advanced', () => {
-                    describe('Variant #1: `Hexadecimal` identifier names generator', () => {
-                        const samplesCount: number = 50;
-                        const expectedEvaluationResult: string = 'aaabbbcccdddeee';
-                        let isEvaluationSuccessful: boolean = true;
-
-                        before(() => {
-                            const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-chained-calls-2.js');
-
-                            for (let i = 0; i < samplesCount; i++) {
-                                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
-                                    code,
-                                    {
-                                        ...NO_ADDITIONAL_NODES_PRESET,
-                                        identifierNamesGenerator: IdentifierNamesGenerator.HexadecimalIdentifierNamesGenerator,
-                                        stringArray: true,
-                                        stringArrayThreshold: 1,
-                                        stringArrayEncoding: [
-                                            StringArrayEncoding.None,
-                                            StringArrayEncoding.Rc4
-                                        ],
-                                        stringArrayWrappersChainedCalls: true,
-                                        stringArrayWrappersCount: 5
-                                    }
-                                ).getObfuscatedCode();
-
-                                const evaluationResult: string = eval(obfuscatedCode);
-
-                                if (evaluationResult !== expectedEvaluationResult) {
-                                    isEvaluationSuccessful = false;
-                                    break;
-                                }
-                            }
-                        });
-
-                        it('should correctly evaluate string array wrappers chained calls', () => {
-                            assert.equal(isEvaluationSuccessful, true);
-                        });
-                    });
-
-                    describe('Variant #2: `Mangled` identifier names generator', () => {
-                        const samplesCount: number = 50;
-                        const expectedEvaluationResult: string = 'aaabbbcccdddeee';
-                        let isEvaluationSuccessful: boolean = true;
-
-                        before(() => {
-                            const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-chained-calls-2.js');
-
-                            for (let i = 0; i < samplesCount; i++) {
-                                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
-                                    code,
-                                    {
-                                        ...NO_ADDITIONAL_NODES_PRESET,
-                                        identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
-                                        stringArray: true,
-                                        stringArrayThreshold: 1,
-                                        stringArrayEncoding: [
-                                            StringArrayEncoding.None,
-                                            StringArrayEncoding.Rc4
-                                        ],
-                                        stringArrayWrappersChainedCalls: true,
-                                        stringArrayWrappersCount: 5
-                                    }
-                                ).getObfuscatedCode();
-
-                                const evaluationResult: string = eval(obfuscatedCode);
-
-                                if (evaluationResult !== expectedEvaluationResult) {
-                                    isEvaluationSuccessful = false;
-                                    break;
-                                }
-                            }
-                        });
-
-                        it('should correctly evaluate string array wrappers chained calls', () => {
-                            assert.equal(isEvaluationSuccessful, true);
-                        });
-                    });
-                });
-            });
-        });
-    });
-
-    describe('Variant #4: string contains non-latin and non-digit characters and `unicodeEscapeSequence` is disabled', () => {
+    describe('Variant #3: string contains non-latin and non-digit characters and `unicodeEscapeSequence` is disabled', () => {
         let testFunc: () => void;
 
         before(() => {
@@ -614,7 +84,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #5: same literal node values', () => {
+    describe('Variant #4: same literal node values', () => {
         const stringArrayRegExp: RegExp = /^var _0x([a-f0-9]){4} *= *\['test'\];/;
         const stringArrayCallRegExp: RegExp = /var test *= *_0x([a-f0-9]){4}\('0x0'\);/;
 
@@ -642,7 +112,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #6: `unicodeEscapeSequence` option is enabled', () => {
+    describe('Variant #5: `unicodeEscapeSequence` option is enabled', () => {
         const regExp: RegExp = /^var test *= *'\\x74\\x65\\x73\\x74';$/;
 
         let obfuscatedCode: string;
@@ -665,7 +135,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #7: `unicodeEscapeSequence` and `stringArray` options are enabled', () => {
+    describe('Variant #6: `unicodeEscapeSequence` and `stringArray` options are enabled', () => {
         const stringArrayRegExp: RegExp = /^var _0x([a-f0-9]){4} *= *\['\\x74\\x65\\x73\\x74'\];/;
         const stringArrayCallRegExp: RegExp = /var test *= *_0x([a-f0-9]){4}\('\\x30\\x78\\x30'\);/;
 
@@ -694,7 +164,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #8: short literal node value', () => {
+    describe('Variant #7: short literal node value', () => {
         const regExp: RegExp = /var test *= *'te';/;
 
         let obfuscatedCode: string;
@@ -717,7 +187,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #9: base64 encoding', () => {
+    describe('Variant #8: base64 encoding', () => {
         const stringArrayRegExp: RegExp = new RegExp(`^var _0x([a-f0-9]){4} *= *\\['${swapLettersCase('dGVzdA==')}'];`);
         const stringArrayCallRegExp: RegExp = /var test *= *_0x([a-f0-9]){4}\('0x0'\);/;
 
@@ -746,7 +216,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #10: rc4 encoding', () => {
+    describe('Variant #9: rc4 encoding', () => {
         describe('Variant #1: single string literal', () => {
             const regExp: RegExp = /var test *= *_0x([a-f0-9]){4}\('0x0', *'.{4}'\);/;
 
@@ -812,7 +282,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #11: none and base64 encoding', () => {
+    describe('Variant #10: none and base64 encoding', () => {
         describe('Variant #1: string array values', () => {
             const samplesCount: number = 300;
             const expectedMatchesChance: number = 0.5;
@@ -866,169 +336,9 @@ describe('StringArrayTransformer', function () {
                 assert.closeTo(base64EncodingMatchesChance, expectedMatchesChance, expectedMatchesDelta);
             });
         });
-
-        describe('Variant #2: `stringArrayWrappersCount` option is enabled', () => {
-            describe('Variant #1: root scope', () => {
-                describe('Variant #1: `1` scope calls wrapper for each encoding type', () => {
-                    const stringArrayWrappersRegExp: RegExp = new RegExp(
-                            'return _0x([a-f0-9]){4,6};' +
-                        '};' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                        // this one may be added or not depends on:
-                        // if all literal values encoded with a single encoding or not
-                        '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
-                        'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
-                        'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
-                        'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
-                    );
-
-                    let obfuscatedCode: string;
-
-                    before(() => {
-                        const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
-
-                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                            code,
-                            {
-                                ...NO_ADDITIONAL_NODES_PRESET,
-                                stringArray: true,
-                                stringArrayEncoding: [
-                                    StringArrayEncoding.None,
-                                    StringArrayEncoding.Base64
-                                ],
-                                stringArrayWrappersCount: 1,
-                                stringArrayThreshold: 1
-                            }
-                        ).getObfuscatedCode();
-                    });
-
-                    it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
-                        assert.match(obfuscatedCode, stringArrayWrappersRegExp);
-                    });
-                });
-
-                describe('Variant #2: `2` scope calls wrappers for each encoding type', () => {
-                    const stringArrayWrappersRegExp: RegExp = new RegExp(
-                            'return _0x([a-f0-9]){4,6};' +
-                        '};' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                        // this one may be added or not depends on:
-                        // if all literal values encoded with a single encoding or not
-                        '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
-                        'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
-                        'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
-                        'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
-                    );
-
-                    let obfuscatedCode: string;
-
-                    before(() => {
-                        const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
-
-                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                            code,
-                            {
-                                ...NO_ADDITIONAL_NODES_PRESET,
-                                stringArray: true,
-                                stringArrayEncoding: [
-                                    StringArrayEncoding.None,
-                                    StringArrayEncoding.Base64
-                                ],
-                                stringArrayWrappersCount: 2,
-                                stringArrayThreshold: 1
-                            }
-                        ).getObfuscatedCode();
-                    });
-
-                    it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
-                        assert.match(obfuscatedCode, stringArrayWrappersRegExp);
-                    });
-                });
-            });
-
-            describe('Variant #2: function scope', () => {
-                describe('Variant #1: `1` scope calls wrapper for each encoding type', () => {
-                    const stringArrayWrappersRegExp: RegExp = new RegExp(
-                        'function test *\\( *\\) *{' +
-                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                            // this one may be added or not depends on:
-                            // if all literal values encoded with a single encoding or not
-                            '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
-                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
-                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
-                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
-                        '}'
-                    );
-
-                    let obfuscatedCode: string;
-
-                    before(() => {
-                        const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
-
-                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                            code,
-                            {
-                                ...NO_ADDITIONAL_NODES_PRESET,
-                                stringArray: true,
-                                stringArrayEncoding: [
-                                    StringArrayEncoding.None,
-                                    StringArrayEncoding.Base64
-                                ],
-                                stringArrayWrappersCount: 1,
-                                stringArrayThreshold: 1
-                            }
-                        ).getObfuscatedCode();
-                    });
-
-                    it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
-                        assert.match(obfuscatedCode, stringArrayWrappersRegExp);
-                    });
-                });
-
-                describe('Variant #2: `2` scope calls wrappers for each encoding type', () => {
-                    const stringArrayWrappersRegExp: RegExp = new RegExp(
-                        'function test *\\( *\\) *{' +
-                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
-                            // this one may be added or not depends on:
-                            // if all literal values encoded with a single encoding or not
-                            '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
-                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
-                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
-                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
-                        '}'
-                    );
-
-                    let obfuscatedCode: string;
-
-                    before(() => {
-                        const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
-
-                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                            code,
-                            {
-                                ...NO_ADDITIONAL_NODES_PRESET,
-                                stringArray: true,
-                                stringArrayEncoding: [
-                                    StringArrayEncoding.None,
-                                    StringArrayEncoding.Base64
-                                ],
-                                stringArrayWrappersCount: 2,
-                                stringArrayThreshold: 1
-                            }
-                        ).getObfuscatedCode();
-                    });
-
-                    it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
-                        assert.match(obfuscatedCode, stringArrayWrappersRegExp);
-                    });
-                });
-            });
-        });
     });
 
-    describe('Variant #12: none and rc4 encoding', () => {
+    describe('Variant #11: none and rc4 encoding', () => {
         describe('Variant #1: string array calls wrapper call', () => {
             const samplesCount: number = 300;
             const expectedMatchesChance: number = 0.5;
@@ -1082,40 +392,9 @@ describe('StringArrayTransformer', function () {
                 assert.closeTo(rc4EncodingMatchesChance, expectedMatchesChance, expectedMatchesDelta);
             });
         });
-
-        describe('Variant #2: `stringArrayWrappersCount` option is enabled', () => {
-            describe('Variant #1: correct evaluation of the scope calls wrappers', () => {
-                const expectedEvaluationResult: string = 'aaabbbcccdddeee';
-                let evaluationResult: string;
-
-                before(() => {
-                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-eval.js');
-
-                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
-                        code,
-                        {
-                            ...NO_ADDITIONAL_NODES_PRESET,
-                            stringArray: true,
-                            stringArrayThreshold: 1,
-                            stringArrayEncoding: [
-                                StringArrayEncoding.None,
-                                StringArrayEncoding.Rc4
-                            ],
-                            stringArrayWrappersCount: 5
-                        }
-                    ).getObfuscatedCode();
-
-                    evaluationResult = eval(obfuscatedCode);
-                });
-
-                it('should correctly evaluate scope calls wrappers', () => {
-                    assert.equal(evaluationResult, expectedEvaluationResult);
-                });
-            });
-        });
     });
 
-    describe('Variant #13: base64 and rc4 encoding', () => {
+    describe('Variant #12: base64 and rc4 encoding', () => {
         describe('Variant #1: single string literal', () => {
             const samplesCount: number = 300;
             const expectedMatchesChance: number = 0.5;
@@ -1169,40 +448,9 @@ describe('StringArrayTransformer', function () {
                 assert.closeTo(rc4EncodingMatchesChance, expectedMatchesChance, expectedMatchesDelta);
             });
         });
-
-        describe('Variant #2: `stringArrayWrappersCount` option is enabled', () => {
-            describe('Variant #1: correct evaluation of the scope calls wrappers', () => {
-                const expectedEvaluationResult: string = 'aaabbbcccdddeee';
-                let evaluationResult: string;
-
-                before(() => {
-                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-eval.js');
-
-                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
-                        code,
-                        {
-                            ...NO_ADDITIONAL_NODES_PRESET,
-                            stringArray: true,
-                            stringArrayThreshold: 1,
-                            stringArrayEncoding: [
-                                StringArrayEncoding.Base64,
-                                StringArrayEncoding.Rc4
-                            ],
-                            stringArrayWrappersCount: 5
-                        }
-                    ).getObfuscatedCode();
-
-                    evaluationResult = eval(obfuscatedCode);
-                });
-
-                it('should correctly evaluate scope calls wrappers', () => {
-                    assert.equal(evaluationResult, expectedEvaluationResult);
-                });
-            });
-        });
     });
 
-    describe('Variant #14: `stringArrayThreshold` option value', () => {
+    describe('Variant #13: `stringArrayThreshold` option value', () => {
         const samples: number = 1000;
         const stringArrayThreshold: number = 0.5;
         const delta: number = 0.1;
@@ -1245,7 +493,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #15: string array calls wrapper name', () => {
+    describe('Variant #14: string array calls wrapper name', () => {
         const regExp: RegExp = /console\[b\('0x0'\)]\('a'\);/;
 
         let obfuscatedCode: string;
@@ -1269,7 +517,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #16: `reservedStrings` option is enabled', () => {
+    describe('Variant #15: `reservedStrings` option is enabled', () => {
         describe('Variant #1: base `reservedStrings` values', () => {
             describe('Variant #1: single reserved string value', () => {
                 const stringLiteralRegExp1: RegExp = /const foo *= *'foo';/;
@@ -1441,7 +689,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #17: object expression key literal', () => {
+    describe('Variant #16: object expression key literal', () => {
         describe('Variant #1: base key literal', () => {
             const stringArrayRegExp: RegExp = /^var _0x([a-f0-9]){4} *= *\['bar'];/;
             const objectExpressionRegExp: RegExp = /var test *= *{'foo' *: *_0x([a-f0-9]){4}\('0x0'\)};/;
@@ -1499,7 +747,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #18: import declaration source literal', () => {
+    describe('Variant #17: import declaration source literal', () => {
         const importDeclarationRegExp: RegExp = /import *{ *bar *} *from *'foo';/;
 
         let obfuscatedCode: string;
@@ -1522,7 +770,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #19: export all declaration source literal', () => {
+    describe('Variant #18: export all declaration source literal', () => {
         const exportAllDeclarationRegExp: RegExp = /export *\* *from *'foo';/;
 
         let obfuscatedCode: string;
@@ -1545,7 +793,7 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #20: export named declaration source literal', () => {
+    describe('Variant #19: export named declaration source literal', () => {
         const exportNamedDeclarationRegExp: RegExp = /export *{ *bar *} *from *'foo';/;
 
         let obfuscatedCode: string;

+ 1 - 0
test/index.spec.ts

@@ -115,6 +115,7 @@ import './functional-tests/node-transformers/simplifying-transformers/expression
 import './functional-tests/node-transformers/simplifying-transformers/if-statement-simplify-transformer/IfStatementSimplifyTransformer.spec';
 import './functional-tests/node-transformers/simplifying-transformers/variable-declarations-merge-transformer/VariableDeclarationsMergeTransformer.spec';
 import './functional-tests/node-transformers/string-array-transformers/string-array-transformer/StringArrayTransformer.spec';
+import './functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/StringArrayScopeCallsWrapperTransformer.spec';
 import './functional-tests/options/OptionsNormalizer.spec';
 import './functional-tests/options/domain-lock/Validation.spec';
 import './functional-tests/storages/string-array-transformers/string-array-storage/StringArrayStorage.spec';

+ 5 - 5
test/unit-tests/utils/ArrayUtils.spec.ts

@@ -116,15 +116,15 @@ describe('ArrayUtils', () => {
     describe('getLastElement', () => {
         describe('empty array', () => {
             const array: string[] = [];
-            const expectedLastElement: null = null;
+            const expectedLastElement: undefined = undefined;
 
-            let lastElement: string | null;
+            let lastElement: string | undefined;
 
             before(() => {
                 lastElement = arrayUtils.getLastElement(array);
             });
 
-            it('should return null if array is empty', () => {
+            it('should return undefined if array is empty', () => {
                 assert.equal(lastElement, expectedLastElement);
             });
         });
@@ -133,7 +133,7 @@ describe('ArrayUtils', () => {
             const array: string[] = ['foo'];
             const expectedLastElement: string = 'foo';
 
-            let lastElement: string | null;
+            let lastElement: string | undefined;
 
             before(() => {
                 lastElement = arrayUtils.getLastElement(array);
@@ -148,7 +148,7 @@ describe('ArrayUtils', () => {
             const array: string[] = ['foo', 'bar', 'baz'];
             const expectedLastElement: string = 'baz';
 
-            let lastElement: string | null;
+            let lastElement: string | undefined;
 
             before(() => {
                 lastElement = arrayUtils.getLastElement(array);

部分文件因为文件数量过多而无法显示