Bladeren bron

Merge pull request #493 from javascript-obfuscator/string-array-random-position

`shuffleStringArray` option
Timofey Kachalov 5 jaren geleden
bovenliggende
commit
c3b0b4f369
63 gewijzigde bestanden met toevoegingen van 1688 en 330 verwijderingen
  1. 2 0
      CHANGELOG.md
  2. 12 0
      README.md
  3. 0 0
      dist/index.browser.js
  4. 0 0
      dist/index.cli.js
  5. 0 0
      dist/index.js
  6. 118 0
      src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts
  7. 5 1
      src/cli/JavaScriptObfuscatorCLI.ts
  8. 1 0
      src/container/ServiceIdentifiers.ts
  9. 7 0
      src/container/modules/analyzers/AnalyzersModule.ts
  10. 2 2
      src/container/modules/storages/StoragesModule.ts
  11. 6 25
      src/custom-nodes/string-array-nodes/StringArrayNode.ts
  12. 5 5
      src/custom-nodes/string-array-nodes/StringArrayRotateFunctionNode.ts
  13. 11 19
      src/custom-nodes/string-array-nodes/group/StringArrayCustomNodeGroup.ts
  14. 1 1
      src/interfaces/IEncodedValue.d.ts
  15. 17 0
      src/interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer.d.ts
  16. 4 4
      src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IIdentifierObfuscatingReplacer.d.ts
  17. 6 2
      src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IObfuscatingReplacer.d.ts
  18. 0 4
      src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/IStringArrayIndexData.d.ts
  19. 1 0
      src/interfaces/options/IOptions.d.ts
  20. 7 1
      src/interfaces/storages/IArrayStorage.d.ts
  21. 7 1
      src/interfaces/storages/IMapStorage.d.ts
  22. 24 0
      src/interfaces/storages/string-array-storage/IStringArrayStorage.d.ts
  23. 6 0
      src/interfaces/storages/string-array-storage/IStringArrayStorageItem.d.ts
  24. 2 2
      src/node-transformers/obfuscating-transformers/CatchClauseTransformer.ts
  25. 4 4
      src/node-transformers/obfuscating-transformers/ClassDeclarationTransformer.ts
  26. 4 4
      src/node-transformers/obfuscating-transformers/FunctionDeclarationTransformer.ts
  27. 3 3
      src/node-transformers/obfuscating-transformers/FunctionTransformer.ts
  28. 3 3
      src/node-transformers/obfuscating-transformers/ImportDeclarationTransformer.ts
  29. 2 2
      src/node-transformers/obfuscating-transformers/LabeledStatementTransformer.ts
  30. 21 4
      src/node-transformers/obfuscating-transformers/LiteralTransformer.ts
  31. 4 4
      src/node-transformers/obfuscating-transformers/VariableDeclarationTransformer.ts
  32. 2 2
      src/node-transformers/obfuscating-transformers/obfuscating-replacers/AbstractObfuscatingReplacer.ts
  33. 21 15
      src/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/BaseIdentifierObfuscatingReplacer.ts
  34. 9 3
      src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/BooleanLiteralObfuscatingReplacer.ts
  35. 15 9
      src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/NumberLiteralObfuscatingReplacer.ts
  36. 55 147
      src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/StringLiteralObfuscatingReplacer.ts
  37. 6 0
      src/options/Options.ts
  38. 1 0
      src/options/normalizer-rules/StringArrayRule.ts
  39. 1 0
      src/options/presets/Default.ts
  40. 1 0
      src/options/presets/NoCustomNodes.ts
  41. 10 2
      src/storages/ArrayStorage.ts
  42. 10 2
      src/storages/MapStorage.ts
  43. 215 15
      src/storages/string-array/StringArrayStorage.ts
  44. 1 1
      src/templates/string-array-nodes/string-array-rotate-function-node/StringArrayRotateFunctionTemplate.ts
  45. 0 3
      src/types/storages/TStringArrayStorage.d.ts
  46. 8 4
      test/dev/dev.ts
  47. 2 1
      test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts
  48. 58 15
      test/functional-tests/node-transformers/obfuscating-transformers/literal-transformer/LiteralTransformer.spec.ts
  49. 27 3
      test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/BaseIdentifierObfuscatingReplacer.spec.ts
  50. 3 0
      test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/fixtures/global-reserved-names.js
  51. 0 0
      test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/fixtures/local-reserved-names.js
  52. 4 0
      test/functional-tests/options/OptionsNormalizer.spec.ts
  53. 308 0
      test/functional-tests/storages/string-array-storage/StringArrayStorage.spec.ts
  54. 1 0
      test/functional-tests/storages/string-array-storage/fixtures/one-string.js
  55. 3 0
      test/functional-tests/storages/string-array-storage/fixtures/three-strings.js
  56. 1 1
      test/helpers/getRegExpMatch.ts
  57. 5 0
      test/index.spec.ts
  58. 318 0
      test/unit-tests/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.spec.ts
  59. 60 0
      test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/BooleanLiteralObfuscatingReplacer.spec.ts
  60. 60 0
      test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/NumberLiteralObfuscatingReplacer.spec.ts
  61. 60 0
      test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/StringLiteralObfuscatingReplacer.spec.ts
  62. 101 14
      test/unit-tests/storages/ArrayStorage.spec.ts
  63. 37 2
      test/unit-tests/storages/MapStorage.spec.ts

+ 2 - 0
CHANGELOG.md

@@ -2,8 +2,10 @@ Change Log
 
 
 v0.23.0
 v0.23.0
 ---
 ---
+* **New option:** `shuffleStringArray` randomly shuffles string array items
 * Fixed https://github.com/javascript-obfuscator/javascript-obfuscator/issues/494
 * Fixed https://github.com/javascript-obfuscator/javascript-obfuscator/issues/494
 * **Internal change:** switched AST parser from `espree` on `acorn`
 * **Internal change:** switched AST parser from `espree` on `acorn`
+* **Internal refactoring:** refactoring of string array storage and related things
 
 
 v0.22.1
 v0.22.1
 ---
 ---

+ 12 - 0
README.md

@@ -306,6 +306,7 @@ Following options are available for the JS Obfuscator:
     rotateStringArray: true,
     rotateStringArray: true,
     seed: 0,
     seed: 0,
     selfDefending: false,
     selfDefending: false,
+    shuffleStringArray: true,
     sourceMap: false,
     sourceMap: false,
     sourceMapBaseUrl: '',
     sourceMapBaseUrl: '',
     sourceMapFileName: '',
     sourceMapFileName: '',
@@ -349,6 +350,7 @@ Following options are available for the JS Obfuscator:
     --rotate-string-array <boolean>
     --rotate-string-array <boolean>
     --seed <string|number>
     --seed <string|number>
     --self-defending <boolean>
     --self-defending <boolean>
+    --shuffle-string-array <boolean>
     --source-map <boolean>
     --source-map <boolean>
     --source-map-base-url <string>
     --source-map-base-url <string>
     --source-map-file-name <string>
     --source-map-file-name <string>
@@ -691,6 +693,13 @@ Type: `boolean` Default: `false`
 
 
 This option makes the output code resilient against formatting and variable renaming. If one tries to use a JavaScript beautifier on the obfuscated code, the code won't work anymore, making it harder to understand and modify it.
 This option makes the output code resilient against formatting and variable renaming. If one tries to use a JavaScript beautifier on the obfuscated code, the code won't work anymore, making it harder to understand and modify it.
 
 
+### `shuffleStringArray`
+Type: `boolean` Default: `true`
+
+##### :warning: [`stringArray`](#stringarray) must be enabled
+
+Randomly shuffles the `stringArray` array items.
+
 ### `sourceMap`
 ### `sourceMap`
 Type: `boolean` Default: `false`
 Type: `boolean` Default: `false`
 
 
@@ -867,6 +876,7 @@ Performance will 50-100% slower than without obfuscation
     renameGlobals: false,
     renameGlobals: false,
     rotateStringArray: true,
     rotateStringArray: true,
     selfDefending: true,
     selfDefending: true,
+    shuffleStringArray: true,
     splitStrings: true,
     splitStrings: true,
     splitStringsChunkLength: '5',
     splitStringsChunkLength: '5',
     stringArray: true,
     stringArray: true,
@@ -896,6 +906,7 @@ Performance will 30-35% slower than without obfuscation
     renameGlobals: false,
     renameGlobals: false,
     rotateStringArray: true,
     rotateStringArray: true,
     selfDefending: true,
     selfDefending: true,
+    shuffleStringArray: true,
     splitStrings: true,
     splitStrings: true,
     splitStringsChunkLength: '10',
     splitStringsChunkLength: '10',
     stringArray: true,
     stringArray: true,
@@ -923,6 +934,7 @@ Performance will slightly slower than without obfuscation
     renameGlobals: false,
     renameGlobals: false,
     rotateStringArray: true,
     rotateStringArray: true,
     selfDefending: true,
     selfDefending: true,
+    shuffleStringArray: true,
     splitStrings: false,
     splitStrings: false,
     stringArray: true,
     stringArray: true,
     stringArrayEncoding: false,
     stringArrayEncoding: 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


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

@@ -0,0 +1,118 @@
+import { inject, injectable, } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import * as estraverse from 'estraverse';
+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 { IStringArrayStorageAnalyzer } from '../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
+import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-storage/IStringArrayStorageItem';
+
+import { NodeGuards } from '../../node/NodeGuards';
+import { NodeMetadata } from '../../node/NodeMetadata';
+
+/**
+ * Adds values of literal nodes to the string array storage
+ */
+@injectable()
+export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer {
+    /**
+     * @type {number}
+     */
+    private static readonly minimumLengthForStringArray: number = 3;
+
+    /**
+     * @type {IOptions}
+     */
+    private readonly options: IOptions;
+
+    /**
+     * @type {randomGenerator}
+     */
+    private readonly randomGenerator: IRandomGenerator;
+
+    /**
+     * @type {IStringArrayStorage}
+     */
+    private readonly stringArrayStorage: IStringArrayStorage;
+
+    /**
+     * @type {Map<ESTree.Literal, IStringArrayStorageItemData>}
+     */
+    private readonly stringArrayStorageData: Map<ESTree.Literal, IStringArrayStorageItemData> = new Map();
+
+    /**
+     * @param {IStringArrayStorage} stringArrayStorage
+     * @param {IRandomGenerator} randomGenerator
+     * @param {IOptions} options
+     */
+    constructor (
+        @inject(ServiceIdentifiers.TStringArrayStorage) stringArrayStorage: IStringArrayStorage,
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
+        @inject(ServiceIdentifiers.IOptions) options: IOptions,
+    ) {
+        this.stringArrayStorage = stringArrayStorage;
+        this.randomGenerator = randomGenerator;
+        this.options = options;
+    }
+
+    /**
+     * @param {Program} astTree
+     */
+    public analyze (astTree: ESTree.Program): void {
+        if (!this.options.stringArray) {
+            return;
+        }
+
+        estraverse.traverse(astTree, {
+            enter: (node: ESTree.Node): estraverse.VisitorOption | void => {
+                if (NodeMetadata.isIgnoredNode(node)) {
+                    return estraverse.VisitorOption.Skip;
+                }
+
+                if (!NodeGuards.isLiteralNode(node)) {
+                    return;
+                }
+
+                this.analyzeLiteralNode(node);
+            }
+        });
+    }
+
+    /**
+     * @param {Literal} literalNode
+     * @returns {IStringArrayStorageItemData | undefined}
+     */
+    public getItemDataForLiteralNode (literalNode: ESTree.Literal): IStringArrayStorageItemData | undefined {
+        return this.stringArrayStorageData.get(literalNode);
+    }
+
+    /**
+     * @param {Literal} literalNode
+     */
+    private analyzeLiteralNode (literalNode: ESTree.Literal): void {
+        if (typeof literalNode.value !== 'string') {
+            return;
+        }
+
+        if (!this.shouldAddValueToStringArray(literalNode.value)) {
+            return;
+        }
+
+        this.stringArrayStorageData.set(
+            literalNode,
+            this.stringArrayStorage.getOrThrow(literalNode.value)
+        );
+    }
+
+    /**
+     * @param {string} value
+     * @returns {boolean}
+     */
+    private shouldAddValueToStringArray (value: string): boolean {
+        return value.length >= StringArrayStorageAnalyzer.minimumLengthForStringArray
+            && this.randomGenerator.getMathRandom() <= this.options.stringArrayThreshold;
+    }
+}

+ 5 - 1
src/cli/JavaScriptObfuscatorCLI.ts

@@ -283,7 +283,7 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
                 BooleanSanitizer
                 BooleanSanitizer
             )
             )
             .option(
             .option(
-                '--rotate-string-array <boolean>', 'Disable rotation of unicode array values during obfuscation',
+                '--rotate-string-array <boolean>', 'Enable rotation of string array values during obfuscation',
                 BooleanSanitizer
                 BooleanSanitizer
             )
             )
             .option(
             .option(
@@ -296,6 +296,10 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
                 'Disables self-defending for obfuscated code',
                 'Disables self-defending for obfuscated code',
                 BooleanSanitizer
                 BooleanSanitizer
             )
             )
+            .option(
+                '--shuffle-string-array <boolean>', 'Randomly shuffles string array items',
+                BooleanSanitizer
+            )
             .option(
             .option(
                 '--source-map <boolean>',
                 '--source-map <boolean>',
                 'Enables source map generation',
                 'Enables source map generation',

+ 1 - 0
src/container/ServiceIdentifiers.ts

@@ -41,6 +41,7 @@ export enum ServiceIdentifiers {
     IRandomGenerator = 'IRandomGenerator',
     IRandomGenerator = 'IRandomGenerator',
     ISourceCode = 'ISourceCode',
     ISourceCode = 'ISourceCode',
     ISourceMapCorrector = 'ISourceMapCorrector',
     ISourceMapCorrector = 'ISourceMapCorrector',
+    IStringArrayStorageAnalyzer = 'IStringArrayStorageAnalyzer',
     ITransformersRunner = 'ITransformersRunner',
     ITransformersRunner = 'ITransformersRunner',
     Newable__ICustomNode = 'Newable<ICustomNode>',
     Newable__ICustomNode = 'Newable<ICustomNode>',
     Newable__TControlFlowStorage = 'Newable<TControlFlowStorage>',
     Newable__TControlFlowStorage = 'Newable<TControlFlowStorage>',

+ 7 - 0
src/container/modules/analyzers/AnalyzersModule.ts

@@ -5,6 +5,7 @@ import { ServiceIdentifiers } from '../../ServiceIdentifiers';
 import { ICalleeDataExtractor } from '../../../interfaces/analyzers/calls-graph-analyzer/ICalleeDataExtractor';
 import { ICalleeDataExtractor } from '../../../interfaces/analyzers/calls-graph-analyzer/ICalleeDataExtractor';
 import { ICallsGraphAnalyzer } from '../../../interfaces/analyzers/calls-graph-analyzer/ICallsGraphAnalyzer';
 import { ICallsGraphAnalyzer } from '../../../interfaces/analyzers/calls-graph-analyzer/ICallsGraphAnalyzer';
 import { IPrevailingKindOfVariablesAnalyzer } from '../../../interfaces/analyzers/calls-graph-analyzer/IPrevailingKindOfVariablesAnalyzer';
 import { IPrevailingKindOfVariablesAnalyzer } from '../../../interfaces/analyzers/calls-graph-analyzer/IPrevailingKindOfVariablesAnalyzer';
+import { IStringArrayStorageAnalyzer } from '../../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
 
 
 import { CalleeDataExtractor } from '../../../enums/analyzers/calls-graph-analyzer/CalleeDataExtractor';
 import { CalleeDataExtractor } from '../../../enums/analyzers/calls-graph-analyzer/CalleeDataExtractor';
 import { CallsGraphAnalyzer } from '../../../analyzers/calls-graph-analyzer/CallsGraphAnalyzer';
 import { CallsGraphAnalyzer } from '../../../analyzers/calls-graph-analyzer/CallsGraphAnalyzer';
@@ -12,6 +13,7 @@ import { FunctionDeclarationCalleeDataExtractor } from '../../../analyzers/calls
 import { FunctionExpressionCalleeDataExtractor } from '../../../analyzers/calls-graph-analyzer/callee-data-extractors/FunctionExpressionCalleeDataExtractor';
 import { FunctionExpressionCalleeDataExtractor } from '../../../analyzers/calls-graph-analyzer/callee-data-extractors/FunctionExpressionCalleeDataExtractor';
 import { ObjectExpressionCalleeDataExtractor } from '../../../analyzers/calls-graph-analyzer/callee-data-extractors/ObjectExpressionCalleeDataExtractor';
 import { ObjectExpressionCalleeDataExtractor } from '../../../analyzers/calls-graph-analyzer/callee-data-extractors/ObjectExpressionCalleeDataExtractor';
 import { PrevailingKindOfVariablesAnalyzer } from '../../../analyzers/prevailing-kind-of-variables-analyzer/PrevailingKindOfVariablesAnalyzer';
 import { PrevailingKindOfVariablesAnalyzer } from '../../../analyzers/prevailing-kind-of-variables-analyzer/PrevailingKindOfVariablesAnalyzer';
+import { StringArrayStorageAnalyzer } from '../../../analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer';
 
 
 export const analyzersModule: interfaces.ContainerModule = new ContainerModule((bind: interfaces.Bind) => {
 export const analyzersModule: interfaces.ContainerModule = new ContainerModule((bind: interfaces.Bind) => {
     // calls graph analyzer
     // calls graph analyzer
@@ -24,6 +26,11 @@ export const analyzersModule: interfaces.ContainerModule = new ContainerModule((
         .to(PrevailingKindOfVariablesAnalyzer)
         .to(PrevailingKindOfVariablesAnalyzer)
         .inSingletonScope();
         .inSingletonScope();
 
 
+    // string array storage analyzer
+    bind<IStringArrayStorageAnalyzer>(ServiceIdentifiers.IStringArrayStorageAnalyzer)
+        .to(StringArrayStorageAnalyzer)
+        .inSingletonScope();
+
     // callee data extractors
     // callee data extractors
     bind<ICalleeDataExtractor>(ServiceIdentifiers.ICalleeDataExtractor)
     bind<ICalleeDataExtractor>(ServiceIdentifiers.ICalleeDataExtractor)
         .to(FunctionDeclarationCalleeDataExtractor)
         .to(FunctionDeclarationCalleeDataExtractor)

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

@@ -3,10 +3,10 @@ import { ServiceIdentifiers } from '../../ServiceIdentifiers';
 
 
 import { TControlFlowStorage } from '../../../types/storages/TControlFlowStorage';
 import { TControlFlowStorage } from '../../../types/storages/TControlFlowStorage';
 import { TCustomNodeGroupStorage } from '../../../types/storages/TCustomNodeGroupStorage';
 import { TCustomNodeGroupStorage } from '../../../types/storages/TCustomNodeGroupStorage';
-import { TStringArrayStorage } from '../../../types/storages/TStringArrayStorage';
 
 
 import { IOptions } from '../../../interfaces/options/IOptions';
 import { IOptions } from '../../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../../interfaces/utils/IRandomGenerator';
 import { IRandomGenerator } from '../../../interfaces/utils/IRandomGenerator';
+import { IStringArrayStorage } from '../../../interfaces/storages/string-array-storage/IStringArrayStorage';
 
 
 import { ControlFlowStorage } from '../../../storages/control-flow/ControlFlowStorage';
 import { ControlFlowStorage } from '../../../storages/control-flow/ControlFlowStorage';
 import { CustomNodeGroupStorage } from '../../../storages/custom-node-group/CustomNodeGroupStorage';
 import { CustomNodeGroupStorage } from '../../../storages/custom-node-group/CustomNodeGroupStorage';
@@ -18,7 +18,7 @@ export const storagesModule: interfaces.ContainerModule = new ContainerModule((b
         .to(CustomNodeGroupStorage)
         .to(CustomNodeGroupStorage)
         .inSingletonScope();
         .inSingletonScope();
 
 
-    bind<TStringArrayStorage>(ServiceIdentifiers.TStringArrayStorage)
+    bind<IStringArrayStorage>(ServiceIdentifiers.TStringArrayStorage)
         .to(StringArrayStorage)
         .to(StringArrayStorage)
         .inSingletonScope();
         .inSingletonScope();
 
 

+ 6 - 25
src/custom-nodes/string-array-nodes/StringArrayNode.ts

@@ -3,11 +3,11 @@ import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
 
 
 import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
 import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
 import { TStatement } from '../../types/node/TStatement';
 import { TStatement } from '../../types/node/TStatement';
-import { TStringArrayStorage } from '../../types/storages/TStringArrayStorage';
 
 
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
 import { ICustomNodeFormatter } from '../../interfaces/custom-nodes/ICustomNodeFormatter';
 import { ICustomNodeFormatter } from '../../interfaces/custom-nodes/ICustomNodeFormatter';
+import { IStringArrayStorage } from '../../interfaces/storages/string-array-storage/IStringArrayStorage';
 
 
 import { initializable } from '../../decorators/Initializable';
 import { initializable } from '../../decorators/Initializable';
 
 
@@ -15,15 +15,14 @@ import { StringArrayTemplate } from '../../templates/string-array-nodes/string-a
 
 
 import { AbstractCustomNode } from '../AbstractCustomNode';
 import { AbstractCustomNode } from '../AbstractCustomNode';
 import { NodeUtils } from '../../node/NodeUtils';
 import { NodeUtils } from '../../node/NodeUtils';
-import { StringArrayStorage } from '../../storages/string-array/StringArrayStorage';
 
 
 @injectable()
 @injectable()
 export class StringArrayNode extends AbstractCustomNode {
 export class StringArrayNode extends AbstractCustomNode {
     /**
     /**
-     * @type {TStringArrayStorage}
+     * @type {IStringArrayStorage}
      */
      */
     @initializable()
     @initializable()
-    private stringArrayStorage!: TStringArrayStorage;
+    private stringArrayStorage!: IStringArrayStorage;
 
 
     /**
     /**
      * @type {string}
      * @type {string}
@@ -31,12 +30,6 @@ export class StringArrayNode extends AbstractCustomNode {
     @initializable()
     @initializable()
     private stringArrayName!: string;
     private stringArrayName!: string;
 
 
-    /**
-     * @type {number}
-     */
-    @initializable()
-    private stringArrayRotateValue!: number;
-
     /**
     /**
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {ICustomNodeFormatter} customNodeFormatter
      * @param {ICustomNodeFormatter} customNodeFormatter
@@ -54,27 +47,15 @@ export class StringArrayNode extends AbstractCustomNode {
     }
     }
 
 
     /**
     /**
-     * @param {TStringArrayStorage} stringArrayStorage
+     * @param {IStringArrayStorage} stringArrayStorage
      * @param {string} stringArrayName
      * @param {string} stringArrayName
-     * @param {number} stringArrayRotateValue
      */
      */
     public initialize (
     public initialize (
-        stringArrayStorage: TStringArrayStorage,
-        stringArrayName: string,
-        stringArrayRotateValue: number
+        stringArrayStorage: IStringArrayStorage,
+        stringArrayName: string
     ): void {
     ): void {
         this.stringArrayStorage = stringArrayStorage;
         this.stringArrayStorage = stringArrayStorage;
         this.stringArrayName = stringArrayName;
         this.stringArrayName = stringArrayName;
-        this.stringArrayRotateValue = stringArrayRotateValue;
-    }
-
-    /**
-     * @returns {TStatement[]}
-     */
-    public getNode (): TStatement[] {
-        (<StringArrayStorage>this.stringArrayStorage).rotateArray(this.stringArrayRotateValue);
-
-        return super.getNode();
     }
     }
 
 
     /**
     /**

+ 5 - 5
src/custom-nodes/string-array-nodes/StringArrayRotateFunctionNode.ts

@@ -38,7 +38,7 @@ export class StringArrayRotateFunctionNode extends AbstractCustomNode {
      * @param {number}
      * @param {number}
      */
      */
     @initializable()
     @initializable()
-    private stringArrayRotateValue!: number;
+    private stringArrayRotationAmount!: number;
 
 
     /**
     /**
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
@@ -62,14 +62,14 @@ export class StringArrayRotateFunctionNode extends AbstractCustomNode {
 
 
     /**
     /**
      * @param {string} stringArrayName
      * @param {string} stringArrayName
-     * @param {number} stringArrayRotateValue
+     * @param {number} stringArrayRotationAmount
      */
      */
     public initialize (
     public initialize (
         stringArrayName: string,
         stringArrayName: string,
-        stringArrayRotateValue: number
+        stringArrayRotationAmount: number
     ): void {
     ): void {
         this.stringArrayName = stringArrayName;
         this.stringArrayName = stringArrayName;
-        this.stringArrayRotateValue = stringArrayRotateValue;
+        this.stringArrayRotationAmount = stringArrayRotationAmount;
     }
     }
 
 
     /**
     /**
@@ -103,7 +103,7 @@ export class StringArrayRotateFunctionNode extends AbstractCustomNode {
                 code,
                 code,
                 timesName,
                 timesName,
                 stringArrayName: this.stringArrayName,
                 stringArrayName: this.stringArrayName,
-                stringArrayRotateValue: NumberUtils.toHex(this.stringArrayRotateValue),
+                stringArrayRotationAmount: NumberUtils.toHex(this.stringArrayRotationAmount),
                 whileFunctionName
                 whileFunctionName
             }),
             }),
             {
             {

+ 11 - 19
src/custom-nodes/string-array-nodes/group/StringArrayCustomNodeGroup.ts

@@ -5,12 +5,12 @@ import { TCustomNodeFactory } from '../../../types/container/custom-nodes/TCusto
 import { TIdentifierNamesGeneratorFactory } from '../../../types/container/generators/TIdentifierNamesGeneratorFactory';
 import { TIdentifierNamesGeneratorFactory } from '../../../types/container/generators/TIdentifierNamesGeneratorFactory';
 import { TInitialData } from '../../../types/TInitialData';
 import { TInitialData } from '../../../types/TInitialData';
 import { TNodeWithStatements } from '../../../types/node/TNodeWithStatements';
 import { TNodeWithStatements } from '../../../types/node/TNodeWithStatements';
-import { TStringArrayStorage } from '../../../types/storages/TStringArrayStorage';
 
 
+import { ICallsGraphData } from '../../../interfaces/analyzers/calls-graph-analyzer/ICallsGraphData';
 import { ICustomNode } from '../../../interfaces/custom-nodes/ICustomNode';
 import { ICustomNode } from '../../../interfaces/custom-nodes/ICustomNode';
 import { IOptions } from '../../../interfaces/options/IOptions';
 import { IOptions } from '../../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../../interfaces/utils/IRandomGenerator';
 import { IRandomGenerator } from '../../../interfaces/utils/IRandomGenerator';
-import { ICallsGraphData } from '../../../interfaces/analyzers/calls-graph-analyzer/ICallsGraphData';
+import { IStringArrayStorage } from '../../../interfaces/storages/string-array-storage/IStringArrayStorage';
 
 
 import { initializable } from '../../../decorators/Initializable';
 import { initializable } from '../../../decorators/Initializable';
 
 
@@ -42,20 +42,20 @@ export class StringArrayCustomNodeGroup extends AbstractCustomNodeGroup {
     private readonly customNodeFactory: TCustomNodeFactory;
     private readonly customNodeFactory: TCustomNodeFactory;
 
 
     /**
     /**
-     * @type {TStringArrayStorage}
+     * @type {IStringArrayStorage}
      */
      */
-    private readonly stringArrayStorage: TStringArrayStorage;
+    private readonly stringArrayStorage: IStringArrayStorage;
 
 
     /**
     /**
      * @param {TCustomNodeFactory} customNodeFactory
      * @param {TCustomNodeFactory} customNodeFactory
-     * @param {TStringArrayStorage} stringArrayStorage
+     * @param {IStringArrayStorage} stringArrayStorage
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {IRandomGenerator} randomGenerator
      * @param {IRandomGenerator} randomGenerator
      * @param {IOptions} options
      * @param {IOptions} options
      */
      */
     constructor (
     constructor (
         @inject(ServiceIdentifiers.Factory__ICustomNode) customNodeFactory: TCustomNodeFactory,
         @inject(ServiceIdentifiers.Factory__ICustomNode) customNodeFactory: TCustomNodeFactory,
-        @inject(ServiceIdentifiers.TStringArrayStorage) stringArrayStorage: TStringArrayStorage,
+        @inject(ServiceIdentifiers.TStringArrayStorage) stringArrayStorage: IStringArrayStorage,
         @inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
         @inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
             identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
             identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
         @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
         @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
@@ -106,21 +106,13 @@ export class StringArrayCustomNodeGroup extends AbstractCustomNodeGroup {
         const stringArrayRotateFunctionNode: ICustomNode<TInitialData<StringArrayRotateFunctionNode>> =
         const stringArrayRotateFunctionNode: ICustomNode<TInitialData<StringArrayRotateFunctionNode>> =
             this.customNodeFactory(CustomNode.StringArrayRotateFunctionNode);
             this.customNodeFactory(CustomNode.StringArrayRotateFunctionNode);
 
 
-        const stringArrayStorageId: string = this.stringArrayStorage.getStorageId();
-
-        const [stringArrayName, stringArrayCallsWrapperName]: string[] = stringArrayStorageId.split('|');
-
-        let stringArrayRotateValue: number;
-
-        if (this.options.rotateStringArray) {
-            stringArrayRotateValue = this.randomGenerator.getRandomInteger(100, 500);
-        } else {
-            stringArrayRotateValue = 0;
-        }
+        const stringArrayName: string = this.stringArrayStorage.getStorageName();
+        const stringArrayCallsWrapperName: string = this.stringArrayStorage.getStorageCallsWrapperName();
+        const stringArrayRotationAmount: number = this.stringArrayStorage.getRotationAmount();
 
 
-        stringArrayNode.initialize(this.stringArrayStorage, stringArrayName, stringArrayRotateValue);
+        stringArrayNode.initialize(this.stringArrayStorage, stringArrayName);
         stringArrayCallsWrapper.initialize(stringArrayName, stringArrayCallsWrapperName);
         stringArrayCallsWrapper.initialize(stringArrayName, stringArrayCallsWrapperName);
-        stringArrayRotateFunctionNode.initialize(stringArrayName, stringArrayRotateValue);
+        stringArrayRotateFunctionNode.initialize(stringArrayName, stringArrayRotationAmount);
 
 
         this.customNodes.set(CustomNode.StringArrayNode, stringArrayNode);
         this.customNodes.set(CustomNode.StringArrayNode, stringArrayNode);
         this.customNodes.set(CustomNode.StringArrayCallsWrapper, stringArrayCallsWrapper);
         this.customNodes.set(CustomNode.StringArrayCallsWrapper, stringArrayCallsWrapper);

+ 1 - 1
src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/IEncodedValue.d.ts → src/interfaces/IEncodedValue.d.ts

@@ -1,4 +1,4 @@
 export interface IEncodedValue {
 export interface IEncodedValue {
     encodedValue: string;
     encodedValue: string;
-    key: string | null;
+    decodeKey: string | null;
 }
 }

+ 17 - 0
src/interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer.d.ts

@@ -0,0 +1,17 @@
+import * as ESTree from 'estree';
+
+import { IAnalyzer } from '../IAnalyzer';
+import { IStringArrayStorageItemData } from '../../storages/string-array-storage/IStringArrayStorageItem';
+
+export interface IStringArrayStorageAnalyzer extends IAnalyzer<void> {
+    /**
+     * @param {Program} astTree
+     */
+    analyze (astTree: ESTree.Program): void;
+
+    /**
+     * @param {Literal} literalNode
+     * @returns {IStringArrayStorageItemData | undefined}
+     */
+    getItemDataForLiteralNode (literalNode: ESTree.Literal): IStringArrayStorageItemData | undefined;
+}

+ 4 - 4
src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IIdentifierObfuscatingReplacer.d.ts

@@ -6,16 +6,16 @@ import { IObfuscatingReplacer } from './IObfuscatingReplacer';
 
 
 export interface IIdentifierObfuscatingReplacer extends IObfuscatingReplacer <ESTree.Identifier> {
 export interface IIdentifierObfuscatingReplacer extends IObfuscatingReplacer <ESTree.Identifier> {
     /**
     /**
-     * @param {string} nodeValue
+     * @param {Identifier} identifierNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      */
      */
-    storeGlobalName (nodeValue: string, lexicalScopeNode: TNodeWithLexicalScope): void;
+    storeGlobalName (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): void;
 
 
     /**
     /**
-     * @param {string} nodeValue
+     * @param {Identifier} identifierNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      */
      */
-    storeLocalName (nodeValue: string, lexicalScopeNode: TNodeWithLexicalScope): void;
+    storeLocalName (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): void;
 
 
     /**
     /**
      * @param {string} name
      * @param {string} name

+ 6 - 2
src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IObfuscatingReplacer.d.ts

@@ -4,10 +4,14 @@ import { TNodeWithLexicalScope } from '../../../../types/node/TNodeWithLexicalSc
 
 
 export interface IObfuscatingReplacer <T = ESTree.Node> {
 export interface IObfuscatingReplacer <T = ESTree.Node> {
     /**
     /**
-     * @param {SimpleLiteral["value"]} nodeValue
+     * @param {Node} node
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @param {number} nodeIdentifier
      * @param {number} nodeIdentifier
      * @returns {T}
      * @returns {T}
      */
      */
-    replace (nodeValue: ESTree.SimpleLiteral['value'], lexicalScopeNode?: TNodeWithLexicalScope, nodeIdentifier?: number): T;
+    replace (
+        node: ESTree.Node,
+        lexicalScopeNode?: TNodeWithLexicalScope,
+        nodeIdentifier?: number
+    ): T;
 }
 }

+ 0 - 4
src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/IStringArrayIndexData.d.ts

@@ -1,4 +0,0 @@
-export interface IStringArrayIndexData {
-    fromCache: boolean;
-    index: string;
-}

+ 1 - 0
src/interfaces/options/IOptions.d.ts

@@ -27,6 +27,7 @@ export interface IOptions {
     readonly rotateStringArray: boolean;
     readonly rotateStringArray: boolean;
     readonly seed: string | number;
     readonly seed: string | number;
     readonly selfDefending: boolean;
     readonly selfDefending: boolean;
+    readonly shuffleStringArray: boolean;
     readonly sourceMap: boolean;
     readonly sourceMap: boolean;
     readonly sourceMapBaseUrl: string;
     readonly sourceMapBaseUrl: string;
     readonly sourceMapFileName: string;
     readonly sourceMapFileName: string;

+ 7 - 1
src/interfaces/storages/IArrayStorage.d.ts

@@ -1,11 +1,17 @@
 import { IInitializable } from '../IInitializable';
 import { IInitializable } from '../IInitializable';
 
 
 export interface IArrayStorage <V> extends IInitializable {
 export interface IArrayStorage <V> extends IInitializable {
+    /**
+     * @param {number} key
+     * @returns {V | undefined}
+     */
+    get (key: number): V | undefined;
+
     /**
     /**
      * @param {number} key
      * @param {number} key
      * @returns {V}
      * @returns {V}
      */
      */
-    get (key: number): V;
+    getOrThrow (key: number): V;
 
 
     /**
     /**
      * @param value
      * @param value

+ 7 - 1
src/interfaces/storages/IMapStorage.d.ts

@@ -1,11 +1,17 @@
 import { IInitializable } from '../IInitializable';
 import { IInitializable } from '../IInitializable';
 
 
 export interface IMapStorage <K, V> extends IInitializable {
 export interface IMapStorage <K, V> extends IInitializable {
+    /**
+     * @param {K} key
+     * @returns {V | undefined}
+     */
+    get (key: K): V | undefined;
+
     /**
     /**
      * @param {K} key
      * @param {K} key
      * @returns {V}
      * @returns {V}
      */
      */
-    get (key: K): V;
+    getOrThrow (key: K): V;
 
 
     /**
     /**
      * @param {V} value
      * @param {V} value

+ 24 - 0
src/interfaces/storages/string-array-storage/IStringArrayStorage.d.ts

@@ -0,0 +1,24 @@
+import { IStringArrayStorageItemData } from './IStringArrayStorageItem';
+
+import { IMapStorage } from '../IMapStorage';
+
+export interface IStringArrayStorage extends IMapStorage <string, IStringArrayStorageItemData> {
+    /**
+     * @returns {number}
+     */
+    getRotationAmount (): number;
+
+    /**
+     * @returns {string}
+     */
+    getStorageName (): string;
+
+    /**
+     * @returns {string}
+     */
+    getStorageCallsWrapperName (): string;
+
+    rotateStorage (): void;
+
+    shuffleStorage (): void;
+}

+ 6 - 0
src/interfaces/storages/string-array-storage/IStringArrayStorageItem.d.ts

@@ -0,0 +1,6 @@
+import { IEncodedValue } from '../../IEncodedValue';
+
+export interface IStringArrayStorageItemData extends IEncodedValue {
+    index: number;
+    value: string;
+}

+ 2 - 2
src/node-transformers/obfuscating-transformers/CatchClauseTransformer.ts

@@ -100,7 +100,7 @@ export class CatchClauseTransformer extends AbstractNodeTransformer {
         lexicalScopeNode: TNodeWithLexicalScope
         lexicalScopeNode: TNodeWithLexicalScope
     ): void {
     ): void {
         if (catchClauseNode.param && NodeGuards.isIdentifierNode(catchClauseNode.param)) {
         if (catchClauseNode.param && NodeGuards.isIdentifierNode(catchClauseNode.param)) {
-            this.identifierObfuscatingReplacer.storeLocalName(catchClauseNode.param.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeLocalName(catchClauseNode.param, lexicalScopeNode);
         }
         }
     }
     }
 
 
@@ -116,7 +116,7 @@ export class CatchClauseTransformer extends AbstractNodeTransformer {
             enter: (node: ESTree.Node, parentNode: ESTree.Node | null): void => {
             enter: (node: ESTree.Node, parentNode: ESTree.Node | null): void => {
                 if (parentNode && NodeGuards.isReplaceableIdentifierNode(node, parentNode)) {
                 if (parentNode && NodeGuards.isReplaceableIdentifierNode(node, parentNode)) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
                     const newIdentifierName: string = newIdentifier.name;
 
 
                     if (node.name !== newIdentifierName) {
                     if (node.name !== newIdentifierName) {

+ 4 - 4
src/node-transformers/obfuscating-transformers/ClassDeclarationTransformer.ts

@@ -130,9 +130,9 @@ export class ClassDeclarationTransformer extends AbstractNodeTransformer {
         isGlobalDeclaration: boolean
         isGlobalDeclaration: boolean
     ): void {
     ): void {
         if (isGlobalDeclaration) {
         if (isGlobalDeclaration) {
-            this.identifierObfuscatingReplacer.storeGlobalName(classDeclarationNode.id.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeGlobalName(classDeclarationNode.id, lexicalScopeNode);
         } else {
         } else {
-            this.identifierObfuscatingReplacer.storeLocalName(classDeclarationNode.id.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeLocalName(classDeclarationNode.id, lexicalScopeNode);
         }
         }
     }
     }
 
 
@@ -159,7 +159,7 @@ export class ClassDeclarationTransformer extends AbstractNodeTransformer {
         for (let i: number = 0; i < cachedReplaceableIdentifierLength; i++) {
         for (let i: number = 0; i < cachedReplaceableIdentifierLength; i++) {
             const replaceableIdentifier: ESTree.Identifier = cachedReplaceableIdentifiers[i];
             const replaceableIdentifier: ESTree.Identifier = cachedReplaceableIdentifiers[i];
             const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
             const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                .replace(replaceableIdentifier.name, lexicalScopeNode);
+                .replace(replaceableIdentifier, lexicalScopeNode);
 
 
             replaceableIdentifier.name = newReplaceableIdentifier.name;
             replaceableIdentifier.name = newReplaceableIdentifier.name;
             NodeMetadata.set(replaceableIdentifier, { renamedIdentifier: true });
             NodeMetadata.set(replaceableIdentifier, { renamedIdentifier: true });
@@ -180,7 +180,7 @@ export class ClassDeclarationTransformer extends AbstractNodeTransformer {
                     && !NodeMetadata.isRenamedIdentifier(node)
                     && !NodeMetadata.isRenamedIdentifier(node)
                 ) {
                 ) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
                     const newIdentifierName: string = newIdentifier.name;
 
 
                     if (node.name !== newIdentifierName) {
                     if (node.name !== newIdentifierName) {

+ 4 - 4
src/node-transformers/obfuscating-transformers/FunctionDeclarationTransformer.ts

@@ -130,9 +130,9 @@ export class FunctionDeclarationTransformer extends AbstractNodeTransformer {
         isGlobalDeclaration: boolean
         isGlobalDeclaration: boolean
     ): void {
     ): void {
         if (isGlobalDeclaration) {
         if (isGlobalDeclaration) {
-            this.identifierObfuscatingReplacer.storeGlobalName(functionDeclarationNode.id.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeGlobalName(functionDeclarationNode.id, lexicalScopeNode);
         } else {
         } else {
-            this.identifierObfuscatingReplacer.storeLocalName(functionDeclarationNode.id.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeLocalName(functionDeclarationNode.id, lexicalScopeNode);
         }
         }
     }
     }
 
 
@@ -159,7 +159,7 @@ export class FunctionDeclarationTransformer extends AbstractNodeTransformer {
         for (let i: number = 0; i < cachedReplaceableIdentifierLength; i++) {
         for (let i: number = 0; i < cachedReplaceableIdentifierLength; i++) {
             const replaceableIdentifier: ESTree.Identifier = cachedReplaceableIdentifiers[i];
             const replaceableIdentifier: ESTree.Identifier = cachedReplaceableIdentifiers[i];
             const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
             const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                .replace(replaceableIdentifier.name, lexicalScopeNode);
+                .replace(replaceableIdentifier, lexicalScopeNode);
 
 
             replaceableIdentifier.name = newReplaceableIdentifier.name;
             replaceableIdentifier.name = newReplaceableIdentifier.name;
             NodeMetadata.set(replaceableIdentifier, { renamedIdentifier: true });
             NodeMetadata.set(replaceableIdentifier, { renamedIdentifier: true });
@@ -181,7 +181,7 @@ export class FunctionDeclarationTransformer extends AbstractNodeTransformer {
                     && !NodeMetadata.isRenamedIdentifier(node)
                     && !NodeMetadata.isRenamedIdentifier(node)
                 ) {
                 ) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
                     const newIdentifierName: string = newIdentifier.name;
 
 
                     if (node.name !== newIdentifierName) {
                     if (node.name !== newIdentifierName) {

+ 3 - 3
src/node-transformers/obfuscating-transformers/FunctionTransformer.ts

@@ -154,13 +154,13 @@ export class FunctionTransformer extends AbstractNodeTransformer {
                 }
                 }
 
 
                 if (NodeGuards.isAssignmentPatternNode(node) && NodeGuards.isIdentifierNode(node.left)) {
                 if (NodeGuards.isAssignmentPatternNode(node) && NodeGuards.isIdentifierNode(node.left)) {
-                    this.identifierObfuscatingReplacer.storeLocalName(node.left.name, lexicalScopeNode);
+                    this.identifierObfuscatingReplacer.storeLocalName(node.left, lexicalScopeNode);
 
 
                     return estraverse.VisitorOption.Skip;
                     return estraverse.VisitorOption.Skip;
                 }
                 }
 
 
                 if (NodeGuards.isIdentifierNode(node)) {
                 if (NodeGuards.isIdentifierNode(node)) {
-                    this.identifierObfuscatingReplacer.storeLocalName(node.name, lexicalScopeNode);
+                    this.identifierObfuscatingReplacer.storeLocalName(node, lexicalScopeNode);
                 }
                 }
             }
             }
         };
         };
@@ -212,7 +212,7 @@ export class FunctionTransformer extends AbstractNodeTransformer {
                     }
                     }
 
 
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
                     const newIdentifierName: string = newIdentifier.name;
 
 
                     if (node.name !== newIdentifierName) {
                     if (node.name !== newIdentifierName) {

+ 3 - 3
src/node-transformers/obfuscating-transformers/ImportDeclarationTransformer.ts

@@ -126,7 +126,7 @@ export class ImportDeclarationTransformer extends AbstractNodeTransformer {
                 return;
                 return;
             }
             }
 
 
-            this.identifierObfuscatingReplacer.storeGlobalName(importSpecifierNode.local.name, lexicalScopeNode);
+            this.identifierObfuscatingReplacer.storeGlobalName(importSpecifierNode.local, lexicalScopeNode);
         });
         });
     }
     }
 
 
@@ -139,7 +139,7 @@ export class ImportDeclarationTransformer extends AbstractNodeTransformer {
 
 
         cachedReplaceableIdentifiers.forEach((replaceableIdentifier: ESTree.Identifier) => {
         cachedReplaceableIdentifiers.forEach((replaceableIdentifier: ESTree.Identifier) => {
             const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
             const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                .replace(replaceableIdentifier.name, lexicalScopeNode);
+                .replace(replaceableIdentifier, lexicalScopeNode);
 
 
             if (replaceableIdentifier.name !== newReplaceableIdentifier.name) {
             if (replaceableIdentifier.name !== newReplaceableIdentifier.name) {
                 replaceableIdentifier.name = newReplaceableIdentifier.name;
                 replaceableIdentifier.name = newReplaceableIdentifier.name;
@@ -162,7 +162,7 @@ export class ImportDeclarationTransformer extends AbstractNodeTransformer {
                     && !NodeMetadata.isRenamedIdentifier(node)
                     && !NodeMetadata.isRenamedIdentifier(node)
                 ) {
                 ) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
                     const newIdentifierName: string = newIdentifier.name;
 
 
                     if (node.name !== newIdentifierName) {
                     if (node.name !== newIdentifierName) {

+ 2 - 2
src/node-transformers/obfuscating-transformers/LabeledStatementTransformer.ts

@@ -106,7 +106,7 @@ export class LabeledStatementTransformer extends AbstractNodeTransformer {
         labeledStatementNode: ESTree.LabeledStatement,
         labeledStatementNode: ESTree.LabeledStatement,
         lexicalScopeNode: TNodeWithLexicalScope
         lexicalScopeNode: TNodeWithLexicalScope
     ): void {
     ): void {
-        this.identifierObfuscatingReplacer.storeLocalName(labeledStatementNode.label.name, lexicalScopeNode);
+        this.identifierObfuscatingReplacer.storeLocalName(labeledStatementNode.label, lexicalScopeNode);
     }
     }
 
 
     /**
     /**
@@ -121,7 +121,7 @@ export class LabeledStatementTransformer extends AbstractNodeTransformer {
             enter: (node: ESTree.Node, parentNode: ESTree.Node | null): void => {
             enter: (node: ESTree.Node, parentNode: ESTree.Node | null): void => {
                 if (parentNode && NodeGuards.isLabelIdentifierNode(node, parentNode)) {
                 if (parentNode && NodeGuards.isLabelIdentifierNode(node, parentNode)) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
 
 
                     node.name = newIdentifier.name;
                     node.name = newIdentifier.name;
                 }
                 }

+ 21 - 4
src/node-transformers/obfuscating-transformers/LiteralTransformer.ts

@@ -7,6 +7,7 @@ import { TLiteralObfuscatingReplacerFactory } from '../../types/container/node-t
 
 
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
 import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+import { IStringArrayStorageAnalyzer } from '../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
 import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
 import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
 
 
 import { LiteralObfuscatingReplacer } from '../../enums/node-transformers/obfuscating-transformers/obfuscating-replacers/LiteralObfuscatingReplacer';
 import { LiteralObfuscatingReplacer } from '../../enums/node-transformers/obfuscating-transformers/obfuscating-replacers/LiteralObfuscatingReplacer';
@@ -24,20 +25,28 @@ export class LiteralTransformer extends AbstractNodeTransformer {
      */
      */
     private readonly literalObfuscatingReplacerFactory: TLiteralObfuscatingReplacerFactory;
     private readonly literalObfuscatingReplacerFactory: TLiteralObfuscatingReplacerFactory;
 
 
+    /**
+     * @type {IStringArrayStorageAnalyzer}
+     */
+    private readonly stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer;
+
     /**
     /**
      * @param {TLiteralObfuscatingReplacerFactory} literalObfuscatingReplacerFactory
      * @param {TLiteralObfuscatingReplacerFactory} literalObfuscatingReplacerFactory
      * @param {IRandomGenerator} randomGenerator
      * @param {IRandomGenerator} randomGenerator
      * @param {IOptions} options
      * @param {IOptions} options
+     * @param {IStringArrayStorageAnalyzer} stringArrayStorageAnalyzer
      */
      */
     constructor (
     constructor (
         @inject(ServiceIdentifiers.Factory__IObfuscatingReplacer)
         @inject(ServiceIdentifiers.Factory__IObfuscatingReplacer)
             literalObfuscatingReplacerFactory: TLiteralObfuscatingReplacerFactory,
             literalObfuscatingReplacerFactory: TLiteralObfuscatingReplacerFactory,
         @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
         @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
-        @inject(ServiceIdentifiers.IOptions) options: IOptions
+        @inject(ServiceIdentifiers.IOptions) options: IOptions,
+        @inject(ServiceIdentifiers.IStringArrayStorageAnalyzer) stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer
     ) {
     ) {
         super(randomGenerator, options);
         super(randomGenerator, options);
 
 
         this.literalObfuscatingReplacerFactory = literalObfuscatingReplacerFactory;
         this.literalObfuscatingReplacerFactory = literalObfuscatingReplacerFactory;
+        this.stringArrayStorageAnalyzer = stringArrayStorageAnalyzer;
     }
     }
 
 
     /**
     /**
@@ -49,6 +58,10 @@ export class LiteralTransformer extends AbstractNodeTransformer {
             case TransformationStage.Obfuscating:
             case TransformationStage.Obfuscating:
                 return {
                 return {
                     enter: (node: ESTree.Node, parentNode: ESTree.Node | null) => {
                     enter: (node: ESTree.Node, parentNode: ESTree.Node | null) => {
+                        if (NodeGuards.isProgramNode(node)) {
+                            this.analyzeNode(node);
+                        }
+
                         if (parentNode && NodeGuards.isLiteralNode(node) && !NodeMetadata.isReplacedLiteral(node)) {
                         if (parentNode && NodeGuards.isLiteralNode(node) && !NodeMetadata.isReplacedLiteral(node)) {
                             return this.transformNode(node, parentNode);
                             return this.transformNode(node, parentNode);
                         }
                         }
@@ -60,6 +73,10 @@ export class LiteralTransformer extends AbstractNodeTransformer {
         }
         }
     }
     }
 
 
+    public analyzeNode (programNode: ESTree.Program): void {
+        this.stringArrayStorageAnalyzer.analyze(programNode);
+    }
+
     /**
     /**
      * @param {Literal} literalNode
      * @param {Literal} literalNode
      * @param {NodeGuards} parentNode
      * @param {NodeGuards} parentNode
@@ -76,21 +93,21 @@ export class LiteralTransformer extends AbstractNodeTransformer {
             case 'boolean':
             case 'boolean':
                 newLiteralNode = this.literalObfuscatingReplacerFactory(
                 newLiteralNode = this.literalObfuscatingReplacerFactory(
                     LiteralObfuscatingReplacer.BooleanLiteralObfuscatingReplacer
                     LiteralObfuscatingReplacer.BooleanLiteralObfuscatingReplacer
-                ).replace(literalNode.value);
+                ).replace(literalNode);
 
 
                 break;
                 break;
 
 
             case 'number':
             case 'number':
                 newLiteralNode = this.literalObfuscatingReplacerFactory(
                 newLiteralNode = this.literalObfuscatingReplacerFactory(
                     LiteralObfuscatingReplacer.NumberLiteralObfuscatingReplacer
                     LiteralObfuscatingReplacer.NumberLiteralObfuscatingReplacer
-                ).replace(literalNode.value);
+                ).replace(literalNode);
 
 
                 break;
                 break;
 
 
             case 'string':
             case 'string':
                 newLiteralNode = this.literalObfuscatingReplacerFactory(
                 newLiteralNode = this.literalObfuscatingReplacerFactory(
                     LiteralObfuscatingReplacer.StringLiteralObfuscatingReplacer
                     LiteralObfuscatingReplacer.StringLiteralObfuscatingReplacer
-                ).replace(literalNode.value);
+                ).replace(literalNode);
 
 
                 break;
                 break;
 
 

+ 4 - 4
src/node-transformers/obfuscating-transformers/VariableDeclarationTransformer.ts

@@ -153,9 +153,9 @@ export class VariableDeclarationTransformer extends AbstractNodeTransformer {
                 }
                 }
 
 
                 if (isGlobalDeclaration) {
                 if (isGlobalDeclaration) {
-                    this.identifierObfuscatingReplacer.storeGlobalName(identifierNode.name, lexicalScopeNode);
+                    this.identifierObfuscatingReplacer.storeGlobalName(identifierNode, lexicalScopeNode);
                 } else {
                 } else {
-                    this.identifierObfuscatingReplacer.storeLocalName(identifierNode.name, lexicalScopeNode);
+                    this.identifierObfuscatingReplacer.storeLocalName(identifierNode, lexicalScopeNode);
                 }
                 }
             }
             }
         );
         );
@@ -287,7 +287,7 @@ export class VariableDeclarationTransformer extends AbstractNodeTransformer {
                 }
                 }
 
 
                 const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
                 const newReplaceableIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                    .replace(replaceableIdentifier.name, lexicalScopeNode);
+                    .replace(replaceableIdentifier, lexicalScopeNode);
 
 
                 replaceableIdentifier.name = newReplaceableIdentifier.name;
                 replaceableIdentifier.name = newReplaceableIdentifier.name;
                 NodeMetadata.set(replaceableIdentifier, { renamedIdentifier: true });
                 NodeMetadata.set(replaceableIdentifier, { renamedIdentifier: true });
@@ -310,7 +310,7 @@ export class VariableDeclarationTransformer extends AbstractNodeTransformer {
                     && !NodeMetadata.isRenamedIdentifier(node)
                     && !NodeMetadata.isRenamedIdentifier(node)
                 ) {
                 ) {
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
                     const newIdentifier: ESTree.Identifier = this.identifierObfuscatingReplacer
-                        .replace(node.name, lexicalScopeNode);
+                        .replace(node, lexicalScopeNode);
                     const newIdentifierName: string = newIdentifier.name;
                     const newIdentifierName: string = newIdentifier.name;
 
 
                     if (node.name !== newIdentifierName) {
                     if (node.name !== newIdentifierName) {

+ 2 - 2
src/node-transformers/obfuscating-transformers/obfuscating-replacers/AbstractObfuscatingReplacer.ts

@@ -25,9 +25,9 @@ export abstract class AbstractObfuscatingReplacer implements IObfuscatingReplace
     }
     }
 
 
     /**
     /**
-     * @param {SimpleLiteral["value"]} nodeValue
+     * @param {Node} node
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @returns {Node}
      * @returns {Node}
      */
      */
-    public abstract replace (nodeValue: ESTree.SimpleLiteral['value'], lexicalScopeNode?: TNodeWithLexicalScope): ESTree.Node;
+    public abstract replace (node: ESTree.Node, lexicalScopeNode?: TNodeWithLexicalScope): ESTree.Node;
 }
 }

+ 21 - 15
src/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/BaseIdentifierObfuscatingReplacer.ts

@@ -40,35 +40,39 @@ export class BaseIdentifierObfuscatingReplacer extends AbstractObfuscatingReplac
     }
     }
 
 
     /**
     /**
-     * @param {string} nodeValue
+     * @param {Identifier} identifierNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @returns {Identifier}
      * @returns {Identifier}
      */
      */
-    public replace (nodeValue: string, lexicalScopeNode: TNodeWithLexicalScope): ESTree.Identifier {
+    public replace (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): ESTree.Identifier {
+        let identifierName: string = identifierNode.name;
+
         if (this.blockScopesMap.has(lexicalScopeNode)) {
         if (this.blockScopesMap.has(lexicalScopeNode)) {
             const namesMap: Map<string, string> = <Map<string, string>>this.blockScopesMap.get(lexicalScopeNode);
             const namesMap: Map<string, string> = <Map<string, string>>this.blockScopesMap.get(lexicalScopeNode);
 
 
-            if (namesMap.has(nodeValue)) {
-                nodeValue = <string>namesMap.get(nodeValue);
+            if (namesMap.has(identifierName)) {
+                identifierName = <string>namesMap.get(identifierName);
             }
             }
         }
         }
 
 
-        return NodeFactory.identifierNode(nodeValue);
+        return NodeFactory.identifierNode(identifierName);
     }
     }
 
 
     /**
     /**
      * Store `nodeName` of global identifiers as key in map with random name as value.
      * Store `nodeName` of global identifiers as key in map with random name as value.
      * Reserved name will be ignored.
      * Reserved name will be ignored.
      *
      *
-     * @param {string} nodeName
+     * @param {Node} identifierNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      */
      */
-    public storeGlobalName (nodeName: string, lexicalScopeNode: TNodeWithLexicalScope): void {
-        if (this.isReservedName(nodeName)) {
+    public storeGlobalName (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): void {
+        const identifierName: string = identifierNode.name;
+
+        if (this.isReservedName(identifierName)) {
             return;
             return;
         }
         }
 
 
-        const identifierName: string = this.identifierNamesGenerator.generateWithPrefix();
+        const newIdentifierName: string = this.identifierNamesGenerator.generateWithPrefix();
 
 
         if (!this.blockScopesMap.has(lexicalScopeNode)) {
         if (!this.blockScopesMap.has(lexicalScopeNode)) {
             this.blockScopesMap.set(lexicalScopeNode, new Map());
             this.blockScopesMap.set(lexicalScopeNode, new Map());
@@ -76,22 +80,24 @@ export class BaseIdentifierObfuscatingReplacer extends AbstractObfuscatingReplac
 
 
         const namesMap: Map<string, string> = <Map<string, string>>this.blockScopesMap.get(lexicalScopeNode);
         const namesMap: Map<string, string> = <Map<string, string>>this.blockScopesMap.get(lexicalScopeNode);
 
 
-        namesMap.set(nodeName, identifierName);
+        namesMap.set(identifierName, newIdentifierName);
     }
     }
 
 
     /**
     /**
      * Store `nodeName` of local identifier as key in map with random name as value.
      * Store `nodeName` of local identifier as key in map with random name as value.
      * Reserved name will be ignored.
      * Reserved name will be ignored.
      *
      *
-     * @param {string} nodeName
+     * @param {Identifier} identifierNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      * @param {TNodeWithLexicalScope} lexicalScopeNode
      */
      */
-    public storeLocalName (nodeName: string, lexicalScopeNode: TNodeWithLexicalScope): void {
-        if (this.isReservedName(nodeName)) {
+    public storeLocalName (identifierNode: ESTree.Identifier, lexicalScopeNode: TNodeWithLexicalScope): void {
+        const identifierName: string = identifierNode.name;
+
+        if (this.isReservedName(identifierName)) {
             return;
             return;
         }
         }
 
 
-        const identifierName: string = this.identifierNamesGenerator.generate();
+        const newIdentifierName: string = this.identifierNamesGenerator.generate();
 
 
         if (!this.blockScopesMap.has(lexicalScopeNode)) {
         if (!this.blockScopesMap.has(lexicalScopeNode)) {
             this.blockScopesMap.set(lexicalScopeNode, new Map());
             this.blockScopesMap.set(lexicalScopeNode, new Map());
@@ -99,7 +105,7 @@ export class BaseIdentifierObfuscatingReplacer extends AbstractObfuscatingReplac
 
 
         const namesMap: Map<string, string> = <Map<string, string>>this.blockScopesMap.get(lexicalScopeNode);
         const namesMap: Map<string, string> = <Map<string, string>>this.blockScopesMap.get(lexicalScopeNode);
 
 
-        namesMap.set(nodeName, identifierName);
+        namesMap.set(identifierName, newIdentifierName);
     }
     }
 
 
     /**
     /**

+ 9 - 3
src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/BooleanLiteralObfuscatingReplacer.ts

@@ -40,11 +40,17 @@ export class BooleanLiteralObfuscatingReplacer extends AbstractObfuscatingReplac
     }
     }
 
 
     /**
     /**
-     * @param {boolean} nodeValue
+     * @param {SimpleLiteral} literalNode
      * @returns {Node}
      * @returns {Node}
      */
      */
-    public replace (nodeValue: boolean): ESTree.Node {
-        return nodeValue
+    public replace (literalNode: ESTree.SimpleLiteral): ESTree.Node {
+        const literalValue: ESTree.SimpleLiteral['value'] = literalNode.value;
+
+        if (typeof literalValue !== 'boolean') {
+            throw new Error('`BooleanLiteralObfuscatingReplacer` should accept only literals with `boolean` value');
+        }
+
+        return literalValue
             ? BooleanLiteralObfuscatingReplacer.getTrueUnaryExpressionNode()
             ? BooleanLiteralObfuscatingReplacer.getTrueUnaryExpressionNode()
             : BooleanLiteralObfuscatingReplacer.getFalseUnaryExpressionNode();
             : BooleanLiteralObfuscatingReplacer.getFalseUnaryExpressionNode();
     }
     }

+ 15 - 9
src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/NumberLiteralObfuscatingReplacer.ts

@@ -27,24 +27,30 @@ export class NumberLiteralObfuscatingReplacer extends AbstractObfuscatingReplace
     }
     }
 
 
     /**
     /**
-     * @param {number} nodeValue
+     * @param {SimpleLiteral} literalNode
      * @returns {Node}
      * @returns {Node}
      */
      */
-    public replace (nodeValue: number): ESTree.Node {
+    public replace (literalNode: ESTree.SimpleLiteral): ESTree.Node {
+        const literalValue: ESTree.SimpleLiteral['value'] = literalNode.value;
+
+        if (typeof literalValue !== 'number') {
+            throw new Error('`NumberLiteralObfuscatingReplacer` should accept only literals with `number` value');
+        }
+
         let rawValue: string;
         let rawValue: string;
 
 
-        if (this.numberLiteralCache.has(nodeValue)) {
-            rawValue = <string>this.numberLiteralCache.get(nodeValue);
+        if (this.numberLiteralCache.has(literalValue)) {
+            rawValue = <string>this.numberLiteralCache.get(literalValue);
         } else {
         } else {
-            if (!NumberUtils.isCeil(nodeValue)) {
-                rawValue = String(nodeValue);
+            if (!NumberUtils.isCeil(literalValue)) {
+                rawValue = String(literalValue);
             } else {
             } else {
-                rawValue = `${Utils.hexadecimalPrefix}${NumberUtils.toHex(nodeValue)}`;
+                rawValue = `${Utils.hexadecimalPrefix}${NumberUtils.toHex(literalValue)}`;
             }
             }
 
 
-            this.numberLiteralCache.set(nodeValue, rawValue);
+            this.numberLiteralCache.set(literalValue, rawValue);
         }
         }
 
 
-        return NodeFactory.literalNode(nodeValue, rawValue);
+        return NodeFactory.literalNode(literalValue, rawValue);
     }
     }
 }
 }

+ 55 - 147
src/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/StringLiteralObfuscatingReplacer.ts

@@ -1,16 +1,16 @@
-import { inject, injectable, } from 'inversify';
+import { inject, injectable, postConstruct } from 'inversify';
 import { ServiceIdentifiers } from '../../../../container/ServiceIdentifiers';
 import { ServiceIdentifiers } from '../../../../container/ServiceIdentifiers';
 
 
 import * as ESTree from 'estree';
 import * as ESTree from 'estree';
 
 
-import { TStringArrayStorage } from '../../../../types/storages/TStringArrayStorage';
-
-import { ICryptUtils } from '../../../../interfaces/utils/ICryptUtils';
-import { IEncodedValue } from '../../../../interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/IEncodedValue';
 import { IEscapeSequenceEncoder } from '../../../../interfaces/utils/IEscapeSequenceEncoder';
 import { IEscapeSequenceEncoder } from '../../../../interfaces/utils/IEscapeSequenceEncoder';
+import { IInitializable } from '../../../../interfaces/IInitializable';
 import { IOptions } from '../../../../interfaces/options/IOptions';
 import { IOptions } from '../../../../interfaces/options/IOptions';
-import { IRandomGenerator } from '../../../../interfaces/utils/IRandomGenerator';
-import { IStringArrayIndexData } from '../../../../interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/IStringArrayIndexData';
+import { IStringArrayStorage } from '../../../../interfaces/storages/string-array-storage/IStringArrayStorage';
+import { IStringArrayStorageAnalyzer } from '../../../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
+import { IStringArrayStorageItemData } from '../../../../interfaces/storages/string-array-storage/IStringArrayStorageItem';
+
+import { initializable } from '../../../../decorators/Initializable';
 
 
 import { StringArrayEncoding } from '../../../../enums/StringArrayEncoding';
 import { StringArrayEncoding } from '../../../../enums/StringArrayEncoding';
 
 
@@ -21,27 +21,7 @@ import { NumberUtils } from '../../../../utils/NumberUtils';
 import { Utils } from '../../../../utils/Utils';
 import { Utils } from '../../../../utils/Utils';
 
 
 @injectable()
 @injectable()
-export class StringLiteralObfuscatingReplacer extends AbstractObfuscatingReplacer {
-    /**
-     * @type {number}
-     */
-    private static readonly minimumLengthForStringArray: number = 3;
-
-    /**
-     * @type {number}
-     */
-    private static readonly rc4KeyLength: number = 4;
-
-    /**
-     * @type {number}
-     */
-    private static readonly rc4KeysCount: number = 50;
-
-    /**
-     * @type {ICryptUtils}
-     */
-    private readonly cryptUtils: ICryptUtils;
-
+export class StringLiteralObfuscatingReplacer extends AbstractObfuscatingReplacer implements IInitializable {
     /**
     /**
      * @type {IEscapeSequenceEncoder}
      * @type {IEscapeSequenceEncoder}
      */
      */
@@ -53,55 +33,38 @@ export class StringLiteralObfuscatingReplacer extends AbstractObfuscatingReplace
     private readonly nodesCache: Map <string, ESTree.Node> = new Map();
     private readonly nodesCache: Map <string, ESTree.Node> = new Map();
 
 
     /**
     /**
-     * @type {IRandomGenerator}
+     * @type {IStringArrayStorage}
      */
      */
-    private readonly randomGenerator: IRandomGenerator;
+    private readonly stringArrayStorage: IStringArrayStorage;
 
 
     /**
     /**
-     * @type {string[]}
+     * @type {IStringArrayStorageAnalyzer}
      */
      */
-    private readonly rc4Keys: string[];
+    private readonly stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer;
 
 
     /**
     /**
-     * @type {Map<string, string>}
+     * @type {string}
      */
      */
-    private readonly stringLiteralHexadecimalIndexCache: Map <string, string> = new Map();
+    @initializable()
+    private stringArrayStorageCallsWrapperName!: string;
 
 
     /**
     /**
-     * @type {TStringArrayStorage}
-     */
-    private readonly stringArrayStorage: TStringArrayStorage;
-
-    /**
-     * @param {TStringArrayStorage} stringArrayStorage
+     * @param {IStringArrayStorage} stringArrayStorage
+     * @param {IStringArrayStorageAnalyzer} stringArrayStorageAnalyzer
      * @param {IEscapeSequenceEncoder} escapeSequenceEncoder
      * @param {IEscapeSequenceEncoder} escapeSequenceEncoder
-     * @param {IRandomGenerator} randomGenerator
-     * @param {ICryptUtils} cryptUtils
      * @param {IOptions} options
      * @param {IOptions} options
      */
      */
     constructor (
     constructor (
-        @inject(ServiceIdentifiers.TStringArrayStorage) stringArrayStorage: TStringArrayStorage,
+        @inject(ServiceIdentifiers.TStringArrayStorage) stringArrayStorage: IStringArrayStorage,
+        @inject(ServiceIdentifiers.IStringArrayStorageAnalyzer) stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer,
         @inject(ServiceIdentifiers.IEscapeSequenceEncoder) escapeSequenceEncoder: IEscapeSequenceEncoder,
         @inject(ServiceIdentifiers.IEscapeSequenceEncoder) escapeSequenceEncoder: IEscapeSequenceEncoder,
-        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
-        @inject(ServiceIdentifiers.ICryptUtils) cryptUtils: ICryptUtils,
         @inject(ServiceIdentifiers.IOptions) options: IOptions
         @inject(ServiceIdentifiers.IOptions) options: IOptions
     ) {
     ) {
-        super(
-            options
-        );
+        super(options);
 
 
         this.stringArrayStorage = stringArrayStorage;
         this.stringArrayStorage = stringArrayStorage;
+        this.stringArrayStorageAnalyzer = stringArrayStorageAnalyzer;
         this.escapeSequenceEncoder = escapeSequenceEncoder;
         this.escapeSequenceEncoder = escapeSequenceEncoder;
-        this.randomGenerator = randomGenerator;
-        this.cryptUtils = cryptUtils;
-
-        this.rc4Keys = this.randomGenerator.getRandomGenerator()
-            .n(
-                () => this.randomGenerator.getRandomGenerator().string({
-                    length: StringLiteralObfuscatingReplacer.rc4KeyLength
-                }),
-                StringLiteralObfuscatingReplacer.rc4KeysCount
-            );
     }
     }
 
 
     /**
     /**
@@ -128,89 +91,46 @@ export class StringLiteralObfuscatingReplacer extends AbstractObfuscatingReplace
         return rc4KeyLiteralNode;
         return rc4KeyLiteralNode;
     }
     }
 
 
-    /**
-     * @param {string} nodeValue
-     * @returns {Node}
-     */
-    public replace (nodeValue: string): ESTree.Node {
-        const useStringArray: boolean = this.canUseStringArray(nodeValue);
-        const cacheKey: string = `${nodeValue}-${String(useStringArray)}`;
-        const useCacheValue: boolean = this.nodesCache.has(cacheKey) && this.options.stringArrayEncoding !== StringArrayEncoding.Rc4;
+    @postConstruct()
+    public initialize (): void {
+        this.stringArrayStorageCallsWrapperName = this.stringArrayStorage.getStorageCallsWrapperName();
 
 
-        if (useCacheValue) {
-            return <ESTree.Node>this.nodesCache.get(cacheKey);
+        if (this.options.shuffleStringArray) {
+            this.stringArrayStorage.shuffleStorage();
         }
         }
 
 
-        const resultNode: ESTree.Node = useStringArray
-            ? this.replaceWithStringArrayCallNode(nodeValue)
-            : this.replaceWithLiteralNode(nodeValue);
-
-        this.nodesCache.set(cacheKey, resultNode);
-
-        return resultNode;
-    }
-
-    /**
-     * @param {string} nodeValue
-     * @returns {boolean}
-     */
-    private canUseStringArray (nodeValue: string): boolean {
-        return (
-            this.options.stringArray &&
-            nodeValue.length >= StringLiteralObfuscatingReplacer.minimumLengthForStringArray &&
-            this.randomGenerator.getMathRandom() <= this.options.stringArrayThreshold
-        );
-    }
-
-    /**
-     * @param {string} value
-     * @param {number} stringArrayStorageLength
-     * @returns {IStringArrayIndexData}
-     */
-    private getStringArrayHexadecimalIndex (value: string, stringArrayStorageLength: number): IStringArrayIndexData {
-        if (this.stringLiteralHexadecimalIndexCache.has(value)) {
-            return {
-                fromCache: true,
-                index: <string>this.stringLiteralHexadecimalIndexCache.get(value)
-            };
+        if (this.options.rotateStringArray) {
+            this.stringArrayStorage.rotateStorage();
         }
         }
-
-        const hexadecimalRawIndex: string = NumberUtils.toHex(stringArrayStorageLength);
-        const hexadecimalIndex: string = `${Utils.hexadecimalPrefix}${hexadecimalRawIndex}`;
-
-        this.stringLiteralHexadecimalIndexCache.set(value, hexadecimalIndex);
-
-        return {
-            fromCache: false,
-            index: hexadecimalIndex
-        };
     }
     }
 
 
     /**
     /**
-     * @param {string} value
-     * @returns {IEncodedValue}
+     * @param {SimpleLiteral} literalNode
+     * @returns {Node}
      */
      */
-    private getEncodedValue (value: string): IEncodedValue {
-        let encodedValue: string;
-        let key: string | null = null;
+    public replace (literalNode: ESTree.SimpleLiteral): ESTree.Node {
+        const literalValue: ESTree.SimpleLiteral['value'] = literalNode.value;
 
 
-        switch (this.options.stringArrayEncoding) {
-            case StringArrayEncoding.Rc4:
-                key = this.randomGenerator.getRandomGenerator().pickone(this.rc4Keys);
-                encodedValue = this.cryptUtils.btoa(this.cryptUtils.rc4(value, key));
+        if (typeof literalValue !== 'string') {
+            throw new Error('`StringLiteralObfuscatingReplacer` should accept only literals with `string` value');
+        }
 
 
-                break;
+        const stringArrayStorageItemData: IStringArrayStorageItemData | undefined = this.stringArrayStorageAnalyzer
+            .getItemDataForLiteralNode(literalNode);
+        const cacheKey: string = `${literalValue}-${Boolean(stringArrayStorageItemData)}`;
+        const useCachedValue: boolean = this.nodesCache.has(cacheKey) && this.options.stringArrayEncoding !== StringArrayEncoding.Rc4;
 
 
-            case StringArrayEncoding.Base64:
-                encodedValue = this.cryptUtils.btoa(value);
+        if (useCachedValue) {
+            return <ESTree.Node>this.nodesCache.get(cacheKey);
+        }
 
 
-                break;
+        const resultNode: ESTree.Node = stringArrayStorageItemData
+            ? this.replaceWithStringArrayCallNode(stringArrayStorageItemData)
+            : this.replaceWithLiteralNode(literalValue);
 
 
-            default:
-                encodedValue = value;
-        }
+        this.nodesCache.set(cacheKey, resultNode);
 
 
-        return { encodedValue, key };
+        return resultNode;
     }
     }
 
 
     /**
     /**
@@ -224,36 +144,24 @@ export class StringLiteralObfuscatingReplacer extends AbstractObfuscatingReplace
     }
     }
 
 
     /**
     /**
-     * @param {string} value
+     * @param {IStringArrayStorageItemData} stringArrayStorageItemData
      * @returns {Node}
      * @returns {Node}
      */
      */
-    private replaceWithStringArrayCallNode (value: string): ESTree.Node {
-        const { encodedValue, key }: IEncodedValue = this.getEncodedValue(value);
-        const escapedValue: string = this.escapeSequenceEncoder.encode(encodedValue, this.options.unicodeEscapeSequence);
-
-        const stringArrayStorageLength: number = this.stringArrayStorage.getLength();
-        const stringArrayStorageCallsWrapperName: string = this.stringArrayStorage.getStorageId().split('|')[1];
-
-        const { fromCache, index }: IStringArrayIndexData = this.getStringArrayHexadecimalIndex(
-            escapedValue,
-            stringArrayStorageLength
-        );
-
-        if (!fromCache) {
-            this.stringArrayStorage.set(stringArrayStorageLength, escapedValue);
-        }
+    private replaceWithStringArrayCallNode (stringArrayStorageItemData: IStringArrayStorageItemData): ESTree.Node {
+        const { index, decodeKey } = stringArrayStorageItemData;
 
 
+        const hexadecimalIndex: string = `${Utils.hexadecimalPrefix}${NumberUtils.toHex(index)}`;
         const callExpressionArgs: (ESTree.Expression | ESTree.SpreadElement)[] = [
         const callExpressionArgs: (ESTree.Expression | ESTree.SpreadElement)[] = [
-            StringLiteralObfuscatingReplacer.getHexadecimalLiteralNode(index)
+            StringLiteralObfuscatingReplacer.getHexadecimalLiteralNode(hexadecimalIndex)
         ];
         ];
 
 
-        if (key) {
+        if (decodeKey) {
             callExpressionArgs.push(StringLiteralObfuscatingReplacer.getRc4KeyLiteralNode(
             callExpressionArgs.push(StringLiteralObfuscatingReplacer.getRc4KeyLiteralNode(
-                this.escapeSequenceEncoder.encode(key, this.options.unicodeEscapeSequence)
+                this.escapeSequenceEncoder.encode(decodeKey, this.options.unicodeEscapeSequence)
             ));
             ));
         }
         }
 
 
-        const stringArrayIdentifierNode: ESTree.Identifier = NodeFactory.identifierNode(stringArrayStorageCallsWrapperName);
+        const stringArrayIdentifierNode: ESTree.Identifier = NodeFactory.identifierNode(this.stringArrayStorageCallsWrapperName);
 
 
         // prevent obfuscation of this identifier
         // prevent obfuscation of this identifier
         NodeMetadata.set(stringArrayIdentifierNode, { renamedIdentifier: true });
         NodeMetadata.set(stringArrayIdentifierNode, { renamedIdentifier: true });

+ 6 - 0
src/options/Options.ts

@@ -188,6 +188,12 @@ export class Options implements IOptions {
     @IsBoolean()
     @IsBoolean()
     public readonly selfDefending!: boolean;
     public readonly selfDefending!: boolean;
 
 
+    /**
+     * @type {boolean}
+     */
+    @IsBoolean()
+    public readonly shuffleStringArray!: boolean;
+
     /**
     /**
      * @type {boolean}
      * @type {boolean}
      */
      */

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

@@ -11,6 +11,7 @@ export const StringArrayRule: TOptionsNormalizerRule = (options: IOptions): IOpt
         options = {
         options = {
             ...options,
             ...options,
             rotateStringArray: false,
             rotateStringArray: false,
+            shuffleStringArray: false,
             stringArray: false,
             stringArray: false,
             stringArrayEncoding: false,
             stringArrayEncoding: false,
             stringArrayThreshold: 0
             stringArrayThreshold: 0

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

@@ -27,6 +27,7 @@ export const DEFAULT_PRESET: TInputOptions = Object.freeze({
     rotateStringArray: true,
     rotateStringArray: true,
     seed: 0,
     seed: 0,
     selfDefending: false,
     selfDefending: false,
+    shuffleStringArray: true,
     sourceMap: false,
     sourceMap: false,
     sourceMapBaseUrl: '',
     sourceMapBaseUrl: '',
     sourceMapFileName: '',
     sourceMapFileName: '',

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

@@ -26,6 +26,7 @@ export const NO_ADDITIONAL_NODES_PRESET: TInputOptions = Object.freeze({
     rotateStringArray: false,
     rotateStringArray: false,
     seed: 0,
     seed: 0,
     selfDefending: false,
     selfDefending: false,
+    shuffleStringArray: false,
     sourceMap: false,
     sourceMap: false,
     sourceMapBaseUrl: '',
     sourceMapBaseUrl: '',
     sourceMapFileName: '',
     sourceMapFileName: '',

+ 10 - 2
src/storages/ArrayStorage.ts

@@ -54,12 +54,20 @@ export abstract class ArrayStorage <V> implements IArrayStorage <V> {
         this.storageId = this.randomGenerator.getRandomString(6);
         this.storageId = this.randomGenerator.getRandomString(6);
     }
     }
 
 
+    /**
+     * @param {number} key
+     * @returns {V | undefined}
+     */
+    public get (key: number): V | undefined {
+        return this.storage[key];
+    }
+
     /**
     /**
      * @param {number} key
      * @param {number} key
      * @returns {V}
      * @returns {V}
      */
      */
-    public get (key: number): V {
-        const value: V | undefined = this.storage[key];
+    public getOrThrow (key: number): V {
+        const value: V | undefined = this.get(key);
 
 
         if (!value) {
         if (!value) {
             throw new Error(`No value found in array storage with key \`${key}\``);
             throw new Error(`No value found in array storage with key \`${key}\``);

+ 10 - 2
src/storages/MapStorage.ts

@@ -49,12 +49,20 @@ export abstract class MapStorage <K, V> implements IMapStorage <K, V> {
         this.storageId = this.randomGenerator.getRandomString(6);
         this.storageId = this.randomGenerator.getRandomString(6);
     }
     }
 
 
+    /**
+     * @param {K} key
+     * @returns {V | undefined}
+     */
+    public get (key: K): V | undefined {
+        return this.storage.get(key);
+    }
+
     /**
     /**
      * @param {K} key
      * @param {K} key
      * @returns {V}
      * @returns {V}
      */
      */
-    public get (key: K): V {
-        const value: V | undefined = this.storage.get(key);
+    public getOrThrow (key: K): V {
+        const value: V | undefined = this.get(key);
 
 
         if (!value) {
         if (!value) {
             throw new Error(`No value found in map storage with key \`${key}\``);
             throw new Error(`No value found in map storage with key \`${key}\``);

+ 215 - 15
src/storages/string-array/StringArrayStorage.ts

@@ -1,17 +1,46 @@
 import { inject, injectable, postConstruct } from 'inversify';
 import { inject, injectable, postConstruct } from 'inversify';
 import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
 import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
 
 
-import { IArrayUtils } from '../../interfaces/utils/IArrayUtils';
-import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
-
 import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
 import { TIdentifierNamesGeneratorFactory } from '../../types/container/generators/TIdentifierNamesGeneratorFactory';
 import { IIdentifierNamesGenerator } from '../../interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator';
 import { IIdentifierNamesGenerator } from '../../interfaces/generators/identifier-names-generators/IIdentifierNamesGenerator';
+
+import { IArrayUtils } from '../../interfaces/utils/IArrayUtils';
+import { ICryptUtils } from '../../interfaces/utils/ICryptUtils';
+import { IEncodedValue } from '../../interfaces/IEncodedValue';
+import { IEscapeSequenceEncoder } from '../../interfaces/utils/IEscapeSequenceEncoder';
 import { IOptions } from '../../interfaces/options/IOptions';
 import { IOptions } from '../../interfaces/options/IOptions';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+import { IStringArrayStorage } from '../../interfaces/storages/string-array-storage/IStringArrayStorage';
+import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-storage/IStringArrayStorageItem';
 
 
-import { ArrayStorage } from '../ArrayStorage';
+import { initializable } from '../../decorators/Initializable';
+
+import { StringArrayEncoding } from '../../enums/StringArrayEncoding';
+
+import { MapStorage } from '../MapStorage';
 
 
 @injectable()
 @injectable()
-export class StringArrayStorage extends ArrayStorage <string> {
+export class StringArrayStorage extends MapStorage <string, IStringArrayStorageItemData> implements IStringArrayStorage {
+    /**
+     * @type {number}
+     */
+    private static readonly minimumRotationAmount: number = 100;
+
+    /**
+     * @type {number}
+     */
+    private static readonly maximumRotationAmount: number = 500;
+
+    /**
+     * @type {number}
+     */
+    private static readonly rc4KeyLength: number = 4;
+
+    /**
+     * @type {number}
+     */
+    private static readonly rc4KeysCount: number = 50;
+
     /**
     /**
      * @type {number}
      * @type {number}
      */
      */
@@ -22,28 +51,74 @@ export class StringArrayStorage extends ArrayStorage <string> {
      */
      */
     private readonly arrayUtils: IArrayUtils;
     private readonly arrayUtils: IArrayUtils;
 
 
+    /**
+     * @type {ICryptUtils}
+     */
+    private readonly cryptUtils: ICryptUtils;
+
+    /**
+     * @type {IEscapeSequenceEncoder}
+     */
+    private readonly escapeSequenceEncoder: IEscapeSequenceEncoder;
+
     /**
     /**
      * @type {IIdentifierNamesGenerator}
      * @type {IIdentifierNamesGenerator}
      */
      */
     private readonly identifierNamesGenerator: IIdentifierNamesGenerator;
     private readonly identifierNamesGenerator: IIdentifierNamesGenerator;
 
 
+    /**
+     * @type {string[]}
+     */
+    private readonly rc4Keys: string[];
+
+    /**
+     * @type {number}
+     */
+    private rotationAmount: number = 0;
+
+    /**
+     * @type {string}
+     */
+    @initializable()
+    private stringArrayStorageName!: string;
+
+    /**
+     * @type {string}
+     */
+    @initializable()
+    private stringArrayStorageCallsWrapperName!: string;
+
     /**
     /**
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {TIdentifierNamesGeneratorFactory} identifierNamesGeneratorFactory
      * @param {IArrayUtils} arrayUtils
      * @param {IArrayUtils} arrayUtils
      * @param {IRandomGenerator} randomGenerator
      * @param {IRandomGenerator} randomGenerator
      * @param {IOptions} options
      * @param {IOptions} options
+     * @param {ICryptUtils} cryptUtils
+     * @param {IEscapeSequenceEncoder} escapeSequenceEncoder
      */
      */
     constructor (
     constructor (
         @inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
         @inject(ServiceIdentifiers.Factory__IIdentifierNamesGenerator)
             identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
             identifierNamesGeneratorFactory: TIdentifierNamesGeneratorFactory,
         @inject(ServiceIdentifiers.IArrayUtils) arrayUtils: IArrayUtils,
         @inject(ServiceIdentifiers.IArrayUtils) arrayUtils: IArrayUtils,
         @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
         @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
-        @inject(ServiceIdentifiers.IOptions) options: IOptions
+        @inject(ServiceIdentifiers.IOptions) options: IOptions,
+        @inject(ServiceIdentifiers.ICryptUtils) cryptUtils: ICryptUtils,
+        @inject(ServiceIdentifiers.IEscapeSequenceEncoder) escapeSequenceEncoder: IEscapeSequenceEncoder
     ) {
     ) {
         super(randomGenerator, options);
         super(randomGenerator, options);
 
 
         this.identifierNamesGenerator = identifierNamesGeneratorFactory(options);
         this.identifierNamesGenerator = identifierNamesGeneratorFactory(options);
         this.arrayUtils = arrayUtils;
         this.arrayUtils = arrayUtils;
+        this.cryptUtils = cryptUtils;
+        this.escapeSequenceEncoder = escapeSequenceEncoder;
+
+        this.rc4Keys = this.randomGenerator.getRandomGenerator()
+            .n(
+                () => this.randomGenerator.getRandomGenerator().string({
+                    length: StringArrayStorage.rc4KeyLength
+                }),
+                StringArrayStorage.rc4KeysCount
+            );
     }
     }
 
 
     @postConstruct()
     @postConstruct()
@@ -54,25 +129,150 @@ export class StringArrayStorage extends ArrayStorage <string> {
             .generate(StringArrayStorage.stringArrayNameLength);
             .generate(StringArrayStorage.stringArrayNameLength);
         const baseStringArrayCallsWrapperName: string = this.identifierNamesGenerator
         const baseStringArrayCallsWrapperName: string = this.identifierNamesGenerator
             .generate(StringArrayStorage.stringArrayNameLength);
             .generate(StringArrayStorage.stringArrayNameLength);
-        const stringArrayName: string = `${this.options.identifiersPrefix}${baseStringArrayName}`;
-        const stringArrayCallsWrapperName: string = `${this.options.identifiersPrefix}${baseStringArrayCallsWrapperName}`;
 
 
-        this.storageId = `${stringArrayName}|${stringArrayCallsWrapperName}`;
+        this.stringArrayStorageName = `${this.options.identifiersPrefix}${baseStringArrayName}`;
+        this.stringArrayStorageCallsWrapperName = `${this.options.identifiersPrefix}${baseStringArrayCallsWrapperName}`;
+
+        this.rotationAmount = this.options.rotateStringArray
+            ? this.randomGenerator.getRandomInteger(
+                StringArrayStorage.minimumRotationAmount,
+                StringArrayStorage.maximumRotationAmount
+            )
+            : 0;
+    }
+
+    /**
+     * @param {string} value
+     * @returns {IStringArrayStorageItemData}
+     */
+    public get (value: string): IStringArrayStorageItemData {
+        return this.getOrSetIfDoesNotExist(value);
+    }
+
+    /**
+     * @returns {number}
+     */
+    public getRotationAmount (): number {
+        return this.rotationAmount;
+    }
+
+    /**
+     * @returns {string}
+     */
+    public getStorageId (): string {
+        return this.stringArrayStorageName;
     }
     }
 
 
     /**
     /**
-     * @param {number} rotationValue
+     * @returns {string}
      */
      */
-    public rotateArray (rotationValue: number): void {
-        this.storage = this.arrayUtils.rotate(this.storage, rotationValue);
+    public getStorageName (): string {
+        return this.getStorageId();
+    }
+
+    /**
+     * @returns {string}
+     */
+    public getStorageCallsWrapperName (): string {
+        return this.stringArrayStorageCallsWrapperName;
+    }
+
+    public rotateStorage (): void {
+        if (!this.getLength()) {
+            return;
+        }
+
+        this.storage = new Map(
+            this.arrayUtils.rotate(
+                Array.from(this.storage.entries()),
+                this.rotationAmount
+            )
+        );
+    }
+
+    public shuffleStorage (): void {
+        this.storage = new Map(
+            this.arrayUtils
+                .shuffle(Array.from(this.storage.entries()))
+                .map<[string, IStringArrayStorageItemData]>(
+                    (
+                        [value, stringArrayStorageItemData]: [string, IStringArrayStorageItemData],
+                        index: number
+                    ) => {
+                        stringArrayStorageItemData.index = index;
+
+                        return [value, stringArrayStorageItemData];
+                    }
+                )
+                .sort((
+                    [, stringArrayStorageItemDataA]: [string, IStringArrayStorageItemData],
+                    [, stringArrayStorageItemDataB]: [string, IStringArrayStorageItemData]
+                ) => stringArrayStorageItemDataA.index - stringArrayStorageItemDataB.index)
+        );
     }
     }
 
 
     /**
     /**
      * @returns {string}
      * @returns {string}
      */
      */
     public toString (): string {
     public toString (): string {
-        return this.storage.map((value: string) => {
-            return `'${value}'`;
-        }).toString();
+        return Array
+            .from(this.storage.values())
+            .map((stringArrayStorageItemData: IStringArrayStorageItemData) => {
+                return `'${this.escapeSequenceEncoder.encode(
+                    stringArrayStorageItemData.encodedValue,
+                    this.options.unicodeEscapeSequence
+                )}'`;
+            }).toString();
+    }
+
+    /**
+     * @param {string} value
+     * @returns {IStringArrayStorageItemData}
+     */
+    private getOrSetIfDoesNotExist (value: string): IStringArrayStorageItemData {
+        const { encodedValue, decodeKey }: IEncodedValue = this.getEncodedValue(value);
+        const storedStringArrayStorageItemData: IStringArrayStorageItemData | undefined = this.storage.get(encodedValue);
+
+        if (storedStringArrayStorageItemData) {
+            return storedStringArrayStorageItemData;
+        }
+
+        const stringArrayStorageItemData: IStringArrayStorageItemData = {
+            encodedValue,
+            decodeKey,
+            value,
+            index: this.getLength()
+        };
+
+        this.storage.set(encodedValue, stringArrayStorageItemData);
+
+        return stringArrayStorageItemData;
+    }
+
+    /**
+     * @param {string} value
+     * @returns {IEncodedValue}
+     */
+    private getEncodedValue (value: string): IEncodedValue {
+        let encodedValue: string;
+        let decodeKey: string | null = null;
+
+        switch (this.options.stringArrayEncoding) {
+            case StringArrayEncoding.Rc4:
+                decodeKey = this.randomGenerator.getRandomGenerator().pickone(this.rc4Keys);
+                encodedValue = this.cryptUtils.btoa(this.cryptUtils.rc4(value, decodeKey));
+
+                break;
+
+            case StringArrayEncoding.Base64:
+                encodedValue = this.cryptUtils.btoa(value);
+
+                break;
+
+            default:
+                encodedValue = value;
+        }
+
+        return { encodedValue, decodeKey };
     }
     }
 }
 }

+ 1 - 1
src/templates/string-array-nodes/string-array-rotate-function-node/StringArrayRotateFunctionTemplate.ts

@@ -11,6 +11,6 @@ export function StringArrayRotateFunctionTemplate (): string {
             };
             };
             
             
             {code}
             {code}
-        })({stringArrayName}, 0x{stringArrayRotateValue});
+        })({stringArrayName}, 0x{stringArrayRotationAmount});
     `;
     `;
 }
 }

+ 0 - 3
src/types/storages/TStringArrayStorage.d.ts

@@ -1,3 +0,0 @@
-import { IArrayStorage } from '../../interfaces/storages/IArrayStorage';
-
-export type TStringArrayStorage = IArrayStorage <string>;

+ 8 - 4
test/dev/dev.ts

@@ -7,14 +7,18 @@ import { NO_ADDITIONAL_NODES_PRESET } from '../../src/options/presets/NoCustomNo
 
 
     let obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
     let obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
         `
         `
-            class Foo {
-                'bar'() {}
-            }
+            var foo = 'foo';
+            var bar = 'bar';
+            var baz = 'baz';
+            var bark = 'bark';
+            
+            console.log(foo, bar, baz, bark);
         `,
         `,
         {
         {
             ...NO_ADDITIONAL_NODES_PRESET,
             ...NO_ADDITIONAL_NODES_PRESET,
             stringArray: true,
             stringArray: true,
-            stringArrayThreshold: 1
+            stringArrayThreshold: 1,
+            shuffleStringArray: true
         }
         }
     ).getObfuscatedCode();
     ).getObfuscatedCode();
 
 

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

@@ -1,6 +1,8 @@
 import { assert } from 'chai';
 import { assert } from 'chai';
 import { TypeFromEnum } from '@gradecam/tsenum';
 import { TypeFromEnum } from '@gradecam/tsenum';
 
 
+import { TInputOptions } from '../../../src/types/options/TInputOptions';
+
 import { IObfuscatedCode } from '../../../src/interfaces/source-code/IObfuscatedCode';
 import { IObfuscatedCode } from '../../../src/interfaces/source-code/IObfuscatedCode';
 
 
 import { SourceMapMode } from '../../../src/enums/source-map/SourceMapMode';
 import { SourceMapMode } from '../../../src/enums/source-map/SourceMapMode';
@@ -15,7 +17,6 @@ import { IdentifierNamesGenerator } from '../../../src/enums/generators/identifi
 import { buildLargeCode } from '../../helpers/buildLargeCode';
 import { buildLargeCode } from '../../helpers/buildLargeCode';
 import { getRegExpMatch } from '../../helpers/getRegExpMatch';
 import { getRegExpMatch } from '../../helpers/getRegExpMatch';
 import { readFileAsString } from '../../helpers/readFileAsString';
 import { readFileAsString } from '../../helpers/readFileAsString';
-import { TInputOptions } from '../../../src/types/options/TInputOptions';
 
 
 describe('JavaScriptObfuscator', () => {
 describe('JavaScriptObfuscator', () => {
     describe('obfuscate', () => {
     describe('obfuscate', () => {

+ 58 - 15
test/functional-tests/node-transformers/obfuscating-transformers/literal-transformer/LiteralTransformer.spec.ts

@@ -6,6 +6,7 @@ import { StringArrayEncoding } from '../../../../../src/enums/StringArrayEncodin
 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 { getRegExpMatch } from '../../../../helpers/getRegExpMatch';
 
 
 import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade';
 import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade';
 
 
@@ -214,26 +215,68 @@ describe('LiteralTransformer', () => {
         });
         });
 
 
         describe('Variant #9: rc4 encoding', () => {
         describe('Variant #9: rc4 encoding', () => {
-            const regExp: RegExp = /var *test *= *_0x([a-f0-9]){4}\('0x0', *'.{4}'\);/;
+            describe('Variant #1: single string literal', () => {
+                const regExp: RegExp = /var *test *= *_0x([a-f0-9]){4}\('0x0', *'.{4}'\);/;
 
 
-            let obfuscatedCode: string;
+                let obfuscatedCode: string;
 
 
-            before(() => {
-                const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
 
 
-                obfuscatedCode = JavaScriptObfuscator.obfuscate(
-                    code,
-                    {
-                        ...NO_ADDITIONAL_NODES_PRESET,
-                        stringArray: true,
-                        stringArrayEncoding: StringArrayEncoding.Rc4,
-                        stringArrayThreshold: 1
-                    }
-                ).getObfuscatedCode();
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            stringArray: true,
+                            stringArrayEncoding: StringArrayEncoding.Rc4,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+                });
+
+                it('should replace literal node value with value from string array encoded using rc4', () => {
+                    assert.match(obfuscatedCode, regExp);
+                });
             });
             });
 
 
-            it('should replace literal node value with value from string array encoded using rc4', () => {
-                assert.match(obfuscatedCode, regExp);
+            describe('Variant #2: multiple string literals', () => {
+                const variableRegExp1: RegExp = /var *test *= *_0x(?:[a-f0-9]){4}\('0x0', *'(.{4})'\);/;
+                const variableRegExp2: RegExp = /var *test *= *_0x(?:[a-f0-9]){4}\('0x1', *'(.{4})'\);/;
+
+                let encodedLiteralValue1: string;
+                let encodedLiteralValue2: string;
+
+                let obfuscatedCode: string;
+
+                before(() => {
+                    const code: string = readFileAsString(__dirname + '/fixtures/same-literal-values.js');
+
+                    obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            seed: 1, // set seed to prevent rare case when all encoded values are the same
+                            stringArray: true,
+                            stringArrayEncoding: StringArrayEncoding.Rc4,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+
+                    encodedLiteralValue1 = getRegExpMatch(obfuscatedCode, variableRegExp1);
+                    encodedLiteralValue2 = getRegExpMatch(obfuscatedCode, variableRegExp2);
+                });
+
+                it('Match #1: should replace literal node value with value from string array encoded using rc4', () => {
+                    assert.match(obfuscatedCode, variableRegExp1);
+                });
+
+                it('Match #2: should replace literal node value with value from string array encoded using rc4', () => {
+                    assert.match(obfuscatedCode, variableRegExp2);
+                });
+
+                it('Should encode same values as two different encoded string array items', () => {
+                    assert.notEqual(encodedLiteralValue1, encodedLiteralValue2);
+                });
             });
             });
         });
         });
 
 

+ 27 - 3
test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/BaseIdentifierObfuscatingReplacer.spec.ts

@@ -7,12 +7,12 @@ import { readFileAsString } from '../../../../../helpers/readFileAsString';
 import { JavaScriptObfuscator } from '../../../../../../src/JavaScriptObfuscatorFacade';
 import { JavaScriptObfuscator } from '../../../../../../src/JavaScriptObfuscatorFacade';
 
 
 describe('BaseIdentifierObfuscatingReplacer', () => {
 describe('BaseIdentifierObfuscatingReplacer', () => {
-    describe('Base rule', () => {
-        describe('Variant #1: default behaviour', () => {
+    describe('Reserved names', () => {
+        describe('Variant #1: ignore local reserved names', () => {
             let obfuscatedCode: string;
             let obfuscatedCode: string;
 
 
             before(() => {
             before(() => {
-                const code: string = readFileAsString(__dirname + '/fixtures/simple-input.js');
+                const code: string = readFileAsString(__dirname + '/fixtures/local-reserved-names.js');
 
 
                 obfuscatedCode = JavaScriptObfuscator.obfuscate(
                 obfuscatedCode = JavaScriptObfuscator.obfuscate(
                     code,
                     code,
@@ -30,5 +30,29 @@ describe('BaseIdentifierObfuscatingReplacer', () => {
                 );
                 );
             });
             });
         });
         });
+
+        describe('Variant #1: ignore global reserved names', () => {
+            let obfuscatedCode: string;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/global-reserved-names.js');
+
+                obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                    code,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        renameGlobals: true,
+                        reservedNames: ['[abc|ghi]']
+                    }
+                ).getObfuscatedCode();
+            });
+
+            it('Should keep reserved names without transformations when `reservedNames` option is enabled', () => {
+                assert.match(
+                    obfuscatedCode,
+                    /var *abc *= *0x1; *var *_0x([a-f0-9]){4,6} *= *0x2; *var *ghi *= *0x3;/
+                );
+            });
+        });
     });
     });
 });
 });

+ 3 - 0
test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/fixtures/global-reserved-names.js

@@ -0,0 +1,3 @@
+var abc = 1;
+var def = 2;
+var ghi = 3;

+ 0 - 0
test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/fixtures/simple-input.js → test/functional-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/identifier-obfuscating-replacers/fixtures/local-reserved-names.js


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

@@ -541,6 +541,7 @@ describe('OptionsNormalizer', () => {
             before(() => {
             before(() => {
                 optionsPreset = getNormalizedOptions({
                 optionsPreset = getNormalizedOptions({
                     ...getDefaultOptions(),
                     ...getDefaultOptions(),
+                    shuffleStringArray: true,
                     stringArray: false,
                     stringArray: false,
                     stringArrayEncoding: StringArrayEncoding.Rc4,
                     stringArrayEncoding: StringArrayEncoding.Rc4,
                     stringArrayThreshold: 0.5,
                     stringArrayThreshold: 0.5,
@@ -549,6 +550,7 @@ describe('OptionsNormalizer', () => {
 
 
                 expectedOptionsPreset = {
                 expectedOptionsPreset = {
                     ...getDefaultOptions(),
                     ...getDefaultOptions(),
+                    shuffleStringArray: false,
                     stringArray: false,
                     stringArray: false,
                     stringArrayEncoding: false,
                     stringArrayEncoding: false,
                     stringArrayThreshold: 0,
                     stringArrayThreshold: 0,
@@ -584,6 +586,7 @@ describe('OptionsNormalizer', () => {
                 optionsPreset = getNormalizedOptions({
                 optionsPreset = getNormalizedOptions({
                     ...getDefaultOptions(),
                     ...getDefaultOptions(),
                     rotateStringArray: true,
                     rotateStringArray: true,
+                    shuffleStringArray: true,
                     stringArray: true,
                     stringArray: true,
                     stringArrayThreshold: 0
                     stringArrayThreshold: 0
                 });
                 });
@@ -591,6 +594,7 @@ describe('OptionsNormalizer', () => {
                 expectedOptionsPreset = {
                 expectedOptionsPreset = {
                     ...getDefaultOptions(),
                     ...getDefaultOptions(),
                     rotateStringArray: false,
                     rotateStringArray: false,
+                    shuffleStringArray: false,
                     stringArray: false,
                     stringArray: false,
                     stringArrayThreshold: 0
                     stringArrayThreshold: 0
                 };
                 };

+ 308 - 0
test/functional-tests/storages/string-array-storage/StringArrayStorage.spec.ts

@@ -0,0 +1,308 @@
+import { assert } from 'chai';
+
+import { NO_ADDITIONAL_NODES_PRESET } from '../../../../src/options/presets/NoCustomNodes';
+
+import { readFileAsString } from '../../../helpers/readFileAsString';
+
+import { JavaScriptObfuscator } from '../../../../src/JavaScriptObfuscatorFacade';
+
+describe('StringArrayStorage', () => {
+    describe('Rotate string array', function () {
+        this.timeout(100000);
+
+        describe('Variant #1: single string array value', () => {
+            const samples: number = 1000;
+            const delta: number = 0.1;
+            const expectedVariantProbability: number = 1;
+
+            const stringArrayVariant1RegExp1: RegExp = /var *_0x([a-f0-9]){4} *= *\['test'];/g;
+            const literalNodeVariant1RegExp: RegExp = /var *test *= *_0x([a-f0-9]){4}\('0x0'\);/g;
+
+            let stringArrayVariant1Probability: number,
+                literalNodeVariant1Probability: number;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/one-string.js');
+
+                let stringArrayVariant1MatchesLength: number = 0;
+                let literalNodeVariant1MatchesLength: number = 0;
+
+               for (let i = 0; i < samples; i++) {
+                   const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                       code,
+                       {
+                           ...NO_ADDITIONAL_NODES_PRESET,
+                           rotateStringArray: true,
+                           stringArray: true,
+                           stringArrayThreshold: 1
+                       }
+                   ).getObfuscatedCode();
+
+                   if (obfuscatedCode.match(stringArrayVariant1RegExp1)) {
+                       stringArrayVariant1MatchesLength++;
+                   }
+
+                   if (obfuscatedCode.match(literalNodeVariant1RegExp)) {
+                       literalNodeVariant1MatchesLength++;
+                   }
+               }
+
+                stringArrayVariant1Probability = stringArrayVariant1MatchesLength / samples;
+                literalNodeVariant1Probability = literalNodeVariant1MatchesLength / samples;
+            });
+
+            describe('String array probability', () => {
+                it('Variant #1: should create single string array variant', () => {
+                    assert.closeTo(stringArrayVariant1Probability, expectedVariantProbability, delta);
+                });
+            });
+
+            describe('Literal node probability', () => {
+                it('Variant #1: should replace literal node with call to string array variant', () => {
+                    assert.closeTo(literalNodeVariant1Probability, expectedVariantProbability, delta);
+                });
+            });
+        });
+
+        describe('Variant #2: Three string array values', () => {
+            const samples: number = 1000;
+            const delta: number = 0.1;
+            const expectedStringArrayVariantProbability: number = 0.33;
+            const expectedLiteralNodeVariantProbability: number = 1;
+
+            const stringArrayVariantsCount: number = 3;
+            const literalNodeVariantsCount: number = 1;
+
+            const stringArrayVariantRegExps: RegExp[] = [
+                /var *_0x([a-f0-9]){4} *= *\['foo', *'bar', *'baz'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['bar', *'baz', *'foo'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['baz', *'foo', *'bar'];/g
+            ];
+            const literalNodeVariantRegExps: RegExp[] = [
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x0'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x1'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x2'\\);`
+                )
+            ];
+
+            const stringArrayVariantProbabilities: number[] = new Array(stringArrayVariantsCount).fill(0);
+            const literalNodeVariantProbabilities: number[] = new Array(literalNodeVariantsCount).fill(0);
+
+            const stringArrayVariantMatchesLength: number[] = new Array(stringArrayVariantsCount).fill(0);
+            const literalNodeVariantMatchesLength: number[] = new Array(literalNodeVariantsCount).fill(0);
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/three-strings.js');
+
+                for (let i = 0; i < samples; i++) {
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            rotateStringArray: true,
+                            stringArray: true,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+
+                    for (let variantIndex = 0; variantIndex < stringArrayVariantsCount; variantIndex++) {
+                        if (obfuscatedCode.match(stringArrayVariantRegExps[variantIndex])) {
+                            stringArrayVariantMatchesLength[variantIndex]++;
+                        }
+
+                        if (obfuscatedCode.match(literalNodeVariantRegExps[variantIndex])) {
+                            literalNodeVariantMatchesLength[variantIndex]++;
+                        }
+                    }
+                }
+
+                for (let variantIndex = 0; variantIndex < stringArrayVariantsCount; variantIndex++) {
+                    stringArrayVariantProbabilities[variantIndex] = stringArrayVariantMatchesLength[variantIndex] / samples;
+                }
+
+                for (let variantIndex = 0; variantIndex < literalNodeVariantsCount; variantIndex++) {
+                    literalNodeVariantProbabilities[variantIndex] = literalNodeVariantMatchesLength[variantIndex] / samples;
+                }
+            });
+
+            describe('String array probability', () => {
+                for (let variantIndex = 0; variantIndex < stringArrayVariantsCount; variantIndex++) {
+                    const variantNumber: number = variantIndex + 1;
+
+                    it(`Variant #${variantNumber}: should create string array variant`, () => {
+                        assert.closeTo(stringArrayVariantProbabilities[variantIndex], expectedStringArrayVariantProbability, delta);
+                    });
+                }
+            });
+
+            describe('Literal node probability', () => {
+                for (let variantIndex = 0; variantIndex < literalNodeVariantsCount; variantIndex++) {
+                    const variantNumber: number = variantIndex + 1;
+
+                    it(`Variant #${variantNumber}: should replace literal node with call to string array variant`, () => {
+                        assert.closeTo(literalNodeVariantProbabilities[variantIndex], expectedLiteralNodeVariantProbability, delta);
+                    });
+                }
+            });
+        });
+    });
+
+    describe('Shuffle string array', function () {
+        this.timeout(100000);
+
+        describe('Variant #1: single string array value', () => {
+            const samples: number = 1000;
+            const delta: number = 0.1;
+            const expectedVariantProbability: number = 1;
+
+            const stringArrayVariantRegExp1: RegExp = /var *_0x([a-f0-9]){4} *= *\['test'];/g;
+            const literalNodeVariant1RegExp: RegExp = /var *test *= *_0x([a-f0-9]){4}\('0x0'\);/g;
+
+            let stringArrayVariant1Probability: number,
+                literalNodeVariant1Probability: number;
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/one-string.js');
+
+                let stringArrayVariant1MatchesLength: number = 0;
+                let literalNodeVariant1MatchesLength: number = 0;
+
+                for (let i = 0; i < samples; i++) {
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            shuffleStringArray: true,
+                            stringArray: true,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+
+                    if (obfuscatedCode.match(stringArrayVariantRegExp1)) {
+                        stringArrayVariant1MatchesLength++;
+                    }
+
+                    if (obfuscatedCode.match(literalNodeVariant1RegExp)) {
+                        literalNodeVariant1MatchesLength++;
+                    }
+                }
+
+                stringArrayVariant1Probability = stringArrayVariant1MatchesLength / samples;
+                literalNodeVariant1Probability = literalNodeVariant1MatchesLength / samples;
+            });
+
+            describe('String array probability', () => {
+                it('Variant #1: should create string array variant', () => {
+                    assert.closeTo(stringArrayVariant1Probability, expectedVariantProbability, delta);
+                });
+            });
+
+            describe('Literal node probability', () => {
+                it('Variant #1: should replace literal node with call to string array variant', () => {
+                    assert.closeTo(literalNodeVariant1Probability, expectedVariantProbability, delta);
+                });
+            });
+        });
+
+        describe('Variant #2: Three string array values', () => {
+            const samples: number = 1000;
+            const delta: number = 0.1;
+            const expectedVariantProbability: number = 0.166;
+
+            const variantsCount: number = 6;
+
+            const stringArrayVariantRegExps: RegExp[] = [
+                /var *_0x([a-f0-9]){4} *= *\['foo', *'bar', *'baz'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['foo', *'baz', *'bar'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['bar', *'foo', *'baz'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['bar', *'baz', *'foo'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['baz', *'foo', *'bar'];/g,
+                /var *_0x([a-f0-9]){4} *= *\['baz', *'bar', *'foo'];/g
+            ];
+
+            const literalNodeVariantRegExps: RegExp[] = [
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x0'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x1'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x2'\\);`
+                ),
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x0'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x2'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x1'\\);`
+                ),
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x1'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x0'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x2'\\);`
+                ),
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x1'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x2'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x0'\\);`
+                ),
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x2'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x0'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x1'\\);`
+                ),
+                new RegExp(
+                    `var *foo *= *_0x([a-f0-9]){4}\\('0x2'\\); *` +
+                    `var *bar *= *_0x([a-f0-9]){4}\\('0x1'\\); *` +
+                    `var *baz *= *_0x([a-f0-9]){4}\\('0x0'\\);`
+                )
+            ];
+
+            const stringArrayVariantProbabilities: number[] = new Array(variantsCount).fill(0);
+            const literalNodeVariantProbabilities: number[] = new Array(variantsCount).fill(0);
+
+            const stringArrayVariantMatchesLength: number[] = new Array(variantsCount).fill(0);
+            const literalNodeVariantMatchesLength: number[] = new Array(variantsCount).fill(0);
+
+            before(() => {
+                const code: string = readFileAsString(__dirname + '/fixtures/three-strings.js');
+
+                for (let i = 0; i < samples; i++) {
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        code,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            shuffleStringArray: true,
+                            stringArray: true,
+                            stringArrayThreshold: 1
+                        }
+                    ).getObfuscatedCode();
+
+                    for (let variantIndex = 0; variantIndex < variantsCount; variantIndex++) {
+                        if (obfuscatedCode.match(stringArrayVariantRegExps[variantIndex])) {
+                            stringArrayVariantMatchesLength[variantIndex]++;
+                        }
+
+                        if (obfuscatedCode.match(literalNodeVariantRegExps[variantIndex])) {
+                            literalNodeVariantMatchesLength[variantIndex]++;
+                        }
+                    }
+                }
+
+                for (let variantIndex = 0; variantIndex < variantsCount; variantIndex++) {
+                    stringArrayVariantProbabilities[variantIndex] = stringArrayVariantMatchesLength[variantIndex] / samples;
+                    literalNodeVariantProbabilities[variantIndex] = literalNodeVariantMatchesLength[variantIndex] / samples;
+                }
+
+            });
+
+            for (let variantIndex = 0; variantIndex < variantsCount; variantIndex++) {
+                const variantNumber: number = variantIndex + 1;
+
+                it(`Variant #${variantNumber}: should create string array variant`, () => {
+                    assert.closeTo(stringArrayVariantProbabilities[variantIndex], expectedVariantProbability, delta);
+                });
+
+                it(`Variant #${variantNumber}: should replace literal node with call to string array variant`, () => {
+                    assert.closeTo(literalNodeVariantProbabilities[variantIndex], expectedVariantProbability, delta);
+                });
+            }
+        });
+    });
+});

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

@@ -0,0 +1 @@
+var test = 'test';

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

@@ -0,0 +1,3 @@
+var foo = 'foo';
+var bar = 'bar';
+var baz = 'baz';

+ 1 - 1
test/helpers/getRegExpMatch.ts

@@ -8,7 +8,7 @@ export function getRegExpMatch (str: string, regExp: RegExp, matchIndex: number
     const match: RegExpMatchArray | null = str.match(regExp);
     const match: RegExpMatchArray | null = str.match(regExp);
 
 
     if (!match) {
     if (!match) {
-        throw new Error(`No matches were found for regular expression \`${regExp.toString()}\``);
+        throw new Error(`No matches were found. String: \`${str}\`, regular expression: \`${regExp.toString()}\``);
     }
     }
 
 
     return (<RegExpMatchArray>match)[matchIndex + 1];
     return (<RegExpMatchArray>match)[matchIndex + 1];

+ 5 - 0
test/index.spec.ts

@@ -7,6 +7,7 @@ require('source-map-support').install();
  */
  */
 import './unit-tests/analyzers/calls-graph-analyzer/CallsGraphAnalyzer.spec';
 import './unit-tests/analyzers/calls-graph-analyzer/CallsGraphAnalyzer.spec';
 import './unit-tests/analyzers/prevailing-kind-of-variables-analyzer/PrevailingKindOfVariablesAnalyzer.spec';
 import './unit-tests/analyzers/prevailing-kind-of-variables-analyzer/PrevailingKindOfVariablesAnalyzer.spec';
+import './unit-tests/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.spec';
 import './unit-tests/cli/sanitizers/ArraySanitizer.spec';
 import './unit-tests/cli/sanitizers/ArraySanitizer.spec';
 import './unit-tests/cli/sanitizers/BooleanSanitizer.spec';
 import './unit-tests/cli/sanitizers/BooleanSanitizer.spec';
 import './unit-tests/cli/sanitizers/IdentifierNamesGeneratorSanitizer.spec';
 import './unit-tests/cli/sanitizers/IdentifierNamesGeneratorSanitizer.spec';
@@ -29,6 +30,9 @@ import './unit-tests/node/node-lexical-scope-utils/NodeLexicalScopeUtils.spec';
 import './unit-tests/node/node-statement-utils/NodeStatementUtils.spec';
 import './unit-tests/node/node-statement-utils/NodeStatementUtils.spec';
 import './unit-tests/node/node-utils/NodeUtils.spec';
 import './unit-tests/node/node-utils/NodeUtils.spec';
 import './unit-tests/node-transformers/preparing-transformers/ObfuscatingGuardsTransformer.spec';
 import './unit-tests/node-transformers/preparing-transformers/ObfuscatingGuardsTransformer.spec';
+import './unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/BooleanLiteralObfuscatingReplacer.spec';
+import './unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/NumberLiteralObfuscatingReplacer.spec';
+import './unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/StringLiteralObfuscatingReplacer.spec';
 import './unit-tests/options/ValidationErrorsFormatter.spec';
 import './unit-tests/options/ValidationErrorsFormatter.spec';
 import './unit-tests/source-code/ObfuscatedCode.spec';
 import './unit-tests/source-code/ObfuscatedCode.spec';
 import './unit-tests/storages/ArrayStorage.spec';
 import './unit-tests/storages/ArrayStorage.spec';
@@ -84,6 +88,7 @@ import './functional-tests/node-transformers/preparing-transformers/obfuscating-
 import './functional-tests/node-transformers/preparing-transformers/obfuscating-guards/conditional-comment-obfuscating-guard/ConditionalCommentObfuscatingGuard.spec';
 import './functional-tests/node-transformers/preparing-transformers/obfuscating-guards/conditional-comment-obfuscating-guard/ConditionalCommentObfuscatingGuard.spec';
 import './functional-tests/node-transformers/preparing-transformers/obfuscating-guards/reserved-string-obfuscating-guard/ReservedStringObfuscatingGuard.spec';
 import './functional-tests/node-transformers/preparing-transformers/obfuscating-guards/reserved-string-obfuscating-guard/ReservedStringObfuscatingGuard.spec';
 import './functional-tests/options/OptionsNormalizer.spec';
 import './functional-tests/options/OptionsNormalizer.spec';
+import './functional-tests/storages/string-array-storage/StringArrayStorage.spec';
 import './functional-tests/templates/debug-protection-nodes/DebugProtectionFunctionCallTemplate.spec';
 import './functional-tests/templates/debug-protection-nodes/DebugProtectionFunctionCallTemplate.spec';
 import './functional-tests/templates/domain-lock-nodes/DomainLockNodeTemplate.spec';
 import './functional-tests/templates/domain-lock-nodes/DomainLockNodeTemplate.spec';
 import './functional-tests/templates/GlobalVariableNoEvalTemplate.spec';
 import './functional-tests/templates/GlobalVariableNoEvalTemplate.spec';

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

@@ -0,0 +1,318 @@
+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 { IStringArrayStorageAnalyzer } from '../../../../src/interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
+import { IStringArrayStorageItemData } from '../../../../src/interfaces/storages/string-array-storage/IStringArrayStorageItem';
+
+import { InversifyContainerFacade } from '../../../../src/container/InversifyContainerFacade';
+import { NodeFactory } from '../../../../src/node/NodeFactory';
+import { NodeMetadata } from '../../../../src/node/NodeMetadata';
+
+const getStringArrayStorageAnalyzer = (options: TInputOptions): IStringArrayStorageAnalyzer => {
+    const inversifyContainerFacade: IInversifyContainerFacade = new InversifyContainerFacade();
+
+    inversifyContainerFacade.load('', '', options);
+
+    return inversifyContainerFacade.get<IStringArrayStorageAnalyzer>(ServiceIdentifiers.IStringArrayStorageAnalyzer);
+};
+
+describe('StringArrayStorageAnalyzer', () => {
+    let stringArrayStorageAnalyzer: IStringArrayStorageAnalyzer;
+
+    describe('analyze', () => {
+        describe('Base analyze of the AST tree', () => {
+            const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+            const literalNode2: ESTree.Literal = NodeFactory.literalNode('bar');
+            const literalNode3: ESTree.Literal = NodeFactory.literalNode('baz');
+
+            const expectedStringArrayStorageItemData1: IStringArrayStorageItemData = {
+                encodedValue: 'foo',
+                decodeKey: null,
+                index: 0,
+                value: 'foo'
+            };
+            const expectedStringArrayStorageItemData2: IStringArrayStorageItemData = {
+                encodedValue: 'bar',
+                decodeKey: null,
+                index: 1,
+                value: 'bar'
+            };
+            const expectedStringArrayStorageItemData3: undefined = undefined;
+
+            let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData3: IStringArrayStorageItemData | undefined;
+
+            before(() => {
+                stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                    stringArrayThreshold: 1
+                });
+
+                const astTree: ESTree.Program = NodeFactory.programNode([
+                    NodeFactory.expressionStatementNode(literalNode1),
+                    NodeFactory.expressionStatementNode(literalNode2)
+                ]);
+
+                stringArrayStorageAnalyzer.analyze(astTree);
+                stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+                stringArrayStorageItemData3 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode3);
+            });
+
+            it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+            });
+
+            it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+            });
+
+            it('Variant #3: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData3, expectedStringArrayStorageItemData3);
+            });
+        });
+
+        describe('Base analyze of the AST tree with string literal nodes with values shorter than allowed length', () => {
+            const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+            const literalNode2: ESTree.Literal = NodeFactory.literalNode('ba');
+
+            const expectedStringArrayStorageItemData1: IStringArrayStorageItemData = {
+                encodedValue: 'foo',
+                decodeKey: null,
+                index: 0,
+                value: 'foo'
+            };
+            const expectedStringArrayStorageItemData2: undefined = undefined;
+
+            let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+            before(() => {
+                stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                    stringArrayThreshold: 1
+                });
+
+                const astTree: ESTree.Program = NodeFactory.programNode([
+                    NodeFactory.expressionStatementNode(literalNode1),
+                    NodeFactory.expressionStatementNode(literalNode2)
+                ]);
+
+                stringArrayStorageAnalyzer.analyze(astTree);
+                stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+            });
+
+            it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+            });
+
+            it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+            });
+        });
+
+        describe('Base analyze of the AST tree with number literal nodes', () => {
+            const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+            const literalNode2: ESTree.Literal = NodeFactory.literalNode(1);
+
+            const expectedStringArrayStorageItemData1: IStringArrayStorageItemData = {
+                encodedValue: 'foo',
+                decodeKey: null,
+                index: 0,
+                value: 'foo'
+            };
+            const expectedStringArrayStorageItemData2: undefined = undefined;
+
+            let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+            before(() => {
+                stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                    stringArrayThreshold: 1
+                });
+
+                const astTree: ESTree.Program = NodeFactory.programNode([
+                    NodeFactory.expressionStatementNode(literalNode1),
+                    NodeFactory.expressionStatementNode(literalNode2)
+                ]);
+
+                stringArrayStorageAnalyzer.analyze(astTree);
+                stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+            });
+
+            it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+            });
+
+            it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+            });
+        });
+
+        describe('Analyzes of the AST tree with ignored nodes', () => {
+            const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+            const literalNode2: ESTree.Literal = NodeFactory.literalNode('bar');
+            NodeMetadata.set(literalNode2, {ignoredNode: true});
+
+            const expectedStringArrayStorageItemData1: IStringArrayStorageItemData = {
+                encodedValue: 'foo',
+                decodeKey: null,
+                index: 0,
+                value: 'foo'
+            };
+            const expectedStringArrayStorageItemData2: undefined = undefined;
+
+            let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+            before(() => {
+                stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                    stringArrayThreshold: 1
+                });
+
+                const astTree: ESTree.Program = NodeFactory.programNode([
+                    NodeFactory.expressionStatementNode(literalNode1),
+                    NodeFactory.expressionStatementNode(literalNode2)
+                ]);
+
+                stringArrayStorageAnalyzer.analyze(astTree);
+                stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+            });
+
+            it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+            });
+
+            it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+            });
+        });
+
+        /**
+         * This test covers rare case when with random value inside `shouldAddValueToStringArray` was `0`
+         * that trigger positive check for method.
+         *
+         * As fix i added check of `this.options.stringArray` option value
+         */
+        describe('Analyzes of the AST tree with disabled string array', () => {
+            const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+            const literalNode2: ESTree.Literal = NodeFactory.literalNode('bar');
+
+            const expectedStringArrayStorageItemData1: undefined = undefined;
+            const expectedStringArrayStorageItemData2: undefined = undefined;
+
+            let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+            let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+            before(() => {
+                stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                    stringArray: false,
+                    stringArrayThreshold: 1
+                });
+                (<any>stringArrayStorageAnalyzer).options.stringArrayThreshold = 1;
+
+                const astTree: ESTree.Program = NodeFactory.programNode([
+                    NodeFactory.expressionStatementNode(literalNode1),
+                    NodeFactory.expressionStatementNode(literalNode2)
+                ]);
+
+                stringArrayStorageAnalyzer.analyze(astTree);
+                stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+            });
+
+            it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+            });
+
+            it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+            });
+        });
+
+        describe('Analyzes of the AST tree with string array threshold', () => {
+            describe('Threshold value: 0', () => {
+                const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+                const literalNode2: ESTree.Literal = NodeFactory.literalNode('bar');
+
+                const expectedStringArrayStorageItemData1: undefined = undefined;
+                const expectedStringArrayStorageItemData2: undefined = undefined;
+
+                let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+                let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+                before(() => {
+                    stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                        stringArrayThreshold: 0
+                    });
+
+                    const astTree: ESTree.Program = NodeFactory.programNode([
+                        NodeFactory.expressionStatementNode(literalNode1),
+                        NodeFactory.expressionStatementNode(literalNode2)
+                    ]);
+
+                    stringArrayStorageAnalyzer.analyze(astTree);
+                    stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                    stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+                });
+
+                it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                    assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+                });
+
+                it('Variant #2: should return correct string array storage item data for literal node #1', () => {
+                    assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+                });
+            });
+
+            describe('Threshold value: 0.5', () => {
+                const literalNode1: ESTree.Literal = NodeFactory.literalNode('foo');
+                const literalNode2: ESTree.Literal = NodeFactory.literalNode('bar');
+
+                const expectedStringArrayStorageItemData1: undefined = undefined;
+                const expectedStringArrayStorageItemData2: IStringArrayStorageItemData = {
+                    encodedValue: 'bar',
+                    decodeKey: null,
+                    index: 0,
+                    value: 'bar'
+                };
+
+                let stringArrayStorageItemData1: IStringArrayStorageItemData | undefined;
+                let stringArrayStorageItemData2: IStringArrayStorageItemData | undefined;
+
+                before(() => {
+                    stringArrayStorageAnalyzer = getStringArrayStorageAnalyzer({
+                        stringArrayThreshold: 0.5,
+                        seed: 1
+                    });
+
+                    const astTree: ESTree.Program = NodeFactory.programNode([
+                        NodeFactory.expressionStatementNode(literalNode1),
+                        NodeFactory.expressionStatementNode(literalNode2)
+                    ]);
+
+                    stringArrayStorageAnalyzer.analyze(astTree);
+                    stringArrayStorageItemData1 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode1);
+                    stringArrayStorageItemData2 = stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode2);
+                });
+
+                it('Variant #1: should return correct string array storage item data for literal node #1', () => {
+                    assert.deepEqual(stringArrayStorageItemData1, expectedStringArrayStorageItemData1);
+                });
+
+                it('Variant #2: should return correct string array storage item data for literal node #2', () => {
+                    assert.deepEqual(stringArrayStorageItemData2, expectedStringArrayStorageItemData2);
+                });
+            });
+        });
+    });
+});

+ 60 - 0
test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/BooleanLiteralObfuscatingReplacer.spec.ts

@@ -0,0 +1,60 @@
+import 'reflect-metadata';
+
+import { assert } from 'chai';
+import * as ESTree from 'estree';
+
+import { IInversifyContainerFacade } from '../../../../../../src/interfaces/container/IInversifyContainerFacade';
+import { IObfuscatingReplacer } from '../../../../../../src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IObfuscatingReplacer';
+
+import { LiteralObfuscatingReplacer } from '../../../../../../src/enums/node-transformers/obfuscating-transformers/obfuscating-replacers/LiteralObfuscatingReplacer';
+import { ServiceIdentifiers } from '../../../../../../src/container/ServiceIdentifiers';
+
+import { InversifyContainerFacade } from '../../../../../../src/container/InversifyContainerFacade';
+import { NodeFactory } from '../../../../../../src/node/NodeFactory';
+
+describe('BooleanLiteralObfuscatingReplacer', () => {
+    describe('replace', () => {
+        let inversifyContainerFacade: IInversifyContainerFacade,
+            obfuscatingReplacer: IObfuscatingReplacer;
+
+        before(() => {
+            inversifyContainerFacade = new InversifyContainerFacade();
+            inversifyContainerFacade.load('', '', {});
+
+            obfuscatingReplacer = inversifyContainerFacade
+                .getNamed(ServiceIdentifiers.IObfuscatingReplacer, LiteralObfuscatingReplacer.BooleanLiteralObfuscatingReplacer);
+        });
+
+        describe('Variant #1: literal value type check', () => {
+            describe('Variant #1: literal values is a `boolean` value', () => {
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode(true);
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should not throw an error if literal values is a `boolean` value', () => {
+                    assert.doesNotThrow(testFunc,);
+                });
+            });
+
+            describe('Variant #2: literal values is not a `boolean` value', () => {
+                const expectedError: ErrorConstructor = Error;
+
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode('foo');
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should throw an error if literal values is not a `boolean` value', () => {
+                    assert.throws(testFunc, expectedError);
+                });
+            });
+        });
+    });
+});

+ 60 - 0
test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/NumberLiteralObfuscatingReplacer.spec.ts

@@ -0,0 +1,60 @@
+import 'reflect-metadata';
+
+import { assert } from 'chai';
+import * as ESTree from 'estree';
+
+import { IInversifyContainerFacade } from '../../../../../../src/interfaces/container/IInversifyContainerFacade';
+import { IObfuscatingReplacer } from '../../../../../../src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IObfuscatingReplacer';
+
+import { LiteralObfuscatingReplacer } from '../../../../../../src/enums/node-transformers/obfuscating-transformers/obfuscating-replacers/LiteralObfuscatingReplacer';
+import { ServiceIdentifiers } from '../../../../../../src/container/ServiceIdentifiers';
+
+import { InversifyContainerFacade } from '../../../../../../src/container/InversifyContainerFacade';
+import { NodeFactory } from '../../../../../../src/node/NodeFactory';
+
+describe('NumberLiteralObfuscatingReplacer', () => {
+    describe('replace', () => {
+        let inversifyContainerFacade: IInversifyContainerFacade,
+            obfuscatingReplacer: IObfuscatingReplacer;
+
+        before(() => {
+            inversifyContainerFacade = new InversifyContainerFacade();
+            inversifyContainerFacade.load('', '', {});
+
+            obfuscatingReplacer = inversifyContainerFacade
+                .getNamed(ServiceIdentifiers.IObfuscatingReplacer, LiteralObfuscatingReplacer.NumberLiteralObfuscatingReplacer);
+        });
+
+        describe('Variant #1: literal value type check', () => {
+            describe('Variant #1: literal values is a `number` value', () => {
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode(1);
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should not throw an error if literal values is a `number` value', () => {
+                    assert.doesNotThrow(testFunc,);
+                });
+            });
+
+            describe('Variant #2: literal values is not a `number` value', () => {
+                const expectedError: ErrorConstructor = Error;
+
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode('foo');
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should throw an error if literal values is not a `number` value', () => {
+                    assert.throws(testFunc, expectedError);
+                });
+            });
+        });
+    });
+});

+ 60 - 0
test/unit-tests/node-transformers/obfuscating-transformers/obfuscating-replacers/literal-obfuscating-replacers/StringLiteralObfuscatingReplacer.spec.ts

@@ -0,0 +1,60 @@
+import 'reflect-metadata';
+
+import { assert } from 'chai';
+import * as ESTree from 'estree';
+
+import { IInversifyContainerFacade } from '../../../../../../src/interfaces/container/IInversifyContainerFacade';
+import { IObfuscatingReplacer } from '../../../../../../src/interfaces/node-transformers/obfuscating-transformers/obfuscating-replacers/IObfuscatingReplacer';
+
+import { LiteralObfuscatingReplacer } from '../../../../../../src/enums/node-transformers/obfuscating-transformers/obfuscating-replacers/LiteralObfuscatingReplacer';
+import { ServiceIdentifiers } from '../../../../../../src/container/ServiceIdentifiers';
+
+import { InversifyContainerFacade } from '../../../../../../src/container/InversifyContainerFacade';
+import { NodeFactory } from '../../../../../../src/node/NodeFactory';
+
+describe('StringLiteralObfuscatingReplacer', () => {
+    describe('replace', () => {
+        let inversifyContainerFacade: IInversifyContainerFacade,
+            obfuscatingReplacer: IObfuscatingReplacer;
+
+        before(() => {
+            inversifyContainerFacade = new InversifyContainerFacade();
+            inversifyContainerFacade.load('', '', {});
+
+            obfuscatingReplacer = inversifyContainerFacade
+                .getNamed(ServiceIdentifiers.IObfuscatingReplacer, LiteralObfuscatingReplacer.StringLiteralObfuscatingReplacer);
+        });
+
+        describe('Variant #1: literal value type check', () => {
+            describe('Variant #1: literal values is a `string` value', () => {
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode('foo');
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should not throw an error if literal values is a `string` value', () => {
+                    assert.doesNotThrow(testFunc,);
+                });
+            });
+
+            describe('Variant #2: literal values is not a `string` value', () => {
+                const expectedError: ErrorConstructor = Error;
+
+                let testFunc: () => void;
+
+                before(() => {
+                    const literalNode: ESTree.Literal = NodeFactory.literalNode(1);
+
+                    testFunc = () => <ESTree.Identifier>obfuscatingReplacer.replace(literalNode);
+                });
+
+                it('should throw an error if literal values is not a `string` value', () => {
+                    assert.throws(testFunc, expectedError);
+                });
+            });
+        });
+    });
+});

+ 101 - 14
test/unit-tests/storages/ArrayStorage.spec.ts

@@ -74,6 +74,23 @@ describe('ArrayStorage', () => {
         });
         });
     });
     });
 
 
+    describe('getStorageId', () => {
+        const storageIdRegExp: RegExp = /^[a-zA-Z0-9]{6}$/;
+
+        let storageId: string;
+
+        before(() => {
+            storage = getStorageInstance<string>();
+            storage.set(storageKey, storageValue);
+
+            storageId = storage.getStorageId();
+        });
+
+        it('should return storage id', () => {
+            assert.match(storageId, storageIdRegExp);
+        });
+    });
+
     describe('get', () => {
     describe('get', () => {
         describe('Variant #1: value exist', () => {
         describe('Variant #1: value exist', () => {
             const expectedValue: string = storageValue;
             const expectedValue: string = storageValue;
@@ -92,6 +109,41 @@ describe('ArrayStorage', () => {
             });
             });
         });
         });
 
 
+        describe('Variant #2: value isn\'t exist', () => {
+            const expectedValue: undefined = undefined;
+
+            let value: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+
+                value = storage.get(storageKey);
+            });
+
+            it('should return undefined if value does not exist in the storage', () => {
+                assert.equal(value, expectedValue);
+            });
+        });
+    });
+
+    describe('getOrThrow', () => {
+        describe('Variant #1: value exist', () => {
+            const expectedValue: string = storageValue;
+
+            let value: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+                storage.set(storageKey, storageValue);
+
+                value = storage.getOrThrow(storageKey);
+            });
+
+            it('should return value from storage by key', () => {
+                assert.equal(value, expectedValue);
+            });
+        });
+
         describe('Variant #2: value isn\'t exist', () => {
         describe('Variant #2: value isn\'t exist', () => {
             const expectedError: ErrorConstructor = Error;
             const expectedError: ErrorConstructor = Error;
 
 
@@ -100,7 +152,7 @@ describe('ArrayStorage', () => {
             before(() => {
             before(() => {
                 storage = getStorageInstance<string>();
                 storage = getStorageInstance<string>();
 
 
-                testFunc = () => storage.get(storageKey);
+                testFunc = () => storage.getOrThrow(storageKey);
             });
             });
 
 
             it('should throw an error', () => {
             it('should throw an error', () => {
@@ -194,27 +246,62 @@ describe('ArrayStorage', () => {
     });
     });
 
 
     describe('mergeWith', () => {
     describe('mergeWith', () => {
-        const secondStorageKey: number = 1;
-        const secondStorageValue: string = 'bar';
+        describe('Base merge', () => {
+            const secondStorageKey: number = 1;
+            const secondStorageValue: string = 'bar';
 
 
-        const expectedArray: string[] = [storageValue, secondStorageValue];
+            const expectedArray: string[] = [storageValue, secondStorageValue];
 
 
-        let array: string[];
+            let array: string[];
 
 
-        before(() => {
-            storage = getStorageInstance<string>();
-            storage.set(storageKey, storageValue);
+            before(() => {
+                storage = getStorageInstance<string>();
+                storage.set(storageKey, storageValue);
+
+                const secondStorage: IArrayStorage <string> = getStorageInstance<string>();
+                secondStorage.set(secondStorageKey, secondStorageValue);
 
 
-            const secondStorage: IArrayStorage <string> = getStorageInstance<string>();
-            secondStorage.set(secondStorageKey, secondStorageValue);
+                storage.mergeWith(secondStorage, false);
 
 
-            storage.mergeWith(secondStorage, false);
+                array = storage.getStorage();
+            });
 
 
-            array = storage.getStorage();
+            it('should merge two storages', () => {
+                assert.deepEqual(array, expectedArray);
+            });
         });
         });
 
 
-        it('should merge two storages', () => {
-            assert.deepEqual(array, expectedArray);
+        describe('Merge with storage id', () => {
+            const secondStorageKey: number = 1;
+            const secondStorageValue: string = 'bar';
+
+            const expectedArray: string[] = [storageValue, secondStorageValue];
+
+            let array: string[];
+            let storageId: string;
+            let expectedStorageId: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+                storage.set(storageKey, storageValue);
+
+                const secondStorage: IArrayStorage <string> = getStorageInstance<string>();
+                expectedStorageId = secondStorage.getStorageId();
+                secondStorage.set(secondStorageKey, secondStorageValue);
+
+                storage.mergeWith(secondStorage, true);
+
+                storageId = storage.getStorageId();
+                array = storage.getStorage();
+            });
+
+            it('should update storage id', () => {
+                assert.deepEqual(storageId, expectedStorageId);
+            });
+
+            it('should merge two storages', () => {
+                assert.deepEqual(array, expectedArray);
+            });
         });
         });
     });
     });
 });
 });

+ 37 - 2
test/unit-tests/storages/MapStorage.spec.ts

@@ -91,6 +91,41 @@ describe('MapStorage', () => {
             });
             });
         });
         });
 
 
+        describe('Variant #2: value isn\'t exist', () => {
+            const expectedValue: undefined = undefined;
+
+            let value: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+
+                value = storage.get(storageKey);
+            });
+
+            it('should return undefined', () => {
+                assert.equal(value, expectedValue);
+            });
+        });
+    });
+
+    describe('getOrThrow', () => {
+        describe('Variant #1: value exist', () => {
+            const expectedValue: string = storageValue;
+
+            let value: string;
+
+            before(() => {
+                storage = getStorageInstance<string>();
+                storage.set(storageKey, storageValue);
+
+                value = storage.getOrThrow(storageKey);
+            });
+
+            it('should return value from storage by key', () => {
+                assert.equal(value, expectedValue);
+            });
+        });
+
         describe('Variant #2: value isn\'t exist', () => {
         describe('Variant #2: value isn\'t exist', () => {
             const expectedError: ErrorConstructor = Error;
             const expectedError: ErrorConstructor = Error;
 
 
@@ -99,7 +134,7 @@ describe('MapStorage', () => {
             before(() => {
             before(() => {
                 storage = getStorageInstance<string>();
                 storage = getStorageInstance<string>();
 
 
-                testFunc = () => storage.get(storageKey);
+                testFunc = () => storage.getOrThrow(storageKey);
             });
             });
 
 
             it('should throw an error', () => {
             it('should throw an error', () => {
@@ -219,7 +254,7 @@ describe('MapStorage', () => {
             storage = getStorageInstance<string>();
             storage = getStorageInstance<string>();
             storage.set(storageKey, storageValue);
             storage.set(storageKey, storageValue);
 
 
-            value = storage.get(storageKey);
+            value = storage.getOrThrow(storageKey);
         });
         });
 
 
         it('should set value to the storage', () => {
         it('should set value to the storage', () => {

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