Browse Source

Merge pull request #738 from javascript-obfuscator/string-array-intermediate-calls-wrapper

`stringArrayIntermediateVariablesCount` option rework
Timofey Kachalov 4 years ago
parent
commit
253b192bf5
67 changed files with 1715 additions and 473 deletions
  1. 2 1
      CHANGELOG.md
  2. 151 73
      README.md
  3. 0 0
      dist/index.browser.js
  4. 0 0
      dist/index.cli.js
  5. 0 0
      dist/index.js
  6. 3 3
      package.json
  7. 2 2
      src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts
  8. 7 2
      src/cli/JavaScriptObfuscatorCLI.ts
  9. 2 0
      src/container/ServiceIdentifiers.ts
  10. 25 4
      src/container/modules/custom-nodes/CustomNodesModule.ts
  11. 8 2
      src/container/modules/storages/StoragesModule.ts
  12. 1 1
      src/custom-code-helpers/self-defending/templates/SelfDefendingNoEvalTemplate.ts
  13. 1 1
      src/custom-code-helpers/string-array/StringArrayCallsWrapperBase64CodeHelper.ts
  14. 7 36
      src/custom-code-helpers/string-array/StringArrayCallsWrapperCodeHelper.ts
  15. 1 1
      src/custom-code-helpers/string-array/StringArrayCallsWrapperRc4CodeHelper.ts
  16. 1 1
      src/custom-code-helpers/string-array/StringArrayCodeHelper.ts
  17. 3 5
      src/custom-code-helpers/string-array/group/StringArrayCodeHelperGroup.ts
  18. 0 8
      src/custom-code-helpers/string-array/templates/string-array-calls-wrapper/StringArrayCallsWrapperIntermediateTemplate.ts
  19. 0 2
      src/custom-code-helpers/string-array/templates/string-array-calls-wrapper/StringArrayCallsWrapperTemplate.ts
  20. 130 0
      src/custom-nodes/string-array-nodes/StringArrayCallNode.ts
  21. 83 0
      src/custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperNode.ts
  22. 4 0
      src/enums/custom-nodes/StringArrayTransformerCustomNode.ts
  23. 1 1
      src/interfaces/IEncodedValue.ts
  24. 1 1
      src/interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer.ts
  25. 13 0
      src/interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperData.ts
  26. 2 1
      src/interfaces/options/IOptions.ts
  27. 0 11
      src/interfaces/storages/string-array-storage/IStringArrayCallsWrapperNames.ts
  28. 26 0
      src/interfaces/storages/string-array-transformers/ILiteralNodesCacheStorage.ts
  29. 2 3
      src/interfaces/storages/string-array-transformers/IStringArrayStorage.ts
  30. 0 0
      src/interfaces/storages/string-array-transformers/IStringArrayStorageItem.ts
  31. 6 0
      src/interfaces/utils/IArrayUtils.ts
  32. 245 62
      src/node-transformers/string-array-transformers/StringArrayTransformer.ts
  33. 7 1
      src/options/Options.ts
  34. 2 0
      src/options/OptionsNormalizer.ts
  35. 2 1
      src/options/normalizer-rules/StringArrayRule.ts
  36. 2 1
      src/options/normalizer-rules/StringArrayThresholdRule.ts
  37. 19 0
      src/options/normalizer-rules/StringArrayWappersChainedCalls.ts
  38. 2 1
      src/options/presets/Default.ts
  39. 1 1
      src/options/presets/HighObfuscation.ts
  40. 1 1
      src/options/presets/MediumObfuscation.ts
  41. 2 1
      src/options/presets/NoCustomNodes.ts
  42. 55 0
      src/storages/string-array-transformers/LiteralNodesCacheStorage.ts
  43. 14 23
      src/storages/string-array-transformers/StringArrayStorage.ts
  44. 7 0
      src/types/container/custom-nodes/TStringArrayTransformerCustomNodeFactory.ts
  45. 7 0
      src/types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperDataByEncoding.ts
  46. 10 0
      src/utils/ArrayUtils.ts
  47. 11 7
      test/dev/dev.ts
  48. 55 55
      test/fixtures/compile-performance.js
  49. 0 31
      test/functional-tests/custom-code-helpers/string-array/StringArrayCallsWrapperCodeHelper.spec.ts
  50. 36 2
      test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts
  51. 410 51
      test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/StringArrayTransformer.spec.ts
  52. 0 11
      test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/intermediate-variables-count-eval.js
  53. 17 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-chained-calls-eval.js
  54. 9 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-count-const.js
  55. 11 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-count-eval.js
  56. 9 0
      test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-count-var.js
  57. 28 4
      test/functional-tests/options/OptionsNormalizer.spec.ts
  58. 3 3
      test/functional-tests/storages/string-array-transformers/string-array-storage/StringArrayStorage.spec.ts
  59. 0 0
      test/functional-tests/storages/string-array-transformers/string-array-storage/fixtures/one-string.js
  60. 0 0
      test/functional-tests/storages/string-array-transformers/string-array-storage/fixtures/three-strings.js
  61. 3 2
      test/index.spec.ts
  62. 8 3
      test/runtime-tests/JavaScriptObfuscatorRuntime.spec.ts
  63. 1 1
      test/unit-tests/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.spec.ts
  64. 157 0
      test/unit-tests/storages/string-array-transformers/literal-nodes-cache/LiteralNodesCacheStorage.spec.ts
  65. 7 7
      test/unit-tests/storages/string-array-transformers/string-array/StringArrayStorage.spec.ts
  66. 47 0
      test/unit-tests/utils/ArrayUtils.spec.ts
  67. 45 45
      yarn.lock

+ 2 - 1
CHANGELOG.md

@@ -2,7 +2,8 @@ Change Log
 
 v2.2.0
 ---
-* **New option:** `stringArrayIntermediateVariablesCount` sets the passed amount of intermediate variables for the `string array`
+* **New option (enabled by default):** `stringArrayWrappersCount` sets the count of wrappers for the `string array` inside each root or function scope
+* **New option (enabled by default):** `stringArrayWrappersChainedCalls` enables the chained calls between `string array` wrappers
 
 v2.1.0
 ---

+ 151 - 73
README.md

@@ -108,91 +108,94 @@ var obfuscationResult = JavaScriptObfuscator.obfuscate(
     {
         compact: false,
         controlFlowFlattening: true,
+        controlFlowFlatteningThreshold: 1,
         numbersToExpressions: true,
         simplify: true,
         shuffleStringArray: true,
-        splitStrings: true
+        splitStrings: true,
+        stringArrayThreshold: 1
     }
 );
 
 console.log(obfuscationResult.getObfuscatedCode());
 /*
-var _0x2302 = [
-    'oAFaE',
-    'foo\x20',
-    'RDjIM',
-    'ZhYJQ',
+var _0x236b = [
+    '8|3|2|4|7|',
+    'izyPY',
+    'ZXpNn',
     'split',
-    'vWUAR',
-    'YIYCI',
-    'map',
+    '1|5|9|6|0',
+    'foo\x20',
+    'xPhYQ',
+    'sKneg',
+    'thwwU',
     'log',
-    '8|7|0|2|9|'
+    'map'
 ];
-(function (_0x27f028, _0xd47344) {
-    var _0x335bc0 = function (_0x29965a) {
-        while (--_0x29965a) {
-            _0x27f028['push'](_0x27f028['shift']());
+(function (_0x34d326, _0xc4ba4e) {
+    var _0xcd0f69 = function (_0x39b54a) {
+        while (--_0x39b54a) {
+            _0x34d326['push'](_0x34d326['shift']());
         }
     };
-    _0x335bc0(++_0xd47344);
-}(_0x2302, 0xaee + -0x1 * 0xa63 + -0x7 * -0x1f));
-var _0x2384 = function (_0x27f028, _0xd47344) {
-    _0x27f028 = _0x27f028 - (0xaee + -0x1 * 0xa63 + -0x1 * 0x8b);
-    var _0x335bc0 = _0x2302[_0x27f028];
-    return _0x335bc0;
+    _0xcd0f69(++_0xc4ba4e);
+}(_0x236b, 0x31 + 0x42 * -0x27 + 0xbbc));
+var _0x17c9 = function (_0x34d326, _0xc4ba4e) {
+    _0x34d326 = _0x34d326 - (0x31 + 0x42 * -0x27 + 0x9dd);
+    var _0xcd0f69 = _0x236b[_0x34d326];
+    return _0xcd0f69;
 };
 (function () {
-    var _0x38ee09 = {
-            'RDjIM': _0x2384('0x3') + '5|4|6|1|3',
-            'ZhYJQ': function (_0x257a32, _0x35b48d) {
-                return _0x257a32 + _0x35b48d;
-            },
-            'oAFaE': function (_0x1c49a8, _0x53ffa5) {
-                return _0x1c49a8 - _0x53ffa5;
+    var _0x32cb02 = _0x17c9, _0x41d9b4 = {
+            'thwwU': _0x32cb02('0x5') + _0x32cb02('0x9'),
+            'xPhYQ': function (_0x176227, _0x3e4c2d) {
+                return _0x176227 + _0x3e4c2d;
             },
-            'vWUAR': function (_0x2c7ad4, _0x33512e) {
-                return _0x2c7ad4 + _0x33512e;
+            'izyPY': function (_0x13389c, _0x58f8bc) {
+                return _0x13389c + _0x58f8bc;
             },
-            'YIYCI': _0x2384('0x5')
-        }, _0x147d16 = _0x38ee09[_0x2384('0x6')][_0x2384('0x8')]('|'), _0x28f080 = -0x239b + -0x49 * 0x65 + 0x4068;
+            'sKneg': _0x32cb02('0xa'),
+            'ZXpNn': function (_0x1e08e1, _0x5cba70) {
+                return _0x1e08e1 - _0x5cba70;
+            }
+        }, _0x2f8df3 = _0x41d9b4[_0x32cb02('0x2')][_0x32cb02('0x8')]('|'), _0x2087f0 = -0x9a * 0x1f + 0x15 * 0x3 + 0x1267;
     while (!![]) {
-        switch (_0x147d16[_0x28f080++]) {
+        switch (_0x2f8df3[_0x2087f0++]) {
         case '0':
-            var _0x263e6c = _0x38ee09[_0x2384('0x7')]('5', -'2');
+            console[_0x32cb02('0x3')](_0x2e20e0);
             continue;
         case '1':
-            console[_0x2384('0x2')](_0x225580);
+            console[_0x32cb02('0x3')](_0x3c882f);
             continue;
         case '2':
-            var _0x225580 = [
+            var _0x39f804 = _0x41d9b4[_0x32cb02('0x0')]('5', -'2');
+            continue;
+        case '3':
+            var _0x700005 = _0x41d9b4[_0x32cb02('0x6')]('5', 0x1818 + 0x11b5 + -0x29ca);
+            continue;
+        case '4':
+            var _0x172286 = [
                 '10',
                 '10',
                 '10',
                 '10',
                 '10'
-            ][_0x2384('0x1')](parseInt);
-            continue;
-        case '3':
-            console[_0x2384('0x2')](_0x1a047b);
-            continue;
-        case '4':
-            console[_0x2384('0x2')](_0x382090);
+            ][_0x32cb02('0x4')](parseInt);
             continue;
         case '5':
-            console[_0x2384('0x2')](_0x450071);
+            console[_0x32cb02('0x3')](_0x700005);
             continue;
         case '6':
-            console['log'](_0x263e6c);
+            console[_0x32cb02('0x3')](_0x172286);
             continue;
         case '7':
-            var _0x382090 = _0x38ee09['ZhYJQ']('5', -0x2093 + 0x103 + 0x1 * 0x1f93);
+            var _0x2e20e0 = _0x41d9b4[_0x32cb02('0x6')](_0x41d9b4[_0x32cb02('0x6')](_0x41d9b4[_0x32cb02('0x1')], -0x2596 + -0x27b * 0x1 + 0xdf * 0x2e), 0x1 * 0x1b06 + -0x1b8e * -0x1 + 0x1 * -0x3693);
             continue;
         case '8':
-            var _0x450071 = _0x38ee09[_0x2384('0x4')]('5', 0x377 + 0x1b9 * 0x9 + -0x1 * 0x12f5);
+            var _0x3c882f = _0x41d9b4[_0x32cb02('0x7')]('5', 0x1dc1 * -0x1 + 0x1d59 + 0x6b);
             continue;
         case '9':
-            var _0x1a047b = _0x38ee09[_0x2384('0x9')](_0x38ee09[_0x2384('0x0')] + (-0xbf6 + -0x24a * 0x2 + 0x23 * 0x79), 0x1 * 0xb68 + 0x6bd + 0x81 * -0x24);
+            console[_0x32cb02('0x3')](_0x39f804);
             continue;
         }
         break;
@@ -360,7 +363,8 @@ Following options are available for the JS Obfuscator:
     splitStringsChunkLength: 10,
     stringArray: true,
     stringArrayEncoding: [],
-    stringArrayIntermediateVariablesCount: true,
+    stringArrayWrappersCount: 1,
+    stringArrayWrappersChainedCalls: true,
     stringArrayThreshold: 0.75,
     target: 'browser',
     transformObjectKeys: false,
@@ -409,7 +413,8 @@ Following options are available for the JS Obfuscator:
     --split-strings-chunk-length <number>
     --string-array <boolean>
     --string-array-encoding '<list>' (comma separated) [none, base64, rc4]
-    --string-array-intermediate-variables-count <number>
+    --string-array-wrappers-count <number>
+    --string-array-wrappers-chained-calls <boolean>
     --string-array-threshold <number>
     --target <string> [browser, browser-no-eval, node]
     --transform-object-keys <boolean>
@@ -950,42 +955,111 @@ stringArrayEncoding: [
 ]
 ```
 
-### `stringArrayIntermediateVariablesCount`
-Type: `number` Default: `0`
+### `stringArrayWrappersCount`
+Type: `number` Default: `1`
 
 ##### :warning: [`stringArray`](#stringarray) option must be enabled
 
-Sets the passed amount of intermediate variables for the `string array`. 
+Sets the count of wrappers for the `string array` inside each root or function scope.
+The actual count of wrappers inside each scope is limited by a count of `literal` nodes within this scope.
 
 Example:
 ```ts
 // Input
 const foo = 'foo';
 const bar = 'bar';
-const baz = 'baz';
+        
+function test () {
+    const baz = 'baz';
+    const bark = 'bark';
+    const hawk = 'hawk';
+}
+
+const eagle = 'eagle';
+
+// Output, stringArrayWrappersCount: 5
+const _0x3018 = [
+    'foo',
+    'bar',
+    'baz',
+    'bark',
+    'hawk',
+    'eagle'
+];
+const _0x380f = function (_0x30182a, _0x380f29) {
+    _0x30182a = _0x30182a - 0x0;
+    let _0x4e002c = _0x3018[_0x30182a];
+    return _0x4e002c;
+};
+const _0xe4db7c = _0x380f;
+const _0x26ca42 = _0x380f;
+const _0x58c610 = _0x380f;
+const foo = _0x58c610('0x0');
+const bar = _0x26ca42('0x1');
+function test() {
+    const _0x500eda = _0x380f;
+    const _0x1d1760 = _0x380f;
+    const _0x4ca8b0 = _0x380f;
+    const _0x4e002c = _0x4ca8b0('0x2');
+    const _0x573b1c = _0x1d1760('0x3');
+    const _0x1fb6ef = _0x500eda('0x4');
+}
+const eagle = _0x26ca42('0x5');
+```
+
+### `stringArrayWrappersChainedCalls`
+Type: `boolean` Default: `true`
+
+##### :warning: [`stringArray`](#stringarray) and [`stringArrayWrappersCount`](#stringArrayWrappersCount) options must be enabled
+
+Enables the chained calls between `string array` wrappers.
 
-console.log(foo, bar, baz);
+Example:
+```ts
+// Input
+const foo = 'foo';
+const bar = 'bar';
+        
+function test () {
+    const baz = 'baz';
+    const bark = 'bark';
+
+    function test1() {
+        const hawk = 'hawk';
+        const eagle = 'eagle';
+    } 
+}
 
-// Output, stringArrayIntermediateVariablesCount: 5
-const _0x513c = [
+// Output, stringArrayWrappersCount: 5, stringArrayWrappersChainedCalls: true
+const _0x4714 = [
     'foo',
     'bar',
     'baz',
-    'log'
+    'bark',
+    'hawk',
+    'eagle'
 ];
-const _0x19a1 = function (_0x513ce6, _0x135380) {
-    _0x513ce6 = _0x513ce6 - 0x0;
-    let _0x19a103 = _0x513c[_0x513ce6];
-    return _0x19a103;
+const _0x2bdb = function (_0x471439, _0x2bdb71) {
+    _0x471439 = _0x471439 - 0x0;
+    let _0x6e47e6 = _0x4714[_0x471439];
+    return _0x6e47e6;
 };
-const _0x3745 = _0x19a1;
-const _0x261a = _0x19a1;
-const _0x2f15 = _0x19a1;
-const _0x4a63 = _0x19a1;
-const _0x2e9d = _0x19a1;
-const foo = _0x4a63('0x0');
-const bar = _0x3745('0x1');
-const baz = _0x2e9d('0x2');
+const _0x1c3d52 = _0x2bdb;
+const _0xd81c2a = _0x2bdb;
+const foo = _0xd81c2a('0x0');
+const bar = _0x1c3d52('0x1');
+function test() {
+    const _0x21a0b4 = _0x1c3d52;
+    const _0x12842d = _0xd81c2a;
+    const _0x6e47e6 = _0x12842d('0x2');
+    const _0x4f3aef = _0x12842d('0x3');
+    function _0x40f1dc() {
+        const _0x468540 = _0x12842d;
+        const _0x1f4b05 = _0x21a0b4;
+        const _0x40a980 = _0x1f4b05('0x4');
+        const _0x4d1285 = _0x468540('0x5');
+    }
+}
 ```
     
 ### `stringArrayThreshold`
@@ -1086,7 +1160,8 @@ Performance will 50-100% slower than without obfuscation
     splitStringsChunkLength: 5,
     stringArray: true,
     stringArrayEncoding: ['rc4'],
-    stringArrayIntermediateVariablesCount: 10,
+    stringArrayWrappersCount: 5,
+    stringArrayWrappersChainedCalls: true,
     stringArrayThreshold: 1,
     transformObjectKeys: true,
     unicodeEscapeSequence: false
@@ -1119,7 +1194,8 @@ Performance will 30-35% slower than without obfuscation
     splitStringsChunkLength: 10,
     stringArray: true,
     stringArrayEncoding: ['base64'],
-    stringArrayIntermediateVariablesCount: 5,
+    stringArrayWrappersCount: 2,
+    stringArrayWrappersChainedCalls: true,
     stringArrayThreshold: 0.75,
     transformObjectKeys: true,
     unicodeEscapeSequence: false
@@ -1149,7 +1225,8 @@ Performance will slightly slower than without obfuscation
     splitStrings: false,
     stringArray: true,
     stringArrayEncoding: [],
-    stringArrayIntermediateVariablesCount: 0,
+    stringArrayWrappersCount: 1,
+    stringArrayWrappersChainedCalls: true,
     stringArrayThreshold: 0.75,
     unicodeEscapeSequence: false
 }
@@ -1176,7 +1253,8 @@ Performance will slightly slower than without obfuscation
     splitStrings: false,
     stringArray: true,
     stringArrayEncoding: [],
-    stringArrayIntermediateVariablesCount: 0,
+    stringArrayWrappersCount: 1,
+    stringArrayWrappersChainedCalls: true,
     stringArrayThreshold: 0.75,
     unicodeEscapeSequence: false
 }

File diff suppressed because it is too large
+ 0 - 0
dist/index.browser.js


File diff suppressed because it is too large
+ 0 - 0
dist/index.cli.js


File diff suppressed because it is too large
+ 0 - 0
dist/index.js


+ 3 - 3
package.json

@@ -62,14 +62,14 @@
     "@types/sinon": "9.0.5",
     "@types/string-template": "1.0.2",
     "@types/webpack-env": "1.15.2",
-    "@typescript-eslint/eslint-plugin": "4.0.1",
-    "@typescript-eslint/parser": "4.0.1",
+    "@typescript-eslint/eslint-plugin": "4.1.0",
+    "@typescript-eslint/parser": "4.1.0",
     "chai": "4.2.0",
     "chai-exclude": "2.0.2",
     "coveralls": "3.1.0",
     "eslint": "7.8.1",
     "eslint-plugin-import": "2.22.0",
-    "eslint-plugin-jsdoc": "30.3.1",
+    "eslint-plugin-jsdoc": "30.3.3",
     "eslint-plugin-no-null": "1.0.2",
     "eslint-plugin-prefer-arrow": "1.2.2",
     "eslint-plugin-unicorn": "21.0.0",

+ 2 - 2
src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts

@@ -6,9 +6,9 @@ import * as ESTree from 'estree';
 
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
-import { IStringArrayStorage } from '../../interfaces/storages/string-array-storage/IStringArrayStorage';
+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-storage/IStringArrayStorageItem';
+import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-transformers/IStringArrayStorageItem';
 
 import { NodeGuards } from '../../node/NodeGuards';
 import { NodeMetadata } from '../../node/NodeMetadata';

+ 7 - 2
src/cli/JavaScriptObfuscatorCLI.ts

@@ -340,10 +340,15 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
                 ArraySanitizer
             )
             .option(
-                '--string-array-intermediate-variables-count <number>',
-                'Sets the passed amount of intermediate variables for the string array.',
+                '--string-array-wrappers-count <number>',
+                'Sets the count of wrappers for the string array inside each root or function scope',
                 parseInt
             )
+            .option(
+                '--string-array-wrappers-chained-calls <boolean>',
+                'Enables the chained calls between string array wrappers',
+                BooleanSanitizer
+            )
             .option(
                 '--string-array-threshold <number>',
                 'The probability that the literal string will be inserted into stringArray (Default: 0.8, Min: 0, Max: 1)',

+ 2 - 0
src/container/ServiceIdentifiers.ts

@@ -12,6 +12,7 @@ export enum ServiceIdentifiers {
     Factory__IObfuscatedCode = 'Factory<IObfuscatedCode>',
     Factory__IObjectExpressionKeysTransformerCustomNode = 'Factory<IObjectExpressionKeysTransformerCustomNode>',
     Factory__IObjectExpressionExtractor = 'Factory<IObjectExpressionExtractor>',
+    Factory__IStringArrayTransformerCustomNode = 'Factory<IStringArrayTransformerCustomNode>',
     Factory__TControlFlowStorage = 'Factory<TControlFlowStorage>',
     IArrayUtils = 'IArrayUtils',
     ICalleeDataExtractor = 'ICalleeDataExtractor',
@@ -31,6 +32,7 @@ export enum ServiceIdentifiers {
     IIdentifierReplacer = 'IIdentifierReplacer',
     IJavaScriptObfuscator = 'IJavaScriptObfuscator',
     ILevelledTopologicalSorter = 'ILevelledTopologicalSorter',
+    ILiteralNodesCacheStorage = 'ILiteralNodesCacheStorage',
     ILogger = 'ILogger',
     INodeGuard = 'INodeGuard',
     INodeTransformer = 'INodeTransformer',

+ 25 - 4
src/container/modules/custom-nodes/CustomNodesModule.ts

@@ -7,6 +7,7 @@ import { ICustomNode } from '../../../interfaces/custom-nodes/ICustomNode';
 import { ControlFlowCustomNode } from '../../../enums/custom-nodes/ControlFlowCustomNode';
 import { DeadCodeInjectionCustomNode } from '../../../enums/custom-nodes/DeadCodeInjectionCustomNode';
 import { ObjectExpressionKeysTransformerCustomNode } from '../../../enums/custom-nodes/ObjectExpressionKeysTransformerCustomNode';
+import { StringArrayTransformerCustomNode } from '../../../enums/custom-nodes/StringArrayTransformerCustomNode';
 
 import { ObjectExpressionVariableDeclarationHostNode } from '../../../custom-nodes/object-expression-keys-transformer-nodes/ObjectExpressionVariableDeclarationHostNode';
 import { BinaryExpressionFunctionNode } from '../../../custom-nodes/control-flow-flattening-nodes/BinaryExpressionFunctionNode';
@@ -17,6 +18,8 @@ import { CallExpressionFunctionNode } from '../../../custom-nodes/control-flow-f
 import { ControlFlowStorageNode } from '../../../custom-nodes/control-flow-flattening-nodes/control-flow-storage-nodes/ControlFlowStorageNode';
 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 { StringLiteralControlFlowStorageCallNode } from '../../../custom-nodes/control-flow-flattening-nodes/control-flow-storage-nodes/StringLiteralControlFlowStorageCallNode';
 import { StringLiteralNode } from '../../../custom-nodes/control-flow-flattening-nodes/StringLiteralNode';
 
@@ -68,6 +71,15 @@ export const customNodesModule: interfaces.ContainerModule = new ContainerModule
         .toConstructor(ObjectExpressionVariableDeclarationHostNode)
         .whenTargetNamed(ObjectExpressionKeysTransformerCustomNode.ObjectExpressionVariableDeclarationHostNode);
 
+    // string array transformer nodes
+    bind<interfaces.Newable<ICustomNode>>(ServiceIdentifiers.Newable__ICustomNode)
+        .toConstructor(StringArrayCallNode)
+        .whenTargetNamed(StringArrayTransformerCustomNode.StringArrayCallNode);
+
+    bind<interfaces.Newable<ICustomNode>>(ServiceIdentifiers.Newable__ICustomNode)
+        .toConstructor(StringArrayScopeCallsWrapperNode)
+        .whenTargetNamed(StringArrayTransformerCustomNode.StringArrayScopeCallsWrapperNode);
+
     // control flow customNode constructor factory
     bind<ICustomNode>(ServiceIdentifiers.Factory__IControlFlowCustomNode)
         .toFactory<ICustomNode>(InversifyContainerFacade
@@ -76,8 +88,7 @@ export const customNodesModule: interfaces.ContainerModule = new ContainerModule
                 ServiceIdentifiers.Factory__IIdentifierNamesGenerator,
                 ServiceIdentifiers.ICustomCodeHelperFormatter,
                 ServiceIdentifiers.IRandomGenerator,
-                ServiceIdentifiers.IOptions,
-                ServiceIdentifiers.IPrevailingKindOfVariablesAnalyzer
+                ServiceIdentifiers.IOptions
             ));
 
     // dead code injection customNode constructor factory
@@ -99,7 +110,17 @@ export const customNodesModule: interfaces.ContainerModule = new ContainerModule
                 ServiceIdentifiers.Factory__IIdentifierNamesGenerator,
                 ServiceIdentifiers.ICustomCodeHelperFormatter,
                 ServiceIdentifiers.IRandomGenerator,
-                ServiceIdentifiers.IOptions,
-                ServiceIdentifiers.IPrevailingKindOfVariablesAnalyzer
+                ServiceIdentifiers.IOptions
+            ));
+
+    // string array transformer customNode constructor factory
+    bind<ICustomNode>(ServiceIdentifiers.Factory__IStringArrayTransformerCustomNode)
+        .toFactory<ICustomNode>(InversifyContainerFacade
+            .getConstructorFactory<StringArrayTransformerCustomNode, ICustomNode>(
+                ServiceIdentifiers.Newable__ICustomNode,
+                ServiceIdentifiers.Factory__IIdentifierNamesGenerator,
+                ServiceIdentifiers.ICustomCodeHelperFormatter,
+                ServiceIdentifiers.IRandomGenerator,
+                ServiceIdentifiers.IOptions
             ));
 });

+ 8 - 2
src/container/modules/storages/StoragesModule.ts

@@ -4,13 +4,15 @@ import { ServiceIdentifiers } from '../../ServiceIdentifiers';
 import { TControlFlowStorage } from '../../../types/storages/TControlFlowStorage';
 import { TCustomCodeHelperGroupStorage } from '../../../types/storages/TCustomCodeHelperGroupStorage';
 
+import { ILiteralNodesCacheStorage } from '../../../interfaces/storages/string-array-transformers/ILiteralNodesCacheStorage';
 import { IOptions } from '../../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../../interfaces/utils/IRandomGenerator';
-import { IStringArrayStorage } from '../../../interfaces/storages/string-array-storage/IStringArrayStorage';
+import { IStringArrayStorage } from '../../../interfaces/storages/string-array-transformers/IStringArrayStorage';
 
 import { ControlFlowStorage } from '../../../storages/custom-nodes/ControlFlowStorage';
 import { CustomCodeHelperGroupStorage } from '../../../storages/custom-code-helpers/CustomCodeHelperGroupStorage';
-import { StringArrayStorage } from '../../../storages/string-array/StringArrayStorage';
+import { LiteralNodesCacheStorage } from '../../../storages/string-array-transformers/LiteralNodesCacheStorage';
+import { StringArrayStorage } from '../../../storages/string-array-transformers/StringArrayStorage';
 
 export const storagesModule: interfaces.ContainerModule = new ContainerModule((bind: interfaces.Bind) => {
     // storages
@@ -18,6 +20,10 @@ export const storagesModule: interfaces.ContainerModule = new ContainerModule((b
         .to(CustomCodeHelperGroupStorage)
         .inSingletonScope();
 
+    bind<ILiteralNodesCacheStorage>(ServiceIdentifiers.ILiteralNodesCacheStorage)
+        .to(LiteralNodesCacheStorage)
+        .inSingletonScope();
+
     bind<IStringArrayStorage>(ServiceIdentifiers.IStringArrayStorage)
         .to(StringArrayStorage)
         .inSingletonScope();

+ 1 - 1
src/custom-code-helpers/self-defending/templates/SelfDefendingNoEvalTemplate.ts

@@ -1,5 +1,5 @@
 /**
- * Notice, that second and third call to recursiveFunc1('indexOf') has cyrillic `е` character instead latin
+ * SelfDefendingTemplate. Enters code in infinity loop.
  *
  * @returns {string}
  */

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

@@ -25,7 +25,7 @@ export class StringArrayCallsWrapperBase64CodeHelper extends StringArrayCallsWra
                 atobPolyfill,
                 atobFunctionName,
                 selfDefendingCode,
-                stringArrayCallsWrapperName: this.stringArrayCallsWrapperNames.name
+                stringArrayCallsWrapperName: this.stringArrayCallsWrapperName
             }
         );
     }

+ 7 - 36
src/custom-code-helpers/string-array/StringArrayCallsWrapperCodeHelper.ts

@@ -9,13 +9,11 @@ import { ICustomCodeHelperObfuscator } from '../../interfaces/custom-code-helper
 import { IEscapeSequenceEncoder } from '../../interfaces/utils/IEscapeSequenceEncoder';
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
-import { IStringArrayCallsWrapperNames } from '../../interfaces/storages/string-array-storage/IStringArrayCallsWrapperNames';
 
 import { initializable } from '../../decorators/Initializable';
 
 import { SelfDefendingTemplate } from './templates/string-array-calls-wrapper/SelfDefendingTemplate';
 import { StringArrayCallsWrapperTemplate } from './templates/string-array-calls-wrapper/StringArrayCallsWrapperTemplate';
-import { StringArrayCallsWrapperIntermediateTemplate } from './templates/string-array-calls-wrapper/StringArrayCallsWrapperIntermediateTemplate';
 
 import { AbstractCustomCodeHelper } from '../AbstractCustomCodeHelper';
 import { NodeUtils } from '../../node/NodeUtils';
@@ -29,10 +27,10 @@ export class StringArrayCallsWrapperCodeHelper extends AbstractCustomCodeHelper
     protected stringArrayName!: string;
 
     /**
-     * @type {IStringArrayCallsWrapperNames}
+     * @type {string}
      */
     @initializable()
-    protected stringArrayCallsWrapperNames!: IStringArrayCallsWrapperNames;
+    protected stringArrayCallsWrapperName!: string;
 
     /**
      * @type {IEscapeSequenceEncoder}
@@ -69,14 +67,14 @@ export class StringArrayCallsWrapperCodeHelper extends AbstractCustomCodeHelper
 
     /**
      * @param {string} stringArrayName
-     * @param {IStringArrayCallsWrapperNames} stringArrayCallsWrapperNames
+     * @param {string} stringArrayCallsWrapperName
      */
     public initialize (
         stringArrayName: string,
-        stringArrayCallsWrapperNames: IStringArrayCallsWrapperNames
+        stringArrayCallsWrapperName: string
     ): void {
         this.stringArrayName = stringArrayName;
-        this.stringArrayCallsWrapperNames = stringArrayCallsWrapperNames;
+        this.stringArrayCallsWrapperName = stringArrayCallsWrapperName;
     }
 
     /**
@@ -92,15 +90,13 @@ export class StringArrayCallsWrapperCodeHelper extends AbstractCustomCodeHelper
      */
     protected getCodeHelperTemplate (): string {
         const decodeCodeHelperTemplate: string = this.getDecodeStringArrayTemplate();
-        const intermediateTemplate: string = this.getIntermediateTemplate();
 
         const preservedNames: string[] = [`^${this.stringArrayName}$`];
 
         return this.customCodeHelperObfuscator.obfuscateTemplate(
             this.customCodeHelperFormatter.formatTemplate(StringArrayCallsWrapperTemplate(), {
                 decodeCodeHelperTemplate,
-                intermediateTemplate,
-                stringArrayCallsWrapperName: this.stringArrayCallsWrapperNames.name,
+                stringArrayCallsWrapperName: this.stringArrayCallsWrapperName,
                 stringArrayName: this.stringArrayName
             }),
             {
@@ -130,34 +126,9 @@ export class StringArrayCallsWrapperCodeHelper extends AbstractCustomCodeHelper
                 this.escapeSequenceEncoder
             ),
             {
-                stringArrayCallsWrapperName: this.stringArrayCallsWrapperNames.name,
+                stringArrayCallsWrapperName: this.stringArrayCallsWrapperName,
                 stringArrayName: this.stringArrayName
             }
         );
     }
-
-    /**
-     * @returns {string}
-     */
-    private getIntermediateTemplate (): string {
-        const stringArrayCallsWrapperIntermediateNamesLength: number = this.stringArrayCallsWrapperNames
-            .intermediateNames
-            .length;
-
-        let intermediateTemplate: string = '';
-
-        for (let i = 0; i < stringArrayCallsWrapperIntermediateNamesLength; i++) {
-            const intermediateName: string = this.stringArrayCallsWrapperNames.intermediateNames[i];
-
-            intermediateTemplate += this.customCodeHelperFormatter.formatTemplate(
-                StringArrayCallsWrapperIntermediateTemplate(),
-                {
-                    intermediateName,
-                    stringArrayCallsWrapperName: this.stringArrayCallsWrapperNames.name
-                }
-            );
-        }
-
-        return intermediateTemplate;
-    }
 }

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

@@ -29,7 +29,7 @@ export class StringArrayCallsWrapperRc4CodeHelper extends StringArrayCallsWrappe
                 atobPolyfill,
                 rc4Polyfill,
                 selfDefendingCode,
-                stringArrayCallsWrapperName: this.stringArrayCallsWrapperNames.name
+                stringArrayCallsWrapperName: this.stringArrayCallsWrapperName
             }
         );
     }

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

@@ -8,7 +8,7 @@ import { ICustomCodeHelperFormatter } from '../../interfaces/custom-code-helpers
 import { ICustomCodeHelperObfuscator } from '../../interfaces/custom-code-helpers/ICustomCodeHelperObfuscator';
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
-import { IStringArrayStorage } from '../../interfaces/storages/string-array-storage/IStringArrayStorage';
+import { IStringArrayStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayStorage';
 
 import { initializable } from '../../decorators/Initializable';
 

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

@@ -11,8 +11,7 @@ import { ICallsGraphData } from '../../../interfaces/analyzers/calls-graph-analy
 import { ICustomCodeHelper } from '../../../interfaces/custom-code-helpers/ICustomCodeHelper';
 import { IOptions } from '../../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../../interfaces/utils/IRandomGenerator';
-import { IStringArrayCallsWrapperNames } from '../../../interfaces/storages/string-array-storage/IStringArrayCallsWrapperNames';
-import { IStringArrayStorage } from '../../../interfaces/storages/string-array-storage/IStringArrayStorage';
+import { IStringArrayStorage } from '../../../interfaces/storages/string-array-transformers/IStringArrayStorage';
 
 import { initializable } from '../../../decorators/Initializable';
 
@@ -139,12 +138,11 @@ export class StringArrayCodeHelperGroup extends AbstractCustomCodeHelperGroup {
             const stringArrayCallsWrapperCodeHelperName: CustomCodeHelper = this.getStringArrayCallsWrapperCodeHelperName(stringArrayEncoding);
             const stringArrayCallsWrapperCodeHelper: ICustomCodeHelper<TInitialData<StringArrayCallsWrapperCodeHelper>> =
                 this.customCodeHelperFactory(stringArrayCallsWrapperCodeHelperName);
-            const stringArrayCallsWrapperNames: IStringArrayCallsWrapperNames =
-                this.stringArrayStorage.getStorageCallsWrapperNames(stringArrayEncoding);
+            const stringArrayCallsWrapperName: string = this.stringArrayStorage.getStorageCallsWrapperName(stringArrayEncoding);
 
             stringArrayCallsWrapperCodeHelper.initialize(
                 stringArrayName,
-                stringArrayCallsWrapperNames
+                stringArrayCallsWrapperName
             );
 
             this.customCodeHelpers.set(stringArrayCallsWrapperCodeHelperName, stringArrayCallsWrapperCodeHelper);

+ 0 - 8
src/custom-code-helpers/string-array/templates/string-array-calls-wrapper/StringArrayCallsWrapperIntermediateTemplate.ts

@@ -1,8 +0,0 @@
-/**
- * @returns {string}
- */
-export function StringArrayCallsWrapperIntermediateTemplate (): string {
-    return `
-        const {intermediateName} = {stringArrayCallsWrapperName};
-    `;
-}

+ 0 - 2
src/custom-code-helpers/string-array/templates/string-array-calls-wrapper/StringArrayCallsWrapperTemplate.ts

@@ -12,7 +12,5 @@ export function StringArrayCallsWrapperTemplate (): string {
         
             return value;
         };
-        
-        {intermediateTemplate}
     `;
 }

+ 130 - 0
src/custom-nodes/string-array-nodes/StringArrayCallNode.ts

@@ -0,0 +1,130 @@
+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 { 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 class StringArrayCallNode extends AbstractCustomNode {
+    /**
+     * @type {string | null}
+     */
+    @initializable()
+    private decodeKey!: string | null;
+
+    /**
+     * @type {number}
+     */
+    @initializable()
+    private index!: number;
+
+    /**
+     * @type {string}
+     */
+    @initializable()
+    private stringArrayCallsWrapperName!: 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} 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
+     * @param {string | null} decodeKey
+     */
+    public initialize (
+        stringArrayCallsWrapperName: string,
+        index: number,
+        decodeKey: string | null
+    ): void {
+        this.stringArrayCallsWrapperName = stringArrayCallsWrapperName;
+        this.index = index;
+        this.decodeKey = decodeKey;
+    }
+
+    /**
+     * @returns {TStatement[]}
+     */
+    protected getNodeStructure (): TStatement[] {
+        const hexadecimalIndex: string = NumberUtils.toHex(this.index);
+        const callExpressionArgs: ESTree.Literal[] = [
+            StringArrayCallNode.getHexadecimalLiteralNode(hexadecimalIndex)
+        ];
+
+        if (this.decodeKey) {
+            callExpressionArgs.push(StringArrayCallNode.getRc4KeyLiteralNode(this.decodeKey));
+        }
+
+        const stringArrayIdentifierNode: ESTree.Identifier =
+            NodeFactory.identifierNode(this.stringArrayCallsWrapperName);
+
+
+        const structure: TStatement = NodeFactory.expressionStatementNode(
+            NodeFactory.callExpressionNode(
+                stringArrayIdentifierNode,
+                callExpressionArgs
+            )
+        );
+
+        NodeUtils.parentizeAst(structure);
+
+        return [structure];
+    }
+}

+ 83 - 0
src/custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperNode.ts

@@ -0,0 +1,83 @@
+import { inject, injectable, } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+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 { AbstractCustomNode } from '../AbstractCustomNode';
+import { NodeFactory } from '../../node/NodeFactory';
+import { NodeUtils } from '../../node/NodeUtils';
+
+@injectable()
+export class StringArrayScopeCallsWrapperNode extends AbstractCustomNode {
+    /**
+     * @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
+     */
+    public initialize (
+        stringArrayScopeCallsWrapperName: string,
+        stringArrayCallsWrapperName: string
+    ): void {
+        this.stringArrayScopeCallsWrapperName = stringArrayScopeCallsWrapperName;
+        this.stringArrayCallsWrapperName = stringArrayCallsWrapperName;
+    }
+
+    /**
+     * @returns {TStatement[]}
+     */
+    protected getNodeStructure (): TStatement[] {
+        const structure: TStatement = NodeFactory.variableDeclarationNode(
+            [
+                NodeFactory.variableDeclaratorNode(
+                    NodeFactory.identifierNode(this.stringArrayScopeCallsWrapperName),
+                    NodeFactory.identifierNode(this.stringArrayCallsWrapperName)
+                )
+            ],
+            'const',
+        );
+
+        NodeUtils.parentizeAst(structure);
+
+        return [structure];
+    }
+}

+ 4 - 0
src/enums/custom-nodes/StringArrayTransformerCustomNode.ts

@@ -0,0 +1,4 @@
+export enum StringArrayTransformerCustomNode {
+    StringArrayCallNode = 'StringArrayCallNode',
+    StringArrayScopeCallsWrapperNode = 'StringArrayScopeCallsWrapperNode'
+}

+ 1 - 1
src/interfaces/IEncodedValue.ts

@@ -1,7 +1,7 @@
 import { TStringArrayEncoding } from '../types/options/TStringArrayEncoding';
 
 export interface IEncodedValue {
-    encoding: TStringArrayEncoding | null;
+    encoding: TStringArrayEncoding;
     encodedValue: string;
     decodeKey: string | null;
 }

+ 1 - 1
src/interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer.ts

@@ -1,7 +1,7 @@
 import * as ESTree from 'estree';
 
 import { IAnalyzer } from '../IAnalyzer';
-import { IStringArrayStorageItemData } from '../../storages/string-array-storage/IStringArrayStorageItem';
+import { IStringArrayStorageItemData } from '../../storages/string-array-transformers/IStringArrayStorageItem';
 
 export interface IStringArrayStorageAnalyzer extends IAnalyzer<[ESTree.Program], void> {
     /**

+ 13 - 0
src/interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperData.ts

@@ -0,0 +1,13 @@
+import { TStringArrayEncoding } from '../../../types/options/TStringArrayEncoding';
+
+export interface IStringArrayScopeCallsWrapperData {
+    /**
+     * @type {TStringArrayEncoding}
+     */
+    encoding: TStringArrayEncoding;
+
+    /**
+     * @type {string[]}
+     */
+    names: string[];
+}

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

@@ -41,7 +41,8 @@ export interface IOptions {
     readonly splitStringsChunkLength: number;
     readonly stringArray: boolean;
     readonly stringArrayEncoding: TStringArrayEncoding[];
-    readonly stringArrayIntermediateVariablesCount: number;
+    readonly stringArrayWrappersChainedCalls: boolean;
+    readonly stringArrayWrappersCount: number;
     readonly stringArrayThreshold: number;
     readonly target: TypeFromEnum<typeof ObfuscationTarget>;
     readonly transformObjectKeys: boolean;

+ 0 - 11
src/interfaces/storages/string-array-storage/IStringArrayCallsWrapperNames.ts

@@ -1,11 +0,0 @@
-export interface IStringArrayCallsWrapperNames {
-    /**
-     * @type {string}
-     */
-    name: string;
-
-    /**
-     * @type {string[]}
-     */
-    intermediateNames: string[];
-}

+ 26 - 0
src/interfaces/storages/string-array-transformers/ILiteralNodesCacheStorage.ts

@@ -0,0 +1,26 @@
+import * as ESTree from 'estree';
+
+import { IMapStorage } from '../IMapStorage';
+import { IStringArrayStorageItemData } from './IStringArrayStorageItem';
+
+export interface ILiteralNodesCacheStorage extends IMapStorage <string, ESTree.Node> {
+    /**
+     * @param {string} literalValue
+     * @param {IStringArrayStorageItemData | undefined} stringArrayStorageItemData
+     * @returns {string}
+     */
+    buildKey (
+        literalValue: string,
+        stringArrayStorageItemData: IStringArrayStorageItemData | undefined,
+    ): string;
+
+    /**
+     * @param {string} key
+     * @param {IStringArrayStorageItemData | undefined} stringArrayStorageItemData
+     * @returns {boolean}
+     */
+    shouldUseCachedValue (
+        key: string,
+        stringArrayStorageItemData: IStringArrayStorageItemData | undefined
+    ): boolean;
+}

+ 2 - 3
src/interfaces/storages/string-array-storage/IStringArrayStorage.ts → src/interfaces/storages/string-array-transformers/IStringArrayStorage.ts

@@ -1,7 +1,6 @@
 import { TStringArrayEncoding } from '../../../types/options/TStringArrayEncoding';
 
 import { IMapStorage } from '../IMapStorage';
-import { IStringArrayCallsWrapperNames } from './IStringArrayCallsWrapperNames';
 import { IStringArrayStorageItemData } from './IStringArrayStorageItem';
 
 export interface IStringArrayStorage extends IMapStorage <string, IStringArrayStorageItemData> {
@@ -17,9 +16,9 @@ export interface IStringArrayStorage extends IMapStorage <string, IStringArraySt
 
     /**
      * @param {TStringArrayEncoding | null} stringArrayEncoding
-     * @returns {IStringArrayCallsWrapperNames}
+     * @returns {string}
      */
-    getStorageCallsWrapperNames (stringArrayEncoding: TStringArrayEncoding | null): IStringArrayCallsWrapperNames;
+    getStorageCallsWrapperName (stringArrayEncoding: TStringArrayEncoding | null): string;
 
     rotateStorage (): void;
 

+ 0 - 0
src/interfaces/storages/string-array-storage/IStringArrayStorageItem.ts → src/interfaces/storages/string-array-transformers/IStringArrayStorageItem.ts


+ 6 - 0
src/interfaces/utils/IArrayUtils.ts

@@ -11,6 +11,12 @@ export interface IArrayUtils {
      */
     findMostOccurringElement <T extends string | number> (array: T[]): T | null;
 
+    /**
+     * @param {T[]} array
+     * @returns {T | null}
+     */
+    getLastElement <T> (array: T[]): T | null;
+
     /**
      * @param array
      * @param times

+ 245 - 62
src/node-transformers/string-array-transformers/StringArrayTransformer.ts

@@ -3,37 +3,69 @@ import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
 
 import * as ESTree from 'estree';
 
+import { TInitialData } from '../../types/TInitialData';
+import { TNodeWithLexicalScope } from '../../types/node/TNodeWithLexicalScope';
+import { TStatement } from '../../types/node/TStatement';
+import { TStringArrayEncoding } from '../../types/options/TStringArrayEncoding';
+import { TStringArrayScopeCallsWrapperDataByEncoding } from '../../types/node-transformers/string-array-transformers/TStringArrayScopeCallsWrapperDataByEncoding';
+import { TStringArrayTransformerCustomNodeFactory } from '../../types/container/custom-nodes/TStringArrayTransformerCustomNodeFactory';
+
+import { IArrayUtils } from '../../interfaces/utils/IArrayUtils';
+import { ICustomNode } from '../../interfaces/custom-nodes/ICustomNode';
 import { IEscapeSequenceEncoder } from '../../interfaces/utils/IEscapeSequenceEncoder';
+import { IIdentifierNamesGenerator } from '../../interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator';
+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 { IStringArrayCallsWrapperNames } from '../../interfaces/storages/string-array-storage/IStringArrayCallsWrapperNames';
-import { IStringArrayStorage } from '../../interfaces/storages/string-array-storage/IStringArrayStorage';
+import { IStringArrayScopeCallsWrapperData } from '../../interfaces/node-transformers/string-array-transformers/IStringArrayScopeCallsWrapperData';
+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-storage/IStringArrayStorageItem';
+import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-transformers/IStringArrayStorageItem';
 import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
 
 import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage';
-import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+import { StringArrayTransformerCustomNode } from '../../enums/custom-nodes/StringArrayTransformerCustomNode';
 
 import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
+import { NodeAppender } from '../../node/NodeAppender';
 import { NodeFactory } from '../../node/NodeFactory';
 import { NodeGuards } from '../../node/NodeGuards';
 import { NodeLiteralUtils } from '../../node/NodeLiteralUtils';
 import { NodeMetadata } from '../../node/NodeMetadata';
 import { NodeUtils } from '../../node/NodeUtils';
-import { NumberUtils } from '../../utils/NumberUtils';
+import { StringArrayCallNode } from '../../custom-nodes/string-array-nodes/StringArrayCallNode';
+import { StringArrayScopeCallsWrapperNode } from '../../custom-nodes/string-array-nodes/StringArrayScopeCallsWrapperNode';
 
 @injectable()
 export class StringArrayTransformer extends AbstractNodeTransformer {
+    /**
+     * @type {IArrayUtils}
+     */
+    private readonly arrayUtils: IArrayUtils;
+
     /**
      * @type {IEscapeSequenceEncoder}
      */
     private readonly escapeSequenceEncoder: IEscapeSequenceEncoder;
 
     /**
-     * @type {Map<string, ESTree.Node>}
+     * @type {IIdentifierNamesGenerator}
      */
-    private readonly nodesCache: Map <string, ESTree.Node> = new Map();
+    private readonly identifierNamesGenerator: IIdentifierNamesGenerator;
+
+    /**
+     * @type {ILiteralNodesCacheStorage}
+     */
+    private readonly literalNodesCacheStorage: ILiteralNodesCacheStorage;
+
+    /**
+     * @type {Map<TNodeWithLexicalScope, TStringArrayScopeCallsWrapperDataByEncoding>}
+     */
+    private readonly stringArrayScopeCallsWrapperDataByEncodingMap: Map<
+        TNodeWithLexicalScope,
+        TStringArrayScopeCallsWrapperDataByEncoding
+    > = new Map();
 
     /**
      * @type {IStringArrayStorage}
@@ -45,49 +77,49 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
      */
     private readonly stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer;
 
+    /**
+     * @type {TStringArrayTransformerCustomNodeFactory}
+     */
+    private readonly stringArrayTransformerCustomNodeFactory: TStringArrayTransformerCustomNodeFactory;
+
+    /**
+     * @type {TNodeWithLexicalScope[]}
+     */
+    private readonly visitedLexicalScopeNodesStack: TNodeWithLexicalScope[] = [];
+
     /**
      * @param {IRandomGenerator} randomGenerator
      * @param {IOptions} options
+     * @param {IArrayUtils} arrayUtils
+     * @param {IEscapeSequenceEncoder} escapeSequenceEncoder
+     * @param {ILiteralNodesCacheStorage} literalNodesCacheStorage
      * @param {IStringArrayStorage} stringArrayStorage
      * @param {IStringArrayStorageAnalyzer} stringArrayStorageAnalyzer
-     * @param {IEscapeSequenceEncoder} escapeSequenceEncoder
+     * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
+     * @param {TStringArrayTransformerCustomNodeFactory} stringArrayTransformerCustomNodeFactory
      */
     public constructor (
         @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
         @inject(ServiceIdentifiers.IOptions) options: IOptions,
+        @inject(ServiceIdentifiers.IArrayUtils) arrayUtils: IArrayUtils,
+        @inject(ServiceIdentifiers.IEscapeSequenceEncoder) escapeSequenceEncoder: IEscapeSequenceEncoder,
+        @inject(ServiceIdentifiers.ILiteralNodesCacheStorage) literalNodesCacheStorage: ILiteralNodesCacheStorage,
         @inject(ServiceIdentifiers.IStringArrayStorage) stringArrayStorage: IStringArrayStorage,
         @inject(ServiceIdentifiers.IStringArrayStorageAnalyzer) stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer,
-        @inject(ServiceIdentifiers.IEscapeSequenceEncoder) escapeSequenceEncoder: IEscapeSequenceEncoder
+        @inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
+            identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
+        @inject(ServiceIdentifiers.Factory__IStringArrayTransformerCustomNode)
+            stringArrayTransformerCustomNodeFactory: TStringArrayTransformerCustomNodeFactory
     ) {
         super(randomGenerator, options);
 
+        this.arrayUtils = arrayUtils;
+        this.escapeSequenceEncoder = escapeSequenceEncoder;
+        this.literalNodesCacheStorage = literalNodesCacheStorage;
         this.stringArrayStorage = stringArrayStorage;
         this.stringArrayStorageAnalyzer = stringArrayStorageAnalyzer;
-        this.escapeSequenceEncoder = escapeSequenceEncoder;
-    }
-
-    /**
-     * @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} literalValue
-     * @returns {Literal}
-     */
-    private static getRc4KeyLiteralNode (literalValue: string): ESTree.Literal {
-        const rc4KeyLiteralNode: ESTree.Literal = NodeFactory.literalNode(literalValue);
-
-        NodeMetadata.set(rc4KeyLiteralNode, { replacedLiteral: true });
-
-        return rc4KeyLiteralNode;
+        this.identifierNamesGenerator = identifierNamesGeneratorFactory(options);
+        this.stringArrayTransformerCustomNodeFactory = stringArrayTransformerCustomNodeFactory;
     }
 
     /**
@@ -103,9 +135,20 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
                             this.prepareNode(node);
                         }
 
+                        if (NodeGuards.isNodeWithLexicalScope(node)) {
+                            this.onLexicalScopeNodeEnter(node);
+                        }
+
                         if (parentNode && NodeGuards.isLiteralNode(node) && !NodeMetadata.isReplacedLiteral(node)) {
                             return this.transformNode(node, parentNode);
                         }
+                    },
+                    leave: (node: ESTree.Node): ESTree.Node | undefined => {
+                        if (NodeGuards.isNodeWithLexicalScope(node)) {
+                            this.onLexicalScopeNodeLeave();
+
+                            return this.transformLexicalScopeNode(node);
+                        }
                     }
                 };
 
@@ -150,21 +193,20 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
 
         const literalValue: ESTree.SimpleLiteral['value'] = literalNode.value;
 
-        const stringArrayStorageItemData: IStringArrayStorageItemData | undefined = this.stringArrayStorageAnalyzer
-            .getItemDataForLiteralNode(literalNode);
-        const cacheKey: string = `${literalValue}-${Boolean(stringArrayStorageItemData)}`;
-        const useCachedValue: boolean = this.nodesCache.has(cacheKey)
-            && stringArrayStorageItemData?.encoding !== StringArrayEncoding.Rc4;
+        const stringArrayStorageItemData: IStringArrayStorageItemData | undefined =
+            this.stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode);
+        const cacheKey: string = this.literalNodesCacheStorage.buildKey(literalValue, stringArrayStorageItemData);
+        const useCachedValue: boolean = this.literalNodesCacheStorage.shouldUseCachedValue(cacheKey, stringArrayStorageItemData);
 
         if (useCachedValue) {
-            return <ESTree.Node>this.nodesCache.get(cacheKey);
+            return <ESTree.Node>this.literalNodesCacheStorage.get(cacheKey);
         }
 
         const resultNode: ESTree.Node = stringArrayStorageItemData
             ? this.getStringArrayCallNode(stringArrayStorageItemData)
             : this.getLiteralNode(literalValue);
 
-        this.nodesCache.set(cacheKey, resultNode);
+        this.literalNodesCacheStorage.set(cacheKey, resultNode);
 
         NodeUtils.parentizeNode(resultNode, parentNode);
 
@@ -184,33 +226,174 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
      * @returns {Node}
      */
     private getStringArrayCallNode (stringArrayStorageItemData: IStringArrayStorageItemData): ESTree.Node {
-        const { index, encoding, decodeKey } = stringArrayStorageItemData;
+        const stringArrayCallsWrapperName: string = this.getStringArrayCallsWrapperName(stringArrayStorageItemData);
+        const { index, decodeKey } = stringArrayStorageItemData;
+
+        const stringArrayCallCustomNode: ICustomNode<TInitialData<StringArrayCallNode>> =
+            this.stringArrayTransformerCustomNodeFactory(StringArrayTransformerCustomNode.StringArrayCallNode);
 
-        const hexadecimalIndex: string = NumberUtils.toHex(index);
-        const callExpressionArgs: (ESTree.Expression | ESTree.SpreadElement)[] = [
-            StringArrayTransformer.getHexadecimalLiteralNode(hexadecimalIndex)
-        ];
+        stringArrayCallCustomNode.initialize(stringArrayCallsWrapperName, index, decodeKey);
 
-        if (decodeKey) {
-            callExpressionArgs.push(StringArrayTransformer.getRc4KeyLiteralNode(decodeKey));
+        const statementNode: TStatement = stringArrayCallCustomNode.getNode()[0];
+
+        if (!NodeGuards.isExpressionStatementNode(statementNode)) {
+            throw new Error('`stringArrayCallCustomNode.getNode()[0]` should returns array with `ExpressionStatement` node');
         }
 
-        const stringArrayCallsWrapperNames: IStringArrayCallsWrapperNames =
-            this.stringArrayStorage.getStorageCallsWrapperNames(encoding);
-        const stringArrayCallsWrapperName: string = stringArrayCallsWrapperNames.intermediateNames.length
-            ? this.randomGenerator
-                .getRandomGenerator()
-                .pickone(stringArrayCallsWrapperNames.intermediateNames)
-            : stringArrayCallsWrapperNames.name;
+        return statementNode.expression;
+    }
 
-        const stringArrayIdentifierNode: ESTree.Identifier = NodeFactory.identifierNode(
-            stringArrayCallsWrapperName
-        );
+    /**
+     * @param {IStringArrayStorageItemData} stringArrayStorageItemData
+     * @returns {string}
+     */
+    private getStringArrayCallsWrapperName (stringArrayStorageItemData: IStringArrayStorageItemData): string {
+        const {encoding} = stringArrayStorageItemData;
 
-        return NodeFactory.callExpressionNode(
-            stringArrayIdentifierNode,
-            callExpressionArgs
-        );
+        const stringArrayCallsWrapperName: string = this.stringArrayStorage.getStorageCallsWrapperName(encoding);
+
+        if (!this.options.stringArrayWrappersCount) {
+            return stringArrayCallsWrapperName;
+        }
+
+        const currentLexicalScopeNode: TNodeWithLexicalScope | null = this.arrayUtils.getLastElement(this.visitedLexicalScopeNodesStack);
+
+        if (!currentLexicalScopeNode) {
+            throw new Error('Cannot find current lexical scope node');
+        }
+
+        const stringArrayScopeCallsWrapperDataByEncoding: TStringArrayScopeCallsWrapperDataByEncoding =
+            this.stringArrayScopeCallsWrapperDataByEncodingMap.get(currentLexicalScopeNode) ?? {};
+        const stringArrayScopeCallsWrapperNames: string[] = stringArrayScopeCallsWrapperDataByEncoding[encoding]?.names ?? [];
+        const isFilledScopeCallsWrapperNamesList: boolean = stringArrayScopeCallsWrapperNames.length === this.options.stringArrayWrappersCount;
+
+        if (!isFilledScopeCallsWrapperNamesList) {
+            const nextScopeCallsWrapperName: string = this.identifierNamesGenerator.generateForLexicalScope(currentLexicalScopeNode);
+
+            stringArrayScopeCallsWrapperNames.push(nextScopeCallsWrapperName);
+            stringArrayScopeCallsWrapperDataByEncoding[encoding] = {
+                encoding,
+                names: stringArrayScopeCallsWrapperNames
+            };
+
+            this.stringArrayScopeCallsWrapperDataByEncodingMap.set(
+                currentLexicalScopeNode,
+                stringArrayScopeCallsWrapperDataByEncoding
+            );
+        }
+
+        return this.randomGenerator.getRandomGenerator().pickone(stringArrayScopeCallsWrapperNames);
+    }
+
+    /**
+     * @param {TNodeWithLexicalScope} lexicalScopeNode
+     */
+    private onLexicalScopeNodeEnter (lexicalScopeNode: TNodeWithLexicalScope): void {
+        this.visitedLexicalScopeNodesStack.push(lexicalScopeNode);
+    }
+
+    private onLexicalScopeNodeLeave (): void {
+        this.visitedLexicalScopeNodesStack.pop();
+    }
+
+    /**
+     * @param {TNodeWithLexicalScope} lexicalScopeNode
+     * @returns {TNodeWithLexicalScope}
+     */
+    private transformLexicalScopeNode (lexicalScopeNode: TNodeWithLexicalScope): TNodeWithLexicalScope {
+        if (!this.options.stringArrayWrappersCount) {
+            return lexicalScopeNode;
+        }
+
+        const lexicalScopeBodyNode: ESTree.Program | ESTree.BlockStatement | ESTree.Expression =
+            NodeGuards.isProgramNode(lexicalScopeNode)
+                ? lexicalScopeNode
+                : lexicalScopeNode.body;
+
+        // invalid lexical scope node
+        if (
+            !lexicalScopeBodyNode.parentNode
+            || !NodeGuards.isNodeWithLexicalScopeStatements(lexicalScopeBodyNode, lexicalScopeBodyNode.parentNode)
+        ) {
+            return lexicalScopeNode;
+        }
+
+        const stringArrayScopeCallsWrapperDataByEncoding: TStringArrayScopeCallsWrapperDataByEncoding | null =
+            this.stringArrayScopeCallsWrapperDataByEncodingMap.get(lexicalScopeNode) ?? null;
+
+        if (!stringArrayScopeCallsWrapperDataByEncoding) {
+            return lexicalScopeNode;
+        }
+
+        const stringArrayScopeCallsWrapperDataList: (IStringArrayScopeCallsWrapperData | undefined)[] =
+            Object.values(stringArrayScopeCallsWrapperDataByEncoding);
+
+        // iterates over data for each encoding type
+        for (const stringArrayScopeCallsWrapperData of stringArrayScopeCallsWrapperDataList) {
+            if (!stringArrayScopeCallsWrapperData) {
+                continue;
+            }
+
+            const {encoding, names} = stringArrayScopeCallsWrapperData;
+
+            // iterates over each name of scope wrapper name
+            for (const stringArrayScopeCallsWrapperName of names) {
+                const upperStringArrayCallsWrapperName: string = this.getUpperStringArrayCallsWrapperName(encoding);
+
+                const stringArrayScopeCallsWrapperNode: ICustomNode<TInitialData<StringArrayScopeCallsWrapperNode>> =
+                    this.stringArrayTransformerCustomNodeFactory(
+                        StringArrayTransformerCustomNode.StringArrayScopeCallsWrapperNode
+                    );
+
+                stringArrayScopeCallsWrapperNode.initialize(
+                    stringArrayScopeCallsWrapperName,
+                    upperStringArrayCallsWrapperName
+                );
+
+                NodeAppender.prepend(
+                    lexicalScopeBodyNode,
+                    stringArrayScopeCallsWrapperNode.getNode()
+                );
+            }
+        }
+
+        return lexicalScopeNode;
+    }
+
+    /**
+     * @param {TStringArrayEncoding} encoding
+     * @returns {string}
+     */
+    private getRootStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
+        return this.stringArrayStorage.getStorageCallsWrapperName(encoding);
+    }
+
+    /**
+     * @param {TStringArrayEncoding} encoding
+     * @returns {string}
+     */
+    private getUpperStringArrayCallsWrapperName (encoding: TStringArrayEncoding): string {
+        const rootStringArrayCallsWrapperName: string = this.getRootStringArrayCallsWrapperName(encoding);
+
+        if (!this.options.stringArrayWrappersChainedCalls) {
+            return rootStringArrayCallsWrapperName;
+        }
+
+        const parentLexicalScope: TNodeWithLexicalScope | null = this.arrayUtils.getLastElement(this.visitedLexicalScopeNodesStack);
+
+        if (!parentLexicalScope) {
+            return rootStringArrayCallsWrapperName;
+        }
+
+        const parentLexicalScopeDataByEncoding = this.stringArrayScopeCallsWrapperDataByEncodingMap
+            .get(parentLexicalScope) ?? null;
+        const parentLexicalScopeNames: string[] | null = parentLexicalScopeDataByEncoding?.[encoding]?.names ?? null;
+
+        return parentLexicalScopeNames?.length
+            ? this.randomGenerator
+                .getRandomGenerator()
+                .pickone(parentLexicalScopeNames)
+            : rootStringArrayCallsWrapperName;
     }
 
     /**

+ 7 - 1
src/options/Options.ts

@@ -297,12 +297,18 @@ export class Options implements IOptions {
     @IsIn([StringArrayEncoding.None, StringArrayEncoding.Base64, StringArrayEncoding.Rc4], { each: true })
     public readonly stringArrayEncoding!: TStringArrayEncoding[];
 
+    /**
+     * @type {boolean}
+     */
+    @IsBoolean()
+    public readonly stringArrayWrappersChainedCalls!: boolean;
+
     /**
      * @type {boolean}
      */
     @IsNumber()
     @Min(0)
-    public readonly stringArrayIntermediateVariablesCount!: number;
+    public readonly stringArrayWrappersCount!: number;
 
     /**
      * @type {number}

+ 2 - 0
src/options/OptionsNormalizer.ts

@@ -18,6 +18,7 @@ import { SplitStringsChunkLengthRule } from './normalizer-rules/SplitStringsChun
 import { StringArrayRule } from './normalizer-rules/StringArrayRule';
 import { StringArrayEncodingRule } from './normalizer-rules/StringArrayEncodingRule';
 import { StringArrayThresholdRule } from './normalizer-rules/StringArrayThresholdRule';
+import { StringArrayWrappersChainedCallsRule } from './normalizer-rules/StringArrayWappersChainedCalls';
 
 @injectable()
 export class OptionsNormalizer implements IOptionsNormalizer {
@@ -38,6 +39,7 @@ export class OptionsNormalizer implements IOptionsNormalizer {
         StringArrayRule,
         StringArrayEncodingRule,
         StringArrayThresholdRule,
+        StringArrayWrappersChainedCallsRule,
     ];
 
     /**

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

@@ -18,7 +18,8 @@ export const StringArrayRule: TOptionsNormalizerRule = (options: IOptions): IOpt
             stringArrayEncoding: [
                 StringArrayEncoding.None
             ],
-            stringArrayIntermediateVariablesCount: 0,
+            stringArrayWrappersChainedCalls: false,
+            stringArrayWrappersCount: 0,
             stringArrayThreshold: 0
         };
     }

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

@@ -17,7 +17,8 @@ export const StringArrayThresholdRule: TOptionsNormalizerRule = (options: IOptio
             stringArrayEncoding: [
                 StringArrayEncoding.None
             ],
-            stringArrayIntermediateVariablesCount: 0,
+            stringArrayWrappersChainedCalls: false,
+            stringArrayWrappersCount: 0,
             stringArrayThreshold: 0
         };
     }

+ 19 - 0
src/options/normalizer-rules/StringArrayWappersChainedCalls.ts

@@ -0,0 +1,19 @@
+import { TOptionsNormalizerRule } from '../../types/options/TOptionsNormalizerRule';
+
+import { IOptions } from '../../interfaces/options/IOptions';
+
+/**
+ * @param {IOptions} options
+ * @returns {IOptions}
+ */
+export const StringArrayWrappersChainedCallsRule: TOptionsNormalizerRule = (options: IOptions): IOptions => {
+    if (options.stringArrayWrappersCount === 0) {
+        options = {
+            ...options,
+            stringArrayWrappersChainedCalls: false,
+            stringArrayWrappersCount: 0
+        };
+    }
+
+    return options;
+};

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

@@ -44,7 +44,8 @@ export const DEFAULT_PRESET: TInputOptions = Object.freeze({
     stringArrayEncoding: [
         StringArrayEncoding.None
     ],
-    stringArrayIntermediateVariablesCount: 0,
+    stringArrayWrappersChainedCalls: true,
+    stringArrayWrappersCount: 1,
     stringArrayThreshold: 0.75,
     target: ObfuscationTarget.Browser,
     transformObjectKeys: false,

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

@@ -16,6 +16,6 @@ export const HIGH_OBFUSCATION_PRESET: TInputOptions = Object.freeze({
     stringArrayEncoding: [
         StringArrayEncoding.Rc4
     ],
-    stringArrayIntermediateVariablesCount: 10,
+    stringArrayWrappersCount: 5,
     stringArrayThreshold: 1
 });

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

@@ -16,6 +16,6 @@ export const MEDIUM_OBFUSCATION_PRESET: TInputOptions = Object.freeze({
     stringArrayEncoding: [
         StringArrayEncoding.Base64
     ],
-    stringArrayIntermediateVariablesCount: 5,
+    stringArrayWrappersCount: 2,
     transformObjectKeys: true
 });

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

@@ -41,7 +41,8 @@ export const NO_ADDITIONAL_NODES_PRESET: TInputOptions = Object.freeze({
     stringArrayEncoding: [
         StringArrayEncoding.None
     ],
-    stringArrayIntermediateVariablesCount: 0,
+    stringArrayWrappersChainedCalls: false,
+    stringArrayWrappersCount: 0,
     stringArrayThreshold: 0,
     target: ObfuscationTarget.Browser,
     transformObjectKeys: false,

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

@@ -0,0 +1,55 @@
+import { inject, injectable } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import * as ESTree from 'estree';
+
+import { ILiteralNodesCacheStorage } from '../../interfaces/storages/string-array-transformers/ILiteralNodesCacheStorage';
+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 { MapStorage } from '../MapStorage';
+
+@injectable()
+export class LiteralNodesCacheStorage extends MapStorage <string, ESTree.Node> implements ILiteralNodesCacheStorage {
+    /**
+     * @param {IRandomGenerator} randomGenerator
+     * @param {IOptions} options
+     */
+    public constructor (
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
+        @inject(ServiceIdentifiers.IOptions) options: IOptions
+    ) {
+        super(randomGenerator, options);
+    }
+
+    /**
+     * @param {string} literalValue
+     * @param {IStringArrayStorageItemData | undefined} stringArrayStorageItemData
+     * @returns {string}
+     */
+    public buildKey (
+        literalValue: string,
+        stringArrayStorageItemData: IStringArrayStorageItemData | undefined,
+    ): string {
+        return `${literalValue}-${Boolean(stringArrayStorageItemData)}`;
+    }
+
+    /**
+     * @param {string} key
+     * @param {IStringArrayStorageItemData | undefined} stringArrayStorageItemData
+     * @returns {boolean}
+     */
+    public shouldUseCachedValue (
+        key: string,
+        stringArrayStorageItemData: IStringArrayStorageItemData | undefined
+    ): boolean {
+        // for each function scope different nodes will be created, so cache have no sense
+        return !this.options.stringArrayWrappersCount
+            // different nodes will be created with different rc4 keys, so cache have no sense
+            && stringArrayStorageItemData?.encoding !== StringArrayEncoding.Rc4
+            && this.storage.has(key);
+    }
+}

+ 14 - 23
src/storages/string-array/StringArrayStorage.ts → src/storages/string-array-transformers/StringArrayStorage.ts

@@ -11,9 +11,8 @@ import { IEscapeSequenceEncoder } from '../../interfaces/utils/IEscapeSequenceEn
 import { IIdentifierNamesGenerator } from '../../interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator';
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
-import { IStringArrayCallsWrapperNames } from '../../interfaces/storages/string-array-storage/IStringArrayCallsWrapperNames';
-import { IStringArrayStorage } from '../../interfaces/storages/string-array-storage/IStringArrayStorage';
-import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-storage/IStringArrayStorageItem';
+import { IStringArrayStorage } from '../../interfaces/storages/string-array-transformers/IStringArrayStorage';
+import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-transformers/IStringArrayStorageItem';
 
 import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
 
@@ -87,9 +86,9 @@ export class StringArrayStorage extends MapStorage <string, IStringArrayStorageI
     private stringArrayStorageName!: string;
 
     /**
-     * @type {Map<TStringArrayEncoding | null, IStringArrayCallsWrapperNames>}
+     * @type {Map<TStringArrayEncoding | null, string>}
      */
-    private readonly stringArrayStorageCallsWrapperNamesMap: Map<TStringArrayEncoding | null, IStringArrayCallsWrapperNames> = new Map();
+    private readonly stringArrayStorageCallsWrapperNamesMap: Map<TStringArrayEncoding | null, string> = new Map();
 
     /**
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
@@ -173,22 +172,23 @@ export class StringArrayStorage extends MapStorage <string, IStringArrayStorageI
      * @param {TStringArrayEncoding | null} stringArrayEncoding
      * @returns {IStringArrayCallsWrapperNames}
      */
-    public getStorageCallsWrapperNames (stringArrayEncoding: TStringArrayEncoding | null): IStringArrayCallsWrapperNames {
-        const storageCallsWrapperName: IStringArrayCallsWrapperNames | null = this.stringArrayStorageCallsWrapperNamesMap
+    public getStorageCallsWrapperName (stringArrayEncoding: TStringArrayEncoding | null): string {
+        const storageCallsWrapperName: string | null = this.stringArrayStorageCallsWrapperNamesMap
             .get(stringArrayEncoding) ?? null;
 
         if (storageCallsWrapperName) {
             return storageCallsWrapperName;
         }
 
-        const newStorageCallsWrapperNames: IStringArrayCallsWrapperNames = this.getStringArrayCallsWrapperNames();
+        const newStorageCallsWrapperName: string = this.identifierNamesGenerator
+            .generateForGlobalScope(StringArrayStorage.stringArrayNameLength);
 
         this.stringArrayStorageCallsWrapperNamesMap.set(
             stringArrayEncoding,
-            newStorageCallsWrapperNames
+            newStorageCallsWrapperName
         );
 
-        return newStorageCallsWrapperNames;
+        return newStorageCallsWrapperName;
     }
 
     public rotateStorage (): void {
@@ -276,6 +276,10 @@ export class StringArrayStorage extends MapStorage <string, IStringArrayStorageI
                 .pickone(this.options.stringArrayEncoding)
             : null;
 
+        if (!encoding) {
+            throw new Error('`stringArrayEncoding` option array is empty');
+        }
+
         switch (encoding) {
             /**
              * For rc4 there is a possible chance of a collision between encoded values that were received from
@@ -329,17 +333,4 @@ export class StringArrayStorage extends MapStorage <string, IStringArrayStorageI
             }
         }
     }
-
-    /**
-     * @returns {IStringArrayCallsWrapperNames}
-     */
-    private getStringArrayCallsWrapperNames (): IStringArrayCallsWrapperNames {
-        return {
-            name: this.identifierNamesGenerator.generateForGlobalScope(StringArrayStorage.stringArrayNameLength),
-            intermediateNames: Array.from(
-                {length: this.options.stringArrayIntermediateVariablesCount},
-                () => this.identifierNamesGenerator.generateForGlobalScope(StringArrayStorage.stringArrayNameLength)
-            )
-        };
-    }
 }

+ 7 - 0
src/types/container/custom-nodes/TStringArrayTransformerCustomNodeFactory.ts

@@ -0,0 +1,7 @@
+import { ICustomNode } from '../../../interfaces/custom-nodes/ICustomNode';
+
+import { StringArrayTransformerCustomNode } from '../../../enums/custom-nodes/StringArrayTransformerCustomNode';
+
+export type TStringArrayTransformerCustomNodeFactory = <
+    TInitialData extends unknown[] = unknown[]
+> (stringArrayTransformerCustomNodeName: StringArrayTransformerCustomNode) => ICustomNode <TInitialData>;

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

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

+ 10 - 0
src/utils/ArrayUtils.ts

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

+ 11 - 7
test/dev/dev.ts

@@ -1,7 +1,6 @@
 'use strict';
 
 import { NO_ADDITIONAL_NODES_PRESET } from '../../src/options/presets/NoCustomNodes';
-import { StringArrayEncoding } from '../../src/enums/StringArrayEncoding';
 
 (function () {
     const JavaScriptObfuscator: any = require('../../index');
@@ -10,19 +9,24 @@ import { StringArrayEncoding } from '../../src/enums/StringArrayEncoding';
         `
             const foo = 'foo';
             const bar = 'bar';
-            const baz = 'baz';
+                    
+            function test () {
+                const baz = 'baz';
+                const bark = 'bark';
             
-            console.log(foo, bar, baz);
+                function test1() {
+                    const hawk = 'hawk';
+                    const eagle = 'eagle';
+                } 
+            }
         `,
         {
             ...NO_ADDITIONAL_NODES_PRESET,
             compact: false,
             stringArray: true,
             stringArrayThreshold: 1,
-            stringArrayIntermediateVariablesCount: 2,
-            stringArrayEncoding: [
-                StringArrayEncoding.Rc4
-            ]
+            stringArrayWrappersChainedCalls: true,
+            stringArrayWrappersCount: 1
         }
     ).getObfuscatedCode();
 

+ 55 - 55
test/fixtures/compile-performance.js

@@ -1797,7 +1797,7 @@
             function BuiltinType(name, modifiers) {
                 if (modifiers === void 0) { modifiers = null; }
                 _super.call(this, modifiers);
-                this.name = name;
+                this.names = name;
             }
             BuiltinType.prototype.visitType = function (visitor, context) {
                 return visitor.visitBuiltintType(this, context);
@@ -1952,11 +1952,11 @@
                 if (type === void 0) { type = null; }
                 _super.call(this, type);
                 if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__facade_lang__["g" /* isString */])(name)) {
-                    this.name = name;
+                    this.names = name;
                     this.builtin = null;
                 }
                 else {
-                    this.name = null;
+                    this.names = null;
                     this.builtin = name;
                 }
             }
@@ -1971,7 +1971,7 @@
             function WriteVarExpr(name, value, type) {
                 if (type === void 0) { type = null; }
                 _super.call(this, __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__facade_lang__["a" /* isPresent */])(type) ? type : value.type);
-                this.name = name;
+                this.names = name;
                 this.value = value;
             }
             WriteVarExpr.prototype.visitExpression = function (visitor, context) {
@@ -2004,7 +2004,7 @@
                 if (type === void 0) { type = null; }
                 _super.call(this, __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__facade_lang__["a" /* isPresent */])(type) ? type : value.type);
                 this.receiver = receiver;
-                this.name = name;
+                this.names = name;
                 this.value = value;
             }
             WritePropExpr.prototype.visitExpression = function (visitor, context) {
@@ -2026,11 +2026,11 @@
                 this.receiver = receiver;
                 this.args = args;
                 if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__facade_lang__["g" /* isString */])(method)) {
-                    this.name = method;
+                    this.names = method;
                     this.builtin = null;
                 }
                 else {
-                    this.name = null;
+                    this.names = null;
                     this.builtin = method;
                 }
             }
@@ -2130,7 +2130,7 @@
         var FnParam = (function () {
             function FnParam(name, type) {
                 if (type === void 0) { type = null; }
-                this.name = name;
+                this.names = name;
                 this.type = type;
             }
             return FnParam;
@@ -2172,7 +2172,7 @@
                 if (type === void 0) { type = null; }
                 _super.call(this, type);
                 this.receiver = receiver;
-                this.name = name;
+                this.names = name;
             }
             ReadPropExpr.prototype.visitExpression = function (visitor, context) {
                 return visitor.visitReadPropExpr(this, context);
@@ -2254,7 +2254,7 @@
                 if (type === void 0) { type = null; }
                 if (modifiers === void 0) { modifiers = null; }
                 _super.call(this, modifiers);
-                this.name = name;
+                this.names = name;
                 this.value = value;
                 this.type = __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__facade_lang__["a" /* isPresent */])(type) ? type : value.type;
             }
@@ -2269,7 +2269,7 @@
                 if (type === void 0) { type = null; }
                 if (modifiers === void 0) { modifiers = null; }
                 _super.call(this, modifiers);
-                this.name = name;
+                this.names = name;
                 this.params = params;
                 this.statements = statements;
                 this.type = type;
@@ -2319,7 +2319,7 @@
                 if (type === void 0) { type = null; }
                 if (modifiers === void 0) { modifiers = null; }
                 _super.call(this, type, modifiers);
-                this.name = name;
+                this.names = name;
             }
             return ClassField;
         }(AbstractClassPart));
@@ -2329,7 +2329,7 @@
                 if (type === void 0) { type = null; }
                 if (modifiers === void 0) { modifiers = null; }
                 _super.call(this, type, modifiers);
-                this.name = name;
+                this.names = name;
                 this.params = params;
                 this.body = body;
             }
@@ -2341,7 +2341,7 @@
                 if (type === void 0) { type = null; }
                 if (modifiers === void 0) { modifiers = null; }
                 _super.call(this, type, modifiers);
-                this.name = name;
+                this.names = name;
                 this.body = body;
             }
             return ClassGetter;
@@ -2351,7 +2351,7 @@
             function ClassStmt(name, parent, fields, getters, constructorMethod, methods, modifiers) {
                 if (modifiers === void 0) { modifiers = null; }
                 _super.call(this, modifiers);
-                this.name = name;
+                this.names = name;
                 this.parent = parent;
                 this.fields = fields;
                 this.getters = getters;
@@ -4378,7 +4378,7 @@
             function CompileAnimationEntryMetadata(name, definitions) {
                 if (name === void 0) { name = null; }
                 if (definitions === void 0) { definitions = null; }
-                this.name = name;
+                this.names = name;
                 this.definitions = definitions;
             }
             return CompileAnimationEntryMetadata;
@@ -4470,7 +4470,7 @@
             function CompileIdentifierMetadata(_a) {
                 var _b = _a === void 0 ? {} : _a, reference = _b.reference, name = _b.name, moduleUrl = _b.moduleUrl, prefix = _b.prefix, value = _b.value;
                 this.reference = reference;
-                this.name = name;
+                this.names = name;
                 this.prefix = prefix;
                 this.moduleUrl = moduleUrl;
                 this.value = value;
@@ -4726,7 +4726,7 @@
             function CompilePipeMetadata(_a) {
                 var _b = _a === void 0 ? {} : _a, type = _b.type, name = _b.name, pure = _b.pure;
                 this.type = type;
-                this.name = name;
+                this.names = name;
                 this.pure = __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_2__facade_lang__["j" /* normalizeBool */])(pure);
             }
             Object.defineProperty(CompilePipeMetadata.prototype, "identifier", {
@@ -6666,7 +6666,7 @@
          */
         var AttrAst = (function () {
             function AttrAst(name, value, sourceSpan) {
-                this.name = name;
+                this.names = name;
                 this.value = value;
                 this.sourceSpan = sourceSpan;
             }
@@ -6678,7 +6678,7 @@
          */
         var BoundElementPropertyAst = (function () {
             function BoundElementPropertyAst(name, type, securityContext, value, unit, sourceSpan) {
-                this.name = name;
+                this.names = name;
                 this.type = type;
                 this.securityContext = securityContext;
                 this.value = value;
@@ -6695,7 +6695,7 @@
          */
         var BoundEventAst = (function () {
             function BoundEventAst(name, target, handler, sourceSpan) {
-                this.name = name;
+                this.names = name;
                 this.target = target;
                 this.handler = handler;
                 this.sourceSpan = sourceSpan;
@@ -6722,7 +6722,7 @@
          */
         var ReferenceAst = (function () {
             function ReferenceAst(name, value, sourceSpan) {
-                this.name = name;
+                this.names = name;
                 this.value = value;
                 this.sourceSpan = sourceSpan;
             }
@@ -6736,7 +6736,7 @@
          */
         var VariableAst = (function () {
             function VariableAst(name, value, sourceSpan) {
-                this.name = name;
+                this.names = name;
                 this.value = value;
                 this.sourceSpan = sourceSpan;
             }
@@ -6750,7 +6750,7 @@
          */
         var ElementAst = (function () {
             function ElementAst(name, attrs, inputs, outputs, references, directives, providers, hasViewContainer, children, ngContentIndex, sourceSpan) {
-                this.name = name;
+                this.names = name;
                 this.attrs = attrs;
                 this.inputs = inputs;
                 this.outputs = outputs;
@@ -7949,7 +7949,7 @@
         }());
         var Attribute = (function () {
             function Attribute(name, value, sourceSpan) {
-                this.name = name;
+                this.names = name;
                 this.value = value;
                 this.sourceSpan = sourceSpan;
             }
@@ -7958,7 +7958,7 @@
         }());
         var Element = (function () {
             function Element(name, attrs, children, sourceSpan, startSourceSpan, endSourceSpan) {
-                this.name = name;
+                this.names = name;
                 this.attrs = attrs;
                 this.children = children;
                 this.sourceSpan = sourceSpan;
@@ -12945,7 +12945,7 @@
             __extends(EmptyError, _super);
             function EmptyError() {
                 var err = _super.call(this, 'no elements in sequence');
-                this.name = err.name = 'EmptyError';
+                this.names = err.name = 'EmptyError';
                 this.stack = err.stack;
                 this.message = err.message;
             }
@@ -15615,7 +15615,7 @@
         }());
         var BoundElementOrDirectiveProperty = (function () {
             function BoundElementOrDirectiveProperty(name, expression, isLiteral, sourceSpan) {
-                this.name = name;
+                this.names = name;
                 this.expression = expression;
                 this.isLiteral = isLiteral;
                 this.sourceSpan = sourceSpan;
@@ -15624,7 +15624,7 @@
         }());
         var ElementOrDirectiveRef = (function () {
             function ElementOrDirectiveRef(name, value, sourceSpan) {
-                this.name = name;
+                this.names = name;
                 this.value = value;
                 this.sourceSpan = sourceSpan;
             }
@@ -18826,7 +18826,7 @@
             __extends(ArgumentOutOfRangeError, _super);
             function ArgumentOutOfRangeError() {
                 var err = _super.call(this, 'argument out of range');
-                this.name = err.name = 'ArgumentOutOfRangeError';
+                this.names = err.name = 'ArgumentOutOfRangeError';
                 this.stack = err.stack;
                 this.message = err.message;
             }
@@ -20017,7 +20017,7 @@
             function PropertyRead(span, receiver, name) {
                 _super.call(this, span);
                 this.receiver = receiver;
-                this.name = name;
+                this.names = name;
             }
             PropertyRead.prototype.visit = function (visitor, context) {
                 if (context === void 0) { context = null; }
@@ -20030,7 +20030,7 @@
             function PropertyWrite(span, receiver, name, value) {
                 _super.call(this, span);
                 this.receiver = receiver;
-                this.name = name;
+                this.names = name;
                 this.value = value;
             }
             PropertyWrite.prototype.visit = function (visitor, context) {
@@ -20044,7 +20044,7 @@
             function SafePropertyRead(span, receiver, name) {
                 _super.call(this, span);
                 this.receiver = receiver;
-                this.name = name;
+                this.names = name;
             }
             SafePropertyRead.prototype.visit = function (visitor, context) {
                 if (context === void 0) { context = null; }
@@ -20084,7 +20084,7 @@
             function BindingPipe(span, exp, name, args) {
                 _super.call(this, span);
                 this.exp = exp;
-                this.name = name;
+                this.names = name;
                 this.args = args;
             }
             BindingPipe.prototype.visit = function (visitor, context) {
@@ -20174,7 +20174,7 @@
             function MethodCall(span, receiver, name, args) {
                 _super.call(this, span);
                 this.receiver = receiver;
-                this.name = name;
+                this.names = name;
                 this.args = args;
             }
             MethodCall.prototype.visit = function (visitor, context) {
@@ -20188,7 +20188,7 @@
             function SafeMethodCall(span, receiver, name, args) {
                 _super.call(this, span);
                 this.receiver = receiver;
-                this.name = name;
+                this.names = name;
                 this.args = args;
             }
             SafeMethodCall.prototype.visit = function (visitor, context) {
@@ -20230,7 +20230,7 @@
             function TemplateBinding(key, keyIsVar, name, expression) {
                 this.key = key;
                 this.keyIsVar = keyIsVar;
-                this.name = name;
+                this.names = name;
                 this.expression = expression;
             }
             return TemplateBinding;
@@ -25905,7 +25905,7 @@
          */
         var NgProbeToken = (function () {
             function NgProbeToken(name, token) {
-                this.name = name;
+                this.names = name;
                 this.token = token;
             }
             return NgProbeToken;
@@ -28493,7 +28493,7 @@
             __extends(ObjectUnsubscribedError, _super);
             function ObjectUnsubscribedError() {
                 var err = _super.call(this, 'object unsubscribed');
-                this.name = err.name = 'ObjectUnsubscribedError';
+                this.names = err.name = 'ObjectUnsubscribedError';
                 this.stack = err.stack;
                 this.message = err.message;
             }
@@ -29530,7 +29530,7 @@
             __extends(AnimationEntryAst, _super);
             function AnimationEntryAst(name, stateDeclarations, stateTransitions) {
                 _super.call(this);
-                this.name = name;
+                this.names = name;
                 this.stateDeclarations = stateDeclarations;
                 this.stateTransitions = stateTransitions;
             }
@@ -29673,7 +29673,7 @@
         var animationCompilationCache = new Map();
         var CompiledAnimationTriggerResult = (function () {
             function CompiledAnimationTriggerResult(name, statesMapStatement, statesVariableName, fnStatement, fnVariable) {
-                this.name = name;
+                this.names = name;
                 this.statesMapStatement = statesMapStatement;
                 this.statesVariableName = statesVariableName;
                 this.fnStatement = fnStatement;
@@ -30637,7 +30637,7 @@
             function IcuPlaceholder(value, name, sourceSpan) {
                 if (name === void 0) { name = ''; }
                 this.value = value;
-                this.name = name;
+                this.names = name;
                 this.sourceSpan = sourceSpan;
             }
             IcuPlaceholder.prototype.visit = function (visitor, context) { return visitor.visitIcuPlaceholder(this, context); };
@@ -31239,7 +31239,7 @@
                 var _this = this;
                 if (unescapedAttrs === void 0) { unescapedAttrs = {}; }
                 if (children === void 0) { children = []; }
-                this.name = name;
+                this.names = name;
                 this.children = children;
                 this.attrs = {};
                 Object.keys(unescapedAttrs).forEach(function (k) {
@@ -32494,7 +32494,7 @@
                     compileResult.dependencies.forEach(function (dep) {
                         dep.placeholder.reference =
                             _this._assertComponentKnown(dep.comp.reference, true).proxyComponentFactory;
-                        dep.placeholder.name = "compFactory_" + dep.comp.name;
+                        dep.placeholder.names = "compFactory_" + dep.comp.name;
                     });
                     if (!this._compilerConfig.useJit) {
                         ngModuleFactory =
@@ -32624,13 +32624,13 @@
                         var vfd = dep;
                         depTemplate = _this._assertComponentLoaded(vfd.comp.reference, false);
                         vfd.placeholder.reference = depTemplate.proxyViewFactory;
-                        vfd.placeholder.name = "viewFactory_" + vfd.comp.name;
+                        vfd.placeholder.names = "viewFactory_" + vfd.comp.name;
                     }
                     else if (dep instanceof __WEBPACK_IMPORTED_MODULE_14__view_compiler_view_compiler__["b" /* ComponentFactoryDependency */]) {
                         var cfd = dep;
                         depTemplate = _this._assertComponentLoaded(cfd.comp.reference, true);
                         cfd.placeholder.reference = depTemplate.proxyComponentFactory;
-                        cfd.placeholder.name = "compFactory_" + cfd.comp.name;
+                        cfd.placeholder.names = "compFactory_" + cfd.comp.name;
                     }
                 });
                 var statements = stylesCompileResult.componentStylesheet.statements.concat(compileResult.statements);
@@ -32649,7 +32649,7 @@
                     var nestedCompileResult = externalStylesheetsByModuleUrl.get(dep.moduleUrl);
                     var nestedStylesArr = _this._resolveAndEvalStylesCompileResult(nestedCompileResult, externalStylesheetsByModuleUrl);
                     dep.valuePlaceholder.reference = nestedStylesArr;
-                    dep.valuePlaceholder.name = "importedStyles" + i;
+                    dep.valuePlaceholder.names = "importedStyles" + i;
                 });
             };
             RuntimeCompiler.prototype._resolveAndEvalStylesCompileResult = function (result, externalStylesheetsByModuleUrl) {
@@ -35232,7 +35232,7 @@
          */
         var AnimationEntryMetadata = (function () {
             function AnimationEntryMetadata(name, definitions) {
-                this.name = name;
+                this.names = name;
                 this.definitions = definitions;
             }
             return AnimationEntryMetadata;
@@ -36381,7 +36381,7 @@
 
         var EventListener = (function () {
             function EventListener(name, callback) {
-                this.name = name;
+                this.names = name;
                 this.callback = callback;
             }
             ;
@@ -41662,7 +41662,7 @@
                 this.parentOutletMap = parentOutletMap;
                 this.location = location;
                 this.resolver = resolver;
-                this.name = name;
+                this.names = name;
                 this.activateEvents = new __WEBPACK_IMPORTED_MODULE_0__angular_core__["EventEmitter"]();
                 this.deactivateEvents = new __WEBPACK_IMPORTED_MODULE_0__angular_core__["EventEmitter"]();
                 parentOutletMap.registerOutlet(name ? name : __WEBPACK_IMPORTED_MODULE_2__shared__["a" /* PRIMARY_OUTLET */], this);
@@ -45392,7 +45392,7 @@
                 this.errors = errors;
                 var err = Error.call(this, errors ?
                 errors.length + " errors occurred during unsubscription:\n  " + errors.map(function (err, i) { return ((i + 1) + ") " + err.toString()); }).join('\n  ') : '');
-                this.name = err.name = 'UnsubscriptionError';
+                this.names = err.name = 'UnsubscriptionError';
                 this.stack = err.stack;
                 this.message = err.message;
             }
@@ -51293,7 +51293,7 @@
                 }
                 var appCompileResult = this._ngModuleCompiler.compile(ngModule, providers);
                 appCompileResult.dependencies.forEach(function (dep) {
-                    dep.placeholder.name = _componentFactoryName(dep.comp);
+                    dep.placeholder.names = _componentFactoryName(dep.comp);
                     dep.placeholder.moduleUrl = _ngfactoryModuleUrl(dep.comp.moduleUrl);
                 });
                 targetStatements.push.apply(targetStatements, appCompileResult.statements);
@@ -51340,7 +51340,7 @@
                 }
                 else if (dep instanceof __WEBPACK_IMPORTED_MODULE_3__view_compiler_view_compiler__["b" /* ComponentFactoryDependency */]) {
                     var cfd = dep;
-                    cfd.placeholder.name = _componentFactoryName(cfd.comp);
+                    cfd.placeholder.names = _componentFactoryName(cfd.comp);
                     cfd.placeholder.moduleUrl = _ngfactoryModuleUrl(cfd.comp.moduleUrl);
                 }
             });
@@ -53455,7 +53455,7 @@
          */
         var AnimationOutput = (function () {
             function AnimationOutput(name, phase, fullPropertyName) {
-                this.name = name;
+                this.names = name;
                 this.phase = phase;
                 this.fullPropertyName = fullPropertyName;
             }
@@ -60428,7 +60428,7 @@
         var NOT_SUPPORTED = 'NOT_SUPPORTED';
         var UpgradeNg1ComponentAdapterBuilder = (function () {
             function UpgradeNg1ComponentAdapterBuilder(name) {
-                this.name = name;
+                this.names = name;
                 this.inputs = [];
                 this.inputsRename = [];
                 this.outputs = [];

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

@@ -53,37 +53,6 @@ describe('StringArrayCallsWrapperCodeHelper', () => {
         });
     });
 
-    describe('`stringArrayIntermediateVariablesCount` option is set', () => {
-        const stringArrayCallRegExp: RegExp = new RegExp(
-                'return _0x([a-f0-9]){4,6};' +
-            '};' +
-            'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-            'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-            'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-            'var test *= *_0x([a-f0-9]){4}\\(\'0x0\'\\);'
-        );
-
-        let obfuscatedCode: string;
-
-        before(() => {
-            const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
-
-            obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                code,
-                {
-                    ...NO_ADDITIONAL_NODES_PRESET,
-                    stringArray: true,
-                    stringArrayThreshold: 1,
-                    stringArrayIntermediateVariablesCount: 3
-                }
-            ).getObfuscatedCode();
-        });
-
-        it('should correctly append `StringArrayCallsWrapperIntermediateTemplate` template into the obfuscated code', () => {
-            assert.match(obfuscatedCode, stringArrayCallRegExp);
-        });
-    });
-
     describe('Preserve string array name', () => {
         const callsWrapperRegExp: RegExp = new RegExp(`` +
             `var b *= *function *\\(c, *d\\) *{ *` +

+ 36 - 2
test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts

@@ -1,8 +1,9 @@
 import { assert } from 'chai';
 import { TypeFromEnum } from '@gradecam/tsenum';
 
-import { TInputOptions } from '../../../src/types/options/TInputOptions';
 import { TDictionary } from '../../../src/types/TDictionary';
+import { TInputOptions } from '../../../src/types/options/TInputOptions';
+import { TOptionsPreset } from '../../../src/types/options/TOptionsPreset';
 
 import { IObfuscatedCode } from '../../../src/interfaces/source-code/IObfuscatedCode';
 
@@ -11,9 +12,11 @@ import { StringArrayEncoding } from '../../../src/enums/StringArrayEncoding';
 
 import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade';
 
+import { HIGH_OBFUSCATION_PRESET } from '../../../src/options/presets/HighObfuscation';
 import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes';
 
 import { IdentifierNamesGenerator } from '../../../src/enums/generators/identifier-names-generators/IdentifierNamesGenerator';
+import { OptionsPreset } from '../../../src/enums/options/presets/OptionsPreset';
 
 import { buildLargeCode } from '../../helpers/buildLargeCode';
 import { getRegExpMatch } from '../../helpers/getRegExpMatch';
@@ -848,7 +851,8 @@ describe('JavaScriptObfuscator', () => {
                             StringArrayEncoding.Base64,
                             StringArrayEncoding.Rc4
                         ],
-                        stringArrayIntermediateVariablesCount: 10,
+                        stringArrayWrappersChainedCalls: true,
+                        stringArrayWrappersCount: 10,
                         stringArrayThreshold: 1,
                         transformObjectKeys: true,
                         unicodeEscapeSequence: false
@@ -1095,4 +1099,34 @@ describe('JavaScriptObfuscator', () => {
             });
         });
     });
+
+    describe('getOptionsByPreset', () => {
+        describe('Variant #1: base behaviour', () => {
+            const optionsPresetName: TOptionsPreset = OptionsPreset.HighObfuscation;
+
+            let options: TInputOptions;
+
+            before(() => {
+                options = JavaScriptObfuscator.getOptionsByPreset(optionsPresetName);
+            });
+
+            it('Should return options for passed options preset name', () => {
+                assert.deepEqual(options, HIGH_OBFUSCATION_PRESET);
+            });
+        });
+
+        describe('Variant #2: unknown options preset name', () => {
+            const optionsPresetName: TOptionsPreset = 'foobar' as TOptionsPreset;
+
+            let testFunc: () => TInputOptions;
+
+            before(() => {
+                testFunc = () => JavaScriptObfuscator.getOptionsByPreset(optionsPresetName);
+            });
+
+            it('Should throws an error when unknown option preset is passed', () => {
+                assert.throws(testFunc, 'Options for preset name `foobar` are not found');
+            });
+        });
+    });
 });

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

@@ -12,7 +12,7 @@ import { swapLettersCase } from '../../../../helpers/swapLettersCase';
 import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade';
 
 describe('StringArrayTransformer', function () {
-    this.timeout(60000);
+    this.timeout(120000);
 
     describe('Variant #1: default behaviour', () => {
         const stringArrayRegExp: RegExp = /^var _0x([a-f0-9]){4} *= *\['test'\];/;
@@ -63,21 +63,156 @@ describe('StringArrayTransformer', function () {
         });
     });
 
-    describe('Variant #3: `stringArrayIntermediateVariablesCount` option is enabled', () => {
-        describe('Variant #1: correct amount of intermediate calls', () => {
+    describe('Variant #3: `stringArrayWrappersCount` option is enabled', () => {
+        describe('Variant #1: root scope', () => {
+            describe('Variant #1: option value value is lower then count `literal` nodes in the scope', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                        'return _0x([a-f0-9]){4,6};' +
+                    '};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
+                    'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
+                    'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 2
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+
+            describe('Variant #2: option value is bigger then count `literal` nodes in the scope', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                        'return _0x([a-f0-9]){4,6};' +
+                    '};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                    'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
+                    'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
+                    'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 5
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+        });
+
+        describe('Variant #2: function scope', () => {
+            describe('Variant #1: option value is lower then count `literal` nodes in the scope', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                    'function test *\\( *\\) *{' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
+                    '}'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 2
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+
+            describe('Variant #2: option value is bigger then count `literal` nodes in the scope', () => {
+                const stringArrayCallRegExp: RegExp = new RegExp(
+                    'function test *\\(\\) *{' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
+                    '}'
+                );
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayWrappersCount: 5
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should add scope calls wrappers', () => {
+                    assert.match(obfuscatedCode, stringArrayCallRegExp);
+                });
+            });
+        });
+
+        describe('Variant #3: prevailing kind of variables', () => {
             const stringArrayCallRegExp: RegExp = new RegExp(
                     'return _0x([a-f0-9]){4,6};' +
                 '};' +
-                'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-                'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-                'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-                'var test *= *_0x([a-f0-9]){4}\\(\'0x0\'\\);'
+                'var _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                'var _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                'var foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
+                'var bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
+                'var baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
             );
 
             let obfuscatedCode: string;
 
             before(() => {
-                const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
+                const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-var.js');
 
                 obfuscatedCode = JavaScriptObfuscator.obfuscate(
                     code,
@@ -85,22 +220,22 @@ describe('StringArrayTransformer', function () {
                         ...NO_ADDITIONAL_NODES_PRESET,
                         stringArray: true,
                         stringArrayThreshold: 1,
-                        stringArrayIntermediateVariablesCount: 3
+                        stringArrayWrappersCount: 2
                     }
                 ).getObfuscatedCode();
             });
 
-            it('should add intermediate calls to the string array calls wrapper', () => {
+            it('should add scope calls wrappers with a correct variables kind', () => {
                 assert.match(obfuscatedCode, stringArrayCallRegExp);
             });
         });
 
-        describe('Variant #2: correct evaluation of the intermediate calls', () => {
-            const expectedEvaluationResult: number = 15;
-            let evaluationResult: number;
+        describe('Variant #4: correct evaluation of the scope calls wrappers', () => {
+            const expectedEvaluationResult: string = 'aaabbbcccdddeee';
+            let evaluationResult: string;
 
             before(() => {
-                const code: string = readFileAsString(__dirname + '/fixtures/intermediate-variables-count-eval.js');
+                const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-eval.js');
 
                 const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
                     code,
@@ -108,17 +243,57 @@ describe('StringArrayTransformer', function () {
                         ...NO_ADDITIONAL_NODES_PRESET,
                         stringArray: true,
                         stringArrayThreshold: 1,
-                        stringArrayIntermediateVariablesCount: 5
+                        stringArrayWrappersCount: 5
                     }
                 ).getObfuscatedCode();
 
                 evaluationResult = eval(obfuscatedCode);
             });
 
-            it('should correctly evaluate intermediate calls', () => {
+            it('should correctly evaluate scope calls wrappers', () => {
                 assert.equal(evaluationResult, expectedEvaluationResult);
             });
         });
+
+        describe('Variant #5: `stringArrayWrappersChainedCalls` option is enabled', () => {
+            describe('Variant #1: correct evaluation of the string array wrappers chained calls', () => {
+                const samplesCount: number = 50;
+                const expectedEvaluationResult: string = 'aaabbbcccdddeee';
+                let isEvaluationSuccessful: boolean = true;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-chained-calls-eval.js');
+
+                    for (let i = 0; i < samplesCount; i++) {
+                        const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                            code,
+                            {
+                                ...NO_ADDITIONAL_NODES_PRESET,
+                                stringArray: true,
+                                stringArrayThreshold: 1,
+                                stringArrayEncoding: [
+                                    StringArrayEncoding.None,
+                                    StringArrayEncoding.Rc4
+                                ],
+                                stringArrayWrappersChainedCalls: true,
+                                stringArrayWrappersCount: 5
+                            }
+                        ).getObfuscatedCode();
+
+                        const evaluationResult: string = eval(obfuscatedCode);
+
+                        if (evaluationResult !== expectedEvaluationResult) {
+                            isEvaluationSuccessful = false;
+                            break;
+                        }
+                    }
+                });
+
+                it('should correctly evaluate string array wrappers chained calls', () => {
+                    assert.equal(isEvaluationSuccessful, true);
+                });
+            });
+        });
     });
 
     describe('Variant #4: string contains non-latin and non-digit characters and `unicodeEscapeSequence` is disabled', () => {
@@ -342,7 +517,7 @@ describe('StringArrayTransformer', function () {
 
     describe('Variant #11: none and base64 encoding', () => {
         describe('Variant #1: string array values', () => {
-            const samplesCount: number = 100;
+            const samplesCount: number = 300;
             const expectedMatchesChance: number = 0.5;
             const expectedMatchesDelta: number = 0.15;
 
@@ -395,48 +570,170 @@ describe('StringArrayTransformer', function () {
             });
         });
 
-        describe('Variant #2: `stringArrayIntermediateVariablesCount` option is enabled', () => {
-            const stringArrayIntermediateCallRegExp: RegExp = new RegExp(
-                    'return _0x([a-f0-9]){4,6};' +
-                '};' +
-                'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-                'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-                'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-                'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-                'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-                'var _0x([a-f0-9]){4} *= *_0x([a-f0-9]){4};' +
-                'var test *= *_0x([a-f0-9]){4}\\(\'0x0\'\\);'
-            );
-
-            let obfuscatedCode: string;
-
-            before(() => {
-                const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
+        describe('Variant #2: `stringArrayWrappersCount` option is enabled', () => {
+            describe('Variant #1: root scope', () => {
+                describe('Variant #1: `1` scope calls wrapper for each encoding type', () => {
+                    const stringArrayWrappersRegExp: RegExp = new RegExp(
+                            'return _0x([a-f0-9]){4,6};' +
+                        '};' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                        // this one may be added or not depends on:
+                        // if all literal values encoded with a single encoding or not
+                        '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
+                        'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
+                        'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
+                        'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
+                    );
+
+                    let obfuscatedCode: string;
+
+                    before(() => {
+                        const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
+
+                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                            code,
+                            {
+                                ...NO_ADDITIONAL_NODES_PRESET,
+                                stringArray: true,
+                                stringArrayEncoding: [
+                                    StringArrayEncoding.None,
+                                    StringArrayEncoding.Base64
+                                ],
+                                stringArrayWrappersCount: 1,
+                                stringArrayThreshold: 1
+                            }
+                        ).getObfuscatedCode();
+                    });
+
+                    it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
+                        assert.match(obfuscatedCode, stringArrayWrappersRegExp);
+                    });
+                });
 
-                obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                    code,
-                    {
-                        ...NO_ADDITIONAL_NODES_PRESET,
-                        stringArray: true,
-                        stringArrayEncoding: [
-                            StringArrayEncoding.None,
-                            StringArrayEncoding.Base64
-                        ],
-                        stringArrayIntermediateVariablesCount: 3,
-                        stringArrayThreshold: 1
-                    }
-                ).getObfuscatedCode();
+                describe('Variant #2: `2` scope calls wrappers for each encoding type', () => {
+                    const stringArrayWrappersRegExp: RegExp = new RegExp(
+                            'return _0x([a-f0-9]){4,6};' +
+                        '};' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                        'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                        // this one may be added or not depends on:
+                        // if all literal values encoded with a single encoding or not
+                        '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
+                        'const foo *= *_0x([a-f0-9]){4,6}\\(\'0x0\'\\);' +
+                        'const bar *= *_0x([a-f0-9]){4,6}\\(\'0x1\'\\);' +
+                        'const baz *= *_0x([a-f0-9]){4,6}\\(\'0x2\'\\);'
+                    );
+
+                    let obfuscatedCode: string;
+
+                    before(() => {
+                        const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
+
+                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                            code,
+                            {
+                                ...NO_ADDITIONAL_NODES_PRESET,
+                                stringArray: true,
+                                stringArrayEncoding: [
+                                    StringArrayEncoding.None,
+                                    StringArrayEncoding.Base64
+                                ],
+                                stringArrayWrappersCount: 2,
+                                stringArrayThreshold: 1
+                            }
+                        ).getObfuscatedCode();
+                    });
+
+                    it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
+                        assert.match(obfuscatedCode, stringArrayWrappersRegExp);
+                    });
+                });
             });
 
-            it('should add intermediate variables for both `none` and `base64` string array wrappers', () => {
-                assert.match(obfuscatedCode, stringArrayIntermediateCallRegExp);
+            describe('Variant #2: function scope', () => {
+                describe('Variant #1: `1` scope calls wrapper for each encoding type', () => {
+                    const stringArrayWrappersRegExp: RegExp = new RegExp(
+                        'function test *\\( *\\) *{' +
+                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                            // this one may be added or not depends on:
+                            // if all literal values encoded with a single encoding or not
+                            '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
+                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
+                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
+                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
+                        '}'
+                    );
+
+                    let obfuscatedCode: string;
+
+                    before(() => {
+                        const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
+
+                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                            code,
+                            {
+                                ...NO_ADDITIONAL_NODES_PRESET,
+                                stringArray: true,
+                                stringArrayEncoding: [
+                                    StringArrayEncoding.None,
+                                    StringArrayEncoding.Base64
+                                ],
+                                stringArrayWrappersCount: 1,
+                                stringArrayThreshold: 1
+                            }
+                        ).getObfuscatedCode();
+                    });
+
+                    it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
+                        assert.match(obfuscatedCode, stringArrayWrappersRegExp);
+                    });
+                });
+
+                describe('Variant #2: `2` scope calls wrappers for each encoding type', () => {
+                    const stringArrayWrappersRegExp: RegExp = new RegExp(
+                        'function test *\\( *\\) *{' +
+                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};' +
+                            // this one may be added or not depends on:
+                            // if all literal values encoded with a single encoding or not
+                            '(?:const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4};)?' +
+                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x3\'\\);' +
+                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x4\'\\);' +
+                            'const _0x([a-f0-9]){4,6} *= *_0x([a-f0-9]){4,6}\\(\'0x5\'\\);' +
+                        '}'
+                    );
+
+                    let obfuscatedCode: string;
+
+                    before(() => {
+                        const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-const.js');
+
+                        obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                            code,
+                            {
+                                ...NO_ADDITIONAL_NODES_PRESET,
+                                stringArray: true,
+                                stringArrayEncoding: [
+                                    StringArrayEncoding.None,
+                                    StringArrayEncoding.Base64
+                                ],
+                                stringArrayWrappersCount: 2,
+                                stringArrayThreshold: 1
+                            }
+                        ).getObfuscatedCode();
+                    });
+
+                    it('should add scope calls wrappers for both `none` and `base64` string array wrappers', () => {
+                        assert.match(obfuscatedCode, stringArrayWrappersRegExp);
+                    });
+                });
             });
         });
     });
 
     describe('Variant #12: none and rc4 encoding', () => {
         describe('Variant #1: string array calls wrapper call', () => {
-            const samplesCount: number = 100;
+            const samplesCount: number = 300;
             const expectedMatchesChance: number = 0.5;
             const expectedMatchesDelta: number = 0.15;
 
@@ -488,11 +785,42 @@ describe('StringArrayTransformer', function () {
                 assert.closeTo(rc4EncodingMatchesChance, expectedMatchesChance, expectedMatchesDelta);
             });
         });
+
+        describe('Variant #2: `stringArrayWrappersCount` option is enabled', () => {
+            describe('Variant #1: correct evaluation of the scope calls wrappers', () => {
+                const expectedEvaluationResult: string = 'aaabbbcccdddeee';
+                let evaluationResult: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-eval.js');
+
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayEncoding: [
+                                StringArrayEncoding.None,
+                                StringArrayEncoding.Rc4
+                            ],
+                            stringArrayWrappersCount: 5
+                        }
+                    ).getObfuscatedCode();
+
+                    evaluationResult = eval(obfuscatedCode);
+                });
+
+                it('should correctly evaluate scope calls wrappers', () => {
+                    assert.equal(evaluationResult, expectedEvaluationResult);
+                });
+            });
+        });
     });
 
     describe('Variant #13: base64 and rc4 encoding', () => {
         describe('Variant #1: single string literal', () => {
-            const samplesCount: number = 100;
+            const samplesCount: number = 300;
             const expectedMatchesChance: number = 0.5;
             const expectedMatchesDelta: number = 0.15;
 
@@ -544,6 +872,37 @@ describe('StringArrayTransformer', function () {
                 assert.closeTo(rc4EncodingMatchesChance, expectedMatchesChance, expectedMatchesDelta);
             });
         });
+
+        describe('Variant #2: `stringArrayWrappersCount` option is enabled', () => {
+            describe('Variant #1: correct evaluation of the scope calls wrappers', () => {
+                const expectedEvaluationResult: string = 'aaabbbcccdddeee';
+                let evaluationResult: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/string-array-wrappers-count-eval.js');
+
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayThreshold: 1,
+                            stringArrayEncoding: [
+                                StringArrayEncoding.Base64,
+                                StringArrayEncoding.Rc4
+                            ],
+                            stringArrayWrappersCount: 5
+                        }
+                    ).getObfuscatedCode();
+
+                    evaluationResult = eval(obfuscatedCode);
+                });
+
+                it('should correctly evaluate scope calls wrappers', () => {
+                    assert.equal(evaluationResult, expectedEvaluationResult);
+                });
+            });
+        });
     });
 
     describe('Variant #14: `stringArrayThreshold` option value', () => {

+ 0 - 11
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/intermediate-variables-count-eval.js

@@ -1,11 +0,0 @@
-function func () {
-    var foo = 1;
-    var bar = 2;
-    var baz = 3;
-    var bark = 4;
-    var hawk = 5;
-
-    return foo + bar + baz + bark + hawk;
-}
-
-func();

+ 17 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-chained-calls-eval.js

@@ -0,0 +1,17 @@
+const foo = 'aaa';
+
+function test () {
+    const bar = 'bbb';
+    const baz = 'ccc';
+
+    function test1 () {
+        const bark = 'ddd';
+        const hawk = 'eee';
+
+        return bark + hawk;
+    }
+
+    return bar + baz + test1();
+}
+
+foo + test();

+ 9 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-count-const.js

@@ -0,0 +1,9 @@
+const foo = 'foo'
+const bar = 'bar';
+const baz = 'baz';
+
+function test () {
+    const bark = 'bark'
+    const hawk = 'hawk';
+    const eagle = 'eagle';
+}

+ 11 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-count-eval.js

@@ -0,0 +1,11 @@
+function func () {
+    var foo = 'aaa';
+    var bar = 'bbb';
+    var baz = 'ccc';
+    var bark = 'ddd';
+    var hawk = 'eee';
+
+    return foo + bar + baz + bark + hawk;
+}
+
+func();

+ 9 - 0
test/functional-tests/node-transformers/string-array-transformers/string-array-transformer/fixtures/string-array-wrappers-count-var.js

@@ -0,0 +1,9 @@
+var foo = 'foo'
+var bar = 'bar';
+var baz = 'baz';
+
+function test () {
+    var bark = 'bark'
+    var hawk = 'hawk';
+    var eagle = 'eagle';
+}

+ 28 - 4
test/functional-tests/options/OptionsNormalizer.spec.ts

@@ -544,7 +544,8 @@ describe('OptionsNormalizer', () => {
                     shuffleStringArray: true,
                     stringArray: false,
                     stringArrayEncoding: [StringArrayEncoding.Rc4],
-                    stringArrayIntermediateVariablesCount: 5,
+                    stringArrayWrappersChainedCalls: true,
+                    stringArrayWrappersCount: 5,
                     stringArrayThreshold: 0.5,
                     rotateStringArray: true
                 });
@@ -554,7 +555,8 @@ describe('OptionsNormalizer', () => {
                     shuffleStringArray: false,
                     stringArray: false,
                     stringArrayEncoding: [StringArrayEncoding.None],
-                    stringArrayIntermediateVariablesCount: 0,
+                    stringArrayWrappersChainedCalls: false,
+                    stringArrayWrappersCount: 0,
                     stringArrayThreshold: 0,
                     rotateStringArray: false
                 };
@@ -592,7 +594,8 @@ describe('OptionsNormalizer', () => {
                     rotateStringArray: true,
                     shuffleStringArray: true,
                     stringArray: true,
-                    stringArrayIntermediateVariablesCount: 5,
+                    stringArrayWrappersChainedCalls: true,
+                    stringArrayWrappersCount: 5,
                     stringArrayThreshold: 0
                 });
 
@@ -601,7 +604,8 @@ describe('OptionsNormalizer', () => {
                     rotateStringArray: false,
                     shuffleStringArray: false,
                     stringArray: false,
-                    stringArrayIntermediateVariablesCount: 0,
+                    stringArrayWrappersChainedCalls: false,
+                    stringArrayWrappersCount: 0,
                     stringArrayThreshold: 0
                 };
             });
@@ -610,5 +614,25 @@ describe('OptionsNormalizer', () => {
                 assert.deepEqual(optionsPreset, expectedOptionsPreset);
             });
         });
+
+        describe('stringArrayWrappersChainedCallsRule', () => {
+            before(() => {
+                optionsPreset = getNormalizedOptions({
+                    ...getDefaultOptions(),
+                    stringArrayWrappersChainedCalls: true,
+                    stringArrayWrappersCount: 0
+                });
+
+                expectedOptionsPreset = {
+                    ...getDefaultOptions(),
+                    stringArrayWrappersChainedCalls: false,
+                    stringArrayWrappersCount: 0
+                };
+            });
+
+            it('should normalize options preset', () => {
+                assert.deepEqual(optionsPreset, expectedOptionsPreset);
+            });
+        });
     });
 });

+ 3 - 3
test/functional-tests/storages/string-array-storage/StringArrayStorage.spec.ts → test/functional-tests/storages/string-array-transformers/string-array-storage/StringArrayStorage.spec.ts

@@ -1,10 +1,10 @@
 import { assert } from 'chai';
 
-import { NO_ADDITIONAL_NODES_PRESET } from '../../../../src/options/presets/NoCustomNodes';
+import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
 
-import { readFileAsString } from '../../../helpers/readFileAsString';
+import { readFileAsString } from '../../../../helpers/readFileAsString';
 
-import { JavaScriptObfuscator } from '../../../../src/JavaScriptObfuscatorFacade';
+import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade';
 
 describe('StringArrayStorage', () => {
     describe('Rotate string array', function () {

+ 0 - 0
test/functional-tests/storages/string-array-storage/fixtures/one-string.js → test/functional-tests/storages/string-array-transformers/string-array-storage/fixtures/one-string.js


+ 0 - 0
test/functional-tests/storages/string-array-storage/fixtures/three-strings.js → test/functional-tests/storages/string-array-transformers/string-array-storage/fixtures/three-strings.js


+ 3 - 2
test/index.spec.ts

@@ -36,7 +36,8 @@ import './unit-tests/options/ValidationErrorsFormatter.spec';
 import './unit-tests/source-code/ObfuscatedCode.spec';
 import './unit-tests/storages/ArrayStorage.spec';
 import './unit-tests/storages/MapStorage.spec';
-import './unit-tests/storages/string-array/StringArrayStorage.spec';
+import './unit-tests/storages/string-array-transformers/literal-nodes-cache/LiteralNodesCacheStorage.spec';
+import './unit-tests/storages/string-array-transformers/string-array/StringArrayStorage.spec';
 import './unit-tests/utils/ArrayUtils.spec';
 import './unit-tests/utils/CryptUtils.spec';
 import './unit-tests/utils/CryptUtilsSwappedAlphabet.spec';
@@ -116,7 +117,7 @@ import './functional-tests/node-transformers/simplifying-transformers/variable-d
 import './functional-tests/node-transformers/string-array-transformers/string-array-transformer/StringArrayTransformer.spec';
 import './functional-tests/options/OptionsNormalizer.spec';
 import './functional-tests/options/domain-lock/Validation.spec';
-import './functional-tests/storages/string-array-storage/StringArrayStorage.spec';
+import './functional-tests/storages/string-array-transformers/string-array-storage/StringArrayStorage.spec';
 
 /**
  * Performance tests

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

@@ -39,7 +39,7 @@ describe('JavaScriptObfuscator runtime eval', function () {
             StringArrayEncoding.Base64,
             StringArrayEncoding.Rc4
         ],
-        stringArrayIntermediateVariablesCount: 20,
+        stringArrayWrappersCount: 5,
         stringArrayThreshold: 1,
         transformObjectKeys: true,
         unicodeEscapeSequence: true
@@ -47,7 +47,7 @@ describe('JavaScriptObfuscator runtime eval', function () {
 
     this.timeout(200000);
 
-    [
+    const options: Partial<TInputOptions>[] = [
         {
             identifierNamesGenerator: IdentifierNamesGenerator.HexadecimalIdentifierNamesGenerator,
             renameGlobals: false
@@ -71,8 +71,13 @@ describe('JavaScriptObfuscator runtime eval', function () {
         {
             identifierNamesGenerator: IdentifierNamesGenerator.MangledShuffledIdentifierNamesGenerator,
             renameGlobals: true
+        },
+        {
+            stringArrayWrappersChainedCalls: true
         }
-    ].forEach((options: Partial<TInputOptions>) => {
+    ];
+
+    options.forEach((options: Partial<TInputOptions>) => {
         const detailedDescription: string = `Identifier names generator: ${options.identifierNamesGenerator}, rename globals: ${options.renameGlobals?.toString()}`;
 
         describe(`Astring. ${detailedDescription}`, () => {

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

@@ -9,7 +9,7 @@ import { TInputOptions } from '../../../../src/types/options/TInputOptions';
 
 import { IInversifyContainerFacade } from '../../../../src/interfaces/container/IInversifyContainerFacade';
 import { IStringArrayStorageAnalyzer } from '../../../../src/interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
-import { IStringArrayStorageItemData } from '../../../../src/interfaces/storages/string-array-storage/IStringArrayStorageItem';
+import { IStringArrayStorageItemData } from '../../../../src/interfaces/storages/string-array-transformers/IStringArrayStorageItem';
 
 import { StringArrayEncoding } from '../../../../src/enums/StringArrayEncoding';
 

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

@@ -0,0 +1,157 @@
+import 'reflect-metadata';
+
+import { assert } from 'chai';
+import * as ESTree from 'estree';
+
+import { ServiceIdentifiers } from '../../../../../src/container/ServiceIdentifiers';
+
+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 { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
+
+import { InversifyContainerFacade } from '../../../../../src/container/InversifyContainerFacade';
+import { NodeFactory } from '../../../../../src/node/NodeFactory';
+
+/**
+ * @returns {IMapStorage<string, V>}
+ */
+const getStorageInstance = (options: TInputOptions = {}): ILiteralNodesCacheStorage => {
+    const inversifyContainerFacade: IInversifyContainerFacade = new InversifyContainerFacade();
+
+    inversifyContainerFacade.load('', '', {
+        ...NO_ADDITIONAL_NODES_PRESET,
+        ...options
+    });
+
+    const storage: ILiteralNodesCacheStorage = inversifyContainerFacade.get(ServiceIdentifiers.ILiteralNodesCacheStorage);
+
+    storage.initialize();
+
+    return storage;
+};
+
+describe('LiteralNodesCacheStorage', () => {
+    describe('buildKey', () => {
+        const expectedCacheKey: string = 'foo-true';
+
+       let cacheKey: string;
+
+        before(() => {
+            const literalNodesCacheStorage: ILiteralNodesCacheStorage = getStorageInstance();
+
+            cacheKey = literalNodesCacheStorage.buildKey(
+                'foo',
+                {
+                    index: 1,
+                    value: '_0x123abc',
+                    encoding: StringArrayEncoding.Rc4,
+                    encodedValue: 'encoded_value',
+                    decodeKey: 'key'
+                }
+            );
+        });
+
+        it('should build a key for the storage', () => {
+            assert.equal(cacheKey, expectedCacheKey);
+        });
+    });
+
+    describe('shouldUseCachedValue', () => {
+        const literalNode: ESTree.Literal =  NodeFactory.literalNode('foo');
+        const key: string = 'key';
+
+        describe('Encoding is not `rc4` and `stringArrayWrappersCount` option is disabled', () => {
+            const expectedResult: boolean = true;
+
+            let result: boolean;
+
+            before(() => {
+                const literalNodesCacheStorage: ILiteralNodesCacheStorage = getStorageInstance({
+                    stringArrayWrappersCount: 0
+                });
+
+                literalNodesCacheStorage.set(key, literalNode);
+
+                result = literalNodesCacheStorage.shouldUseCachedValue(
+                    key,
+                    {
+                        index: 1,
+                        value: '_0x123abc',
+                        encoding: StringArrayEncoding.Base64,
+                        encodedValue: 'encoded_value',
+                        decodeKey: 'key'
+                    },
+                );
+            });
+
+            it('should check if can use cached value', () => {
+                assert.equal(result, expectedResult);
+            });
+        });
+
+        describe('Encoding is `rc4` and `stringArrayWrappersCount` option is disabled', () => {
+            const expectedResult: boolean = false
+
+            let result: boolean;
+
+            before(() => {
+                const literalNodesCacheStorage: ILiteralNodesCacheStorage = getStorageInstance({
+                    stringArrayWrappersCount: 0
+                });
+
+                literalNodesCacheStorage.set(key, literalNode);
+
+                result = literalNodesCacheStorage.shouldUseCachedValue(
+                    key,
+                    {
+                        index: 1,
+                        value: '_0x123abc',
+                        encoding: StringArrayEncoding.Rc4,
+                        encodedValue: 'encoded_value',
+                        decodeKey: 'key'
+                    },
+                );
+            });
+
+            it('should check if can use cached value', () => {
+                assert.equal(result, expectedResult);
+            });
+        });
+
+        describe('Encoding is not `rc4` and `stringArrayWrappersCount` option is enabled', () => {
+            const expectedResult: boolean = false;
+
+            let result: boolean;
+
+            before(() => {
+                const literalNodesCacheStorage: ILiteralNodesCacheStorage = getStorageInstance({
+                    stringArray: true,
+                    stringArrayThreshold: 1,
+                    stringArrayWrappersCount: 5
+                });
+
+                literalNodesCacheStorage.set(key, literalNode);
+
+                result = literalNodesCacheStorage.shouldUseCachedValue(
+                    key,
+                    {
+                        index: 1,
+                        value: '_0x123abc',
+                        encoding: StringArrayEncoding.Base64,
+                        encodedValue: 'encoded_value',
+                        decodeKey: 'key'
+                    },
+                );
+            });
+
+            it('should check if can use cached value', () => {
+                assert.equal(result, expectedResult);
+            });
+        });
+    });
+});

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

@@ -2,18 +2,18 @@ import 'reflect-metadata';
 
 import { assert } from 'chai';
 
-import { ServiceIdentifiers } from '../../../../src/container/ServiceIdentifiers';
+import { ServiceIdentifiers } from '../../../../../src/container/ServiceIdentifiers';
 
-import { TInputOptions } from '../../../../src/types/options/TInputOptions';
+import { TInputOptions } from '../../../../../src/types/options/TInputOptions';
 
-import { IInversifyContainerFacade } from '../../../../src/interfaces/container/IInversifyContainerFacade';
-import { IStringArrayStorage } from '../../../../src/interfaces/storages/string-array-storage/IStringArrayStorage';
+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/StringArrayEncoding';
 
-import { NO_ADDITIONAL_NODES_PRESET } from '../../../../src/options/presets/NoCustomNodes';
+import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
 
-import { InversifyContainerFacade } from '../../../../src/container/InversifyContainerFacade';
+import { InversifyContainerFacade } from '../../../../../src/container/InversifyContainerFacade';
 
 /**
  * @returns {IMapStorage<string, V>}

+ 47 - 0
test/unit-tests/utils/ArrayUtils.spec.ts

@@ -113,6 +113,53 @@ describe('ArrayUtils', () => {
         });
     });
 
+    describe('getLastElement', () => {
+        describe('empty array', () => {
+            const array: string[] = [];
+            const expectedLastElement: null = null;
+
+            let lastElement: string | null;
+
+            before(() => {
+                lastElement = arrayUtils.getLastElement(array);
+            });
+
+            it('should return null if array is empty', () => {
+                assert.equal(lastElement, expectedLastElement);
+            });
+        });
+
+        describe('array length: `1`', () => {
+            const array: string[] = ['foo'];
+            const expectedLastElement: string = 'foo';
+
+            let lastElement: string | null;
+
+            before(() => {
+                lastElement = arrayUtils.getLastElement(array);
+            });
+
+            it('should return first element for array with length: `1`', () => {
+                assert.equal(lastElement, expectedLastElement);
+            });
+        });
+
+        describe('array length: `3`', () => {
+            const array: string[] = ['foo', 'bar', 'baz'];
+            const expectedLastElement: string = 'baz';
+
+            let lastElement: string | null;
+
+            before(() => {
+                lastElement = arrayUtils.getLastElement(array);
+            });
+
+            it('should return last element for array with length: `3`', () => {
+                assert.equal(lastElement, expectedLastElement);
+            });
+        });
+    });
+
     describe('rotate', () => {
         let array: number[],
             rotatedArray: number[];

+ 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/eslint-plugin@4.0.1":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.0.1.tgz#88bde9239e29d688315718552cf80a3490491017"
-  integrity sha512-pQZtXupCn11O4AwpYVUX4PDFfmIJl90ZgrEBg0CEcqlwvPiG0uY81fimr1oMFblZnpKAq6prrT9a59pj1x58rw==
+"@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==
   dependencies:
-    "@typescript-eslint/experimental-utils" "4.0.1"
-    "@typescript-eslint/scope-manager" "4.0.1"
+    "@typescript-eslint/experimental-utils" "4.1.0"
+    "@typescript-eslint/scope-manager" "4.1.0"
     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/experimental-utils@4.0.1":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.0.1.tgz#7d9a3ab6821ad5274dad2186c1aa0d93afd696eb"
-  integrity sha512-gAqOjLiHoED79iYTt3F4uSHrYmg/GPz/zGezdB0jAdr6S6gwNiR/j7cTZ8nREKVzMVKLd9G3xbg1sV9GClW3sw==
+"@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==
   dependencies:
     "@types/json-schema" "^7.0.3"
-    "@typescript-eslint/scope-manager" "4.0.1"
-    "@typescript-eslint/types" "4.0.1"
-    "@typescript-eslint/typescript-estree" "4.0.1"
+    "@typescript-eslint/scope-manager" "4.1.0"
+    "@typescript-eslint/types" "4.1.0"
+    "@typescript-eslint/typescript-estree" "4.1.0"
     eslint-scope "^5.0.0"
     eslint-utils "^2.0.0"
 
-"@typescript-eslint/parser@4.0.1":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.0.1.tgz#73772080db7a7a4534a35d719e006f503e664dc3"
-  integrity sha512-1+qLmXHNAWSQ7RB6fdSQszAiA7JTwzakj5cNYjBTUmpH2cqilxMZEIV+DRKjVZs8NzP3ALmKexB0w/ExjcK9Iw==
+"@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==
   dependencies:
-    "@typescript-eslint/scope-manager" "4.0.1"
-    "@typescript-eslint/types" "4.0.1"
-    "@typescript-eslint/typescript-estree" "4.0.1"
+    "@typescript-eslint/scope-manager" "4.1.0"
+    "@typescript-eslint/types" "4.1.0"
+    "@typescript-eslint/typescript-estree" "4.1.0"
     debug "^4.1.1"
 
-"@typescript-eslint/scope-manager@4.0.1":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.0.1.tgz#24d93c3000bdfcc5a157dc4d32b742405a8631b5"
-  integrity sha512-u3YEXVJ8jsj7QCJk3om0Y457fy2euEOkkzxIB/LKU3MdyI+FJ2gI0M4aKEaXzwCSfNDiZ13a3lDo5DVozc+XLQ==
+"@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==
   dependencies:
-    "@typescript-eslint/types" "4.0.1"
-    "@typescript-eslint/visitor-keys" "4.0.1"
+    "@typescript-eslint/types" "4.1.0"
+    "@typescript-eslint/visitor-keys" "4.1.0"
 
-"@typescript-eslint/types@4.0.1":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.0.1.tgz#1cf72582f764931f085cb8230ff215980fe467b2"
-  integrity sha512-S+gD3fgbkZYW2rnbjugNMqibm9HpEjqZBZkTiI3PwbbNGWmAcxolWIUwZ0SKeG4Dy2ktpKKaI/6+HGYVH8Qrlg==
+"@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/typescript-estree@4.0.1":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.0.1.tgz#29a43c7060641ec51c902d9f50ac7c5866ec479f"
-  integrity sha512-zGzleORFXrRWRJAMLTB2iJD1IZbCPkg4hsI8mGdpYlKaqzvKYSEWVAYh14eauaR+qIoZVWrXgYSXqLtTlxotiw==
+"@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==
   dependencies:
-    "@typescript-eslint/types" "4.0.1"
-    "@typescript-eslint/visitor-keys" "4.0.1"
+    "@typescript-eslint/types" "4.1.0"
+    "@typescript-eslint/visitor-keys" "4.1.0"
     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/visitor-keys@4.0.1":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.0.1.tgz#d4e8de62775f2a6db71c7e8539633680039fdd6c"
-  integrity sha512-yBSqd6FjnTzbg5RUy9J+9kJEyQjTI34JdGMJz+9ttlJzLCnGkBikxw+N5n2VDcc3CesbIEJ0MnZc5uRYnrEnCw==
+"@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==
   dependencies:
-    "@typescript-eslint/types" "4.0.1"
+    "@typescript-eslint/types" "4.1.0"
     eslint-visitor-keys "^2.0.0"
 
 "@webassemblyjs/[email protected]":
@@ -2035,10 +2035,10 @@ [email protected]:
     resolve "^1.17.0"
     tsconfig-paths "^3.9.0"
 
[email protected].1:
-  version "30.3.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-30.3.1.tgz#be38fec54ccd07011b19a9dfee040e166c1414f0"
-  integrity sha512-185ARou6Wj/68DP0g9kLLBnvmVwgg6/E/7Z8Z7Dz7Z63WgvRNaSvOLQiXkzIOEwstQfwI9PCuFPh4qBJov907A==
[email protected].3:
+  version "30.3.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-30.3.3.tgz#25ddf2e59164a6182c5a9e12e6560d69e252013d"
+  integrity sha512-u2z0LUTm00KLrWB2h7y+hkzYcTIDIi87efXRnv36cFvs23hIOuT77FdZNDBxbB4dxquHBNsyxLpyEuERYUVNEA==
   dependencies:
     comment-parser "^0.7.6"
     debug "^4.1.1"

Some files were not shown because too many files changed in this diff