UNPKG

26.2 kBJavaScriptView Raw
1var Ap = Array.prototype;
2var slice = Ap.slice;
3var map = Ap.map;
4var each = Ap.forEach;
5var Op = Object.prototype;
6var objToStr = Op.toString;
7var funObjStr = objToStr.call(function(){});
8var strObjStr = objToStr.call("");
9var hasOwn = Op.hasOwnProperty;
10
11// A type is an object with a .check method that takes a value and returns
12// true or false according to whether the value matches the type.
13
14function Type(check, name) {
15 var self = this;
16 if (!(self instanceof Type)) {
17 throw new Error("Type constructor cannot be invoked without 'new'");
18 }
19
20 // Unfortunately we can't elegantly reuse isFunction and isString,
21 // here, because this code is executed while defining those types.
22 if (objToStr.call(check) !== funObjStr) {
23 throw new Error(check + " is not a function");
24 }
25
26 // The `name` parameter can be either a function or a string.
27 var nameObjStr = objToStr.call(name);
28 if (!(nameObjStr === funObjStr ||
29 nameObjStr === strObjStr)) {
30 throw new Error(name + " is neither a function nor a string");
31 }
32
33 Object.defineProperties(self, {
34 name: { value: name },
35 check: {
36 value: function(value, deep) {
37 var result = check.call(self, value, deep);
38 if (!result && deep && objToStr.call(deep) === funObjStr)
39 deep(self, value);
40 return result;
41 }
42 }
43 });
44}
45
46var Tp = Type.prototype;
47
48// Throughout this file we use Object.defineProperty to prevent
49// redefinition of exported properties.
50exports.Type = Type;
51
52// Like .check, except that failure triggers an AssertionError.
53Tp.assert = function(value, deep) {
54 if (!this.check(value, deep)) {
55 var str = shallowStringify(value);
56 throw new Error(str + " does not match type " + this);
57 }
58 return true;
59};
60
61function shallowStringify(value) {
62 if (isObject.check(value))
63 return "{" + Object.keys(value).map(function(key) {
64 return key + ": " + value[key];
65 }).join(", ") + "}";
66
67 if (isArray.check(value))
68 return "[" + value.map(shallowStringify).join(", ") + "]";
69
70 return JSON.stringify(value);
71}
72
73Tp.toString = function() {
74 var name = this.name;
75
76 if (isString.check(name))
77 return name;
78
79 if (isFunction.check(name))
80 return name.call(this) + "";
81
82 return name + " type";
83};
84
85var builtInCtorFns = [];
86var builtInCtorTypes = [];
87var builtInTypes = {};
88exports.builtInTypes = builtInTypes;
89
90function defBuiltInType(example, name) {
91 var objStr = objToStr.call(example);
92
93 var type = new Type(function(value) {
94 return objToStr.call(value) === objStr;
95 }, name);
96
97 builtInTypes[name] = type;
98
99 if (example && typeof example.constructor === "function") {
100 builtInCtorFns.push(example.constructor);
101 builtInCtorTypes.push(type);
102 }
103
104 return type;
105}
106
107// These types check the underlying [[Class]] attribute of the given
108// value, rather than using the problematic typeof operator. Note however
109// that no subtyping is considered; so, for instance, isObject.check
110// returns false for [], /./, new Date, and null.
111var isString = defBuiltInType("truthy", "string");
112var isFunction = defBuiltInType(function(){}, "function");
113var isArray = defBuiltInType([], "array");
114var isObject = defBuiltInType({}, "object");
115var isRegExp = defBuiltInType(/./, "RegExp");
116var isDate = defBuiltInType(new Date, "Date");
117var isNumber = defBuiltInType(3, "number");
118var isBoolean = defBuiltInType(true, "boolean");
119var isNull = defBuiltInType(null, "null");
120var isUndefined = defBuiltInType(void 0, "undefined");
121
122// There are a number of idiomatic ways of expressing types, so this
123// function serves to coerce them all to actual Type objects. Note that
124// providing the name argument is not necessary in most cases.
125function toType(from, name) {
126 // The toType function should of course be idempotent.
127 if (from instanceof Type)
128 return from;
129
130 // The Def type is used as a helper for constructing compound
131 // interface types for AST nodes.
132 if (from instanceof Def)
133 return from.type;
134
135 // Support [ElemType] syntax.
136 if (isArray.check(from))
137 return Type.fromArray(from);
138
139 // Support { someField: FieldType, ... } syntax.
140 if (isObject.check(from))
141 return Type.fromObject(from);
142
143 if (isFunction.check(from)) {
144 var bicfIndex = builtInCtorFns.indexOf(from);
145 if (bicfIndex >= 0) {
146 return builtInCtorTypes[bicfIndex];
147 }
148
149 // If isFunction.check(from), and from is not a built-in
150 // constructor, assume from is a binary predicate function we can
151 // use to define the type.
152 return new Type(from, name);
153 }
154
155 // As a last resort, toType returns a type that matches any value that
156 // is === from. This is primarily useful for literal values like
157 // toType(null), but it has the additional advantage of allowing
158 // toType to be a total function.
159 return new Type(function(value) {
160 return value === from;
161 }, isUndefined.check(name) ? function() {
162 return from + "";
163 } : name);
164}
165
166// Returns a type that matches the given value iff any of type1, type2,
167// etc. match the value.
168Type.or = function(/* type1, type2, ... */) {
169 var types = [];
170 var len = arguments.length;
171 for (var i = 0; i < len; ++i)
172 types.push(toType(arguments[i]));
173
174 return new Type(function(value, deep) {
175 for (var i = 0; i < len; ++i)
176 if (types[i].check(value, deep))
177 return true;
178 return false;
179 }, function() {
180 return types.join(" | ");
181 });
182};
183
184Type.fromArray = function(arr) {
185 if (!isArray.check(arr)) {
186 throw new Error("");
187 }
188 if (arr.length !== 1) {
189 throw new Error("only one element type is permitted for typed arrays");
190 }
191 return toType(arr[0]).arrayOf();
192};
193
194Tp.arrayOf = function() {
195 var elemType = this;
196 return new Type(function(value, deep) {
197 return isArray.check(value) && value.every(function(elem) {
198 return elemType.check(elem, deep);
199 });
200 }, function() {
201 return "[" + elemType + "]";
202 });
203};
204
205Type.fromObject = function(obj) {
206 var fields = Object.keys(obj).map(function(name) {
207 return new Field(name, obj[name]);
208 });
209
210 return new Type(function(value, deep) {
211 return isObject.check(value) && fields.every(function(field) {
212 return field.type.check(value[field.name], deep);
213 });
214 }, function() {
215 return "{ " + fields.join(", ") + " }";
216 });
217};
218
219function Field(name, type, defaultFn, hidden) {
220 var self = this;
221
222 if (!(self instanceof Field)) {
223 throw new Error("Field constructor cannot be invoked without 'new'");
224 }
225 isString.assert(name);
226
227 type = toType(type);
228
229 var properties = {
230 name: { value: name },
231 type: { value: type },
232 hidden: { value: !!hidden }
233 };
234
235 if (isFunction.check(defaultFn)) {
236 properties.defaultFn = { value: defaultFn };
237 }
238
239 Object.defineProperties(self, properties);
240}
241
242var Fp = Field.prototype;
243
244Fp.toString = function() {
245 return JSON.stringify(this.name) + ": " + this.type;
246};
247
248Fp.getValue = function(obj) {
249 var value = obj[this.name];
250
251 if (!isUndefined.check(value))
252 return value;
253
254 if (this.defaultFn)
255 value = this.defaultFn.call(obj);
256
257 return value;
258};
259
260// Define a type whose name is registered in a namespace (the defCache) so
261// that future definitions will return the same type given the same name.
262// In particular, this system allows for circular and forward definitions.
263// The Def object d returned from Type.def may be used to configure the
264// type d.type by calling methods such as d.bases, d.build, and d.field.
265Type.def = function(typeName) {
266 isString.assert(typeName);
267 return hasOwn.call(defCache, typeName)
268 ? defCache[typeName]
269 : defCache[typeName] = new Def(typeName);
270};
271
272// In order to return the same Def instance every time Type.def is called
273// with a particular name, those instances need to be stored in a cache.
274var defCache = Object.create(null);
275
276function Def(typeName) {
277 var self = this;
278 if (!(self instanceof Def)) {
279 throw new Error("Def constructor cannot be invoked without 'new'");
280 }
281
282 Object.defineProperties(self, {
283 typeName: { value: typeName },
284 baseNames: { value: [] },
285 ownFields: { value: Object.create(null) },
286
287 // These two are populated during finalization.
288 allSupertypes: { value: Object.create(null) }, // Includes own typeName.
289 supertypeList: { value: [] }, // Linear inheritance hierarchy.
290 allFields: { value: Object.create(null) }, // Includes inherited fields.
291 fieldNames: { value: [] }, // Non-hidden keys of allFields.
292
293 type: {
294 value: new Type(function(value, deep) {
295 return self.check(value, deep);
296 }, typeName)
297 }
298 });
299}
300
301Def.fromValue = function(value) {
302 if (value && typeof value === "object") {
303 var type = value.type;
304 if (typeof type === "string" &&
305 hasOwn.call(defCache, type)) {
306 var d = defCache[type];
307 if (d.finalized) {
308 return d;
309 }
310 }
311 }
312
313 return null;
314};
315
316var Dp = Def.prototype;
317
318Dp.isSupertypeOf = function(that) {
319 if (that instanceof Def) {
320 if (this.finalized !== true ||
321 that.finalized !== true) {
322 throw new Error("");
323 }
324 return hasOwn.call(that.allSupertypes, this.typeName);
325 } else {
326 throw new Error(that + " is not a Def");
327 }
328};
329
330// Note that the list returned by this function is a copy of the internal
331// supertypeList, *without* the typeName itself as the first element.
332exports.getSupertypeNames = function(typeName) {
333 if (!hasOwn.call(defCache, typeName)) {
334 throw new Error("");
335 }
336 var d = defCache[typeName];
337 if (d.finalized !== true) {
338 throw new Error("");
339 }
340 return d.supertypeList.slice(1);
341};
342
343// Returns an object mapping from every known type in the defCache to the
344// most specific supertype whose name is an own property of the candidates
345// object.
346exports.computeSupertypeLookupTable = function(candidates) {
347 var table = {};
348 var typeNames = Object.keys(defCache);
349 var typeNameCount = typeNames.length;
350
351 for (var i = 0; i < typeNameCount; ++i) {
352 var typeName = typeNames[i];
353 var d = defCache[typeName];
354 if (d.finalized !== true) {
355 throw new Error("" + typeName);
356 }
357 for (var j = 0; j < d.supertypeList.length; ++j) {
358 var superTypeName = d.supertypeList[j];
359 if (hasOwn.call(candidates, superTypeName)) {
360 table[typeName] = superTypeName;
361 break;
362 }
363 }
364 }
365
366 return table;
367};
368
369Dp.checkAllFields = function(value, deep) {
370 var allFields = this.allFields;
371 if (this.finalized !== true) {
372 throw new Error("" + this.typeName);
373 }
374
375 function checkFieldByName(name) {
376 var field = allFields[name];
377 var type = field.type;
378 var child = field.getValue(value);
379 return type.check(child, deep);
380 }
381
382 return isObject.check(value)
383 && Object.keys(allFields).every(checkFieldByName);
384};
385
386Dp.check = function(value, deep) {
387 if (this.finalized !== true) {
388 throw new Error(
389 "prematurely checking unfinalized type " + this.typeName
390 );
391 }
392
393 // A Def type can only match an object value.
394 if (!isObject.check(value))
395 return false;
396
397 var vDef = Def.fromValue(value);
398 if (!vDef) {
399 // If we couldn't infer the Def associated with the given value,
400 // and we expected it to be a SourceLocation or a Position, it was
401 // probably just missing a "type" field (because Esprima does not
402 // assign a type property to such nodes). Be optimistic and let
403 // this.checkAllFields make the final decision.
404 if (this.typeName === "SourceLocation" ||
405 this.typeName === "Position") {
406 return this.checkAllFields(value, deep);
407 }
408
409 // Calling this.checkAllFields for any other type of node is both
410 // bad for performance and way too forgiving.
411 return false;
412 }
413
414 // If checking deeply and vDef === this, then we only need to call
415 // checkAllFields once. Calling checkAllFields is too strict when deep
416 // is false, because then we only care about this.isSupertypeOf(vDef).
417 if (deep && vDef === this)
418 return this.checkAllFields(value, deep);
419
420 // In most cases we rely exclusively on isSupertypeOf to make O(1)
421 // subtyping determinations. This suffices in most situations outside
422 // of unit tests, since interface conformance is checked whenever new
423 // instances are created using builder functions.
424 if (!this.isSupertypeOf(vDef))
425 return false;
426
427 // The exception is when deep is true; then, we recursively check all
428 // fields.
429 if (!deep)
430 return true;
431
432 // Use the more specific Def (vDef) to perform the deep check, but
433 // shallow-check fields defined by the less specific Def (this).
434 return vDef.checkAllFields(value, deep)
435 && this.checkAllFields(value, false);
436};
437
438Dp.bases = function() {
439 var args = slice.call(arguments);
440 var bases = this.baseNames;
441
442 if (this.finalized) {
443 if (args.length !== bases.length) {
444 throw new Error("");
445 }
446 for (var i = 0; i < args.length; i++) {
447 if (args[i] !== bases[i]) {
448 throw new Error("");
449 }
450 }
451 return this;
452 }
453
454 args.forEach(function(baseName) {
455 isString.assert(baseName);
456
457 // This indexOf lookup may be O(n), but the typical number of base
458 // names is very small, and indexOf is a native Array method.
459 if (bases.indexOf(baseName) < 0)
460 bases.push(baseName);
461 });
462
463 return this; // For chaining.
464};
465
466// False by default until .build(...) is called on an instance.
467Object.defineProperty(Dp, "buildable", { value: false });
468
469var builders = {};
470exports.builders = builders;
471
472// This object is used as prototype for any node created by a builder.
473var nodePrototype = {};
474
475// Call this function to define a new method to be shared by all AST
476// nodes. The replaced method (if any) is returned for easy wrapping.
477exports.defineMethod = function(name, func) {
478 var old = nodePrototype[name];
479
480 // Pass undefined as func to delete nodePrototype[name].
481 if (isUndefined.check(func)) {
482 delete nodePrototype[name];
483
484 } else {
485 isFunction.assert(func);
486
487 Object.defineProperty(nodePrototype, name, {
488 enumerable: true, // For discoverability.
489 configurable: true, // For delete proto[name].
490 value: func
491 });
492 }
493
494 return old;
495};
496
497var isArrayOfString = isString.arrayOf();
498
499// Calling the .build method of a Def simultaneously marks the type as
500// buildable (by defining builders[getBuilderName(typeName)]) and
501// specifies the order of arguments that should be passed to the builder
502// function to create an instance of the type.
503Dp.build = function(/* param1, param2, ... */) {
504 var self = this;
505
506 var newBuildParams = slice.call(arguments);
507 isArrayOfString.assert(newBuildParams);
508
509 // Calling Def.prototype.build multiple times has the effect of merely
510 // redefining this property.
511 Object.defineProperty(self, "buildParams", {
512 value: newBuildParams,
513 writable: false,
514 enumerable: false,
515 configurable: true
516 });
517
518 if (self.buildable) {
519 // If this Def is already buildable, update self.buildParams and
520 // continue using the old builder function.
521 return self;
522 }
523
524 // Every buildable type will have its "type" field filled in
525 // automatically. This includes types that are not subtypes of Node,
526 // like SourceLocation, but that seems harmless (TODO?).
527 self.field("type", String, function() { return self.typeName });
528
529 // Override Dp.buildable for this Def instance.
530 Object.defineProperty(self, "buildable", { value: true });
531
532 Object.defineProperty(builders, getBuilderName(self.typeName), {
533 enumerable: true,
534
535 value: function() {
536 var args = arguments;
537 var argc = args.length;
538 var built = Object.create(nodePrototype);
539
540 if (!self.finalized) {
541 throw new Error(
542 "attempting to instantiate unfinalized type " +
543 self.typeName
544 );
545 }
546
547 function add(param, i) {
548 if (hasOwn.call(built, param))
549 return;
550
551 var all = self.allFields;
552 if (!hasOwn.call(all, param)) {
553 throw new Error("" + param);
554 }
555
556 var field = all[param];
557 var type = field.type;
558 var value;
559
560 if (isNumber.check(i) && i < argc) {
561 value = args[i];
562 } else if (field.defaultFn) {
563 // Expose the partially-built object to the default
564 // function as its `this` object.
565 value = field.defaultFn.call(built);
566 } else {
567 var message = "no value or default function given for field " +
568 JSON.stringify(param) + " of " + self.typeName + "(" +
569 self.buildParams.map(function(name) {
570 return all[name];
571 }).join(", ") + ")";
572 throw new Error(message);
573 }
574
575 if (!type.check(value)) {
576 throw new Error(
577 shallowStringify(value) +
578 " does not match field " + field +
579 " of type " + self.typeName
580 );
581 }
582
583 // TODO Could attach getters and setters here to enforce
584 // dynamic type safety.
585 built[param] = value;
586 }
587
588 self.buildParams.forEach(function(param, i) {
589 add(param, i);
590 });
591
592 Object.keys(self.allFields).forEach(function(param) {
593 add(param); // Use the default value.
594 });
595
596 // Make sure that the "type" field was filled automatically.
597 if (built.type !== self.typeName) {
598 throw new Error("");
599 }
600
601 return built;
602 }
603 });
604
605 return self; // For chaining.
606};
607
608function getBuilderName(typeName) {
609 return typeName.replace(/^[A-Z]+/, function(upperCasePrefix) {
610 var len = upperCasePrefix.length;
611 switch (len) {
612 case 0: return "";
613 // If there's only one initial capital letter, just lower-case it.
614 case 1: return upperCasePrefix.toLowerCase();
615 default:
616 // If there's more than one initial capital letter, lower-case
617 // all but the last one, so that XMLDefaultDeclaration (for
618 // example) becomes xmlDefaultDeclaration.
619 return upperCasePrefix.slice(
620 0, len - 1).toLowerCase() +
621 upperCasePrefix.charAt(len - 1);
622 }
623 });
624}
625exports.getBuilderName = getBuilderName;
626
627function getStatementBuilderName(typeName) {
628 typeName = getBuilderName(typeName);
629 return typeName.replace(/(Expression)?$/, "Statement");
630}
631exports.getStatementBuilderName = getStatementBuilderName;
632
633// The reason fields are specified using .field(...) instead of an object
634// literal syntax is somewhat subtle: the object literal syntax would
635// support only one key and one value, but with .field(...) we can pass
636// any number of arguments to specify the field.
637Dp.field = function(name, type, defaultFn, hidden) {
638 if (this.finalized) {
639 console.error("Ignoring attempt to redefine field " +
640 JSON.stringify(name) + " of finalized type " +
641 JSON.stringify(this.typeName));
642 return this;
643 }
644 this.ownFields[name] = new Field(name, type, defaultFn, hidden);
645 return this; // For chaining.
646};
647
648var namedTypes = {};
649exports.namedTypes = namedTypes;
650
651// Like Object.keys, but aware of what fields each AST type should have.
652function getFieldNames(object) {
653 var d = Def.fromValue(object);
654 if (d) {
655 return d.fieldNames.slice(0);
656 }
657
658 if ("type" in object) {
659 throw new Error(
660 "did not recognize object of type " +
661 JSON.stringify(object.type)
662 );
663 }
664
665 return Object.keys(object);
666}
667exports.getFieldNames = getFieldNames;
668
669// Get the value of an object property, taking object.type and default
670// functions into account.
671function getFieldValue(object, fieldName) {
672 var d = Def.fromValue(object);
673 if (d) {
674 var field = d.allFields[fieldName];
675 if (field) {
676 return field.getValue(object);
677 }
678 }
679
680 return object[fieldName];
681}
682exports.getFieldValue = getFieldValue;
683
684// Iterate over all defined fields of an object, including those missing
685// or undefined, passing each field name and effective value (as returned
686// by getFieldValue) to the callback. If the object has no corresponding
687// Def, the callback will never be called.
688exports.eachField = function(object, callback, context) {
689 getFieldNames(object).forEach(function(name) {
690 callback.call(this, name, getFieldValue(object, name));
691 }, context);
692};
693
694// Similar to eachField, except that iteration stops as soon as the
695// callback returns a truthy value. Like Array.prototype.some, the final
696// result is either true or false to indicates whether the callback
697// returned true for any element or not.
698exports.someField = function(object, callback, context) {
699 return getFieldNames(object).some(function(name) {
700 return callback.call(this, name, getFieldValue(object, name));
701 }, context);
702};
703
704// This property will be overridden as true by individual Def instances
705// when they are finalized.
706Object.defineProperty(Dp, "finalized", { value: false });
707
708Dp.finalize = function() {
709 var self = this;
710
711 // It's not an error to finalize a type more than once, but only the
712 // first call to .finalize does anything.
713 if (!self.finalized) {
714 var allFields = self.allFields;
715 var allSupertypes = self.allSupertypes;
716
717 self.baseNames.forEach(function(name) {
718 var def = defCache[name];
719 if (def instanceof Def) {
720 def.finalize();
721 extend(allFields, def.allFields);
722 extend(allSupertypes, def.allSupertypes);
723 } else {
724 var message = "unknown supertype name " +
725 JSON.stringify(name) +
726 " for subtype " +
727 JSON.stringify(self.typeName);
728 throw new Error(message);
729 }
730 });
731
732 // TODO Warn if fields are overridden with incompatible types.
733 extend(allFields, self.ownFields);
734 allSupertypes[self.typeName] = self;
735
736 self.fieldNames.length = 0;
737 for (var fieldName in allFields) {
738 if (hasOwn.call(allFields, fieldName) &&
739 !allFields[fieldName].hidden) {
740 self.fieldNames.push(fieldName);
741 }
742 }
743
744 // Types are exported only once they have been finalized.
745 Object.defineProperty(namedTypes, self.typeName, {
746 enumerable: true,
747 value: self.type
748 });
749
750 Object.defineProperty(self, "finalized", { value: true });
751
752 // A linearization of the inheritance hierarchy.
753 populateSupertypeList(self.typeName, self.supertypeList);
754
755 if (self.buildable && self.supertypeList.lastIndexOf("Expression") >= 0) {
756 wrapExpressionBuilderWithStatement(self.typeName);
757 }
758 }
759};
760
761// Adds an additional builder for Expression subtypes
762// that wraps the built Expression in an ExpressionStatements.
763function wrapExpressionBuilderWithStatement(typeName) {
764 var wrapperName = getStatementBuilderName(typeName);
765
766 // skip if the builder already exists
767 if (builders[wrapperName]) return;
768
769 // the builder function to wrap with builders.ExpressionStatement
770 var wrapped = builders[getBuilderName(typeName)];
771
772 // skip if there is nothing to wrap
773 if (!wrapped) return;
774
775 builders[wrapperName] = function() {
776 return builders.expressionStatement(wrapped.apply(builders, arguments));
777 };
778}
779
780function populateSupertypeList(typeName, list) {
781 list.length = 0;
782 list.push(typeName);
783
784 var lastSeen = Object.create(null);
785
786 for (var pos = 0; pos < list.length; ++pos) {
787 typeName = list[pos];
788 var d = defCache[typeName];
789 if (d.finalized !== true) {
790 throw new Error("");
791 }
792
793 // If we saw typeName earlier in the breadth-first traversal,
794 // delete the last-seen occurrence.
795 if (hasOwn.call(lastSeen, typeName)) {
796 delete list[lastSeen[typeName]];
797 }
798
799 // Record the new index of the last-seen occurrence of typeName.
800 lastSeen[typeName] = pos;
801
802 // Enqueue the base names of this type.
803 list.push.apply(list, d.baseNames);
804 }
805
806 // Compaction loop to remove array holes.
807 for (var to = 0, from = to, len = list.length; from < len; ++from) {
808 if (hasOwn.call(list, from)) {
809 list[to++] = list[from];
810 }
811 }
812
813 list.length = to;
814}
815
816function extend(into, from) {
817 Object.keys(from).forEach(function(name) {
818 into[name] = from[name];
819 });
820
821 return into;
822};
823
824exports.finalize = function() {
825 Object.keys(defCache).forEach(function(name) {
826 defCache[name].finalize();
827 });
828};