Selaa lähdekoodia

Merge pull request #744 from javascript-obfuscator/string-array-wrappers-type-option

WIP New option `stringArrayWrappersType`
Timofey Kachalov 4 vuotta sitten
vanhempi
commit
a5ad91eb8c
63 muutettua tiedostoa jossa 1365 lisäystä ja 272 poistoa
  1. 4 0
      CHANGELOG.md
  2. 19 0
      README.md
  3. 0 0
      dist/index.browser.js
  4. 0 0
      dist/index.cli.js
  5. 0 0
      dist/index.js
  6. 4 4
      package.json
  7. 8 1
      src/cli/JavaScriptObfuscatorCLI.ts
  8. 2 1
      src/container/ServiceIdentifiers.ts
  9. 8 3
      src/container/modules/custom-nodes/CustomNodesModule.ts
  10. 10 4
      src/container/modules/storages/StoragesModule.ts
  11. 1 1
      src/custom-code-helpers/string-array/group/StringArrayCodeHelperGroup.ts
  12. 77 0
      src/custom-nodes/string-array-nodes/AbstractStringArrayCallNode.ts
  13. 5 32
      src/custom-nodes/string-array-nodes/StringArrayCallNode.ts
  14. 137 0
      src/custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperFunctionNode.ts
  15. 2 2
      src/custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperVariableNode.ts
  16. 2 1
      src/enums/custom-nodes/StringArrayTransformerCustomNode.ts
  17. 0 0
      src/enums/node-transformers/string-array-transformers/StringArrayEncoding.ts
  18. 9 0
      src/enums/node-transformers/string-array-transformers/StringArrayWrappersType.ts
  19. 18 0
      src/interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperLexicalScopeData.ts
  20. 1 1
      src/interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperNamesData.ts
  21. 2 0
      src/interfaces/options/IOptions.ts
  22. 6 0
      src/interfaces/storages/IArrayStorage.ts
  23. 0 10
      src/interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperDataStorage.ts
  24. 10 0
      src/interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperLexicalScopeDataStorage.ts
  25. 10 0
      src/interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperNamesDataStorage.ts
  26. 5 0
      src/interfaces/storages/string-array-transformers/IVisitedLexicalScopeNodesStackStorage.ts
  27. 165 46
      src/node-transformers/string-array-transformers/StringArrayScopeCallsWrapperTransformer.ts
  28. 165 42
      src/node-transformers/string-array-transformers/StringArrayTransformer.ts
  29. 9 1
      src/options/Options.ts
  30. 1 1
      src/options/normalizer-rules/StringArrayEncodingRule.ts
  31. 1 1
      src/options/normalizer-rules/StringArrayRule.ts
  32. 1 1
      src/options/normalizer-rules/StringArrayThresholdRule.ts
  33. 3 1
      src/options/presets/Default.ts
  34. 3 1
      src/options/presets/HighObfuscation.ts
  35. 1 1
      src/options/presets/MediumObfuscation.ts
  36. 3 1
      src/options/presets/NoCustomNodes.ts
  37. 14 0
      src/storages/ArrayStorage.ts
  38. 1 1
      src/storages/string-array-transformers/LiteralNodesCacheStorage.ts
  39. 28 0
      src/storages/string-array-transformers/StringArrayScopeCallsWrapperLexicalScopeDataStorage.ts
  40. 5 5
      src/storages/string-array-transformers/StringArrayScopeCallsWrapperNamesDataStorage.ts
  41. 1 1
      src/storages/string-array-transformers/StringArrayStorage.ts
  42. 15 2
      src/storages/string-array-transformers/VisitedLexicalScopeNodesStackStorage.ts
  43. 0 7
      src/types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperDataByEncoding.ts
  44. 7 0
      src/types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperNamesDataByEncoding.ts
  45. 1 1
      src/types/options/TStringArrayEncoding.ts
  46. 5 0
      src/types/options/TStringArrayWrappersType.ts
  47. 30 13
      test/dev/dev.ts
  48. 1 1
      test/functional-tests/custom-code-helpers/string-array/StringArrayCallsWrapperCodeHelper.spec.ts
  49. 1 1
      test/functional-tests/custom-code-helpers/string-array/group/StringArrayCodeHelperGroup.spec.ts
  50. 3 1
      test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts
  51. 293 2
      test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/StringArrayScopeCallsWrapperTransformer.spec.ts
  52. 5 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-scope-calls-wrapper-transformer/fixtures/wrappers-count-const-no-root-wrappers.js
  53. 1 1
      test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/StringArrayTransformer.spec.ts
  54. 1 1
      test/functional-tests/options/OptionsNormalizer.spec.ts
  55. 1 1
      test/performance-tests/JavaScriptObfuscatorMemory.spec.ts
  56. 3 1
      test/runtime-tests/JavaScriptObfuscatorRuntime.spec.ts
  57. 1 1
      test/unit-tests/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.spec.ts
  58. 1 1
      test/unit-tests/cli/utils/CLIUtils.spec.ts
  59. 75 0
      test/unit-tests/storages/ArrayStorage.spec.ts
  60. 1 1
      test/unit-tests/storages/string-array-transformers/literal-nodes-cache/LiteralNodesCacheStorage.spec.ts
  61. 1 1
      test/unit-tests/storages/string-array-transformers/string-array/StringArrayStorage.spec.ts
  62. 133 28
      test/unit-tests/storages/string-array-transformers/visited-lexical-scope-nodes-stack/VisitedLexicalScopeNodesStackStorage.spec.ts
  63. 45 45
      yarn.lock

+ 4 - 0
CHANGELOG.md

@@ -1,5 +1,9 @@
 Change Log
 
+v2.3.0
+---
+* **New option:** `stringArrayWrappersType` allows to select a type of the wrappers that are appending by the `stringArrayWrappersCount` option
+
 v2.2.1
 ---
 * Fixed https://github.com/javascript-obfuscator/javascript-obfuscator/issues/745

+ 19 - 0
README.md

@@ -365,6 +365,7 @@ Following options are available for the JS Obfuscator:
     stringArrayEncoding: [],
     stringArrayWrappersCount: 1,
     stringArrayWrappersChainedCalls: true,
+    stringArrayWrappersType: 'variable',
     stringArrayThreshold: 0.75,
     target: 'browser',
     transformObjectKeys: false,
@@ -415,6 +416,7 @@ Following options are available for the JS Obfuscator:
     --string-array-encoding '<list>' (comma separated) [none, base64, rc4]
     --string-array-wrappers-count <number>
     --string-array-wrappers-chained-calls <boolean>
+    --string-array-wrappers-type <string> [variable, function]
     --string-array-threshold <number>
     --target <string> [browser, browser-no-eval, node]
     --transform-object-keys <boolean>
@@ -1061,6 +1063,19 @@ function test() {
     }
 }
 ```
+
+### `stringArrayWrappersType`
+Type: `string` Default: `variable`
+
+##### :warning: [`stringArray`](#stringarray) and [`stringArrayWrappersCount`](#stringArrayWrappersCount) options must be enabled
+
+Allows to select a type of the wrappers that are appending by the `stringArrayWrappersCount` option.
+
+Available values:
+* `'variable'`: appends variable wrappers. Fast performance.
+* `'function'`: appends function wrappers. More slow performance than with `variable` but allows to additionally shift the `stringArray` index.
+
+Highly recommended to use `function` wrappers for higher obfuscation when a performance loss doesn't have a high impact on an obfuscated application.
     
 ### `stringArrayThreshold`
 Type: `number` Default: `0.8` Min: `0` Max: `1`
@@ -1162,6 +1177,7 @@ Performance will 50-100% slower than without obfuscation
     stringArrayEncoding: ['rc4'],
     stringArrayWrappersCount: 5,
     stringArrayWrappersChainedCalls: true,
+    stringArrayWrappersType: 'function',
     stringArrayThreshold: 1,
     transformObjectKeys: true,
     unicodeEscapeSequence: false
@@ -1196,6 +1212,7 @@ Performance will 30-35% slower than without obfuscation
     stringArrayEncoding: ['base64'],
     stringArrayWrappersCount: 2,
     stringArrayWrappersChainedCalls: true,
+    stringArrayWrappersType: 'variable',
     stringArrayThreshold: 0.75,
     transformObjectKeys: true,
     unicodeEscapeSequence: false
@@ -1227,6 +1244,7 @@ Performance will slightly slower than without obfuscation
     stringArrayEncoding: [],
     stringArrayWrappersCount: 1,
     stringArrayWrappersChainedCalls: true,
+    stringArrayWrappersType: 'variable',
     stringArrayThreshold: 0.75,
     unicodeEscapeSequence: false
 }
@@ -1255,6 +1273,7 @@ Performance will slightly slower than without obfuscation
     stringArrayEncoding: [],
     stringArrayWrappersCount: 1,
     stringArrayWrappersChainedCalls: true,
+    stringArrayWrappersType: 'variable',
     stringArrayThreshold: 0.75,
     unicodeEscapeSequence: false
 }

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/index.browser.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/index.cli.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/index.js


+ 4 - 4
package.json

@@ -1,6 +1,6 @@
 {
   "name": "javascript-obfuscator",
-  "version": "2.2.1",
+  "version": "2.3.0",
   "description": "JavaScript obfuscator",
   "keywords": [
     "obfuscator",
@@ -59,14 +59,14 @@
     "@types/sinon": "9.0.5",
     "@types/string-template": "1.0.2",
     "@types/webpack-env": "1.15.2",
-    "@typescript-eslint/eslint-plugin": "4.1.0",
-    "@typescript-eslint/parser": "4.1.0",
+    "@typescript-eslint/eslint-plugin": "4.1.1",
+    "@typescript-eslint/parser": "4.1.1",
     "chai": "4.2.0",
     "chai-exclude": "2.0.2",
     "coveralls": "3.1.0",
     "eslint": "7.9.0",
     "eslint-plugin-import": "2.22.0",
-    "eslint-plugin-jsdoc": "30.4.2",
+    "eslint-plugin-jsdoc": "30.5.1",
     "eslint-plugin-no-null": "1.0.2",
     "eslint-plugin-prefer-arrow": "1.2.2",
     "eslint-plugin-unicorn": "21.0.0",

+ 8 - 1
src/cli/JavaScriptObfuscatorCLI.ts

@@ -15,7 +15,8 @@ import { LoggingPrefix } from '../enums/logger/LoggingPrefix';
 import { ObfuscationTarget } from '../enums/ObfuscationTarget';
 import { OptionsPreset } from '../enums/options/presets/OptionsPreset';
 import { SourceMapMode } from '../enums/source-map/SourceMapMode';
-import { StringArrayEncoding } from '../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../enums/node-transformers/string-array-transformers/StringArrayEncoding';
+import { StringArrayWrappersType } from '../enums/node-transformers/string-array-transformers/StringArrayWrappersType';
 
 import { DEFAULT_PRESET } from '../options/presets/Default';
 
@@ -349,6 +350,12 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
                 'Enables the chained calls between string array wrappers',
                 BooleanSanitizer
             )
+            .option(
+                '--string-array-wrappers-type <string>',
+                'Allows to select a type of the wrappers that are appending by the `--string-array-wrappers-count` option. ' +
+                `Values: ${CLIUtils.stringifyOptionAvailableValues(StringArrayWrappersType)}. ` +
+                `Default: ${StringArrayWrappersType.Variable}`
+            )
             .option(
                 '--string-array-threshold <number>',
                 'The probability that the literal string will be inserted into stringArray (Default: 0.8, Min: 0, Max: 1)',

+ 2 - 1
src/container/ServiceIdentifiers.ts

@@ -50,7 +50,8 @@ export enum ServiceIdentifiers {
     IScopeIdentifiersTraverser = 'IScopeIdentifiersTraverser',
     ISourceCode = 'ISourceCode',
     IScopeAnalyzer = 'IScopeAnalyzer',
-    IStringArrayScopeCallsWrapperDataStorage = 'IStringArrayScopeCallsWrapperDataStorage',
+    IStringArrayScopeCallsWrapperLexicalScopeDataStorage = 'IStringArrayScopeCallsWrapperLexicalScopeDataStorage',
+    IStringArrayScopeCallsWrapperNamesDataStorage = 'IStringArrayScopeCallsWrapperNamesDataStorage',
     IStringArrayStorage = 'IStringArrayStorage',
     IStringArrayStorageAnalyzer = 'IStringArrayStorageAnalyzer',
     IVisitedLexicalScopeNodesStackStorage = 'IVisitedLexicalScopeNodesStackStorage',

+ 8 - 3
src/container/modules/custom-nodes/CustomNodesModule.ts

@@ -19,7 +19,8 @@ import { ControlFlowStorageNode } from '../../../custom-nodes/control-flow-flatt
 import { ExpressionWithOperatorControlFlowStorageCallNode } from '../../../custom-nodes/control-flow-flattening-nodes/control-flow-storage-nodes/ExpressionWithOperatorControlFlowStorageCallNode';
 import { LogicalExpressionFunctionNode } from '../../../custom-nodes/control-flow-flattening-nodes/LogicalExpressionFunctionNode';
 import { StringArrayCallNode } from '../../../custom-nodes/string-array-nodes/StringArrayCallNode';
-import { StringArrayScopeCallsWrapperNode } from '../../../custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperNode';
+import { StringArrayScopeCallsWrapperFunctionNode } from '../../../custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperFunctionNode';
+import { StringArrayScopeCallsWrapperVariableNode } from '../../../custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperVariableNode';
 import { StringLiteralControlFlowStorageCallNode } from '../../../custom-nodes/control-flow-flattening-nodes/control-flow-storage-nodes/StringLiteralControlFlowStorageCallNode';
 import { StringLiteralNode } from '../../../custom-nodes/control-flow-flattening-nodes/StringLiteralNode';
 
@@ -77,8 +78,12 @@ export const customNodesModule: interfaces.ContainerModule = new ContainerModule
         .whenTargetNamed(StringArrayTransformerCustomNode.StringArrayCallNode);
 
     bind<interfaces.Newable<ICustomNode>>(ServiceIdentifiers.Newable__ICustomNode)
-        .toConstructor(StringArrayScopeCallsWrapperNode)
-        .whenTargetNamed(StringArrayTransformerCustomNode.StringArrayScopeCallsWrapperNode);
+        .toConstructor(StringArrayScopeCallsWrapperFunctionNode)
+        .whenTargetNamed(StringArrayTransformerCustomNode.StringArrayScopeCallsWrapperFunctionNode);
+
+    bind<interfaces.Newable<ICustomNode>>(ServiceIdentifiers.Newable__ICustomNode)
+        .toConstructor(StringArrayScopeCallsWrapperVariableNode)
+        .whenTargetNamed(StringArrayTransformerCustomNode.StringArrayScopeCallsWrapperVariableNode);
 
     // control flow customNode constructor factory
     bind<ICustomNode>(ServiceIdentifiers.Factory__IControlFlowCustomNode)

+ 10 - 4
src/container/modules/storages/StoragesModule.ts

@@ -7,14 +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 { IStringArrayScopeCallsWrapperLexicalScopeDataStorage } from '../../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperLexicalScopeDataStorage';
+import { IStringArrayScopeCallsWrapperNamesDataStorage } from '../../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperNamesDataStorage';
 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 { StringArrayScopeCallsWrapperLexicalScopeDataStorage } from '../../../storages/string-array-transformers/StringArrayScopeCallsWrapperLexicalScopeDataStorage';
+import { StringArrayScopeCallsWrapperNamesDataStorage } from '../../../storages/string-array-transformers/StringArrayScopeCallsWrapperNamesDataStorage';
 import { StringArrayStorage } from '../../../storages/string-array-transformers/StringArrayStorage';
 import { VisitedLexicalScopeNodesStackStorage } from '../../../storages/string-array-transformers/VisitedLexicalScopeNodesStackStorage';
 
@@ -32,8 +34,12 @@ export const storagesModule: interfaces.ContainerModule = new ContainerModule((b
         .to(StringArrayStorage)
         .inSingletonScope();
 
-    bind<IStringArrayScopeCallsWrapperDataStorage>(ServiceIdentifiers.IStringArrayScopeCallsWrapperDataStorage)
-        .to(StringArrayScopeCallsWrapperDataStorage)
+    bind<IStringArrayScopeCallsWrapperLexicalScopeDataStorage>(ServiceIdentifiers.IStringArrayScopeCallsWrapperLexicalScopeDataStorage)
+        .to(StringArrayScopeCallsWrapperLexicalScopeDataStorage)
+        .inSingletonScope();
+
+    bind<IStringArrayScopeCallsWrapperNamesDataStorage>(ServiceIdentifiers.IStringArrayScopeCallsWrapperNamesDataStorage)
+        .to(StringArrayScopeCallsWrapperNamesDataStorage)
         .inSingletonScope();
 
     bind<IVisitedLexicalScopeNodesStackStorage>(ServiceIdentifiers.IVisitedLexicalScopeNodesStackStorage)

+ 1 - 1
src/custom-code-helpers/string-array/group/StringArrayCodeHelperGroup.ts

@@ -17,7 +17,7 @@ import { initializable } from '../../../decorators/Initializable';
 
 import { CustomCodeHelper } from '../../../enums/custom-code-helpers/CustomCodeHelper';
 import { ObfuscationEvent } from '../../../enums/event-emitters/ObfuscationEvent';
-import { StringArrayEncoding } from '../../../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../../enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { AbstractCustomCodeHelperGroup } from '../../AbstractCustomCodeHelperGroup';
 import { NodeAppender } from '../../../node/NodeAppender';

+ 77 - 0
src/custom-nodes/string-array-nodes/AbstractStringArrayCallNode.ts

@@ -0,0 +1,77 @@
+import { inject, injectable, } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import * as ESTree from 'estree';
+
+import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
+
+import { ICustomCodeHelperFormatter } from '../../interfaces/custom-code-helpers/ICustomCodeHelperFormatter';
+import { IOptions } from '../../interfaces/options/IOptions';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+
+import { AbstractCustomNode } from '../AbstractCustomNode';
+import { NodeFactory } from '../../node/NodeFactory';
+import { NodeMetadata } from '../../node/NodeMetadata';
+import { NodeUtils } from '../../node/NodeUtils';
+import { NumberUtils } from '../../utils/NumberUtils';
+
+@injectable()
+export abstract class AbstractStringArrayCallNode extends AbstractCustomNode {
+    /**
+     * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
+     * @param {ICustomCodeHelperFormatter} customCodeHelperFormatter
+     * @param {IRandomGenerator} randomGenerator
+     * @param {IOptions} options
+     */
+    protected constructor (
+        @inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
+            identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
+        @inject(ServiceIdentifiers.ICustomCodeHelperFormatter) customCodeHelperFormatter: ICustomCodeHelperFormatter,
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
+        @inject(ServiceIdentifiers.IOptions) options: IOptions
+    ) {
+        super(
+            identifierNamesGeneratorFactory,
+            customCodeHelperFormatter,
+            randomGenerator,
+            options
+        );
+    }
+
+    /**
+     * @param {number} index
+     * @returns {Expression}
+     */
+    protected getHexadecimalNode (index: number): ESTree.Expression {
+        const isPositive: boolean = index >= 0;
+        const normalizedIndex: number = Math.abs(index);
+
+        const hexadecimalIndex: string = NumberUtils.toHex(normalizedIndex);
+        const hexadecimalLiteralNode: ESTree.Literal = NodeFactory.literalNode(hexadecimalIndex);
+
+        NodeMetadata.set(hexadecimalLiteralNode, { replacedLiteral: true });
+
+        const hexadecimalNode: ESTree.Expression = isPositive
+            ? hexadecimalLiteralNode
+            : NodeFactory.unaryExpressionNode(
+                '-',
+                hexadecimalLiteralNode
+            );
+
+        NodeUtils.parentizeAst(hexadecimalNode);
+        
+        return hexadecimalNode;
+    }
+
+    /**
+     * @param {string} decodeKey
+     * @returns {Literal}
+     */
+    protected getRc4KeyLiteralNode (decodeKey: string): ESTree.Literal {
+        const rc4KeyLiteralNode: ESTree.Literal = NodeFactory.literalNode(decodeKey);
+
+        NodeMetadata.set(rc4KeyLiteralNode, { replacedLiteral: true });
+
+        return rc4KeyLiteralNode;
+    }
+}

+ 5 - 32
src/custom-nodes/string-array-nodes/StringArrayCallNode.ts

@@ -12,14 +12,12 @@ import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
 
 import { initializable } from '../../decorators/Initializable';
 
-import { AbstractCustomNode } from '../AbstractCustomNode';
+import { AbstractStringArrayCallNode } from './AbstractStringArrayCallNode';
 import { NodeFactory } from '../../node/NodeFactory';
-import { NodeMetadata } from '../../node/NodeMetadata';
 import { NodeUtils } from '../../node/NodeUtils';
-import { NumberUtils } from '../../utils/NumberUtils';
 
 @injectable()
-export class StringArrayCallNode extends AbstractCustomNode {
+export class StringArrayCallNode extends AbstractStringArrayCallNode {
     /**
      * @type {string | null}
      */
@@ -60,30 +58,6 @@ export class StringArrayCallNode extends AbstractCustomNode {
         );
     }
 
-    /**
-     * @param {string} hexadecimalIndex
-     * @returns {Literal}
-     */
-    private static getHexadecimalLiteralNode (hexadecimalIndex: string): ESTree.Literal {
-        const hexadecimalLiteralNode: ESTree.Literal = NodeFactory.literalNode(hexadecimalIndex);
-
-        NodeMetadata.set(hexadecimalLiteralNode, { replacedLiteral: true });
-
-        return hexadecimalLiteralNode;
-    }
-
-    /**
-     * @param {string} decodeKey
-     * @returns {Literal}
-     */
-    private static getRc4KeyLiteralNode (decodeKey: string): ESTree.Literal {
-        const rc4KeyLiteralNode: ESTree.Literal = NodeFactory.literalNode(decodeKey);
-
-        NodeMetadata.set(rc4KeyLiteralNode, { replacedLiteral: true });
-
-        return rc4KeyLiteralNode;
-    }
-
     /**
      * @param {string} stringArrayCallsWrapperName
      * @param {number} index
@@ -103,13 +77,12 @@ export class StringArrayCallNode extends AbstractCustomNode {
      * @returns {TStatement[]}
      */
     protected getNodeStructure (): TStatement[] {
-        const hexadecimalIndex: string = NumberUtils.toHex(this.index);
-        const callExpressionArgs: ESTree.Literal[] = [
-            StringArrayCallNode.getHexadecimalLiteralNode(hexadecimalIndex)
+        const callExpressionArgs: ESTree.Expression[] = [
+            this.getHexadecimalNode(this.index)
         ];
 
         if (this.decodeKey) {
-            callExpressionArgs.push(StringArrayCallNode.getRc4KeyLiteralNode(this.decodeKey));
+            callExpressionArgs.push(this.getRc4KeyLiteralNode(this.decodeKey));
         }
 
         const stringArrayIdentifierNode: ESTree.Identifier =

+ 137 - 0
src/custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperFunctionNode.ts

@@ -0,0 +1,137 @@
+import { inject, injectable, } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import * as ESTree from 'estree';
+
+import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
+import { TStatement } from '../../types/node/TStatement';
+
+import { ICustomCodeHelperFormatter } from '../../interfaces/custom-code-helpers/ICustomCodeHelperFormatter';
+import { IOptions } from '../../interfaces/options/IOptions';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+
+import { initializable } from '../../decorators/Initializable';
+
+import { AbstractStringArrayCallNode } from './AbstractStringArrayCallNode';
+import { NodeFactory } from '../../node/NodeFactory';
+import { NodeUtils } from '../../node/NodeUtils';
+
+@injectable()
+export class StringArrayScopeCallsWrapperFunctionNode extends AbstractStringArrayCallNode {
+    /**
+     * @type {number}
+     */
+    @initializable()
+    private shiftedIndex!: number;
+
+    /**
+     * @type {string}
+     */
+    @initializable()
+    private stringArrayCallsWrapperName!: string;
+
+    /**
+     * @type {string}
+     */
+    @initializable()
+    private stringArrayScopeCallsWrapperName!: string;
+
+
+    /**
+     * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
+     * @param {ICustomCodeHelperFormatter} customCodeHelperFormatter
+     * @param {IRandomGenerator} randomGenerator
+     * @param {IOptions} options
+     */
+    public constructor (
+        @inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
+            identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
+        @inject(ServiceIdentifiers.ICustomCodeHelperFormatter) customCodeHelperFormatter: ICustomCodeHelperFormatter,
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
+        @inject(ServiceIdentifiers.IOptions) options: IOptions
+    ) {
+        super(
+            identifierNamesGeneratorFactory,
+            customCodeHelperFormatter,
+            randomGenerator,
+            options
+        );
+    }
+
+    /**
+     * @param {string} stringArrayScopeCallsWrapperName
+     * @param {string} stringArrayCallsWrapperName
+     * @param {number} shiftedIndex
+     */
+    public initialize (
+        stringArrayScopeCallsWrapperName: string,
+        stringArrayCallsWrapperName: string,
+        shiftedIndex: number
+    ): void {
+        this.stringArrayScopeCallsWrapperName = stringArrayScopeCallsWrapperName;
+        this.stringArrayCallsWrapperName = stringArrayCallsWrapperName;
+        this.shiftedIndex = shiftedIndex;
+    }
+
+    /**
+     * @returns {TStatement[]}
+     */
+    protected getNodeStructure (): TStatement[] {
+        // identifiers of function expression parameters
+        // as a temporary names use random strings
+        const firstParameterIdentifierNode: ESTree.Identifier = NodeFactory.identifierNode(this.randomGenerator.getRandomString(6));
+        const secondParameterIdentifierNode: ESTree.Identifier = NodeFactory.identifierNode(this.randomGenerator.getRandomString(6));
+
+        // identifiers of call to the parent string array scope wrapper
+        // as a temporary names use random strings
+        const firstCallArgumentIdentifierNode: ESTree.Identifier = NodeFactory.identifierNode(this.randomGenerator.getRandomString(6));
+        const secondCallArgumentIdentifierNode: ESTree.Identifier = NodeFactory.identifierNode(this.randomGenerator.getRandomString(6));
+
+        // function expression node
+        const functionExpressionNode: ESTree.FunctionExpression =  NodeFactory.functionExpressionNode(
+            [
+                firstParameterIdentifierNode,
+                secondParameterIdentifierNode,
+            ],
+            NodeFactory.blockStatementNode([
+                NodeFactory.returnStatementNode(
+                    NodeFactory.callExpressionNode(
+                        NodeFactory.identifierNode(this.stringArrayCallsWrapperName),
+                        [
+                            NodeFactory.binaryExpressionNode(
+                                '-',
+                                firstCallArgumentIdentifierNode,
+                                this.getHexadecimalNode(this.shiftedIndex)
+                            ),
+                            secondCallArgumentIdentifierNode
+                        ]
+                    )
+                )
+            ])
+        );
+
+        const structure: TStatement = NodeFactory.variableDeclarationNode(
+            [
+                NodeFactory.variableDeclaratorNode(
+                    NodeFactory.identifierNode(this.stringArrayScopeCallsWrapperName),
+                    functionExpressionNode
+                )
+            ],
+            'const',
+        );
+
+        NodeUtils.parentizeAst(structure);
+
+        // have to generate names for both parameter and call identifiers
+        const firstParameterName: string = this.identifierNamesGenerator.generateForLexicalScope(functionExpressionNode);
+        const secondParameterName: string = this.identifierNamesGenerator.generateForLexicalScope(functionExpressionNode);
+
+        firstParameterIdentifierNode.name = firstParameterName;
+        secondParameterIdentifierNode.name = secondParameterName;
+
+        firstCallArgumentIdentifierNode.name = firstParameterName;
+        secondCallArgumentIdentifierNode.name = secondParameterName;
+
+        return [structure];
+    }
+}

+ 2 - 2
src/custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperNode.ts → src/custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperVariableNode.ts

@@ -10,12 +10,12 @@ import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
 
 import { initializable } from '../../decorators/Initializable';
 
-import { AbstractCustomNode } from '../AbstractCustomNode';
+import { AbstractStringArrayCallNode } from './AbstractStringArrayCallNode';
 import { NodeFactory } from '../../node/NodeFactory';
 import { NodeUtils } from '../../node/NodeUtils';
 
 @injectable()
-export class StringArrayScopeCallsWrapperNode extends AbstractCustomNode {
+export class StringArrayScopeCallsWrapperVariableNode extends AbstractStringArrayCallNode {
     /**
      * @type {string}
      */

+ 2 - 1
src/enums/custom-nodes/StringArrayTransformerCustomNode.ts

@@ -1,4 +1,5 @@
 export enum StringArrayTransformerCustomNode {
     StringArrayCallNode = 'StringArrayCallNode',
-    StringArrayScopeCallsWrapperNode = 'StringArrayScopeCallsWrapperNode'
+    StringArrayScopeCallsWrapperFunctionNode = 'StringArrayScopeCallsWrapperFunctionNode',
+    StringArrayScopeCallsWrapperVariableNode = 'StringArrayScopeCallsWrapperVariableNode'
 }

+ 0 - 0
src/enums/StringArrayEncoding.ts → src/enums/node-transformers/string-array-transformers/StringArrayEncoding.ts


+ 9 - 0
src/enums/node-transformers/string-array-transformers/StringArrayWrappersType.ts

@@ -0,0 +1,9 @@
+import { MakeEnum } from '@gradecam/tsenum';
+
+export const StringArrayWrappersType: Readonly<{
+    Variable: 'variable';
+    Function: 'function';
+}> = MakeEnum({
+    Variable: 'variable',
+    Function: 'function',
+});

+ 18 - 0
src/interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperLexicalScopeData.ts

@@ -0,0 +1,18 @@
+import { TNodeWithLexicalScopeStatements } from '../../../types/node/TNodeWithLexicalScopeStatements';
+
+export interface IStringArrayScopeCallsWrapperLexicalScopeData {
+    /**
+     * @type {TNodeWithLexicalScopeStatements | null}
+     */
+    parentLexicalScopeBodyNode: TNodeWithLexicalScopeStatements | null;
+
+    /**
+     * @type {number}
+     */
+    resultShiftedIndex: number;
+
+    /**
+     * @type {number}
+     */
+    scopeShiftedIndex: number;
+}

+ 1 - 1
src/interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperData.ts → src/interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperNamesData.ts

@@ -1,6 +1,6 @@
 import { TStringArrayEncoding } from '../../../types/options/TStringArrayEncoding';
 
-export interface IStringArrayScopeCallsWrapperData {
+export interface IStringArrayScopeCallsWrapperNamesData {
     /**
      * @type {TStringArrayEncoding}
      */

+ 2 - 0
src/interfaces/options/IOptions.ts

@@ -2,6 +2,7 @@ import { TypeFromEnum } from '@gradecam/tsenum';
 
 import { TOptionsPreset } from '../../types/options/TOptionsPreset';
 import { TStringArrayEncoding } from '../../types/options/TStringArrayEncoding';
+import { TStringArrayWrappersType } from '../../types/options/TStringArrayWrappersType';
 
 import { IdentifierNamesGenerator } from '../../enums/generators/identifier-names-generators/IdentifierNamesGenerator';
 import { ObfuscationTarget } from '../../enums/ObfuscationTarget';
@@ -43,6 +44,7 @@ export interface IOptions {
     readonly stringArrayEncoding: TStringArrayEncoding[];
     readonly stringArrayWrappersChainedCalls: boolean;
     readonly stringArrayWrappersCount: number;
+    readonly stringArrayWrappersType: TStringArrayWrappersType;
     readonly stringArrayThreshold: number;
     readonly target: TypeFromEnum<typeof ObfuscationTarget>;
     readonly transformObjectKeys: boolean;

+ 6 - 0
src/interfaces/storages/IArrayStorage.ts

@@ -1,6 +1,12 @@
 import { IInitializable } from '../IInitializable';
 
 export interface IArrayStorage <V> extends IInitializable {
+    /**
+     * @param {number} key
+     * @returns {V | undefined}
+     */
+    delete (key: number): V | undefined;
+
     /**
      * @param {number} key
      * @returns {V | undefined}

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

@@ -1,10 +0,0 @@
-import { TNodeWithLexicalScopeStatements } from '../../../types/node/TNodeWithLexicalScopeStatements';
-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<
-    TNodeWithLexicalScopeStatements,
-    TStringArrayScopeCallsWrapperDataByEncoding
-> {}

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

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

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

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

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

@@ -8,6 +8,11 @@ export interface IVisitedLexicalScopeNodesStackStorage extends IArrayStorage<TNo
      */
     getLastElement (): TNodeWithLexicalScopeStatements | undefined;
 
+    /**
+     * @returns {TNodeWithLexicalScopeStatements | undefined}
+     */
+    getPenultimateElement (): TNodeWithLexicalScopeStatements | undefined;
+
     /**
      * @returns {TNodeWithLexicalScopeStatements | undefined}
      */

+ 165 - 46
src/node-transformers/string-array-transformers/StringArrayScopeCallsWrapperTransformer.ts

@@ -5,26 +5,30 @@ import * as ESTree from 'estree';
 
 import { TInitialData } from '../../types/TInitialData';
 import { TNodeWithLexicalScopeStatements } from '../../types/node/TNodeWithLexicalScopeStatements';
-import { TStringArrayEncoding } from '../../types/options/TStringArrayEncoding';
-import { TStringArrayScopeCallsWrapperDataByEncoding } from '../../types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperDataByEncoding';
+import { TStatement } from '../../types/node/TStatement';
+import { TStringArrayScopeCallsWrapperNamesDataByEncoding } from '../../types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperNamesDataByEncoding';
 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 { IStringArrayScopeCallsWrapperLexicalScopeData } from '../../interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperLexicalScopeData';
+import { IStringArrayScopeCallsWrapperLexicalScopeDataStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperLexicalScopeDataStorage';
+import { IStringArrayScopeCallsWrapperNamesData } from '../../interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperNamesData';
+import { IStringArrayScopeCallsWrapperNamesDataStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperNamesDataStorage';
 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 { StringArrayWrappersType } from '../../enums/node-transformers/string-array-transformers/StringArrayWrappersType';
 
 import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
 import { NodeAppender } from '../../node/NodeAppender';
 import { NodeGuards } from '../../node/NodeGuards';
-import { StringArrayScopeCallsWrapperNode } from '../../custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperNode';
+import { StringArrayScopeCallsWrapperVariableNode } from '../../custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperVariableNode';
+import { StringArrayScopeCallsWrapperFunctionNode } from '../../custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperFunctionNode';
 
 @injectable()
 export class StringArrayScopeCallsWrapperTransformer extends AbstractNodeTransformer {
@@ -34,9 +38,14 @@ export class StringArrayScopeCallsWrapperTransformer extends AbstractNodeTransfo
     private readonly stringArrayStorage: IStringArrayStorage;
 
     /**
-     * @type {IStringArrayScopeCallsWrapperDataStorage}
+     * @type {IStringArrayScopeCallsWrapperLexicalScopeDataStorage}
      */
-    private readonly stringArrayScopeCallsWrapperDataStorage: IStringArrayScopeCallsWrapperDataStorage;
+    private readonly stringArrayScopeCallsWrapperLexicalScopeDataStorage: IStringArrayScopeCallsWrapperLexicalScopeDataStorage;
+
+    /**
+     * @type {IStringArrayScopeCallsWrapperNamesDataStorage}
+     */
+    private readonly stringArrayScopeCallsWrapperNamesDataStorage: IStringArrayScopeCallsWrapperNamesDataStorage;
 
     /**
      * @type {TStringArrayTransformerCustomNodeFactory}
@@ -53,7 +62,8 @@ export class StringArrayScopeCallsWrapperTransformer extends AbstractNodeTransfo
      * @param {IOptions} options
      * @param {IVisitedLexicalScopeNodesStackStorage} visitedLexicalScopeNodesStackStorage
      * @param {IStringArrayStorage} stringArrayStorage
-     * @param {IStringArrayScopeCallsWrapperDataStorage} stringArrayScopeCallsWrapperDataStorage
+     * @param {IStringArrayScopeCallsWrapperNamesDataStorage} stringArrayScopeCallsWrapperNamesDataStorage
+     * @param {IStringArrayScopeCallsWrapperLexicalScopeDataStorage} stringArrayScopeCallsWrapperLexicalScopeDataStorage
      * @param {TStringArrayTransformerCustomNodeFactory} stringArrayTransformerCustomNodeFactory
      */
     public constructor (
@@ -61,7 +71,8 @@ export class StringArrayScopeCallsWrapperTransformer extends AbstractNodeTransfo
         @inject(ServiceIdentifiers.IOptions) options: IOptions,
         @inject(ServiceIdentifiers.IVisitedLexicalScopeNodesStackStorage) visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage,
         @inject(ServiceIdentifiers.IStringArrayStorage) stringArrayStorage: IStringArrayStorage,
-        @inject(ServiceIdentifiers.IStringArrayScopeCallsWrapperDataStorage) stringArrayScopeCallsWrapperDataStorage: IStringArrayScopeCallsWrapperDataStorage,
+        @inject(ServiceIdentifiers.IStringArrayScopeCallsWrapperNamesDataStorage) stringArrayScopeCallsWrapperNamesDataStorage: IStringArrayScopeCallsWrapperNamesDataStorage,
+        @inject(ServiceIdentifiers.IStringArrayScopeCallsWrapperLexicalScopeDataStorage) stringArrayScopeCallsWrapperLexicalScopeDataStorage: IStringArrayScopeCallsWrapperLexicalScopeDataStorage,
         @inject(ServiceIdentifiers.Factory__IStringArrayTransformerCustomNode)
             stringArrayTransformerCustomNodeFactory: TStringArrayTransformerCustomNodeFactory
     ) {
@@ -69,7 +80,8 @@ export class StringArrayScopeCallsWrapperTransformer extends AbstractNodeTransfo
 
         this.visitedLexicalScopeNodesStackStorage = visitedLexicalScopeNodesStackStorage;
         this.stringArrayStorage = stringArrayStorage;
-        this.stringArrayScopeCallsWrapperDataStorage = stringArrayScopeCallsWrapperDataStorage;
+        this.stringArrayScopeCallsWrapperNamesDataStorage = stringArrayScopeCallsWrapperNamesDataStorage;
+        this.stringArrayScopeCallsWrapperLexicalScopeDataStorage = stringArrayScopeCallsWrapperLexicalScopeDataStorage;
         this.stringArrayTransformerCustomNodeFactory = stringArrayTransformerCustomNodeFactory;
     }
 
@@ -111,23 +123,25 @@ export class StringArrayScopeCallsWrapperTransformer extends AbstractNodeTransfo
     public transformNode (
         lexicalScopeBodyNode: TNodeWithLexicalScopeStatements
     ): TNodeWithLexicalScopeStatements {
-        const stringArrayScopeCallsWrapperDataByEncoding: TStringArrayScopeCallsWrapperDataByEncoding | null =
-            this.stringArrayScopeCallsWrapperDataStorage.get(lexicalScopeBodyNode) ?? null;
+        const stringArrayScopeCallsWrapperNamesDataByEncoding: TStringArrayScopeCallsWrapperNamesDataByEncoding | null =
+            this.stringArrayScopeCallsWrapperNamesDataStorage.get(lexicalScopeBodyNode) ?? null;
+        const stringArrayScopeCallsWrapperNamesLexicalScopeData: IStringArrayScopeCallsWrapperLexicalScopeData | null =
+            this.stringArrayScopeCallsWrapperLexicalScopeDataStorage.get(lexicalScopeBodyNode) ?? null;
 
-        if (!stringArrayScopeCallsWrapperDataByEncoding) {
+        if (!stringArrayScopeCallsWrapperNamesDataByEncoding || !stringArrayScopeCallsWrapperNamesLexicalScopeData) {
             return lexicalScopeBodyNode;
         }
 
-        const stringArrayScopeCallsWrapperDataList: (IStringArrayScopeCallsWrapperData | undefined)[] =
-            Object.values(stringArrayScopeCallsWrapperDataByEncoding);
+        const stringArrayScopeCallsWrapperNamesDataList: (IStringArrayScopeCallsWrapperNamesData | undefined)[] =
+            Object.values(stringArrayScopeCallsWrapperNamesDataByEncoding);
 
         // iterates over data for each encoding type
-        for (const stringArrayScopeCallsWrapperData of stringArrayScopeCallsWrapperDataList) {
-            if (!stringArrayScopeCallsWrapperData) {
+        for (const stringArrayScopeCallsWrapperNamesData of stringArrayScopeCallsWrapperNamesDataList) {
+            if (!stringArrayScopeCallsWrapperNamesData) {
                 continue;
             }
 
-            const {encoding, names} = stringArrayScopeCallsWrapperData;
+            const {names} = stringArrayScopeCallsWrapperNamesData;
             const namesLength: number = names.length;
 
             /**
@@ -136,21 +150,23 @@ export class StringArrayScopeCallsWrapperTransformer extends AbstractNodeTransfo
              */
             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
-                    );
+                const [
+                    upperStringArrayCallsWrapperName,
+                    upperStringArrayCallsWrapperShiftedIndex,
+                ] = this.getUpperStringArrayCallsWrapperName(
+                    stringArrayScopeCallsWrapperNamesData,
+                    stringArrayScopeCallsWrapperNamesLexicalScopeData,
+                );
 
-                stringArrayScopeCallsWrapperNode.initialize(
+                const stringArrayScopeCallsWrapperNode: TStatement[] = this.getStringArrayScopeCallsWrapperNode(
                     stringArrayScopeCallsWrapperName,
-                    upperStringArrayCallsWrapperName
+                    upperStringArrayCallsWrapperName,
+                    upperStringArrayCallsWrapperShiftedIndex
                 );
 
                 NodeAppender.prepend(
                     lexicalScopeBodyNode,
-                    stringArrayScopeCallsWrapperNode.getNode()
+                    stringArrayScopeCallsWrapperNode
                 );
             }
         }
@@ -159,40 +175,143 @@ export class StringArrayScopeCallsWrapperTransformer extends AbstractNodeTransfo
     }
 
     /**
-     * @param {TStringArrayEncoding} encoding
-     * @returns {string}
+     * @param {IStringArrayScopeCallsWrapperNamesData} stringArrayScopeCallsWrapperNamesData
+     * @param {IStringArrayScopeCallsWrapperLexicalScopeData} stringArrayScopeCallsWrapperNamesLexicalScopeData
+     * @returns {[name: string, index: number]}
      */
-    private getRootStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
-        return this.stringArrayStorage.getStorageCallsWrapperName(encoding);
+    private getRootStringArrayCallsWrapperData (
+        stringArrayScopeCallsWrapperNamesData: IStringArrayScopeCallsWrapperNamesData,
+        stringArrayScopeCallsWrapperNamesLexicalScopeData: IStringArrayScopeCallsWrapperLexicalScopeData
+    ): [name: string, index: number] {
+        const {encoding} = stringArrayScopeCallsWrapperNamesData;
+        const {resultShiftedIndex} = stringArrayScopeCallsWrapperNamesLexicalScopeData;
+
+        return [
+            this.stringArrayStorage.getStorageCallsWrapperName(encoding),
+            resultShiftedIndex
+        ];
     }
 
     /**
-     * @param {TStringArrayEncoding} encoding
-     * @returns {string}
+     * @param {IStringArrayScopeCallsWrapperNamesData} stringArrayScopeCallsWrapperNamesData
+     * @param {IStringArrayScopeCallsWrapperLexicalScopeData} stringArrayScopeCallsWrapperNamesLexicalScopeData
+     * @returns {[name: string, index: number]}
      */
-    private getUpperStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
-        const rootStringArrayCallsWrapperName: string = this.getRootStringArrayCallsWrapperName(encoding);
+    private getUpperStringArrayCallsWrapperName (
+        stringArrayScopeCallsWrapperNamesData: IStringArrayScopeCallsWrapperNamesData,
+        stringArrayScopeCallsWrapperNamesLexicalScopeData: IStringArrayScopeCallsWrapperLexicalScopeData
+    ): [name: string, index: number] {
+        const {encoding} = stringArrayScopeCallsWrapperNamesData;
+        const {scopeShiftedIndex} = stringArrayScopeCallsWrapperNamesLexicalScopeData;
+
+        const rootStringArrayCallsWrapperData = this.getRootStringArrayCallsWrapperData(
+            stringArrayScopeCallsWrapperNamesData,
+            stringArrayScopeCallsWrapperNamesLexicalScopeData
+        );
 
         if (!this.options.stringArrayWrappersChainedCalls) {
-            return rootStringArrayCallsWrapperName;
+            return rootStringArrayCallsWrapperData;
         }
 
-        const parentLexicalScopeBodyNode: TNodeWithLexicalScopeStatements | undefined =
-            this.visitedLexicalScopeNodesStackStorage.getLastElement();
+        const parentLexicalScopeBodyNode: TNodeWithLexicalScopeStatements | null =
+            this.visitedLexicalScopeNodesStackStorage.getLastElement()
+            ?? null;
 
         if (!parentLexicalScopeBodyNode) {
-            return rootStringArrayCallsWrapperName;
+            return rootStringArrayCallsWrapperData;
         }
 
-        const parentLexicalScopeDataByEncoding = this.stringArrayScopeCallsWrapperDataStorage
+        const parentLexicalScopeNamesDataByEncoding: TStringArrayScopeCallsWrapperNamesDataByEncoding | null = this.stringArrayScopeCallsWrapperNamesDataStorage
             .get(parentLexicalScopeBodyNode) ?? null;
-        const parentLexicalScopeNames: string[] | null = parentLexicalScopeDataByEncoding?.[encoding]?.names ?? null;
+        const parentLexicalScopeNames: string[] | null = parentLexicalScopeNamesDataByEncoding?.[encoding]?.names ?? null;
+
+        if (!parentLexicalScopeNames?.length) {
+            return rootStringArrayCallsWrapperData;
+        }
 
-        return parentLexicalScopeNames?.length
-            ? this.randomGenerator
-                .getRandomGenerator()
-                .pickone(parentLexicalScopeNames)
-            : rootStringArrayCallsWrapperName;
+        const upperStringArrayCallsWrapperName: string = this.randomGenerator
+            .getRandomGenerator()
+            .pickone(parentLexicalScopeNames);
+
+        return [
+            upperStringArrayCallsWrapperName,
+            scopeShiftedIndex
+        ];
+    }
+
+    /**
+     * @param {string} stringArrayScopeCallsWrapperName
+     * @param {string} upperStringArrayCallsWrapperName
+     * @param {number} stringArrayScopeCallsWrapperShiftedIndex
+     * @returns {TStatement[]}
+     */
+    private getStringArrayScopeCallsWrapperNode (
+        stringArrayScopeCallsWrapperName: string,
+        upperStringArrayCallsWrapperName: string,
+        stringArrayScopeCallsWrapperShiftedIndex: number
+    ): TStatement[] {
+        switch (this.options.stringArrayWrappersType) {
+            case StringArrayWrappersType.Function:
+                return this.getStringArrayScopeCallsWrapperFunctionNode(
+                    stringArrayScopeCallsWrapperName,
+                    upperStringArrayCallsWrapperName,
+                    stringArrayScopeCallsWrapperShiftedIndex
+                );
+
+            case StringArrayWrappersType.Variable:
+            default:
+                return this.getStringArrayScopeCallsWrapperVariableNode(
+                    stringArrayScopeCallsWrapperName,
+                    upperStringArrayCallsWrapperName
+                );
+        }
+    }
+
+    /**
+     * @param {string} stringArrayScopeCallsWrapperName
+     * @param {string} upperStringArrayCallsWrapperName
+     * @returns {TStatement[]}
+     */
+    private getStringArrayScopeCallsWrapperVariableNode (
+        stringArrayScopeCallsWrapperName: string,
+        upperStringArrayCallsWrapperName: string
+    ): TStatement[] {
+        const stringArrayScopeCallsWrapperVariableNode: ICustomNode<TInitialData<StringArrayScopeCallsWrapperVariableNode>> =
+            this.stringArrayTransformerCustomNodeFactory(
+                StringArrayTransformerCustomNode.StringArrayScopeCallsWrapperVariableNode
+            );
+
+        stringArrayScopeCallsWrapperVariableNode.initialize(
+            stringArrayScopeCallsWrapperName,
+            upperStringArrayCallsWrapperName
+        );
+
+        return stringArrayScopeCallsWrapperVariableNode.getNode();
+    }
+
+    /**
+     * @param {string} stringArrayScopeCallsWrapperName
+     * @param {string} upperStringArrayCallsWrapperName
+     * @param {number} stringArrayScopeCallsWrapperShiftedIndex
+     * @returns {TStatement[]}
+     */
+    private getStringArrayScopeCallsWrapperFunctionNode (
+        stringArrayScopeCallsWrapperName: string,
+        upperStringArrayCallsWrapperName: string,
+        stringArrayScopeCallsWrapperShiftedIndex: number
+    ): TStatement[] {
+        const stringArrayScopeCallsWrapperFunctionNode: ICustomNode<TInitialData<StringArrayScopeCallsWrapperFunctionNode>> =
+            this.stringArrayTransformerCustomNodeFactory(
+                StringArrayTransformerCustomNode.StringArrayScopeCallsWrapperFunctionNode
+            );
+
+        stringArrayScopeCallsWrapperFunctionNode.initialize(
+            stringArrayScopeCallsWrapperName,
+            upperStringArrayCallsWrapperName,
+            stringArrayScopeCallsWrapperShiftedIndex
+        );
+
+        return stringArrayScopeCallsWrapperFunctionNode.getNode();
     }
 
     /**

+ 165 - 42
src/node-transformers/string-array-transformers/StringArrayTransformer.ts

@@ -3,21 +3,22 @@ import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
 
 import * as ESTree from 'estree';
 
+import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
 import { TInitialData } from '../../types/TInitialData';
 import { TNodeWithLexicalScopeStatements } from '../../types/node/TNodeWithLexicalScopeStatements';
 import { TStatement } from '../../types/node/TStatement';
-import { TStringArrayEncoding } from '../../types/options/TStringArrayEncoding';
-import { TStringArrayScopeCallsWrapperDataByEncoding } from '../../types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperDataByEncoding';
+import { TStringArrayScopeCallsWrapperNamesDataByEncoding } from '../../types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperNamesDataByEncoding';
 import { TStringArrayTransformerCustomNodeFactory } from '../../types/container/custom-nodes/TStringArrayTransformerCustomNodeFactory';
 
 import { ICustomNode } from '../../interfaces/custom-nodes/ICustomNode';
 import { IEscapeSequenceEncoder } from '../../interfaces/utils/IEscapeSequenceEncoder';
 import { IIdentifierNamesGenerator } from '../../interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator';
-import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
 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 { IStringArrayScopeCallsWrapperLexicalScopeData } from '../../interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperLexicalScopeData';
+import { IStringArrayScopeCallsWrapperLexicalScopeDataStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperLexicalScopeDataStorage';
+import { IStringArrayScopeCallsWrapperNamesDataStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperNamesDataStorage';
 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';
@@ -26,6 +27,7 @@ import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
 
 import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage';
 import { StringArrayTransformerCustomNode } from '../../enums/custom-nodes/StringArrayTransformerCustomNode';
+import { StringArrayWrappersType } from '../../enums/node-transformers/string-array-transformers/StringArrayWrappersType';
 
 import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
 import { NodeFactory } from '../../node/NodeFactory';
@@ -37,6 +39,16 @@ import { StringArrayCallNode } from '../../custom-nodes/string-array-nodes/Strin
 
 @injectable()
 export class StringArrayTransformer extends AbstractNodeTransformer {
+    /**
+     * @type {number}
+     */
+    private static readonly minShiftedIndexValue: number = -1000;
+
+    /**
+     * @type {number}
+     */
+    private static readonly maxShiftedIndexValue: number = 1000;
+
     /**
      * @type {IEscapeSequenceEncoder}
      */
@@ -63,9 +75,14 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
     private readonly stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer;
 
     /**
-     * @type {IStringArrayScopeCallsWrapperDataStorage}
+     * @type {IStringArrayScopeCallsWrapperLexicalScopeDataStorage}
+     */
+    private readonly stringArrayScopeCallsWrapperLexicalScopeDataStorage: IStringArrayScopeCallsWrapperLexicalScopeDataStorage;
+
+    /**
+     * @type {IStringArrayScopeCallsWrapperNamesDataStorage}
      */
-    private readonly stringArrayScopeCallsWrapperDataStorage: IStringArrayScopeCallsWrapperDataStorage;
+    private readonly stringArrayScopeCallsWrapperNamesDataStorage: IStringArrayScopeCallsWrapperNamesDataStorage;
 
     /**
      * @type {TStringArrayTransformerCustomNodeFactory}
@@ -84,7 +101,8 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
      * @param {ILiteralNodesCacheStorage} literalNodesCacheStorage
      * @param {IVisitedLexicalScopeNodesStackStorage} visitedLexicalScopeNodesStackStorage
      * @param {IStringArrayStorage} stringArrayStorage
-     * @param {IStringArrayScopeCallsWrapperDataStorage} stringArrayScopeCallsWrapperDataStorage
+     * @param {IStringArrayScopeCallsWrapperNamesDataStorage} stringArrayScopeCallsWrapperNamesDataStorage
+     * @param {IStringArrayScopeCallsWrapperLexicalScopeDataStorage} stringArrayScopeCallsWrapperLexicalScopeDataStorage
      * @param {IStringArrayStorageAnalyzer} stringArrayStorageAnalyzer
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {TStringArrayTransformerCustomNodeFactory} stringArrayTransformerCustomNodeFactory
@@ -96,7 +114,10 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
         @inject(ServiceIdentifiers.ILiteralNodesCacheStorage) literalNodesCacheStorage: ILiteralNodesCacheStorage,
         @inject(ServiceIdentifiers.IVisitedLexicalScopeNodesStackStorage) visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage,
         @inject(ServiceIdentifiers.IStringArrayStorage) stringArrayStorage: IStringArrayStorage,
-        @inject(ServiceIdentifiers.IStringArrayScopeCallsWrapperDataStorage) stringArrayScopeCallsWrapperDataStorage: IStringArrayScopeCallsWrapperDataStorage,
+        @inject(ServiceIdentifiers.IStringArrayScopeCallsWrapperNamesDataStorage)
+            stringArrayScopeCallsWrapperNamesDataStorage: IStringArrayScopeCallsWrapperNamesDataStorage,
+        @inject(ServiceIdentifiers.IStringArrayScopeCallsWrapperLexicalScopeDataStorage)
+            stringArrayScopeCallsWrapperLexicalScopeDataStorage: IStringArrayScopeCallsWrapperLexicalScopeDataStorage,
         @inject(ServiceIdentifiers.IStringArrayStorageAnalyzer) stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer,
         @inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
             identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
@@ -109,7 +130,8 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
         this.literalNodesCacheStorage = literalNodesCacheStorage;
         this.visitedLexicalScopeNodesStackStorage = visitedLexicalScopeNodesStackStorage;
         this.stringArrayStorage = stringArrayStorage;
-        this.stringArrayScopeCallsWrapperDataStorage = stringArrayScopeCallsWrapperDataStorage;
+        this.stringArrayScopeCallsWrapperNamesDataStorage = stringArrayScopeCallsWrapperNamesDataStorage;
+        this.stringArrayScopeCallsWrapperLexicalScopeDataStorage = stringArrayScopeCallsWrapperLexicalScopeDataStorage;
         this.stringArrayStorageAnalyzer = stringArrayStorageAnalyzer;
         this.identifierNamesGenerator = identifierNamesGeneratorFactory(options);
         this.stringArrayTransformerCustomNodeFactory = stringArrayTransformerCustomNodeFactory;
@@ -208,8 +230,8 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
      * @returns {Node}
      */
     private getStringArrayCallNode (stringArrayStorageItemData: IStringArrayStorageItemData): ESTree.Node {
-        const stringArrayCallsWrapperName: string = this.getStringArrayCallsWrapperName(stringArrayStorageItemData);
-        const { index, decodeKey } = stringArrayStorageItemData;
+        const [stringArrayCallsWrapperName, index] = this.getStringArrayCallsWrapperData(stringArrayStorageItemData);
+        const {decodeKey } = stringArrayStorageItemData;
 
         const stringArrayCallCustomNode: ICustomNode<TInitialData<StringArrayCallNode>> =
             this.stringArrayTransformerCustomNodeFactory(StringArrayTransformerCustomNode.StringArrayCallNode);
@@ -227,58 +249,159 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
 
     /**
      * @param {IStringArrayStorageItemData} stringArrayStorageItemData
-     * @returns {string}
+     * @returns {[name: string, index: number]}
      */
-    private getStringArrayCallsWrapperName (stringArrayStorageItemData: IStringArrayStorageItemData): string {
-        const {encoding} = stringArrayStorageItemData;
-
+    private getStringArrayCallsWrapperData (
+        stringArrayStorageItemData: IStringArrayStorageItemData
+    ): [name: string, index: number] {
         return !this.options.stringArrayWrappersCount
-            ? this.getRootStringArrayCallsWrapperName(encoding)
-            : this.getUpperStringArrayCallsWrapperName(encoding);
-
+            ? this.getRootStringArrayCallsWrapperData(stringArrayStorageItemData)
+            : this.getUpperStringArrayCallsWrapperData(stringArrayStorageItemData);
     }
 
     /**
-     * @param {TStringArrayEncoding} encoding
-     * @returns {string}
+     * @param {IStringArrayStorageItemData} stringArrayStorageItemData
+     * @returns {[name: string, index: number]}
      */
-    private getRootStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
-        return this.stringArrayStorage.getStorageCallsWrapperName(encoding);
+    private getRootStringArrayCallsWrapperData (
+        stringArrayStorageItemData: IStringArrayStorageItemData
+    ): [name: string, index: number] {
+        const {encoding, index} = stringArrayStorageItemData;
+
+        const rootStringArrayCallsWrapperName: string = this.stringArrayStorage.getStorageCallsWrapperName(encoding);
+
+        return [
+            rootStringArrayCallsWrapperName,
+            index
+        ];
     }
 
     /**
-     * @param {TStringArrayEncoding} encoding
-     * @returns {string}
+     * @param {IStringArrayStorageItemData} stringArrayStorageItemData
+     * @returns {[name: string, index: number]}
      */
-    private getUpperStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
-        const currentLexicalScopeBodyNode: TNodeWithLexicalScopeStatements | undefined =
-            this.visitedLexicalScopeNodesStackStorage.getLastElement();
+    private getUpperStringArrayCallsWrapperData (
+        stringArrayStorageItemData: IStringArrayStorageItemData
+    ): [name: string, index: number] {
+        const {encoding, index} = stringArrayStorageItemData;
+        const currentLexicalScopeBodyNode: TNodeWithLexicalScopeStatements | null =
+            this.visitedLexicalScopeNodesStackStorage.getLastElement() ?? null;
+        const parentLexicalScopeBodyNode: TNodeWithLexicalScopeStatements | null =
+            this.visitedLexicalScopeNodesStackStorage.getPenultimateElement() ?? null;
 
         if (!currentLexicalScopeBodyNode) {
             throw new Error('Cannot find current lexical scope body node');
         }
 
-        const stringArrayScopeCallsWrapperDataByEncoding: TStringArrayScopeCallsWrapperDataByEncoding =
-            this.stringArrayScopeCallsWrapperDataStorage.get(currentLexicalScopeBodyNode) ?? {};
-        const stringArrayScopeCallsWrapperNames: string[] = stringArrayScopeCallsWrapperDataByEncoding[encoding]?.names ?? [];
+        const stringArrayScopeCallsWrapperNamesDataByEncoding: TStringArrayScopeCallsWrapperNamesDataByEncoding =
+            this.getAndUpdateStringArrayScopeCallsWrapperNamesDataByEncoding(
+                currentLexicalScopeBodyNode,
+                stringArrayStorageItemData
+            );
+        const stringArrayScopeCallsWrapperLexicalScopeData: IStringArrayScopeCallsWrapperLexicalScopeData =
+            this.getAndUpdateStringArrayScopeCallsWrapperLexicalScopeData(
+                currentLexicalScopeBodyNode,
+                parentLexicalScopeBodyNode
+            );
+
+        const stringArrayScopeCallsWrapperNames: string[] = stringArrayScopeCallsWrapperNamesDataByEncoding[encoding]?.names ?? [];
+        const randomUpperStringArrayCallsWrapperName: string = this.randomGenerator
+            .getRandomGenerator()
+            .pickone(stringArrayScopeCallsWrapperNames);
+
+        const resultIndex: number = stringArrayScopeCallsWrapperLexicalScopeData
+            ? stringArrayScopeCallsWrapperLexicalScopeData.resultShiftedIndex + index
+            : index;
+
+        return [
+            randomUpperStringArrayCallsWrapperName,
+            resultIndex
+        ];
+    }
+
+    /**
+     * @param {TNodeWithLexicalScopeStatements} currentLexicalScopeBodyNode
+     * @param {IStringArrayStorageItemData} stringArrayStorageItemData
+     * @returns {TStringArrayScopeCallsWrapperNamesDataByEncoding}
+     */
+    private getAndUpdateStringArrayScopeCallsWrapperNamesDataByEncoding (
+        currentLexicalScopeBodyNode: TNodeWithLexicalScopeStatements,
+        stringArrayStorageItemData: IStringArrayStorageItemData
+    ): TStringArrayScopeCallsWrapperNamesDataByEncoding {
+        const {encoding} = stringArrayStorageItemData;
+        const stringArrayScopeCallsWrapperNamesDataByEncoding: TStringArrayScopeCallsWrapperNamesDataByEncoding =
+            this.stringArrayScopeCallsWrapperNamesDataStorage.get(currentLexicalScopeBodyNode)
+            ?? {};
+
+        const stringArrayScopeCallsWrapperNames: string[] = stringArrayScopeCallsWrapperNamesDataByEncoding[encoding]?.names ?? [];
         const isFilledScopeCallsWrapperNamesList: boolean = stringArrayScopeCallsWrapperNames.length === this.options.stringArrayWrappersCount;
 
-        if (!isFilledScopeCallsWrapperNamesList) {
-            const nextScopeCallsWrapperName: string = this.identifierNamesGenerator.generateNext();
+        if (isFilledScopeCallsWrapperNamesList) {
+            return stringArrayScopeCallsWrapperNamesDataByEncoding;
+        }
+
+        const nextScopeCallsWrapperName: string = this.identifierNamesGenerator.generateNext();
 
-            stringArrayScopeCallsWrapperNames.push(nextScopeCallsWrapperName);
-            stringArrayScopeCallsWrapperDataByEncoding[encoding] = {
-                encoding,
-                names: stringArrayScopeCallsWrapperNames
-            };
+        stringArrayScopeCallsWrapperNamesDataByEncoding[encoding] = {
+            encoding,
+            names: [
+                ...stringArrayScopeCallsWrapperNames,
+                nextScopeCallsWrapperName
+            ]
+        };
 
-            this.stringArrayScopeCallsWrapperDataStorage.set(
-                currentLexicalScopeBodyNode,
-                stringArrayScopeCallsWrapperDataByEncoding
-            );
+        this.stringArrayScopeCallsWrapperNamesDataStorage.set(
+            currentLexicalScopeBodyNode,
+            stringArrayScopeCallsWrapperNamesDataByEncoding
+        );
+
+        return stringArrayScopeCallsWrapperNamesDataByEncoding;
+    }
+
+    /**
+     * @param {TNodeWithLexicalScopeStatements} currentLexicalScopeBodyNode
+     * @param {TNodeWithLexicalScopeStatements | null} parentLexicalScopeBodyNode
+     * @returns {IStringArrayScopeCallsWrapperLexicalScopeData}
+     * @private
+     */
+    private getAndUpdateStringArrayScopeCallsWrapperLexicalScopeData (
+        currentLexicalScopeBodyNode: TNodeWithLexicalScopeStatements,
+        parentLexicalScopeBodyNode: TNodeWithLexicalScopeStatements | null
+    ): IStringArrayScopeCallsWrapperLexicalScopeData {
+        const storedLexicalScopeData: IStringArrayScopeCallsWrapperLexicalScopeData | null =
+            this.stringArrayScopeCallsWrapperLexicalScopeDataStorage.get(currentLexicalScopeBodyNode)
+            ?? null;
+
+        if (storedLexicalScopeData) {
+            return storedLexicalScopeData;
         }
 
-        return this.randomGenerator.getRandomGenerator().pickone(stringArrayScopeCallsWrapperNames);
+        const parentLexicalScopeData: IStringArrayScopeCallsWrapperLexicalScopeData | null = parentLexicalScopeBodyNode
+            ? this.stringArrayScopeCallsWrapperLexicalScopeDataStorage.get(parentLexicalScopeBodyNode) ?? null
+            : null;
+
+        const scopeShiftedIndex: number = this.options.stringArrayWrappersType === StringArrayWrappersType.Function
+            ? this.randomGenerator.getRandomInteger(
+                StringArrayTransformer.minShiftedIndexValue,
+                StringArrayTransformer.maxShiftedIndexValue
+            )
+            : 0;
+        const resultShiftedIndex: number = parentLexicalScopeData
+            ? parentLexicalScopeData.resultShiftedIndex + scopeShiftedIndex
+            : scopeShiftedIndex;
+
+        const lexicalScopeData: IStringArrayScopeCallsWrapperLexicalScopeData = {
+            parentLexicalScopeBodyNode,
+            resultShiftedIndex,
+            scopeShiftedIndex
+        };
+
+        this.stringArrayScopeCallsWrapperLexicalScopeDataStorage.set(
+            currentLexicalScopeBodyNode,
+            lexicalScopeData
+        );
+
+        return lexicalScopeData;
     }
 
     /**

+ 9 - 1
src/options/Options.ts

@@ -23,6 +23,7 @@ import {
 import { TInputOptions } from '../types/options/TInputOptions';
 import { TOptionsPreset } from '../types/options/TOptionsPreset';
 import { TStringArrayEncoding } from '../types/options/TStringArrayEncoding';
+import { TStringArrayWrappersType } from '../types/options/TStringArrayWrappersType';
 
 import { IOptions } from '../interfaces/options/IOptions';
 import { IOptionsNormalizer } from '../interfaces/options/IOptionsNormalizer';
@@ -31,7 +32,8 @@ import { IdentifierNamesGenerator } from '../enums/generators/identifier-names-g
 import { ObfuscationTarget } from '../enums/ObfuscationTarget';
 import { OptionsPreset } from '../enums/options/presets/OptionsPreset';
 import { SourceMapMode } from '../enums/source-map/SourceMapMode';
-import { StringArrayEncoding } from '../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../enums/node-transformers/string-array-transformers/StringArrayEncoding';
+import { StringArrayWrappersType } from '../enums/node-transformers/string-array-transformers/StringArrayWrappersType';
 
 import { DEFAULT_PRESET } from './presets/Default';
 import { LOW_OBFUSCATION_PRESET } from './presets/LowObfuscation';
@@ -310,6 +312,12 @@ export class Options implements IOptions {
     @Min(0)
     public readonly stringArrayWrappersCount!: number;
 
+    /**
+     * @type {TStringArrayWrappersType}
+     */
+    @IsIn([StringArrayWrappersType.Variable, StringArrayWrappersType.Function])
+    public readonly stringArrayWrappersType!: TStringArrayWrappersType;
+
     /**
      * @type {number}
      */

+ 1 - 1
src/options/normalizer-rules/StringArrayEncodingRule.ts

@@ -2,7 +2,7 @@ import { TOptionsNormalizerRule } from '../../types/options/TOptionsNormalizerRu
 
 import { IOptions } from '../../interfaces/options/IOptions';
 
-import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 /**
  * @param {IOptions} options

+ 1 - 1
src/options/normalizer-rules/StringArrayRule.ts

@@ -2,7 +2,7 @@ import { TOptionsNormalizerRule } from '../../types/options/TOptionsNormalizerRu
 
 import { IOptions } from '../../interfaces/options/IOptions';
 
-import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 /**
  * @param {IOptions} options

+ 1 - 1
src/options/normalizer-rules/StringArrayThresholdRule.ts

@@ -2,7 +2,7 @@ import { TOptionsNormalizerRule } from '../../types/options/TOptionsNormalizerRu
 
 import { IOptions } from '../../interfaces/options/IOptions';
 
-import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 /**
  * @param {IOptions} options

+ 3 - 1
src/options/presets/Default.ts

@@ -4,7 +4,8 @@ import { IdentifierNamesGenerator } from '../../enums/generators/identifier-name
 import { ObfuscationTarget } from '../../enums/ObfuscationTarget';
 import { OptionsPreset } from '../../enums/options/presets/OptionsPreset';
 import { SourceMapMode } from '../../enums/source-map/SourceMapMode';
-import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding';
+import { StringArrayWrappersType } from '../../enums/node-transformers/string-array-transformers/StringArrayWrappersType';
 
 export const DEFAULT_PRESET: TInputOptions = Object.freeze({
     compact: true,
@@ -46,6 +47,7 @@ export const DEFAULT_PRESET: TInputOptions = Object.freeze({
     ],
     stringArrayWrappersChainedCalls: true,
     stringArrayWrappersCount: 1,
+    stringArrayWrappersType: StringArrayWrappersType.Variable,
     stringArrayThreshold: 0.75,
     target: ObfuscationTarget.Browser,
     transformObjectKeys: false,

+ 3 - 1
src/options/presets/HighObfuscation.ts

@@ -1,7 +1,8 @@
 import { TInputOptions } from '../../types/options/TInputOptions';
 
 import { OptionsPreset } from '../../enums/options/presets/OptionsPreset';
-import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding';
+import { StringArrayWrappersType } from '../../enums/node-transformers/string-array-transformers/StringArrayWrappersType';
 
 import { MEDIUM_OBFUSCATION_PRESET } from './MediumObfuscation';
 
@@ -17,5 +18,6 @@ export const HIGH_OBFUSCATION_PRESET: TInputOptions = Object.freeze({
         StringArrayEncoding.Rc4
     ],
     stringArrayWrappersCount: 5,
+    stringArrayWrappersType: StringArrayWrappersType.Function,
     stringArrayThreshold: 1
 });

+ 1 - 1
src/options/presets/MediumObfuscation.ts

@@ -1,7 +1,7 @@
 import { TInputOptions } from '../../types/options/TInputOptions';
 
 import { OptionsPreset } from '../../enums/options/presets/OptionsPreset';
-import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { LOW_OBFUSCATION_PRESET } from './LowObfuscation';
 

+ 3 - 1
src/options/presets/NoCustomNodes.ts

@@ -3,7 +3,8 @@ import { TInputOptions } from '../../types/options/TInputOptions';
 import { IdentifierNamesGenerator } from '../../enums/generators/identifier-names-generators/IdentifierNamesGenerator';
 import { ObfuscationTarget } from '../../enums/ObfuscationTarget';
 import { SourceMapMode } from '../../enums/source-map/SourceMapMode';
-import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding';
+import { StringArrayWrappersType } from '../../enums/node-transformers/string-array-transformers/StringArrayWrappersType';
 
 export const NO_ADDITIONAL_NODES_PRESET: TInputOptions = Object.freeze({
     compact: true,
@@ -43,6 +44,7 @@ export const NO_ADDITIONAL_NODES_PRESET: TInputOptions = Object.freeze({
     ],
     stringArrayWrappersChainedCalls: false,
     stringArrayWrappersCount: 0,
+    stringArrayWrappersType: StringArrayWrappersType.Variable,
     stringArrayThreshold: 0,
     target: ObfuscationTarget.Browser,
     transformObjectKeys: false,

+ 14 - 0
src/storages/ArrayStorage.ts

@@ -54,6 +54,20 @@ export abstract class ArrayStorage <V> implements IArrayStorage <V> {
         this.storageId = this.randomGenerator.getRandomString(6);
     }
 
+    /**
+     * @param {number} key
+     * @returns {V | undefined}
+     */
+    public delete (key: number): V | undefined {
+        const deletedElement: V | undefined = this.storage.splice(key, 1)[0] ?? undefined;
+
+        if (deletedElement) {
+            this.storageLength--;
+        }
+
+        return deletedElement;
+    }
+
     /**
      * @param {number} key
      * @returns {V | undefined}

+ 1 - 1
src/storages/string-array-transformers/LiteralNodesCacheStorage.ts

@@ -8,7 +8,7 @@ import { IOptions } from '../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
 import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-transformers/IStringArrayStorageItem';
 
-import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { MapStorage } from '../MapStorage';
 

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

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

+ 5 - 5
src/storages/string-array-transformers/StringArrayScopeCallsWrapperDataStorage.ts → src/storages/string-array-transformers/StringArrayScopeCallsWrapperNamesDataStorage.ts

@@ -2,19 +2,19 @@ import { inject, injectable } from 'inversify';
 import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
 
 import { TNodeWithLexicalScopeStatements } from '../../types/node/TNodeWithLexicalScopeStatements';
-import { TStringArrayScopeCallsWrapperDataByEncoding } from '../../types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperDataByEncoding';
+import { TStringArrayScopeCallsWrapperNamesDataByEncoding } from '../../types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperNamesDataByEncoding';
 
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
-import { IStringArrayScopeCallsWrapperDataStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperDataStorage';
+import { IStringArrayScopeCallsWrapperNamesDataStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayScopeCallsWrapperNamesDataStorage';
 
 import { MapStorage } from '../MapStorage';
 
 @injectable()
-export class StringArrayScopeCallsWrapperDataStorage extends MapStorage <
+export class StringArrayScopeCallsWrapperNamesDataStorage extends MapStorage <
     TNodeWithLexicalScopeStatements,
-    TStringArrayScopeCallsWrapperDataByEncoding
-> implements IStringArrayScopeCallsWrapperDataStorage {
+    TStringArrayScopeCallsWrapperNamesDataByEncoding
+> implements IStringArrayScopeCallsWrapperNamesDataStorage {
     /**
      * @param {IRandomGenerator} randomGenerator
      * @param {IOptions} options

+ 1 - 1
src/storages/string-array-transformers/StringArrayStorage.ts

@@ -14,7 +14,7 @@ import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
 import { IStringArrayStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayStorage';
 import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-transformers/IStringArrayStorageItem';
 
-import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { MapStorage } from '../MapStorage';
 

+ 15 - 2
src/storages/string-array-transformers/VisitedLexicalScopeNodesStackStorage.ts

@@ -39,17 +39,30 @@ export class VisitedLexicalScopeNodesStackStorage extends ArrayStorage <TNodeWit
         return this.arrayUtils.getLastElement(this.getStorage());
     }
 
+    /**
+     * @returns {TNodeWithLexicalScopeStatements | undefined}
+     */
+    public getPenultimateElement (): TNodeWithLexicalScopeStatements | undefined {
+        const storageLength: number = this.getLength();
+
+        return this.get(storageLength - 2) ?? undefined;
+    }
+
     /**
      * @param {TNodeWithLexicalScopeStatements} nodeWithLexicalScopeStatements
      */
     public push (nodeWithLexicalScopeStatements: TNodeWithLexicalScopeStatements): void {
-        this.storage.push(nodeWithLexicalScopeStatements);
+        const storageLength: number = this.getLength();
+
+        this.set(storageLength, nodeWithLexicalScopeStatements);
     }
 
     /**
      * @returns {TNodeWithLexicalScopeStatements| undefined}
      */
     public pop (): TNodeWithLexicalScopeStatements | undefined {
-        return this.storage.pop();
+        const storageLength: number = this.getLength();
+
+        return this.delete(storageLength - 1);
     }
 }

+ 0 - 7
src/types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperDataByEncoding.ts

@@ -1,7 +0,0 @@
-import { TStringArrayEncoding } from '../../options/TStringArrayEncoding';
-
-import { IStringArrayScopeCallsWrapperData } from '../../../interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperData';
-
-export type TStringArrayScopeCallsWrapperDataByEncoding = Partial<{
-    [key in TStringArrayEncoding]: IStringArrayScopeCallsWrapperData;
-}>;

+ 7 - 0
src/types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperNamesDataByEncoding.ts

@@ -0,0 +1,7 @@
+import { TStringArrayEncoding } from '../../options/TStringArrayEncoding';
+
+import { IStringArrayScopeCallsWrapperNamesData } from '../../../interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperNamesData';
+
+export type TStringArrayScopeCallsWrapperNamesDataByEncoding = Partial<{
+    [key in TStringArrayEncoding]: IStringArrayScopeCallsWrapperNamesData;
+}>;

+ 1 - 1
src/types/options/TStringArrayEncoding.ts

@@ -1,5 +1,5 @@
 import { TypeFromEnum } from '@gradecam/tsenum';
 
-import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 export type TStringArrayEncoding = TypeFromEnum<typeof StringArrayEncoding>;

+ 5 - 0
src/types/options/TStringArrayWrappersType.ts

@@ -0,0 +1,5 @@
+import { TypeFromEnum } from '@gradecam/tsenum';
+
+import { StringArrayWrappersType } from '../../enums/node-transformers/string-array-transformers/StringArrayWrappersType';
+
+export type TStringArrayWrappersType = TypeFromEnum<typeof StringArrayWrappersType>;

+ 30 - 13
test/dev/dev.ts

@@ -2,35 +2,52 @@
 
 import { NO_ADDITIONAL_NODES_PRESET } from '../../src/options/presets/NoCustomNodes';
 import { IdentifierNamesGenerator } from '../../src/enums/generators/identifier-names-generators/IdentifierNamesGenerator';
+import { StringArrayWrappersType } from '../../src/enums/node-transformers/string-array-transformers/StringArrayWrappersType';
+import { StringArrayEncoding } from '../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 (function () {
     const JavaScriptObfuscator: any = require('../../index');
 
     let obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
         `
-            const foo = 'foo'
-            const bar = 'bar';
-            const baz = 'baz';
+            const foo = 'aaa';
+
+            function test (a, b) {
+                const bar = 'bbb';
+                const baz = 'ccc';
+            
+                function test1 (a, b) {
+                    const bark = 'ddd';
+                    const hawk = 'eee';
+            
+                    function test2 (a, b) {
+                        const bark = 'ddd';
+                        const hawk = 'eee';
+            
+                        return bark + hawk;
+                    }
+            
+                    return bark + hawk;
+                }
             
-            function test (arg = 'bark') {
-                const hawk = 'hawk';
-                const eagle = 'eagle';
-                
-                console.log(arg, hawk, eagle);
+                return bar + baz + test1();
             }
             
-            test();
+            foo + test();
         `,
         {
             ...NO_ADDITIONAL_NODES_PRESET,
             compact: false,
             identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
-            renameGlobals: true,
             stringArray: true,
-            transformObjectKeys: true,
             stringArrayThreshold: 1,
-            stringArrayWrappersChainedCalls: false,
-            stringArrayWrappersCount: 2
+            stringArrayEncoding: [
+                StringArrayEncoding.None,
+                StringArrayEncoding.Rc4
+            ],
+            stringArrayWrappersChainedCalls: true,
+            stringArrayWrappersCount: 5,
+            stringArrayWrappersType: StringArrayWrappersType.Function
         }
     ).getObfuscatedCode();
 

+ 1 - 1
test/functional-tests/custom-code-helpers/string-array/StringArrayCallsWrapperCodeHelper.spec.ts

@@ -1,7 +1,7 @@
 import { assert } from 'chai';
 
 import { IdentifierNamesGenerator } from '../../../../src/enums/generators/identifier-names-generators/IdentifierNamesGenerator';
-import { StringArrayEncoding } from '../../../../src/enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { NO_ADDITIONAL_NODES_PRESET } from '../../../../src/options/presets/NoCustomNodes';
 

+ 1 - 1
test/functional-tests/custom-code-helpers/string-array/group/StringArrayCodeHelperGroup.spec.ts

@@ -1,6 +1,6 @@
 import { assert } from 'chai';
 
-import { StringArrayEncoding } from '../../../../../src/enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
 import { IdentifierNamesGenerator } from '../../../../../src/enums/generators/identifier-names-generators/IdentifierNamesGenerator';
 
 import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';

+ 3 - 1
test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts

@@ -8,7 +8,8 @@ import { TOptionsPreset } from '../../../src/types/options/TOptionsPreset';
 import { IObfuscatedCode } from '../../../src/interfaces/source-code/IObfuscatedCode';
 
 import { SourceMapMode } from '../../../src/enums/source-map/SourceMapMode';
-import { StringArrayEncoding } from '../../../src/enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
+import { StringArrayWrappersType } from '../../../src/enums/node-transformers/string-array-transformers/StringArrayWrappersType';
 
 import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade';
 
@@ -853,6 +854,7 @@ describe('JavaScriptObfuscator', () => {
                         ],
                         stringArrayWrappersChainedCalls: true,
                         stringArrayWrappersCount: 10,
+                        stringArrayWrappersType: StringArrayWrappersType.Function,
                         stringArrayThreshold: 1,
                         transformObjectKeys: true,
                         unicodeEscapeSequence: false

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

@@ -1,7 +1,8 @@
 import { assert } from 'chai';
 
 import { IdentifierNamesGenerator } from '../../../../../src/enums/generators/identifier-names-generators/IdentifierNamesGenerator';
-import { StringArrayEncoding } from '../../../../../src/enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
+import { StringArrayWrappersType } from '../../../../../src/enums/node-transformers/string-array-transformers/StringArrayWrappersType';
 
 import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
 
@@ -16,7 +17,7 @@ describe('StringArrayScopeCallsWrapperTransformer', function () {
         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};' +
+                        '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};' +
@@ -572,6 +573,296 @@ describe('StringArrayScopeCallsWrapperTransformer', function () {
                 });
             });
         });
+
+        describe('Variant #7: `stringArrayWrappersType` option has `Function` value', () => {
+            const hexadecimalIndexMatch: string = '0x[a-z0-9]{1,3}';
+
+            describe('Variant #1: base', () => {
+                    const stringArrayCallRegExp: RegExp = new RegExp(
+                        'const f *= *function *\\(c, *d\\) *{' +
+                            `return b\\(c *-(?: -)?'${hexadecimalIndexMatch}', *d\\);` +
+                        '};' +
+                        `const foo *= *f\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                        `const bar *= *f\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                        `const baz *= *f\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                        'function test *\\( *\\) *{' +
+                            'const g *= *function *\\(c, *d\\) *{' +
+                                `return b\\(c *-(?: -)?'${hexadecimalIndexMatch}', *d\\);` +
+                            '};' +
+                            `const c *= *g\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                            `const d *= *g\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                            `const e *= *g\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                        '}'
+                    );
+
+                    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,
+                                stringArrayWrappersChainedCalls: false,
+                                stringArrayWrappersCount: 1,
+                                stringArrayWrappersType: StringArrayWrappersType.Function
+                            }
+                        ).getObfuscatedCode();
+                    });
+
+                    it('should add correct scope calls wrappers', () => {
+                        assert.match(obfuscatedCode, stringArrayCallRegExp);
+                    });
+                });
+
+            describe('Variant #2: correct chained calls', () => {
+                    const stringArrayCallRegExp: RegExp = new RegExp(
+                        'const f *= *function *\\(c, *d\\) *{' +
+                            `return b\\(c *-(?: -)?'${hexadecimalIndexMatch}', *d\\);` +
+                        '};' +
+                        `const foo *= *f\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                        `const bar *= *f\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                        `const baz *= *f\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                        'function test *\\( *\\) *{' +
+                            'const g *= *function *\\(c, *d\\) *{' +
+                                `return f\\(c *-(?: -)?'${hexadecimalIndexMatch}', *d\\);` +
+                            '};' +
+                            `const c *= *g\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                            `const d *= *g\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                            `const e *= *g\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                        '}'
+                    );
+
+                    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,
+                                stringArrayWrappersChainedCalls: true,
+                                stringArrayWrappersCount: 1,
+                                stringArrayWrappersType: StringArrayWrappersType.Function
+                            }
+                        ).getObfuscatedCode();
+                    });
+
+                    it('should add correct scope calls wrappers', () => {
+                        assert.match(obfuscatedCode, stringArrayCallRegExp);
+                    });
+                });
+
+            describe('Variant #3: no wrappers on a root scope', () => {
+                    const stringArrayCallRegExp: RegExp = new RegExp(
+                            'return e;' +
+                        '};' +
+                        'function test *\\( *\\) *{' +
+                            'const f *= *function *\\(c, *d\\) *{' +
+                                `return b\\(c *-(?: -)?'${hexadecimalIndexMatch}', *d\\);` +
+                            '};' +
+                            `const c *= *f\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                            `const d *= *f\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                            `const e *= *f\\(-? *'${hexadecimalIndexMatch}'\\);` +
+                        '}'
+                    );
+
+                    let obfuscatedCode: string;
+
+                    before(() => {
+                        const code: string = readFileAsString(__dirname + '/fixtures/wrappers-count-const-no-root-wrappers.js');
+
+                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                            code,
+                            {
+                                ...NO_ADDITIONAL_NODES_PRESET,
+                                identifierNamesGenerator: IdentifierNamesGenerator.MangledIdentifierNamesGenerator,
+                                stringArray: true,
+                                stringArrayThreshold: 1,
+                                stringArrayWrappersChainedCalls: true,
+                                stringArrayWrappersCount: 1,
+                                stringArrayWrappersType: StringArrayWrappersType.Function
+                            }
+                        ).getObfuscatedCode();
+                    });
+
+                    it('should add correct scope calls wrappers', () => {
+                        assert.match(obfuscatedCode, stringArrayCallRegExp);
+                    });
+                });
+
+            describe('Variant #4: 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
+                                        ],
+                                        stringArrayWrappersChainedCalls: true,
+                                        stringArrayWrappersCount: 5,
+                                        stringArrayWrappersType: StringArrayWrappersType.Function
+                                    }
+                                ).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
+                                        ],
+                                        stringArrayWrappersChainedCalls: true,
+                                        stringArrayWrappersCount: 5,
+                                        stringArrayWrappersType: StringArrayWrappersType.Function
+                                    }
+                                ).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,
+                                        stringArrayWrappersType: StringArrayWrappersType.Function
+                                    }
+                                ).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,
+                                        stringArrayWrappersType: StringArrayWrappersType.Function
+                                    }
+                                ).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', () => {

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

@@ -0,0 +1,5 @@
+function test () {
+    const foo = 'foo'
+    const bar = 'bar';
+    const baz = 'baz';
+}

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

@@ -1,7 +1,7 @@
 import { assert } from 'chai';
 
 import { IdentifierNamesGenerator } from '../../../../../src/enums/generators/identifier-names-generators/IdentifierNamesGenerator';
-import { StringArrayEncoding } from '../../../../../src/enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
 

+ 1 - 1
test/functional-tests/options/OptionsNormalizer.spec.ts

@@ -10,7 +10,7 @@ import { IInversifyContainerFacade } from '../../../src/interfaces/container/IIn
 import { IOptions } from '../../../src/interfaces/options/IOptions';
 import { IOptionsNormalizer } from '../../../src/interfaces/options/IOptionsNormalizer';
 
-import { StringArrayEncoding } from '../../../src/enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { DEFAULT_PRESET } from '../../../src/options/presets/Default';
 

+ 1 - 1
test/performance-tests/JavaScriptObfuscatorMemory.spec.ts

@@ -2,7 +2,7 @@ import { assert } from 'chai';
 
 import { readFileAsString } from '../helpers/readFileAsString';
 
-import { StringArrayEncoding } from '../../src/enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { JavaScriptObfuscator } from '../../src/JavaScriptObfuscatorFacade';
 

+ 3 - 1
test/runtime-tests/JavaScriptObfuscatorRuntime.spec.ts

@@ -2,8 +2,9 @@ import { assert } from 'chai';
 
 import { TInputOptions } from '../../src/types/options/TInputOptions';
 
-import { StringArrayEncoding } from '../../src/enums/StringArrayEncoding';
 import { IdentifierNamesGenerator } from '../../src/enums/generators/identifier-names-generators/IdentifierNamesGenerator';
+import { StringArrayEncoding } from '../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
+import { StringArrayWrappersType } from '../../src/enums/node-transformers/string-array-transformers/StringArrayWrappersType';
 
 import { evaluateInWorker } from '../helpers/evaluateInWorker';
 import { readFileAsString } from '../helpers/readFileAsString';
@@ -41,6 +42,7 @@ describe('JavaScriptObfuscator runtime eval', function () {
         ],
         stringArrayWrappersChainedCalls: true,
         stringArrayWrappersCount: 5,
+        stringArrayWrappersType: StringArrayWrappersType.Function,
         stringArrayThreshold: 1,
         transformObjectKeys: true,
         unicodeEscapeSequence: true

+ 1 - 1
test/unit-tests/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.spec.ts

@@ -11,7 +11,7 @@ import { IInversifyContainerFacade } from '../../../../src/interfaces/container/
 import { IStringArrayStorageAnalyzer } from '../../../../src/interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
 import { IStringArrayStorageItemData } from '../../../../src/interfaces/storages/string-array-transformers/IStringArrayStorageItem';
 
-import { StringArrayEncoding } from '../../../../src/enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { InversifyContainerFacade } from '../../../../src/container/InversifyContainerFacade';
 import { NodeFactory } from '../../../../src/node/NodeFactory';

+ 1 - 1
test/unit-tests/cli/utils/CLIUtils.spec.ts

@@ -2,7 +2,7 @@ import { assert } from 'chai';
 
 import { TInputOptions } from '../../../../src/types/options/TInputOptions';
 
-import { StringArrayEncoding } from '../../../../src/enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { CLIUtils } from '../../../../src/cli/utils/CLIUtils';
 

+ 75 - 0
test/unit-tests/storages/ArrayStorage.spec.ts

@@ -245,6 +245,81 @@ describe('ArrayStorage', () => {
         });
     });
 
+    describe('delete', () => {
+        describe('Variant #1: value exist', () => {
+            const expectedUpdatedStorage: string[] = [
+                'foo',
+                'baz'
+            ];
+            const expectedUpdatedStorageLength: number = 2;
+            const expectedDeletedValue: string = 'bar';
+
+            let updatedStorage: string[];
+            let updatedStorageLength: number;
+            let deletedValue: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+                storage.set(0, 'foo');
+                storage.set(1, 'bar');
+                storage.set(2, 'baz');
+
+                deletedValue = storage.delete(1);
+                updatedStorage = storage.getStorage();
+                updatedStorageLength = storage.getLength();
+            });
+
+            it('should delete value from the storage by index', () => {
+                assert.deepEqual(updatedStorage, expectedUpdatedStorage);
+            });
+
+            it('should update storage length', () => {
+                assert.deepEqual(updatedStorageLength, expectedUpdatedStorageLength);
+            });
+
+            it('should return deleted value', () => {
+                assert.equal(deletedValue, expectedDeletedValue);
+            });
+        });
+
+        describe('Variant #2: value isn\'t exist', () => {
+            const expectedUpdatedStorage: string[] = [
+                'foo',
+                'bar',
+                'baz'
+            ];
+            const expectedUpdatedStorageLength: number = 3;
+            const expectedDeletedValue: undefined = undefined;
+
+            let updatedStorage: string[];
+            let updatedStorageLength: number;
+            let deletedValue: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+                storage.set(0, 'foo');
+                storage.set(1, 'bar');
+                storage.set(2, 'baz');
+
+                deletedValue = storage.delete(3);
+                updatedStorage = storage.getStorage();
+                updatedStorageLength = storage.getLength();
+            });
+
+            it('should keep storage the same', () => {
+                assert.deepEqual(updatedStorage, expectedUpdatedStorage);
+            });
+
+            it('should keep storage length', () => {
+                assert.deepEqual(updatedStorageLength, expectedUpdatedStorageLength);
+            });
+
+            it('should return undefined', () => {
+                assert.equal(deletedValue, expectedDeletedValue);
+            });
+        });
+    });
+
     describe('mergeWith', () => {
         describe('Base merge', () => {
             const secondStorageKey: number = 1;

+ 1 - 1
test/unit-tests/storages/string-array-transformers/literal-nodes-cache/LiteralNodesCacheStorage.spec.ts

@@ -10,7 +10,7 @@ import { TInputOptions } from '../../../../../src/types/options/TInputOptions';
 import { IInversifyContainerFacade } from '../../../../../src/interfaces/container/IInversifyContainerFacade';
 import { ILiteralNodesCacheStorage } from '../../../../../src/interfaces/storages/string-array-transformers/ILiteralNodesCacheStorage';
 
-import { StringArrayEncoding } from '../../../../../src/enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
 

+ 1 - 1
test/unit-tests/storages/string-array-transformers/string-array/StringArrayStorage.spec.ts

@@ -9,7 +9,7 @@ import { TInputOptions } from '../../../../../src/types/options/TInputOptions';
 import { IInversifyContainerFacade } from '../../../../../src/interfaces/container/IInversifyContainerFacade';
 import { IStringArrayStorage } from '../../../../../src/interfaces/storages/string-array-transformers/IStringArrayStorage';
 
-import { StringArrayEncoding } from '../../../../../src/enums/StringArrayEncoding';
+import { StringArrayEncoding } from '../../../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding';
 
 import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
 

+ 133 - 28
test/unit-tests/storages/string-array-transformers/visited-lexical-scope-nodes-stack/VisitedLexicalScopeNodesStackStorage.spec.ts

@@ -67,6 +67,79 @@ describe('VisitedLexicalScopeNodesStackStorage', () => {
         });
     });
 
+    describe('getPenultimateElement', () => {
+        describe('Variant #1: three array elements', () => {
+            const firstElement: TNodeWithLexicalScopeStatements = NodeFactory.blockStatementNode([
+                NodeFactory.expressionStatementNode(
+                    NodeFactory.literalNode('first')
+                )
+            ]);
+            const expectedSecondElement: TNodeWithLexicalScopeStatements = NodeFactory.blockStatementNode([
+                NodeFactory.expressionStatementNode(
+                    NodeFactory.literalNode('second')
+                )
+            ]);
+            const lastElement: TNodeWithLexicalScopeStatements =  NodeFactory.blockStatementNode([
+                NodeFactory.expressionStatementNode(
+                    NodeFactory.literalNode('last')
+                )
+            ]);
+
+            let penultimateElement: TNodeWithLexicalScopeStatements | undefined;
+
+            before(() => {
+                const visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage = getStorageInstance();
+
+                visitedLexicalScopeNodesStackStorage.push(firstElement);
+                visitedLexicalScopeNodesStackStorage.push(expectedSecondElement);
+                visitedLexicalScopeNodesStackStorage.push(lastElement);
+                penultimateElement = visitedLexicalScopeNodesStackStorage.getPenultimateElement();
+            });
+
+            it('should return a penultimate element from the stack', () => {
+                assert.equal(penultimateElement, expectedSecondElement);
+            });
+        });
+
+        describe('Variant #2: one array element', () => {
+            const expectedPenultimateElement: undefined = undefined;
+            const firstElement: TNodeWithLexicalScopeStatements = NodeFactory.blockStatementNode([
+                NodeFactory.expressionStatementNode(
+                    NodeFactory.literalNode('first')
+                )
+            ]);
+
+            let penultimateElement: TNodeWithLexicalScopeStatements | undefined;
+
+            before(() => {
+                const visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage = getStorageInstance();
+
+                visitedLexicalScopeNodesStackStorage.push(firstElement);
+                penultimateElement = visitedLexicalScopeNodesStackStorage.getPenultimateElement();
+            });
+
+            it('should return a penultimate element from the stack', () => {
+                assert.equal(penultimateElement, expectedPenultimateElement);
+            });
+        });
+
+        describe('Variant #3: empty array', () => {
+            const expectedPenultimateElement: undefined = undefined;
+
+            let penultimateElement: TNodeWithLexicalScopeStatements | undefined;
+
+            before(() => {
+                const visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage = getStorageInstance();
+
+                penultimateElement = visitedLexicalScopeNodesStackStorage.getPenultimateElement();
+            });
+
+            it('should return a penultimate element from the stack', () => {
+                assert.equal(penultimateElement, expectedPenultimateElement);
+            });
+        });
+    });
+
     describe('push', () => {
         const firstElement: TNodeWithLexicalScopeStatements = NodeFactory.blockStatementNode([
             NodeFactory.expressionStatementNode(
@@ -99,40 +172,72 @@ describe('VisitedLexicalScopeNodesStackStorage', () => {
     });
 
     describe('pop', () => {
-        const firstElement: TNodeWithLexicalScopeStatements = NodeFactory.blockStatementNode([
-            NodeFactory.expressionStatementNode(
-                NodeFactory.literalNode('first')
-            )
-        ]);
-        const secondElement: TNodeWithLexicalScopeStatements = NodeFactory.blockStatementNode([
-            NodeFactory.expressionStatementNode(
-                NodeFactory.literalNode('second')
-            )
-        ]);
-        const expectedStorage: TNodeWithLexicalScopeStatements[] = [
-            firstElement
-        ];
-        const expectedPoppedElement: TNodeWithLexicalScopeStatements = secondElement;
+        describe('Variant #1: few elements', () => {
+            const firstElement: TNodeWithLexicalScopeStatements = NodeFactory.blockStatementNode([
+                NodeFactory.expressionStatementNode(
+                    NodeFactory.literalNode('first')
+                )
+            ]);
+            const secondElement: TNodeWithLexicalScopeStatements = NodeFactory.blockStatementNode([
+                NodeFactory.expressionStatementNode(
+                    NodeFactory.literalNode('second')
+                )
+            ]);
+            const expectedStorage: TNodeWithLexicalScopeStatements[] = [
+                firstElement
+            ];
+            const expectedPoppedElement: TNodeWithLexicalScopeStatements = secondElement;
+
+            let storage: TNodeWithLexicalScopeStatements[];
+            let poppedElement: TNodeWithLexicalScopeStatements | undefined;
+
+            before(() => {
+                const visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage = getStorageInstance();
+
+                visitedLexicalScopeNodesStackStorage.push(firstElement);
+                visitedLexicalScopeNodesStackStorage.push(secondElement);
+
+                poppedElement = visitedLexicalScopeNodesStackStorage.pop();
+                storage = visitedLexicalScopeNodesStackStorage.getStorage();
+            });
+
+            it('should pop a last element from the storage', () => {
+                assert.deepEqual(storage, expectedStorage);
+            });
+
+            it('should return a popped element from the storage', () => {
+                assert.equal(poppedElement, expectedPoppedElement);
+            });
+        });
 
-        let storage: TNodeWithLexicalScopeStatements[];
-        let poppedElement: TNodeWithLexicalScopeStatements | undefined;
+        describe('Variant #2: single element', () => {
+            const firstElement: TNodeWithLexicalScopeStatements = NodeFactory.blockStatementNode([
+                NodeFactory.expressionStatementNode(
+                    NodeFactory.literalNode('first')
+                )
+            ]);
+            const expectedStorage: TNodeWithLexicalScopeStatements[] = [];
+            const expectedPoppedElement: TNodeWithLexicalScopeStatements = firstElement;
 
-        before(() => {
-            const visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage = getStorageInstance();
+            let storage: TNodeWithLexicalScopeStatements[];
+            let poppedElement: TNodeWithLexicalScopeStatements | undefined;
 
-            visitedLexicalScopeNodesStackStorage.push(firstElement);
-            visitedLexicalScopeNodesStackStorage.push(secondElement);
+            before(() => {
+                const visitedLexicalScopeNodesStackStorage: IVisitedLexicalScopeNodesStackStorage = getStorageInstance();
 
-            poppedElement = visitedLexicalScopeNodesStackStorage.pop();
-            storage = visitedLexicalScopeNodesStackStorage.getStorage();
-        });
+                visitedLexicalScopeNodesStackStorage.push(firstElement);
 
-        it('should pop a last element from the storage', () => {
-            assert.deepEqual(storage, expectedStorage);
-        });
+                poppedElement = visitedLexicalScopeNodesStackStorage.pop();
+                storage = visitedLexicalScopeNodesStackStorage.getStorage();
+            });
+
+            it('should pop a last element from the storage', () => {
+                assert.deepEqual(storage, expectedStorage);
+            });
 
-        it('should return a popped element from the storage', () => {
-            assert.equal(poppedElement, expectedPoppedElement);
+            it('should return a popped element from the storage', () => {
+                assert.equal(poppedElement, expectedPoppedElement);
+            });
         });
     });
 });

+ 45 - 45
yarn.lock

@@ -465,61 +465,61 @@
   resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a"
   integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ==
 
-"@typescript-eslint/[email protected].0":
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.1.0.tgz#7d309f60815ff35e9627ad85e41928d7b7fd443f"
-  integrity sha512-U+nRJx8XDUqJxYF0FCXbpmD9nWt/xHDDG0zsw1vrVYAmEAuD/r49iowfurjSL2uTA2JsgtpsyG7mjO7PHf2dYw==
+"@typescript-eslint/[email protected].1":
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.1.1.tgz#78d5b18e259b13c2f4ec41dd9105af269a161a75"
+  integrity sha512-Hoxyt99EA9LMmqo/5PuWWPeWeB3mKyvibfJ1Hy5SfiUpjE8Nqp+5QNd9fOkzL66+fqvIWSIE+Ett16LGMzCGnQ==
   dependencies:
-    "@typescript-eslint/experimental-utils" "4.1.0"
-    "@typescript-eslint/scope-manager" "4.1.0"
+    "@typescript-eslint/experimental-utils" "4.1.1"
+    "@typescript-eslint/scope-manager" "4.1.1"
     debug "^4.1.1"
     functional-red-black-tree "^1.0.1"
     regexpp "^3.0.0"
     semver "^7.3.2"
     tsutils "^3.17.1"
 
-"@typescript-eslint/[email protected].0":
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.1.0.tgz#263d7225645c09a411c8735eeffd417f50f49026"
-  integrity sha512-paEYLA37iqRIDPeQwAmoYSiZ3PiHsaAc3igFeBTeqRHgPnHjHLJ9OGdmP6nwAkF65p2QzEsEBtpjNUBWByNWzA==
+"@typescript-eslint/[email protected].1":
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.1.1.tgz#52ff4e37c93113eb96385a4e6d075abece1ea72d"
+  integrity sha512-jzYsNciHoa4Z3c1URtmeT/bamYm8Dwfw6vuN3WHIE/BXb1iC4KveAnXDErTAZtPVxTYBaYn3n2gbt6F6D2rm1A==
   dependencies:
     "@types/json-schema" "^7.0.3"
-    "@typescript-eslint/scope-manager" "4.1.0"
-    "@typescript-eslint/types" "4.1.0"
-    "@typescript-eslint/typescript-estree" "4.1.0"
+    "@typescript-eslint/scope-manager" "4.1.1"
+    "@typescript-eslint/types" "4.1.1"
+    "@typescript-eslint/typescript-estree" "4.1.1"
     eslint-scope "^5.0.0"
     eslint-utils "^2.0.0"
 
-"@typescript-eslint/[email protected].0":
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.1.0.tgz#9b0409411725f14cd7faa81a664e5051225961db"
-  integrity sha512-hM/WNCQTzDHgS0Ke3cR9zPndL3OTKr9OoN9CL3UqulsAjYDrglSwIIgswSmHBcSbOzLmgaMARwrQEbIumIglvQ==
+"@typescript-eslint/[email protected].1":
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.1.1.tgz#324b4b35e314075adbc92bd8330cf3ef0c88cf3e"
+  integrity sha512-NLIhmicpKGfJbdXyQBz9j48PA6hq6e+SDOoXy7Ak6bq1ebGqbgG+fR1UIDAuay6OjQdot69c/URu2uLlsP8GQQ==
   dependencies:
-    "@typescript-eslint/scope-manager" "4.1.0"
-    "@typescript-eslint/types" "4.1.0"
-    "@typescript-eslint/typescript-estree" "4.1.0"
+    "@typescript-eslint/scope-manager" "4.1.1"
+    "@typescript-eslint/types" "4.1.1"
+    "@typescript-eslint/typescript-estree" "4.1.1"
     debug "^4.1.1"
 
-"@typescript-eslint/[email protected].0":
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.1.0.tgz#9e389745ee9cfe12252ed1e9958808abd6b3a683"
-  integrity sha512-HD1/u8vFNnxwiHqlWKC/Pigdn0Mvxi84Y6GzbZ5f5sbLrFKu0al02573Er+D63Sw67IffVUXR0uR8rpdfdk+vA==
+"@typescript-eslint/[email protected].1":
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.1.1.tgz#bdb8526e82435f32b4ccd9dd4cec01af97b48850"
+  integrity sha512-0W8TTobCvIIQ2FsrYTffyZGAAFUyIbEHq5EYJb1m7Rpd005jrnOvKOo8ywCLhs/Bm17C+KsrUboBvBAARQVvyA==
   dependencies:
-    "@typescript-eslint/types" "4.1.0"
-    "@typescript-eslint/visitor-keys" "4.1.0"
+    "@typescript-eslint/types" "4.1.1"
+    "@typescript-eslint/visitor-keys" "4.1.1"
 
-"@typescript-eslint/[email protected].0":
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.1.0.tgz#edbd3fec346f34e13ce7aa176b03b497a32c496a"
-  integrity sha512-rkBqWsO7m01XckP9R2YHVN8mySOKKY2cophGM8K5uDK89ArCgahItQYdbg/3n8xMxzu2elss+an1TphlUpDuJw==
+"@typescript-eslint/[email protected].1":
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.1.1.tgz#57500c4a86b28cb47094c1a62f1177ea279a09cb"
+  integrity sha512-zrBiqOKYerMTllKcn+BP+i1b7LW/EbMMYytroXMxUTvFPn1smkCu0D7lSAx29fTUO4jnwV0ljSvYQtn2vNrNxA==
 
-"@typescript-eslint/[email protected].0":
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.1.0.tgz#394046ead25164494218c0e3d6b960695ea967f6"
-  integrity sha512-r6et57qqKAWU173nWyw31x7OfgmKfMEcjJl9vlJEzS+kf9uKNRr4AVTRXfTCwebr7bdiVEkfRY5xGnpPaNPe4Q==
+"@typescript-eslint/[email protected].1":
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.1.1.tgz#2015a84d71303ecdb6f46efd807ac19a51aab490"
+  integrity sha512-2AUg5v0liVBsqbGxBphbJ0QbGqSRVaF5qPoTPWcxop+66vMdU1h4CCvHxTC47+Qb+Pr4l2RhXDd41JNpwcQEKw==
   dependencies:
-    "@typescript-eslint/types" "4.1.0"
-    "@typescript-eslint/visitor-keys" "4.1.0"
+    "@typescript-eslint/types" "4.1.1"
+    "@typescript-eslint/visitor-keys" "4.1.1"
     debug "^4.1.1"
     globby "^11.0.1"
     is-glob "^4.0.1"
@@ -527,12 +527,12 @@
     semver "^7.3.2"
     tsutils "^3.17.1"
 
-"@typescript-eslint/[email protected].0":
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.1.0.tgz#b2d528c9484e7eda1aa4f86ccf0432fb16e4d545"
-  integrity sha512-+taO0IZGCtCEsuNTTF2Q/5o8+fHrlml8i9YsZt2AiDCdYEJzYlsmRY991l/6f3jNXFyAWepdQj7n8Na6URiDRQ==
+"@typescript-eslint/[email protected].1":
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.1.1.tgz#bb05664bf4bea28dc120d1da94f3027d42ab0f6f"
+  integrity sha512-/EOOXbA2ferGLG6RmCHEQ0lTTLkOlXYDgblCmQk3tIU7mTPLm4gKhFMeeUSe+bcchTUsKeCk8xcpbop5Zr/8Rw==
   dependencies:
-    "@typescript-eslint/types" "4.1.0"
+    "@typescript-eslint/types" "4.1.1"
     eslint-visitor-keys "^2.0.0"
 
 "@webassemblyjs/[email protected]":
@@ -2035,10 +2035,10 @@ [email protected]:
     resolve "^1.17.0"
     tsconfig-paths "^3.9.0"
 
-eslint-plugin-jsdoc@30.4.2:
-  version "30.4.2"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-30.4.2.tgz#d3113442a2b285e902903d91b987e2d6d4409006"
-  integrity sha512-Czm3gXyP1Ztibyh+J3t7osQ+AUrXhJp9CQfpo6cME8LwrlDpl2newDL18UO1Yk+TngcLKUyMt0byfpHjwhqNBg==
+eslint-plugin-jsdoc@30.5.1:
+  version "30.5.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-30.5.1.tgz#b024295db3ce3b258909419d9bcf747b8e65e2a7"
+  integrity sha512-cY3YNxdhFcQVkcQLnZw/iZGsTPMuWa9yWZclorMWkjdHprBQX0TMWMEcmJYM3IjHp1HJr7aD0Z0sCRifEBhnzg==
   dependencies:
     comment-parser "^0.7.6"
     debug "^4.1.1"

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä