-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmetaclass.js
181 lines (171 loc) · 6.95 KB
/
metaclass.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
'use strict';
const uneval_string_escape = {
'"': '\\"',
"'": "\\'",
'\t': '\\t',
'\r': '\\r',
'\n': '\\n',
'\\': '\\\\'
};
/**
* Returns a string so that when it is evaluated, it will be the original string.
*
* (It escapes characters to equivalent hex or escape sequence representations if they
* are not printable)
*
* @param s {string} The string to uneval
* @returns {string} eval(return) === s
*/
const uneval_string = s => "'$'".replace('$', String(s).replace(
/(["'\t\r\n\\])|([\x00-\x09])|([\x10-\x19\x7F-\xFF])|([ -~])|([\u0100-\u0FFF])|([\u1000-\uFFFF])/g,
(_, escape, one_digit, two_digit, printable, three_digit, four_digit) =>
one_digit ? '\\x0' + one_digit.charCodeAt(0).toString(16) :
two_digit ? '\\x' + two_digit.charCodeAt(0).toString(16) :
three_digit ? '\\u0' + three_digit.charCodeAt(0).toString(16) :
four_digit ? '\\u' + four_digit.charCodeAt(0).toString(16) :
(printable || uneval_string_escape[escape])
));
// Name of the symbol that holds the internal function to be called when a MetaClass object is called.
const context_symbol_name = '\t\n\uFFFF\u0100\xFF\'\\""\'##__context_for_metaclass_function__@@' + Math.random() + (+new Date()) + '__##\'""\\\'';
const context_symbol = Symbol.for(context_symbol_name);
// The body of the MetaClass function. `arguments.callee` will be the MetaClass object,
// and it calls `arguments.callee[context_symbol]`
const metaclass_function_body = (
'return arguments.callee[Symbol.for(' + uneval_string(context_symbol_name) + ')].f(arguments, new.target, this)'
);
// const global = Function('return this')();
/**
* Gets an objects constructor by going up it's prototype chain.
*
* @param obj The object to check the prototype chain of.
* @throws {Error} If no constructor found, the constructor has a getter, or if it is not configurable.
* @returns {function} The constructor.
*/
const get_constructor = obj => {
while (obj !== null) {
const constructor_descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor');
if (constructor_descriptor !== undefined) {
if (!('value' in constructor_descriptor)) {
throw Error('MetaClass prototype constructor must be a value (No getter/setters)');
}
if (!constructor_descriptor.configurable) {
throw Error('MetaClass prototype constructor must be configurable (To set to new value)');
}
return constructor_descriptor.value;
} else {
obj = Object.getPrototypeOf(obj);
}
}
throw Error('Could not find the constructor in MetaClass prototype.');
};
/**
* Returns true if an object has an internal [[Construct]] method.
*
* @param obj {function} The object to check for.
* @returns {boolean}
*/
const is_constructable = obj => {
if (Object(obj) !== obj) {
return false;
}
try {
Reflect.construct(String, [], obj);
} catch (e) {
return false;
}
return true;
};
/**
* Returns true if an object is a class.
*
* @param obj {function} the object to check.
* @returns {boolean}
*/
const is_class = obj => {
if (typeof obj !== 'function') {
return false;
}
/* There are three ways to define functions (excluding the function expression and class expression):
* function A() {} // Function statement. toString: "function A() {}"
* class B {} // Class statement. toString: "class B {}"
* const C = { C() {} }.C; // Method. toString: "C() {}"
* const D = () => {}; // Arrow function expression. toString: "() => {}"
*/
const name = Function.prototype.toString.call(obj);
// Need to check for whitespace as method name might start with `class` (e.g. `({ classXXX() {} }).classXXX`)
// And also need to check if there is no name as it might be an anonymous class with no whitespace
// (e.g. `(class{ /* class definition */ })`)
return /^class(?:\s+|{)/.test(name);
};
// The MetaClass class.
// Call with the name of the class and the prototype of instances of the class.
module.exports = class MetaClass extends Function {
/**
* @param name {string} Name of the class.
* @param prototype {Object} prototype of the class.
*/
constructor(name, prototype) {
name = String(name);
if (typeof prototype !== 'object') {
throw TypeError('MetaClass prototype must be an object.');
}
const constructor = get_constructor(prototype);
// Makes the length the same
const args = [];
const length = constructor.length;
for (let i = 0; i < length; ++i) {
args.push('placeholder_' + i);
}
// The last argument is the length of the function
args.push(metaclass_function_body);
void({
[name]: super(...args)
});
// const constructor_is_constructable = is_constructable(constructor);
const that = this;
const constructor_is_class = is_class(constructor);
Object.defineProperty(this, context_symbol, {
value: {
constructor: constructor,
// is_constructable: constructor_is_constructable,
is_class: constructor_is_class,
f: /* constructor_is_constructable */ constructor_is_class ?
((args, target) => Reflect.construct(constructor, args, target || that)) :
((args, target, t) => {
t = Object(t) === t && t instanceof that ? t : Object.create((target || that).prototype);
const ret_that = Function.prototype.apply.call(constructor, t, args);
return Object(ret_that) === ret_that ? ret_that : t;
})
},
writable: false,
enumerable: false,
configurable: true
});
Object.defineProperty(this, 'name', {value: name, writable: false, enumerable: false, configurable: true});
Object.defineProperty(this, Symbol.toStringTag, {value: name, writable: false, enumerable: false, configurable: true});
Object.defineProperty(this, 'displayName', {value: name, writable: false, enumerable: false, configurable: true});
Object.defineProperty(this, 'prototype', {value: prototype, writable: false, enumerable: false, configurable: false});
// Object.defineProperty(this, 'length', {value: constructor.length, writable: false, enumerable: false, configurable: true})
Object.defineProperty(prototype, 'constructor', { value: this, writable: true, enumerable: false, configurable: true });
}
// No longer need to forward direct `call`s.
/*call(this_arg, ...args) {
return this.apply(this_arg, args);
}
apply(this_arg, args) {
if (/*!this[context_symbol].is_constructable || * / !this[context_symbol].is_class) {
return Function.prototype.apply.call(this[context_symbol].constructor, this_arg, args);
}
// TODO: Check that this is correct
// But this is only called if a traditional function class inherits from a
const constructed = this[context_symbol].f(args, (this_arg && this_arg.constructor) || undefined);
Object.defineProperties(this_arg, Object.getOwnPropertyDescriptors(constructed));
return this_arg;
}*/
toString() {
return this.name;
}
toSource() {
return '(new MetaClass(' + uneval_string(this.name) + ', [[prototype]])';
}
};