mirror of
https://github.com/javascript-obfuscator/javascript-obfuscator
synced 2026-08-09 12:42:29 +00:00
Merge pull request #187 from javascript-obfuscator/initializable-decorator-improvements
Initializable decorator improvements
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+160
-36
@@ -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;
|
||||
const originalMethod: Function = methodDescriptor.value;
|
||||
/**
|
||||
* Stage #1: initialize target metadata
|
||||
*/
|
||||
initializeTargetMetadata(initializedTargetMetadataKey, false, target);
|
||||
initializeTargetMetadata(initializablePropertiesSetMetadataKey, new Set(), target);
|
||||
initializeTargetMetadata(wrappedMethodsSetMetadataKey, new Set(), target);
|
||||
|
||||
Object.defineProperty(target, propertyKey, {
|
||||
...propertyDescriptor,
|
||||
get: function (): any {
|
||||
if (this[metadataPropertyKey] === undefined) {
|
||||
throw new Error(`Property \`${propertyKey}\` is not initialized! Initialize it first!`);
|
||||
}
|
||||
/**
|
||||
* Stage #2: wrap target methods
|
||||
*/
|
||||
wrapTargetMethodsInInitializedCheck(target, initializeMethodName);
|
||||
wrapInitializeMethodInInitializeCheck(target, initializeMethodName, propertyKey);
|
||||
|
||||
return this[metadataPropertyKey];
|
||||
},
|
||||
set: function (newVal: any): void {
|
||||
this[metadataPropertyKey] = newVal;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(target, initializeMethodKey, {
|
||||
...methodDescriptor,
|
||||
value: function (): void {
|
||||
originalMethod.apply(this, arguments);
|
||||
|
||||
if (this[propertyKey]) {}
|
||||
}
|
||||
});
|
||||
|
||||
return propertyDescriptor;
|
||||
/**
|
||||
* 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, propertyName, {
|
||||
...methodDescriptor,
|
||||
value: function (): void {
|
||||
if (!Reflect.getMetadata(initializedTargetMetadataKey, this)) {
|
||||
throw new Error(`Class should be initialized with \`${initializeMethodName}()\` method`);
|
||||
}
|
||||
|
||||
return originalMethod.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
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 this[initializablePropertyMetadataKey];
|
||||
},
|
||||
set: function (newVal: any): void {
|
||||
this[initializablePropertyMetadataKey] = newVal;
|
||||
}
|
||||
});
|
||||
|
||||
return propertyDescriptor;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 (): void {
|
||||
public initialize (property: string): void {
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user