Explorar o código

Merge pull request #187 from javascript-obfuscator/initializable-decorator-improvements

Initializable decorator improvements
Timofey Kachalov %!s(int64=7) %!d(string=hai) anos
pai
achega
83aac1fa08

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
dist/index.js


+ 154 - 30
src/decorators/Initializable.ts

@@ -2,54 +2,178 @@
 
 import { IInitializable } from '../interfaces/IInitializable';
 
+const defaultDescriptor: PropertyDescriptor = {
+    configurable: true,
+    enumerable: true
+};
+const initializedTargetMetadataKey: string = '_initialized';
+const initializablePropertiesSetMetadataKey: string = '_initializablePropertiesSet';
+const wrappedMethodsSetMetadataKey: string = '_wrappedMethodsSet';
+const constructorMethodName: string = 'constructor';
+
 /**
- * @param {string} initializeMethodKey
- * @returns {any}
+ * @param {string} initializeMethodName
+ * @returns {(target: IInitializable, propertyKey: (string | symbol)) => any}
  */
 export function initializable (
-    initializeMethodKey: string = 'initialize'
+    initializeMethodName: string = 'initialize'
 ): (target: IInitializable, propertyKey: string | symbol) => any {
     const decoratorName: string = Object.keys(this)[0];
 
     return (target: IInitializable, propertyKey: string | symbol): PropertyDescriptor => {
-        const descriptor: PropertyDescriptor = {
-            configurable: true,
-            enumerable: true
-        };
-        const initializeMethod: Function = target[initializeMethodKey];
+        const initializeMethod: Function = target[initializeMethodName];
 
         if (!initializeMethod || typeof initializeMethod !== 'function') {
-            throw new Error(`\`${initializeMethodKey}\` method with initialization logic not ` +
-                `found. \`@${decoratorName}\` decorator requires \`${initializeMethodKey}\` method`);
+            throw new Error(`\`${initializeMethodName}\` method with initialization logic not ` +
+                `found. \`@${decoratorName}\` decorator requires \`${initializeMethodName}\` method`);
         }
 
-        const metadataPropertyKey: string = `_${propertyKey}`;
-        const propertyDescriptor: PropertyDescriptor = Object.getOwnPropertyDescriptor(target, metadataPropertyKey) || descriptor;
-        const methodDescriptor: PropertyDescriptor = Object.getOwnPropertyDescriptor(target, initializeMethodKey) || descriptor;
+        /**
+         * Stage #1: initialize target metadata
+         */
+        initializeTargetMetadata(initializedTargetMetadataKey, false, target);
+        initializeTargetMetadata(initializablePropertiesSetMetadataKey, new Set(), target);
+        initializeTargetMetadata(wrappedMethodsSetMetadataKey, new Set(), target);
+
+        /**
+         * Stage #2: wrap target methods
+         */
+        wrapTargetMethodsInInitializedCheck(target, initializeMethodName);
+        wrapInitializeMethodInInitializeCheck(target, initializeMethodName, propertyKey);
+
+        /**
+         * Stage #3: wrap target properties
+         */
+        return wrapInitializableProperty(target, propertyKey);
+    };
+}
+
+/**
+ * @param {string} metadataKey
+ * @param metadataValue
+ * @param {IInitializable} target
+ */
+function initializeTargetMetadata (metadataKey: string, metadataValue: any, target: IInitializable): void {
+    const hasInitializedMetadata: boolean = Reflect.hasMetadata(metadataKey, target);
+
+    if (!hasInitializedMetadata) {
+        Reflect.defineMetadata(metadataKey, metadataValue, target);
+    }
+}
+
+/**
+ * Wraps all target methods with additional logic that check that this methods will called after `initialize` method
+ *
+ * @param {IInitializable} target
+ * @param {string} initializeMethodName
+ */
+function wrapTargetMethodsInInitializedCheck (target: IInitializable, initializeMethodName: string): void {
+    const ownPropertyNames: string[] = Object.getOwnPropertyNames(target);
+    const prohibitedPropertyNames: string[] = [initializeMethodName, constructorMethodName];
+
+    ownPropertyNames.forEach((propertyName: string) => {
+        const initializablePropertiesSet: Set <string | symbol> = Reflect
+            .getMetadata(initializablePropertiesSetMetadataKey, target);
+        const wrappedMethodsSet: Set <string | symbol> = Reflect
+            .getMetadata(wrappedMethodsSetMetadataKey, target);
+
+        const isProhibitedPropertyName: boolean = prohibitedPropertyNames.includes(propertyName)
+            || initializablePropertiesSet.has(propertyName)
+            || wrappedMethodsSet.has(propertyName);
+
+        if (isProhibitedPropertyName) {
+            return;
+        }
+
+        const targetProperty: any = target[propertyName];
+
+        if (typeof targetProperty !== 'function') {
+            return;
+        }
+
+        const methodDescriptor: PropertyDescriptor = Object
+            .getOwnPropertyDescriptor(target, propertyName) || defaultDescriptor;
         const originalMethod: Function = methodDescriptor.value;
 
-        Object.defineProperty(target, propertyKey, {
-            ...propertyDescriptor,
-            get: function (): any {
-                if (this[metadataPropertyKey] === undefined) {
-                    throw new Error(`Property \`${propertyKey}\` is not initialized! Initialize it first!`);
+        Object.defineProperty(target, propertyName, {
+            ...methodDescriptor,
+            value: function (): void {
+                if (!Reflect.getMetadata(initializedTargetMetadataKey, this)) {
+                    throw new Error(`Class should be initialized with \`${initializeMethodName}()\` method`);
                 }
 
-                return this[metadataPropertyKey];
-            },
-            set: function (newVal: any): void {
-                this[metadataPropertyKey] = newVal;
+                return originalMethod.apply(this, arguments);
             }
         });
-        Object.defineProperty(target, initializeMethodKey, {
-            ...methodDescriptor,
-            value: function (): void {
-                originalMethod.apply(this, arguments);
 
-                if (this[propertyKey]) {}
+        wrappedMethodsSet.add(propertyName);
+    });
+}
+
+/**
+ * Wraps `initialize` method with additional logic to check that `initialized` properties will set
+ *
+ * @param {IInitializable} target
+ * @param {string} initializeMethodName
+ * @param {string | symbol} propertyKey
+ */
+function wrapInitializeMethodInInitializeCheck (
+    target: IInitializable,
+    initializeMethodName: string,
+    propertyKey: string | symbol
+): void {
+    const methodDescriptor: PropertyDescriptor = Object
+        .getOwnPropertyDescriptor(target, initializeMethodName) || defaultDescriptor;
+    const originalMethod: Function = methodDescriptor.value;
+
+    Object.defineProperty(target, initializeMethodName, {
+        ...methodDescriptor,
+        value: function (): void {
+            /**
+             * should define metadata before `initialize` method call,
+             * because of cases when other methods will called inside `initialize` method
+             */
+            Reflect.defineMetadata(initializedTargetMetadataKey, true, this);
+
+            const result: any = originalMethod.apply(this, arguments);
+
+            if (this[propertyKey]) {}
+
+            return result;
+        }
+    });
+}
+
+/**
+ * Wraps initializable property in additional checks
+ *
+ * @param {IInitializable} target
+ * @param {string | symbol} propertyKey
+ * @returns {PropertyDescriptor}
+ */
+function wrapInitializableProperty (target: IInitializable, propertyKey: string | symbol): PropertyDescriptor {
+    const initializablePropertiesSet: Set <string | symbol> = Reflect
+        .getMetadata(initializablePropertiesSetMetadataKey, target);
+
+    initializablePropertiesSet.add(propertyKey);
+
+    const initializablePropertyMetadataKey: string = `_${propertyKey}`;
+    const propertyDescriptor: PropertyDescriptor = Object
+            .getOwnPropertyDescriptor(target, initializablePropertyMetadataKey) || defaultDescriptor;
+
+    Object.defineProperty(target, propertyKey, {
+        ...propertyDescriptor,
+        get: function (): any {
+            if (this[initializablePropertyMetadataKey] === undefined) {
+                throw new Error(`Property \`${propertyKey}\` is not initialized! Initialize it first!`);
             }
-        });
 
-        return propertyDescriptor;
-    };
+            return this[initializablePropertyMetadataKey];
+        },
+        set: function (newVal: any): void {
+            this[initializablePropertyMetadataKey] = newVal;
+        }
+    });
+
+    return propertyDescriptor;
 }

+ 1 - 2
src/storages/custom-node-group/CustomNodeGroupStorage.ts

@@ -46,8 +46,7 @@ export class CustomNodeGroupStorage extends MapStorage <ICustomNodeGroup> {
 
     @postConstruct()
     public initialize (): void {
-        this.storage = new Map <string, ICustomNodeGroup>();
-        this.storageId = this.randomGenerator.getRandomString(6);
+        super.initialize();
 
         CustomNodeGroupStorage.customNodeGroupsList.forEach((customNodeGroupName: CustomNodeGroup) => {
             const customNodeGroup: ICustomNodeGroup = this.customNodeGroupFactory(

+ 113 - 15
test/unit-tests/decorators/initializable/Initializable.spec.ts

@@ -1,3 +1,5 @@
+import 'reflect-metadata';
+
 import { assert } from 'chai';
 
 import { initializable } from '../../../../src/decorators/Initializable';
@@ -28,29 +30,125 @@ describe('@initializable', () => {
             });
         });
 
-        describe('variant #2: custom initialization method name is passed', () => {
-            const testFunc: () => void = () => {
-                class Foo implements IInitializable {
-                    @initializable()
-                    public property!: string;
+        describe('variant #2: `initialize` method should be called first', () => {
+            describe('variant #1: `initialize` method was called first', () => {
+                const testFunc: () => void = () => {
+                    class Foo {
+                        @initializable()
+                        public property!: string;
+
+                        public initialize (property: string): void {
+                            this.property = property;
+                        }
 
-                    public initialize (): void {
+                        public bar (): void {}
                     }
 
-                    public bar (property: string): void {
-                        this.property = property;
+                    const foo: Foo = new Foo();
+
+                    foo.initialize('baz');
+                    foo.bar();
+                };
+
+                it('shouldn\'t throw an error if `initialize` method was called first', () => {
+                    assert.doesNotThrow(testFunc, /Class should be initialized/);
+                });
+            });
+
+            describe('variant #2: other method was called inside `initialize` method with initialization of the property', () => {
+                const testFunc: () => void = () => {
+                    class Foo {
+                        @initializable()
+                        public property!: string;
+
+                        public initialize (property: string): void {
+                            this.innerInitialize(property);
+                        }
+
+                        public innerInitialize (property: string): void {
+                            this.property = property;
+                        }
                     }
-                }
 
-                const foo: Foo = new Foo();
+                    const foo: Foo = new Foo();
 
-                foo.bar('baz');
+                    foo.initialize('baz');
+                };
 
-                foo.property;
-            };
+                it('shouldn\'t throw an error if other method was called inside `initialize` method', () => {
+                    assert.doesNotThrow(testFunc, /Class should be initialized/);
+                });
+            });
 
-            it('shouldn\'t throws an errors if custom initialization method name is passed', () => {
-                assert.doesNotThrow(testFunc, Error);
+            describe('variant #3: other method was called inside `initialize` method without initialization of the property', () => {
+                const testFunc: () => void = () => {
+                    class Foo {
+                        @initializable()
+                        public property!: string;
+
+                        public initialize (property: string): void {
+                            this.innerInitialize(property);
+                        }
+
+                        public innerInitialize (property: string): void {
+                        }
+                    }
+
+                    const foo: Foo = new Foo();
+
+                    foo.initialize('baz');
+                };
+
+                it('should throws an error if other method was called inside `initialize` method without initialization of the property', () => {
+                    assert.throws(testFunc, /Property `property` is not initialized/);
+                });
+            });
+
+            describe('variant #4: `initialize` method wasn\'t called first', () => {
+                const testFunc: () => void = () => {
+                    class Foo {
+                        @initializable()
+                        public property!: string;
+
+                        public initialize (property: string): void {
+                            this.property = property;
+                        }
+
+                        public bar (): void {}
+                    }
+
+                    const foo: Foo = new Foo();
+
+                    foo.bar();
+                    foo.initialize('baz');
+                };
+
+                it('should throws an error if `initialize` method wasn\'t called first', () => {
+                    assert.throws(testFunc, /Class should be initialized/);
+                });
+            });
+
+            describe('variant #5: `initialize` method wasn\'t called', () => {
+                const testFunc: () => void = () => {
+                    class Foo {
+                        @initializable()
+                        public property!: string;
+
+                        public initialize (property: string): void {
+                            this.property = property;
+                        }
+
+                        public bar (): void {}
+                    }
+
+                    const foo: Foo = new Foo();
+
+                    foo.bar();
+                };
+
+                it('should throws an error if `initialize` method wasn\'t called first', () => {
+                    assert.throws(testFunc, /Class should be initialized/);
+                });
             });
         });
 

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio