소스 검색

Merge pull request #664 from javascript-obfuscator/numbers-to-expressions

`numbersToExpressions` option
Timofey Kachalov 4 년 전
부모
커밋
a0761f55fa
31개의 변경된 파일1245개의 추가작업 그리고 23개의 파일을 삭제
  1. 4 0
      CHANGELOG.md
  2. 19 0
      README.md
  3. 0 0
      dist/index.browser.js
  4. 0 0
      dist/index.cli.js
  5. 0 0
      dist/index.js
  6. 4 4
      package.json
  7. 1 0
      src/JavaScriptObfuscator.ts
  8. 130 0
      src/analyzers/number-numerical-expression-analyzer/NumberNumericalExpressionAnalyzer.ts
  9. 4 0
      src/cli/JavaScriptObfuscatorCLI.ts
  10. 1 0
      src/container/ServiceIdentifiers.ts
  11. 7 0
      src/container/modules/analyzers/AnalyzersModule.ts
  12. 7 1
      src/container/modules/node-transformers/ConvertingTransformersModule.ts
  13. 2 0
      src/custom-code-helpers/CustomCodeHelperObfuscator.ts
  14. 1 0
      src/enums/node-transformers/NodeTransformer.ts
  15. 9 0
      src/interfaces/analyzers/number-numerical-expression-analyzer/INumberNumericalExpressionAnalyzer.ts
  16. 1 0
      src/interfaces/options/IOptions.ts
  17. 175 0
      src/node-transformers/converting-transformers/NumberToNumericalExpressionTransformer.ts
  18. 6 0
      src/options/Options.ts
  19. 1 0
      src/options/presets/Default.ts
  20. 1 0
      src/options/presets/NoCustomNodes.ts
  21. 1 0
      src/types/analyzers/number-numerical-expression-analyzer/TNumberNumericalExpressionData.ts
  22. 82 0
      src/utils/NumberUtils.ts
  23. 8 5
      test/dev/dev.ts
  24. 1 0
      test/functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec.ts
  25. 167 0
      test/functional-tests/node-transformers/converting-transformers/numbers-to-numerical-expressions-transformer/NumbersToNumericalExpressionsTransformer.spec.ts
  26. 3 0
      test/functional-tests/node-transformers/converting-transformers/numbers-to-numerical-expressions-transformer/fixtures/non-computed-object-key.js
  27. 2 0
      test/index.spec.ts
  28. 1 0
      test/runtime-tests/JavaScriptObfuscatorRuntime.spec.ts
  29. 178 0
      test/unit-tests/analyzers/number-numerical-expression-analyzer/NumberNumericalExpressionAnalyzer.spec.ts
  30. 417 1
      test/unit-tests/utils/NumberUtils.spec.ts
  31. 12 12
      yarn.lock

+ 4 - 0
CHANGELOG.md

@@ -1,5 +1,9 @@
 Change Log
 
+v1.6.0
+---
+* **New option:** `numbersToExpressions` enables numbers conversion to expressions
+
 v1.5.2
 ---
 * Prevented mutation of the name sequences of `mangled` identifier name generators

+ 19 - 0
README.md

@@ -329,6 +329,7 @@ Following options are available for the JS Obfuscator:
     identifiersPrefix: '',
     inputFileName: '',
     log: false,
+    numbersToExpressions: false,
     renameGlobals: false,
     renameProperties: false,
     reservedNames: [],
@@ -375,6 +376,7 @@ Following options are available for the JS Obfuscator:
     --identifiers-dictionary '<list>' (comma separated)
     --identifiers-prefix <string>
     --log <boolean>
+    --numbers-to-expressions <boolean>
     --rename-globals <boolean>
     --rename-properties <boolean>
     --reserved-names '<list>' (comma separated)
@@ -667,6 +669,20 @@ Type: `boolean` Default: `false`
 
 Enables logging of the information to the console.
 
+### `numbersToExpressions`
+Type: `boolean` Default: `false`
+
+Enables numbers conversion to expressions
+
+Example: 
+```ts
+// input
+const foo = 1234;
+
+// output
+const foo=-0xd93+-0x10b4+0x41*0x67+0x84e*0x3+-0xff8;
+```
+
 ### `renameGlobals`
 Type: `boolean` Default: `false`
 
@@ -985,6 +1001,7 @@ Performance will 50-100% slower than without obfuscation
     disableConsoleOutput: true,
     identifierNamesGenerator: 'hexadecimal',
     log: false,
+    numbersToExpressions: true,
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,
@@ -1016,6 +1033,7 @@ Performance will 30-35% slower than without obfuscation
     disableConsoleOutput: true,
     identifierNamesGenerator: 'hexadecimal',
     log: false,
+    numbersToExpressions: false,
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,
@@ -1045,6 +1063,7 @@ Performance will slightly slower than without obfuscation
     disableConsoleOutput: true,
     identifierNamesGenerator: 'hexadecimal',
     log: false,
+    numbersToExpressions: false,
     renameGlobals: false,
     rotateStringArray: true,
     selfDefending: true,

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/index.browser.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/index.cli.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/index.js


+ 4 - 4
package.json

@@ -1,6 +1,6 @@
 {
   "name": "javascript-obfuscator",
-  "version": "1.5.2",
+  "version": "1.6.0",
   "description": "JavaScript obfuscator",
   "keywords": [
     "obfuscator",
@@ -54,7 +54,7 @@
     "@types/mkdirp": "1.0.1",
     "@types/mocha": "7.0.2",
     "@types/multimatch": "4.0.0",
-    "@types/node": "14.0.20",
+    "@types/node": "14.0.22",
     "@types/rimraf": "3.0.0",
     "@types/sinon": "9.0.4",
     "@types/string-template": "1.0.2",
@@ -65,7 +65,7 @@
     "coveralls": "3.1.0",
     "eslint": "7.4.0",
     "eslint-plugin-import": "2.22.0",
-    "eslint-plugin-jsdoc": "29.1.2",
+    "eslint-plugin-jsdoc": "29.1.3",
     "eslint-plugin-no-null": "1.0.2",
     "eslint-plugin-prefer-arrow": "1.2.1",
     "eslint-plugin-unicorn": "20.1.0",
@@ -83,7 +83,7 @@
     "typescript": "3.9.6",
     "webpack": "4.43.0",
     "webpack-cli": "3.3.12",
-    "webpack-node-externals": "2.3.0"
+    "webpack-node-externals": "2.5.0"
   },
   "repository": {
     "type": "git",

+ 1 - 0
src/JavaScriptObfuscator.ts

@@ -75,6 +75,7 @@ export class JavaScriptObfuscator implements IJavaScriptObfuscator {
         NodeTransformer.MemberExpressionTransformer,
         NodeTransformer.MetadataTransformer,
         NodeTransformer.MethodDefinitionTransformer,
+        NodeTransformer.NumberToNumericalExpressionTransformer,
         NodeTransformer.ObfuscatingGuardsTransformer,
         NodeTransformer.ObjectExpressionKeysTransformer,
         NodeTransformer.ObjectExpressionTransformer,

+ 130 - 0
src/analyzers/number-numerical-expression-analyzer/NumberNumericalExpressionAnalyzer.ts

@@ -0,0 +1,130 @@
+import { injectable, inject } from 'inversify';
+
+import { TNumberNumericalExpressionData } from '../../types/analyzers/number-numerical-expression-analyzer/TNumberNumericalExpressionData';
+
+import { INumberNumericalExpressionAnalyzer } from '../../interfaces/analyzers/number-numerical-expression-analyzer/INumberNumericalExpressionAnalyzer';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import { NumberUtils } from '../../utils/NumberUtils';
+
+/**
+ * Based on https://gist.github.com/da411d/0e59f79dcf4603cdabf0024a10eeb6fe
+ */
+@injectable()
+export class NumberNumericalExpressionAnalyzer implements INumberNumericalExpressionAnalyzer {
+    /**
+     * @type {number}
+     */
+    private static readonly additionalParts: number = 3;
+
+    /**
+     * @type {Map<number, number[]>}
+     */
+    private readonly numberFactorsMap: Map<number, number[]> = new Map();
+
+    /**
+     * @type {IRandomGenerator}
+     */
+    private readonly randomGenerator: IRandomGenerator;
+
+    /**
+     * @param {IRandomGenerator} randomGenerator
+     */
+    public constructor (
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator
+    ) {
+        this.randomGenerator = randomGenerator;
+    }
+
+    /**
+     * @param {number} number
+     * @returns {TNumberNumericalExpressionData}
+     */
+    public analyze (number: number): TNumberNumericalExpressionData {
+        if (isNaN(number)) {
+            throw new Error('Given value is NaN');
+        }
+
+        if (NumberUtils.isUnsafeNumber(number)) {
+            return [number];
+        }
+
+        const additionParts: number[] = this.generateAdditionParts(number);
+
+        return additionParts.map((addition: number) => this.mixWithMultiplyParts(addition));
+    }
+
+    /**
+     * @param {number} number
+     * @returns {number[]}
+     */
+    private generateAdditionParts (number: number): number[] {
+        const additionParts = [];
+
+        const upperNumberLimit: number = Math.min(Math.abs(number * 2), Number.MAX_SAFE_INTEGER);
+
+        const from: number = Math.min(-10000, -upperNumberLimit);
+        const to: number = Math.max(10000, upperNumberLimit);
+
+        let temporarySum = 0;
+
+        for (let i = 0; i < NumberNumericalExpressionAnalyzer.additionalParts; i++) {
+            if (i < NumberNumericalExpressionAnalyzer.additionalParts - 1) {
+                // trailing parts
+
+                let addition: number = this.randomGenerator.getRandomInteger(from, to);
+                const isUnsafeCombination: boolean = NumberUtils.isUnsafeNumber(temporarySum + addition);
+
+                // we have to flip sign if total expression sum overflows over safe integer limits
+                if (isUnsafeCombination) {
+                    addition = -addition;
+                }
+
+                additionParts.push(addition);
+                temporarySum += addition;
+            } else {
+                const combination: number = number - temporarySum;
+                const isUnsafeCombination: boolean = NumberUtils.isUnsafeNumber(combination);
+
+                // last part
+                if (isUnsafeCombination) {
+                    additionParts.push(0 - temporarySum);
+                    additionParts.push(number);
+                } else {
+                    additionParts.push(combination);
+                }
+            }
+        }
+
+        return additionParts;
+    }
+
+    /**
+     * @param {number} number
+     * @returns {number | number[]}
+     */
+    private mixWithMultiplyParts (number: number): number | number[] {
+        const shouldMixWithMultiplyParts: boolean = this.randomGenerator.getMathRandom() > 0.5;
+
+        if (!shouldMixWithMultiplyParts || number === 0) {
+            return number;
+        }
+
+        let factors: number[] | null = this.numberFactorsMap.get(number) ?? null;
+
+        if (!factors) {
+           factors = NumberUtils.getFactors(number);
+           this.numberFactorsMap.set(number, factors);
+        }
+
+        if (!factors.length) {
+            return number;
+        }
+
+        const factor: number = factors[this.randomGenerator.getRandomInteger(0, factors.length - 1)];
+
+        return [factor, number / factor];
+    }
+}

+ 4 - 0
src/cli/JavaScriptObfuscatorCLI.ts

@@ -249,6 +249,10 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
                 '--log <boolean>', 'Enables logging of the information to the console',
                 BooleanSanitizer
             )
+            .option(
+                '--numbers-to-expressions <boolean>', 'Enables numbers conversion to expressions',
+                BooleanSanitizer
+            )
             .option(
                 '--reserved-names <list> (comma separated, without whitespaces)',
                 'Disables obfuscation and generation of identifiers, which being matched by passed RegExp patterns (comma separated)',

+ 1 - 0
src/container/ServiceIdentifiers.ts

@@ -37,6 +37,7 @@ export enum ServiceIdentifiers {
     INodeGuard = 'INodeGuard',
     INodeTransformer = 'INodeTransformer',
     INodeTransformerNamesGroupsBuilder = 'INodeTransformerNamesGroupsBuilder',
+    INumberNumericalExpressionAnalyzer = 'INumberNumericalExpressionAnalyzer',
     IObfuscationEventEmitter = 'IObfuscationEventEmitter',
     IObfuscatedCode = 'IObfuscatedCode',
     IOptions = 'IOptions',

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

@@ -4,6 +4,7 @@ import { ServiceIdentifiers } from '../../ServiceIdentifiers';
 
 import { ICalleeDataExtractor } from '../../../interfaces/analyzers/calls-graph-analyzer/ICalleeDataExtractor';
 import { ICallsGraphAnalyzer } from '../../../interfaces/analyzers/calls-graph-analyzer/ICallsGraphAnalyzer';
+import { INumberNumericalExpressionAnalyzer } from '../../../interfaces/analyzers/number-numerical-expression-analyzer/INumberNumericalExpressionAnalyzer';
 import { IPrevailingKindOfVariablesAnalyzer } from '../../../interfaces/analyzers/calls-graph-analyzer/IPrevailingKindOfVariablesAnalyzer';
 import { IScopeAnalyzer } from '../../../interfaces/analyzers/scope-analyzer/IScopeAnalyzer';
 import { IStringArrayStorageAnalyzer } from '../../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer';
@@ -12,6 +13,7 @@ import { CalleeDataExtractor } from '../../../enums/analyzers/calls-graph-analyz
 import { CallsGraphAnalyzer } from '../../../analyzers/calls-graph-analyzer/CallsGraphAnalyzer';
 import { FunctionDeclarationCalleeDataExtractor } from '../../../analyzers/calls-graph-analyzer/callee-data-extractors/FunctionDeclarationCalleeDataExtractor';
 import { FunctionExpressionCalleeDataExtractor } from '../../../analyzers/calls-graph-analyzer/callee-data-extractors/FunctionExpressionCalleeDataExtractor';
+import { NumberNumericalExpressionAnalyzer } from '../../../analyzers/number-numerical-expression-analyzer/NumberNumericalExpressionAnalyzer';
 import { ObjectExpressionCalleeDataExtractor } from '../../../analyzers/calls-graph-analyzer/callee-data-extractors/ObjectExpressionCalleeDataExtractor';
 import { PrevailingKindOfVariablesAnalyzer } from '../../../analyzers/prevailing-kind-of-variables-analyzer/PrevailingKindOfVariablesAnalyzer';
 import { ScopeAnalyzer } from '../../../analyzers/scope-analyzer/ScopeAnalyzer';
@@ -23,6 +25,11 @@ export const analyzersModule: interfaces.ContainerModule = new ContainerModule((
         .to(CallsGraphAnalyzer)
         .inSingletonScope();
 
+    // number numerical expression analyzer
+    bind<INumberNumericalExpressionAnalyzer>(ServiceIdentifiers.INumberNumericalExpressionAnalyzer)
+        .to(NumberNumericalExpressionAnalyzer)
+        .inSingletonScope();
+
     // prevailing kind of variables analyzer
     bind<IPrevailingKindOfVariablesAnalyzer>(ServiceIdentifiers.IPrevailingKindOfVariablesAnalyzer)
         .to(PrevailingKindOfVariablesAnalyzer)

+ 7 - 1
src/container/modules/node-transformers/ConvertingTransformersModule.ts

@@ -8,14 +8,15 @@ import { IObjectExpressionExtractor } from '../../../interfaces/node-transformer
 import { NodeTransformer } from '../../../enums/node-transformers/NodeTransformer';
 import { ObjectExpressionExtractor } from '../../../enums/node-transformers/converting-transformers/properties-extractors/ObjectExpressionExtractor';
 
+import { BasePropertiesExtractor } from '../../../node-transformers/converting-transformers/object-expression-extractors/BasePropertiesExtractor';
 import { ObjectExpressionToVariableDeclarationExtractor } from '../../../node-transformers/converting-transformers/object-expression-extractors/ObjectExpressionToVariableDeclarationExtractor';
 import { MemberExpressionTransformer } from '../../../node-transformers/converting-transformers/MemberExpressionTransformer';
 import { MethodDefinitionTransformer } from '../../../node-transformers/converting-transformers/MethodDefinitionTransformer';
+import { NumberToNumericalExpressionTransformer } from '../../../node-transformers/converting-transformers/NumberToNumericalExpressionTransformer';
 import { ObjectExpressionKeysTransformer } from '../../../node-transformers/converting-transformers/ObjectExpressionKeysTransformer';
 import { ObjectExpressionTransformer } from '../../../node-transformers/converting-transformers/ObjectExpressionTransformer';
 import { SplitStringTransformer } from '../../../node-transformers/converting-transformers/SplitStringTransformer';
 import { TemplateLiteralTransformer } from '../../../node-transformers/converting-transformers/TemplateLiteralTransformer';
-import { BasePropertiesExtractor } from '../../../node-transformers/converting-transformers/object-expression-extractors/BasePropertiesExtractor';
 
 export const convertingTransformersModule: interfaces.ContainerModule = new ContainerModule((bind: interfaces.Bind) => {
     // converting transformers
@@ -27,6 +28,11 @@ export const convertingTransformersModule: interfaces.ContainerModule = new Cont
         .to(MethodDefinitionTransformer)
         .whenTargetNamed(NodeTransformer.MethodDefinitionTransformer);
 
+
+    bind<INodeTransformer>(ServiceIdentifiers.INodeTransformer)
+        .to(NumberToNumericalExpressionTransformer)
+        .whenTargetNamed(NodeTransformer.NumberToNumericalExpressionTransformer);
+
     bind<INodeTransformer>(ServiceIdentifiers.INodeTransformer)
         .to(ObjectExpressionKeysTransformer)
         .whenTargetNamed(NodeTransformer.ObjectExpressionKeysTransformer);

+ 2 - 0
src/custom-code-helpers/CustomCodeHelperObfuscator.ts

@@ -47,6 +47,8 @@ export class CustomCodeHelperObfuscator implements ICustomCodeHelperObfuscator {
                 ...NO_ADDITIONAL_NODES_PRESET,
                 identifierNamesGenerator: this.options.identifierNamesGenerator,
                 identifiersDictionary: this.options.identifiersDictionary,
+                numbersToExpressions: this.options.numbersToExpressions,
+                simplify: this.options.simplify,
                 seed: this.randomGenerator.getRawSeed(),
                 ...additionalOptions
             }

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

@@ -11,6 +11,7 @@ export enum NodeTransformer {
     MemberExpressionTransformer = 'MemberExpressionTransformer',
     MetadataTransformer = 'MetadataTransformer',
     MethodDefinitionTransformer = 'MethodDefinitionTransformer',
+    NumberToNumericalExpressionTransformer = 'NumberToNumericalExpressionTransformer',
     ObfuscatingGuardsTransformer = 'ObfuscatingGuardsTransformer',
     ObjectExpressionKeysTransformer = 'ObjectExpressionKeysTransformer',
     ObjectExpressionTransformer = 'ObjectExpressionTransformer',

+ 9 - 0
src/interfaces/analyzers/number-numerical-expression-analyzer/INumberNumericalExpressionAnalyzer.ts

@@ -0,0 +1,9 @@
+import { TNumberNumericalExpressionData } from '../../../types/analyzers/number-numerical-expression-analyzer/TNumberNumericalExpressionData';
+
+export interface INumberNumericalExpressionAnalyzer {
+    /**
+     * @param {number} number
+     * @returns {TNumberNumericalExpressionData}
+     */
+    analyze (number: number): TNumberNumericalExpressionData;
+}

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

@@ -21,6 +21,7 @@ export interface IOptions {
     readonly identifiersPrefix: string;
     readonly inputFileName: string;
     readonly log: boolean;
+    readonly numbersToExpressions: boolean;
     readonly renameGlobals: boolean;
     readonly renameProperties: boolean;
     readonly reservedNames: string[];

+ 175 - 0
src/node-transformers/converting-transformers/NumberToNumericalExpressionTransformer.ts

@@ -0,0 +1,175 @@
+import { inject, injectable, } from 'inversify';
+import { ServiceIdentifiers } from '../../container/ServiceIdentifiers';
+
+import * as ESTree from 'estree';
+
+import { TNumberNumericalExpressionData } from '../../types/analyzers/number-numerical-expression-analyzer/TNumberNumericalExpressionData';
+
+import { INumberNumericalExpressionAnalyzer } from '../../interfaces/analyzers/number-numerical-expression-analyzer/INumberNumericalExpressionAnalyzer';
+import { IOptions } from '../../interfaces/options/IOptions';
+import { IRandomGenerator } from '../../interfaces/utils/IRandomGenerator';
+import { IVisitor } from '../../interfaces/node-transformers/IVisitor';
+
+import { NodeTransformationStage } from '../../enums/node-transformers/NodeTransformationStage';
+
+import { AbstractNodeTransformer } from '../AbstractNodeTransformer';
+import { NodeGuards } from '../../node/NodeGuards';
+import { NodeFactory } from '../../node/NodeFactory';
+import { NumberUtils } from '../../utils/NumberUtils';
+
+/**
+ * replaces:
+ *     var number = 123;
+ *
+ * on:
+ *     var number = 50 + (100 * 2) - 127;
+ */
+@injectable()
+export class NumberToNumericalExpressionTransformer extends AbstractNodeTransformer {
+    /**
+     * @type {INumberNumericalExpressionAnalyzer}
+     */
+    private readonly numberNumericalExpressionAnalyzer: INumberNumericalExpressionAnalyzer;
+
+    /**
+     * @param {INumberNumericalExpressionAnalyzer} numberNumericalExpressionAnalyzer
+     * @param {IRandomGenerator} randomGenerator
+     * @param {IOptions} options
+     */
+    public constructor (
+        @inject(ServiceIdentifiers.INumberNumericalExpressionAnalyzer)
+            numberNumericalExpressionAnalyzer: INumberNumericalExpressionAnalyzer,
+        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
+        @inject(ServiceIdentifiers.IOptions) options: IOptions
+    ) {
+        super(randomGenerator, options);
+
+        this.numberNumericalExpressionAnalyzer = numberNumericalExpressionAnalyzer;
+    }
+
+    /**
+     * @param {NodeTransformationStage} nodeTransformationStage
+     * @returns {IVisitor | null}
+     */
+    public getVisitor (nodeTransformationStage: NodeTransformationStage): IVisitor | null {
+        if (!this.options.numbersToExpressions) {
+            return null;
+        }
+
+        switch (nodeTransformationStage) {
+            case NodeTransformationStage.Converting:
+                return {
+                    leave: (node: ESTree.Node, parentNode: ESTree.Node | null): ESTree.Node | undefined => {
+                        if (parentNode && NodeGuards.isLiteralNode(node)) {
+                            return this.transformNode(node, parentNode);
+                        }
+                    }
+                };
+
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * @param {Literal} literalNode
+     * @param {NodeGuards} parentNode
+     * @returns {NodeGuards}
+     */
+    public transformNode (literalNode: ESTree.Literal, parentNode: ESTree.Node): ESTree.Node {
+        if (typeof literalNode.value !== 'number') {
+            return literalNode;
+        }
+
+        if (NodeGuards.isPropertyNode(parentNode) && !parentNode.computed) {
+            return literalNode;
+        }
+
+        const numberNumericalExpressionData: TNumberNumericalExpressionData = this.numberNumericalExpressionAnalyzer.analyze(literalNode.value);
+
+        return this.convertNumericalExpressionDataToNode(numberNumericalExpressionData);
+    }
+
+    /**
+     * @param {TNumberNumericalExpressionData} numberNumericalExpressionData
+     * @param {ESTree.BinaryOperator} operator
+     * @returns {ESTree.BinaryExpression | ESTree.Literal | ESTree.UnaryExpression}
+     */
+    private convertNumericalExpressionDataToNode (
+        numberNumericalExpressionData: TNumberNumericalExpressionData,
+        operator: ESTree.BinaryOperator = '+'
+    ): ESTree.BinaryExpression | ESTree.Literal | ESTree.UnaryExpression {
+        const numberNumericalExpressionDataLength: number = numberNumericalExpressionData.length;
+
+        const leftParts: TNumberNumericalExpressionData = numberNumericalExpressionDataLength > 1
+            ? numberNumericalExpressionData.slice(0, numberNumericalExpressionDataLength - 1)
+            : [numberNumericalExpressionData[0]];
+        const rightParts: TNumberNumericalExpressionData = numberNumericalExpressionDataLength > 1
+            ? numberNumericalExpressionData.slice(-1)
+            : [];
+
+        // trailing iterations
+        if (rightParts.length) {
+            return this.convertPartsToBinaryExpression(operator, leftParts, rightParts);
+        }
+
+        const firstLeftPartOrNumber: number | number[] | null = leftParts[0] ?? null;
+
+        // last iteration when only single left part is left
+        return Array.isArray(firstLeftPartOrNumber)
+            ? this.convertNumericalExpressionDataToNode(firstLeftPartOrNumber, '*')
+            : this.convertPartOrNumberToLiteralNode(firstLeftPartOrNumber);
+    }
+
+    /**
+     * @param {ESTree.BinaryOperator} operator
+     * @param {TNumberNumericalExpressionData} leftParts
+     * @param {TNumberNumericalExpressionData} rightParts
+     * @returns {ESTree.BinaryExpression}
+     */
+    private convertPartsToBinaryExpression (
+        operator: ESTree.BinaryOperator,
+        leftParts: TNumberNumericalExpressionData,
+        rightParts: TNumberNumericalExpressionData
+    ): ESTree.BinaryExpression {
+        const rightPartOrNumber: number | number[] = rightParts[0];
+
+        if (Array.isArray(rightPartOrNumber)) {
+            // right part is array with multiply numbers
+            return NodeFactory.binaryExpressionNode(
+                operator,
+                this.convertNumericalExpressionDataToNode(leftParts),
+                this.convertNumericalExpressionDataToNode(rightPartOrNumber, '*')
+            );
+        } else {
+            // right part is number
+            return NodeFactory.binaryExpressionNode(
+                operator,
+                this.convertNumericalExpressionDataToNode(leftParts),
+                this.convertPartOrNumberToLiteralNode(rightPartOrNumber)
+            );
+        }
+    }
+
+    /**
+     * @param {number | number[]} partOrNumber
+     * @returns {ESTree.Literal}
+     */
+    private convertPartOrNumberToLiteralNode (
+        partOrNumber: number | number[]
+    ): ESTree.Literal | ESTree.UnaryExpression {
+        const number: number = Array.isArray(partOrNumber)
+            ? partOrNumber[0]
+            : partOrNumber;
+        const isPositiveNumber: boolean = NumberUtils.isPositive(number);
+
+        const literalNode: ESTree.Literal = NodeFactory.literalNode(Math.abs(number));
+
+        return isPositiveNumber
+            ? literalNode
+            : NodeFactory.unaryExpressionNode(
+                '-',
+                literalNode
+            );
+    }
+}

+ 6 - 0
src/options/Options.ts

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

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

@@ -21,6 +21,7 @@ export const DEFAULT_PRESET: TInputOptions = Object.freeze({
     identifiersDictionary: [],
     inputFileName: '',
     log: false,
+    numbersToExpressions: false,
     renameGlobals: false,
     renameProperties: false,
     reservedNames: [],

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

@@ -20,6 +20,7 @@ export const NO_ADDITIONAL_NODES_PRESET: TInputOptions = Object.freeze({
     identifiersDictionary: [],
     inputFileName: '',
     log: false,
+    numbersToExpressions: false,
     renameGlobals: false,
     renameProperties: false,
     reservedNames: [],

+ 1 - 0
src/types/analyzers/number-numerical-expression-analyzer/TNumberNumericalExpressionData.ts

@@ -0,0 +1 @@
+export type TNumberNumericalExpressionData = (number | number[])[];

+ 82 - 0
src/utils/NumberUtils.ts

@@ -24,4 +24,86 @@ export class NumberUtils {
             ? number % 1 === 0
             : true;
     }
+
+    /**
+     * @param {number} number
+     * @returns {boolean}
+     */
+    public static isPositive (number: number): boolean {
+        if (isNaN(number)) {
+            throw new Error('Given value is NaN');
+        }
+
+        if (number > 0) {
+            return true;
+        }
+
+        if (number < 0) {
+            return false;
+        }
+
+        if (1 / number === Number.POSITIVE_INFINITY) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @param {number} number
+     * @returns {boolean}
+     */
+    public static isUnsafeNumber (number: number): boolean {
+        if (isNaN(number)) {
+            throw new Error('Given value is NaN');
+        }
+
+        return number < Number.MIN_SAFE_INTEGER || number > Number.MAX_SAFE_INTEGER;
+    }
+
+    /**
+     * Returns all factors of a number
+     * Based on https://stackoverflow.com/a/43204663
+     *
+     * @param {number} number
+     * @returns {number[]}
+     */
+    public static getFactors (number: number): number[] {
+        if (number === 0) {
+            throw new Error('Invalid number. Allowed only non-zero number');
+        }
+
+        number = Math.abs(number);
+
+        // special case for 1
+        if (number === 1) {
+            return [-number, number];
+        }
+
+        const factors: number[] = [];
+
+        const root: number = Math.sqrt(number);
+        const isEven: boolean = number % 2 === 0;
+        const incrementValue: number = isEven ? 1 : 2;
+
+        for (
+            let currentFactor = 1;
+            currentFactor <= root;
+            currentFactor += incrementValue
+        ) {
+            if (number % currentFactor !== 0) {
+                continue;
+            }
+
+            factors.push(...[-currentFactor, currentFactor]);
+
+            const compliment: number = number / currentFactor;
+
+            if (compliment !== currentFactor) {
+                factors.push(...[-compliment, compliment]);
+            }
+        }
+
+        return factors.sort((a: number, b: number) => a - b);
+    }
 }

+ 8 - 5
test/dev/dev.ts

@@ -1,6 +1,5 @@
 'use strict';
 
-import { IdentifierNamesGenerator } from '../../src/enums/generators/identifier-names-generators/IdentifierNamesGenerator';
 import { NO_ADDITIONAL_NODES_PRESET } from '../../src/options/presets/NoCustomNodes';
 
 (function () {
@@ -8,13 +7,17 @@ import { NO_ADDITIONAL_NODES_PRESET } from '../../src/options/presets/NoCustomNo
 
     let obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
         `
-            function foo (arg1, arg2, arg3) {
-                console.log(arg1, arg2, arg3);
-            }
+            const foo = 0;
+            const bar = 'abc';
+            
+            console.log(foo, bar);
         `,
         {
             ...NO_ADDITIONAL_NODES_PRESET,
-            identifierNamesGenerator: IdentifierNamesGenerator.MangledShuffledIdentifierNamesGenerator
+            compact: false,
+            numbersToExpressions: true,
+            stringArray: true,
+            stringArrayThreshold: 1
         }
     ).getObfuscatedCode();
 

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

@@ -810,6 +810,7 @@ describe('JavaScriptObfuscator', () => {
                         deadCodeInjection: true,
                         deadCodeInjectionThreshold: 1,
                         disableConsoleOutput: false,
+                        numbersToExpressions: true,
                         simplify: true,
                         renameProperties: true,
                         rotateStringArray: true,

+ 167 - 0
test/functional-tests/node-transformers/converting-transformers/numbers-to-numerical-expressions-transformer/NumbersToNumericalExpressionsTransformer.spec.ts

@@ -0,0 +1,167 @@
+import { assert } from 'chai';
+
+import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';
+
+import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade';
+import { readFileAsString } from '../../../../helpers/readFileAsString';
+
+describe('NumbersToNumericalExpressionsTransformer', function () {
+    this.timeout(60000);
+
+    describe('Variant #1: base', () => {
+        const initialNumber: number = -50;
+        const lastNumber: number = 50;
+
+        let areValidExpressions: boolean = true;
+
+        before(() => {
+            for (let i = initialNumber; i < lastNumber; i++) {
+                const number: number = i;
+                const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                    `${number};`,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        numbersToExpressions: true
+                    }
+                ).getObfuscatedCode();
+
+                const result: number = eval(obfuscatedCode);
+
+                if (result !== number) {
+                    areValidExpressions = false;
+                    break;
+                }
+            }
+        });
+
+        it('should correctly transform numbers to expressions', () => {
+            assert.isTrue(areValidExpressions);
+        });
+    });
+
+    describe('Variant #2: safe integers', () => {
+        describe('Variant #1: max safe integer', () => {
+            const number: number = Number.MAX_SAFE_INTEGER;
+            const samplesCount: number = 15;
+
+            let areValidExpressions: boolean = true;
+
+            before(() => {
+                for (let i = 0; i < samplesCount; i++) {
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        `${number};`,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            numbersToExpressions: true
+                        }
+                    ).getObfuscatedCode();
+
+                    const result: number = eval(obfuscatedCode);
+
+                    if (result !== number) {
+                        areValidExpressions = false;
+                        break;
+                    }
+                }
+            });
+
+            it('should correctly transform numbers to expressions', () => {
+                assert.isTrue(areValidExpressions);
+            });
+        });
+
+        describe('Variant #2: max unsafe integer', () => {
+            const unsafeIntegerRegExp: RegExp = /0x20000000000000;/;
+            const number: number = Number.MAX_SAFE_INTEGER + 1;
+
+            let obfuscatedCode: string;
+
+            before(() => {
+                obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                    `${number};`,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        numbersToExpressions: true
+                    }
+                ).getObfuscatedCode();
+            });
+
+            it('should not transform unsafe integer to expressions', () => {
+                assert.match(obfuscatedCode, unsafeIntegerRegExp);
+            });
+        });
+
+        describe('Variant #3: min safe integer', () => {
+            const number: number = Number.MIN_SAFE_INTEGER;
+            const samplesCount: number = 15;
+
+            let areValidExpressions: boolean = true;
+
+            before(() => {
+                for (let i = 0; i < samplesCount; i++) {
+                    const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(
+                        `${number};`,
+                        {
+                            ...NO_ADDITIONAL_NODES_PRESET,
+                            numbersToExpressions: true
+                        }
+                    ).getObfuscatedCode();
+
+                    const result: number = eval(obfuscatedCode);
+
+                    if (result !== number) {
+                        areValidExpressions = false;
+                        break;
+                    }
+                }
+            });
+
+            it('should correctly transform numbers to expressions', () => {
+                assert.isTrue(areValidExpressions);
+            });
+        });
+
+        describe('Variant #4: min unsafe integer', () => {
+            const unsafeIntegerRegExp: RegExp = /-0x20000000000000;/;
+            const number: number = Number.MIN_SAFE_INTEGER - 1;
+
+            let obfuscatedCode: string;
+
+            before(() => {
+                obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                    `${number};`,
+                    {
+                        ...NO_ADDITIONAL_NODES_PRESET,
+                        numbersToExpressions: true
+                    }
+                ).getObfuscatedCode();
+            });
+
+            it('should not transform unsafe integer to expressions', () => {
+                assert.match(obfuscatedCode, unsafeIntegerRegExp);
+            });
+        });
+    });
+
+    describe('Variant #3: parent node is non-computed object property', () => {
+        const regExp: RegExp = /const foo *= *{1: *'bar'};/;
+
+        let obfuscatedCode: string;
+
+        before(() => {
+            const code: string = readFileAsString(__dirname + '/fixtures/non-computed-object-key.js');
+
+            obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                code,
+                {
+                    ...NO_ADDITIONAL_NODES_PRESET,
+                    numbersToExpressions: true
+                }
+            ).getObfuscatedCode();
+        });
+
+        it('should not replace non-computed object property literal with expression', () => {
+            assert.match(obfuscatedCode,  regExp);
+        });
+    });
+});

+ 3 - 0
test/functional-tests/node-transformers/converting-transformers/numbers-to-numerical-expressions-transformer/fixtures/non-computed-object-key.js

@@ -0,0 +1,3 @@
+const foo = {
+    1: 'bar'
+};

+ 2 - 0
test/index.spec.ts

@@ -6,6 +6,7 @@ require('source-map-support').install();
  * Unit tests
  */
 import './unit-tests/analyzers/calls-graph-analyzer/CallsGraphAnalyzer.spec';
+import './unit-tests/analyzers/number-numerical-expression-analyzer/NumberNumericalExpressionAnalyzer.spec';
 import './unit-tests/analyzers/prevailing-kind-of-variables-analyzer/PrevailingKindOfVariablesAnalyzer.spec';
 import './unit-tests/analyzers/scope-analyzer/ScopeAnalyzer.spec';
 import './unit-tests/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.spec';
@@ -87,6 +88,7 @@ import './functional-tests/node-transformers/control-flow-transformers/control-f
 import './functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/string-litertal-control-flow-replacer/StringLiteralControlFlowReplacer.spec';
 import './functional-tests/node-transformers/converting-transformers/member-expression-transformer/MemberExpressionTransformer.spec';
 import './functional-tests/node-transformers/converting-transformers/method-definition-transformer/MethodDefinitionTransformer.spec';
+import './functional-tests/node-transformers/converting-transformers/numbers-to-numerical-expressions-transformer/NumbersToNumericalExpressionsTransformer.spec';
 import './functional-tests/node-transformers/converting-transformers/object-expression-keys-transformer/ObjectExpressionKeysTransformer.spec';
 import './functional-tests/node-transformers/converting-transformers/object-expression-transformer/ObjectExpressionTransformer.spec';
 import './functional-tests/node-transformers/converting-transformers/split-string-transformer/SplitStringTransformer.spec';

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

@@ -25,6 +25,7 @@ describe('JavaScriptObfuscator runtime eval', function () {
         debugProtection: true,
         disableConsoleOutput: true,
         domainLock: ['obfuscator.io'],
+        numbersToExpressions: true,
         simplify: true,
         renameProperties: true,
         reservedNames: ['generate', 'sha256'],

+ 178 - 0
test/unit-tests/analyzers/number-numerical-expression-analyzer/NumberNumericalExpressionAnalyzer.spec.ts

@@ -0,0 +1,178 @@
+import 'reflect-metadata';
+
+import { assert } from 'chai';
+
+import { ServiceIdentifiers } from '../../../../src/container/ServiceIdentifiers';
+
+import { TNumberNumericalExpressionData } from '../../../../src/types/analyzers/number-numerical-expression-analyzer/TNumberNumericalExpressionData';
+
+import { IInversifyContainerFacade } from '../../../../src/interfaces/container/IInversifyContainerFacade';
+import { INumberNumericalExpressionAnalyzer } from '../../../../src/interfaces/analyzers/number-numerical-expression-analyzer/INumberNumericalExpressionAnalyzer';
+
+import { InversifyContainerFacade } from '../../../../src/container/InversifyContainerFacade';
+
+/**
+ * @param {TNumberNumericalExpressionData} data
+ * @returns {string}
+ */
+const numberNumericalExpressionDataToString = (data: TNumberNumericalExpressionData) =>
+    data
+        .map((part: number | number[]) => Array.isArray(part) ? part.join('*') : part)
+        .join('+')
+        .replace(/\+-/g, '-');
+
+describe('NumberNumericalExpressionAnalyzer', function() {
+    let numberNumericalExpressionAnalyzer: INumberNumericalExpressionAnalyzer;
+
+    this.timeout(10000);
+
+    before(() => {
+        const inversifyContainerFacade: IInversifyContainerFacade = new InversifyContainerFacade();
+
+        inversifyContainerFacade.load('', '', {});
+        numberNumericalExpressionAnalyzer = inversifyContainerFacade
+            .get<INumberNumericalExpressionAnalyzer>(ServiceIdentifiers.INumberNumericalExpressionAnalyzer);
+    });
+
+    describe('analyze', () => {
+        let evaluatedResult: number;
+
+        describe('Positive numbers', () => {
+            describe('Variant #1: positive number', () => {
+                const number: number = 1234;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
+            });
+
+            describe('Variant #2: positive zero number', () => {
+                const number: number = 0;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
+            });
+
+            describe('Variant #3: positive big number', () => {
+                const number: number = Number.MAX_SAFE_INTEGER;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
+            });
+
+            describe('Variant #4: positive unsafe big number', () => {
+                const number: number = Number.MAX_SAFE_INTEGER + 1;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
+            });
+        });
+
+        describe('Negative numbers', () => {
+            describe('Variant #1: negative number', () => {
+                const number: number = -1234;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
+            });
+
+            describe('Variant #2: negative zero number', () => {
+                const number: number = -0;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
+            });
+
+            describe('Variant #3: negative big number', () => {
+                const number: number = Number.MIN_SAFE_INTEGER;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
+            });
+
+            describe('Variant #4: negative unsafe number', () => {
+                const number: number = Number.MIN_SAFE_INTEGER - 1;
+
+                before(() => {
+                    const numberNumericalExpressionData: TNumberNumericalExpressionData =
+                        numberNumericalExpressionAnalyzer.analyze(number);
+
+                    evaluatedResult = eval(numberNumericalExpressionDataToString(numberNumericalExpressionData));
+                });
+
+                it('should return correct number numerical expression data', () => {
+                    assert.equal(number, evaluatedResult);
+                });
+            });
+        });
+
+        describe('NaN', () => {
+            const number: number = NaN;
+
+            let testFunc: () => void;
+
+            before(() => {
+                testFunc = () => numberNumericalExpressionAnalyzer.analyze(number);
+            });
+
+            it('should throw error', () => {
+                assert.throw(testFunc, 'Given value is NaN');
+            });
+        });
+    });
+});

+ 417 - 1
test/unit-tests/utils/NumberUtils.spec.ts

@@ -2,7 +2,9 @@ import { assert } from 'chai';
 
 import { NumberUtils } from '../../../src/utils/NumberUtils';
 
-describe('NumberUtils', () => {
+describe('NumberUtils', function () {
+    this.timeout(30000);
+
     describe('toHex', () => {
         describe('Variant #1: number `0`', () => {
             const number: number = 0;
@@ -128,4 +130,418 @@ describe('NumberUtils', () => {
             });
         });
     });
+
+    describe('isPositive', () => {
+        describe('Variant #1: positive integer', () => {
+            const number: number = 2;
+            const expectedResult: boolean = true;
+
+            let result: boolean;
+
+            before(() => {
+                result = NumberUtils.isPositive(number);
+            });
+
+            it('should return true', () => {
+                assert.equal(result, expectedResult);
+            });
+        });
+
+        describe('Variant #2: positive zero', () => {
+            const number: number = 0;
+            const expectedResult: boolean = true;
+
+            let result: boolean;
+
+            before(() => {
+                result = NumberUtils.isPositive(number);
+            });
+
+            it('should return true', () => {
+                assert.equal(result, expectedResult);
+            });
+        });
+
+        describe('Variant #3: negative integer', () => {
+            const number: number = -2;
+            const expectedResult: boolean = false;
+
+            let result: boolean;
+
+            before(() => {
+                result = NumberUtils.isPositive(number);
+            });
+
+            it('should return false', () => {
+                assert.equal(result, expectedResult);
+            });
+        });
+
+        describe('Variant #4: negative zero', () => {
+            const number: number = -0;
+            const expectedResult: boolean = false;
+
+            let result: boolean;
+
+            before(() => {
+                result = NumberUtils.isPositive(number);
+            });
+
+            it('should return false', () => {
+                assert.equal(result, expectedResult);
+            });
+        });
+
+        describe('Variant #5: NaN', () => {
+            const number: number = NaN;
+
+            let testFunc: () => void;
+
+            before(() => {
+                testFunc = () => NumberUtils.isPositive(number);
+            });
+
+            it('should throw an error', () => {
+                assert.throw(testFunc, 'Given value is NaN');
+            });
+        });
+    });
+
+    describe('isUnsafeNumber', () => {
+        describe('Positive number', () => {
+            describe('Variant #1: positive small safe integer', () => {
+                const number: number = 100;
+                const expectedResult: boolean = false
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return false', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+
+            describe('Variant #2: positive big safe integer', () => {
+                const number: number = Number.MAX_SAFE_INTEGER;
+                const expectedResult: boolean = false
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return false', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+
+            describe('Variant #3: positive unsafe integer', () => {
+                const number: number = Number.MAX_SAFE_INTEGER + 1;
+                const expectedResult: boolean = true
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return true', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+        });
+
+        describe('Negative number', () => {
+            describe('Variant #1: negative small safe integer', () => {
+                const number: number = -100;
+                const expectedResult: boolean = false
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return false', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+
+            describe('Variant #2: negative big safe integer', () => {
+                const number: number = Number.MIN_SAFE_INTEGER;
+                const expectedResult: boolean = false
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return false', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+
+            describe('Variant #3: negative unsafe integer', () => {
+                const number: number = Number.MIN_SAFE_INTEGER - 1;
+                const expectedResult: boolean = true
+
+                let result: boolean;
+
+                before(() => {
+                    result = NumberUtils.isUnsafeNumber(number);
+                });
+
+                it('should return true', () => {
+                    assert.equal(result, expectedResult);
+                });
+            });
+        });
+
+        describe('NaN', () => {
+            const number: number = NaN;
+
+            let testFunc: () => void;
+
+            before(() => {
+                testFunc = () => NumberUtils.isUnsafeNumber(number);
+            });
+
+            it('should throw an error', () => {
+                assert.throw(testFunc, 'Given value is NaN');
+            });
+        });
+    });
+
+    describe('getFactors', () => {
+        describe('Positive numbers', () => {
+            describe('Variant #1: positive number `1`', () => {
+                const number: number = 1;
+                const expectedFactors: number[] = [
+                    -1,
+                    1
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #2: positive number `2`', () => {
+                const number: number = 2;
+                const expectedFactors: number[] = [
+                    -2,
+                    -1,
+                    1,
+                    2
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #3: positive number `100`', () => {
+                const number: number = 100;
+                const expectedFactors: number[] = [
+                    -100,
+                    -50,
+                    -25,
+                    -20,
+                    -10,
+                    -5,
+                    -4,
+                    -2,
+                    -1,
+                    1,
+                    2,
+                    4,
+                    5,
+                    10,
+                    20,
+                    25,
+                    50,
+                    100
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #4: positive number `9007199254740991`', () => {
+                const number: number = 9007199254740991;
+                const expectedFactors: number[] = [
+                    Number.MIN_SAFE_INTEGER,
+                    -1416003655831,
+                    -129728784761,
+                    -441650591,
+                    -20394401,
+                    -69431,
+                    -6361,
+                    -1,
+                    1,
+                    6361,
+                    69431,
+                    20394401,
+                    441650591,
+                    129728784761,
+                    1416003655831,
+                    Number.MAX_SAFE_INTEGER
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+        })
+
+        describe('Negative numbers', () => {
+            describe('Variant #1: negative number `-1`', () => {
+                const number: number = -1;
+                const expectedFactors: number[] = [
+                    -1,
+                    1
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #2: negative number `-2`', () => {
+                const number: number = -2;
+                const expectedFactors: number[] = [
+                    -2,
+                    -1,
+                    1,
+                    2
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #3: negative number `-100`', () => {
+                const number: number = -100;
+                const expectedFactors: number[] = [
+                    -100,
+                    -50,
+                    -25,
+                    -20,
+                    -10,
+                    -5,
+                    -4,
+                    -2,
+                    -1,
+                    1,
+                    2,
+                    4,
+                    5,
+                    10,
+                    20,
+                    25,
+                    50,
+                    100
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+
+            describe('Variant #4: negative number `-9007199254740991`', () => {
+                const number: number = -9007199254740991;
+                const expectedFactors: number[] = [
+                    Number.MIN_SAFE_INTEGER,
+                    -1416003655831,
+                    -129728784761,
+                    -441650591,
+                    -20394401,
+                    -69431,
+                    -6361,
+                    -1,
+                    1,
+                    6361,
+                    69431,
+                    20394401,
+                    441650591,
+                    129728784761,
+                    1416003655831,
+                    Number.MAX_SAFE_INTEGER
+                ];
+
+                let factors: number[];
+
+                before(() => {
+                    factors = NumberUtils.getFactors(number);
+                });
+
+                it('should return factors of a number', () => {
+                    assert.deepEqual(factors, expectedFactors);
+                });
+            });
+        })
+
+        describe('zero number', () => {
+            const number: number = 0;
+
+            let testFunc: () => void;
+
+            before(() => {
+                testFunc = () => NumberUtils.getFactors(number);
+            });
+
+            it('should throw an error', () => {
+                assert.throw(testFunc, Error);
+            });
+        })
+    });
 });

+ 12 - 12
yarn.lock

@@ -369,10 +369,10 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.3.tgz#6356df2647de9eac569f9a52eda3480fa9e70b4d"
   integrity sha512-01s+ac4qerwd6RHD+mVbOEsraDHSgUaefQlEdBbUolnQFjKwCr7luvAlEwW1RFojh67u0z4OUTjPn9LEl4zIkA==
 
-"@types/[email protected]0":
-  version "14.0.20"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.20.tgz#0da05cddbc761e1fa98af88a17244c8c1ff37231"
-  integrity sha512-MRn/NP3dee8yL5QhbSA6riuwkS+UOcsPUMOIOG3KMUQpuor/2TopdRBu8QaaB4fGU+gz/bzyDWt0FtUbeJ8H1A==
+"@types/[email protected]2":
+  version "14.0.22"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.22.tgz#23ea4d88189cec7d58f9e6b66f786b215eb61bdc"
+  integrity sha512-emeGcJvdiZ4Z3ohbmw93E/64jRzUHAItSHt8nF7M4TGgQTiWqFVGB8KNpLGFmUHmHLvjvBgFwVlqNcq+VuGv9g==
 
 "@types/normalize-package-data@^2.4.0":
   version "2.4.0"
@@ -1923,10 +1923,10 @@ [email protected]:
     resolve "^1.17.0"
     tsconfig-paths "^3.9.0"
 
[email protected].2:
-  version "29.1.2"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-29.1.2.tgz#542beba99946cb8d184e5261ebc632b24673f825"
-  integrity sha512-s1uJcPcjZQ4Z5i98T3zWkp/gzJ9AtHGXXg0zxd0wCnFzX8RQU6awdUokUlBFMoWZJZxdCAXDtEIQBRfr/Lrsjw==
[email protected].3:
+  version "29.1.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-29.1.3.tgz#288c3faf142fbf78e6abff6a838db23af8d6749f"
+  integrity sha512-HEB8jPsWBGu++LffL4K8VZ7VXz0HELI1I3Qkv/+5oSekgrAo6I0AVgl5abecLbTQQZo0OaEcmTptiIspwDOu1w==
   dependencies:
     comment-parser "^0.7.5"
     debug "^4.1.1"
@@ -5432,10 +5432,10 @@ [email protected]:
     v8-compile-cache "^2.1.1"
     yargs "^13.3.2"
 
-webpack-node-externals@2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-2.3.0.tgz#c884c07103de080284e17c195fe87c68e2b9c668"
-  integrity sha512-d1scCn/L5hv73GMOlqSTO6ykLWOiUrZfn54xQYf7u0yGLlUSf5trq6HV/Gw8JIpH2NEyXS7bJec1gk9YR/Qdqw==
+webpack-node-externals@2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-2.5.0.tgz#8d50f3289c71bc2b921a8da228e0b652acc503f1"
+  integrity sha512-g7/Z7Q/gsP8GkJkKZuJggn6RSb5PvxW1YD5vvmRZIxaSxAzkqjfL5n9CslVmNYlSqBVCyiqFgOqVS2IOObCSRg==
 
 webpack-sources@^1.4.0, webpack-sources@^1.4.1:
   version "1.4.3"

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.