blob: 8d5ff3b0db284568efd38dd73cafaa562b304bc9 [file] [log] [blame]
var Ap = Array.prototype;
var slice = Ap.slice;
var map = Ap.map;
var each = Ap.forEach;
var Op = Object.prototype;
var objToStr = Op.toString;
var funObjStr = objToStr.call(function(){});
var strObjStr = objToStr.call("");
var hasOwn = Op.hasOwnProperty;
module.exports = function () {
var exports = {};
// A type is an object with a .check method that takes a value and returns
// true or false according to whether the value matches the type.
function Type(check, name) {
var self = this;
if (!(self instanceof Type)) {
throw new Error("Type constructor cannot be invoked without 'new'");
}
// Unfortunately we can't elegantly reuse isFunction and isString,
// here, because this code is executed while defining those types.
if (objToStr.call(check) !== funObjStr) {
throw new Error(check + " is not a function");
}
// The `name` parameter can be either a function or a string.
var nameObjStr = objToStr.call(name);
if (!(nameObjStr === funObjStr ||
nameObjStr === strObjStr)) {
throw new Error(name + " is neither a function nor a string");
}
Object.defineProperties(self, {
name: {value: name},
check: {
value: function (value, deep) {
var result = check.call(self, value, deep);
if (!result && deep && objToStr.call(deep) === funObjStr)
deep(self, value);
return result;
}
}
});
}
var Tp = Type.prototype;
// Throughout this file we use Object.defineProperty to prevent
// redefinition of exported properties.
exports.Type = Type;
// Like .check, except that failure triggers an AssertionError.
Tp.assert = function (value, deep) {
if (!this.check(value, deep)) {
var str = shallowStringify(value);
throw new Error(str + " does not match type " + this);
}
return true;
};
function shallowStringify(value) {
if (isObject.check(value))
return "{" + Object.keys(value).map(function (key) {
return key + ": " + value[key];
}).join(", ") + "}";
if (isArray.check(value))
return "[" + value.map(shallowStringify).join(", ") + "]";
return JSON.stringify(value);
}
Tp.toString = function () {
var name = this.name;
if (isString.check(name))
return name;
if (isFunction.check(name))
return name.call(this) + "";
return name + " type";
};
var builtInCtorFns = [];
var builtInCtorTypes = [];
var builtInTypes = {};
exports.builtInTypes = builtInTypes;
function defBuiltInType(example, name) {
var objStr = objToStr.call(example);
var type = new Type(function (value) {
return objToStr.call(value) === objStr;
}, name);
builtInTypes[name] = type;
if (example && typeof example.constructor === "function") {
builtInCtorFns.push(example.constructor);
builtInCtorTypes.push(type);
}
return type;
}
// These types check the underlying [[Class]] attribute of the given
// value, rather than using the problematic typeof operator. Note however
// that no subtyping is considered; so, for instance, isObject.check
// returns false for [], /./, new Date, and null.
var isString = defBuiltInType("truthy", "string");
var isFunction = defBuiltInType(function () {}, "function");
var isArray = defBuiltInType([], "array");
var isObject = defBuiltInType({}, "object");
var isRegExp = defBuiltInType(/./, "RegExp");
var isDate = defBuiltInType(new Date, "Date");
var isNumber = defBuiltInType(3, "number");
var isBoolean = defBuiltInType(true, "boolean");
var isNull = defBuiltInType(null, "null");
var isUndefined = defBuiltInType(void 0, "undefined");
// There are a number of idiomatic ways of expressing types, so this
// function serves to coerce them all to actual Type objects. Note that
// providing the name argument is not necessary in most cases.
function toType(from, name) {
// The toType function should of course be idempotent.
if (from instanceof Type)
return from;
// The Def type is used as a helper for constructing compound
// interface types for AST nodes.
if (from instanceof Def)
return from.type;
// Support [ElemType] syntax.
if (isArray.check(from))
return Type.fromArray(from);
// Support { someField: FieldType, ... } syntax.
if (isObject.check(from))
return Type.fromObject(from);
if (isFunction.check(from)) {
var bicfIndex = builtInCtorFns.indexOf(from);
if (bicfIndex >= 0) {
return builtInCtorTypes[bicfIndex];
}
// If isFunction.check(from), and from is not a built-in
// constructor, assume from is a binary predicate function we can
// use to define the type.
return new Type(from, name);
}
// As a last resort, toType returns a type that matches any value that
// is === from. This is primarily useful for literal values like
// toType(null), but it has the additional advantage of allowing
// toType to be a total function.
return new Type(function (value) {
return value === from;
}, isUndefined.check(name) ? function () {
return from + "";
} : name);
}
// Returns a type that matches the given value iff any of type1, type2,
// etc. match the value.
Type.or = function (/* type1, type2, ... */) {
var types = [];
var len = arguments.length;
for (var i = 0; i < len; ++i)
types.push(toType(arguments[i]));
return new Type(function (value, deep) {
for (var i = 0; i < len; ++i)
if (types[i].check(value, deep))
return true;
return false;
}, function () {
return types.join(" | ");
});
};
Type.fromArray = function (arr) {
if (!isArray.check(arr)) {
throw new Error("");
}
if (arr.length !== 1) {
throw new Error("only one element type is permitted for typed arrays");
}
return toType(arr[0]).arrayOf();
};
Tp.arrayOf = function () {
var elemType = this;
return new Type(function (value, deep) {
return isArray.check(value) && value.every(function (elem) {
return elemType.check(elem, deep);
});
}, function () {
return "[" + elemType + "]";
});
};
Type.fromObject = function (obj) {
var fields = Object.keys(obj).map(function (name) {
return new Field(name, obj[name]);
});
return new Type(function (value, deep) {
return isObject.check(value) && fields.every(function (field) {
return field.type.check(value[field.name], deep);
});
}, function () {
return "{ " + fields.join(", ") + " }";
});
};
function Field(name, type, defaultFn, hidden) {
var self = this;
if (!(self instanceof Field)) {
throw new Error("Field constructor cannot be invoked without 'new'");
}
isString.assert(name);
type = toType(type);
var properties = {
name: {value: name},
type: {value: type},
hidden: {value: !!hidden}
};
if (isFunction.check(defaultFn)) {
properties.defaultFn = {value: defaultFn};
}
Object.defineProperties(self, properties);
}
var Fp = Field.prototype;
Fp.toString = function () {
return JSON.stringify(this.name) + ": " + this.type;
};
Fp.getValue = function (obj) {
var value = obj[this.name];
if (!isUndefined.check(value))
return value;
if (this.defaultFn)
value = this.defaultFn.call(obj);
return value;
};
// Define a type whose name is registered in a namespace (the defCache) so
// that future definitions will return the same type given the same name.
// In particular, this system allows for circular and forward definitions.
// The Def object d returned from Type.def may be used to configure the
// type d.type by calling methods such as d.bases, d.build, and d.field.
Type.def = function (typeName) {
isString.assert(typeName);
return hasOwn.call(defCache, typeName)
? defCache[typeName]
: defCache[typeName] = new Def(typeName);
};
// In order to return the same Def instance every time Type.def is called
// with a particular name, those instances need to be stored in a cache.
var defCache = Object.create(null);
function Def(typeName) {
var self = this;
if (!(self instanceof Def)) {
throw new Error("Def constructor cannot be invoked without 'new'");
}
Object.defineProperties(self, {
typeName: {value: typeName},
baseNames: {value: []},
ownFields: {value: Object.create(null)},
// These two are populated during finalization.
allSupertypes: {value: Object.create(null)}, // Includes own typeName.
supertypeList: {value: []}, // Linear inheritance hierarchy.
allFields: {value: Object.create(null)}, // Includes inherited fields.
fieldNames: {value: []}, // Non-hidden keys of allFields.
type: {
value: new Type(function (value, deep) {
return self.check(value, deep);
}, typeName)
}
});
}
Def.fromValue = function (value) {
if (value && typeof value === "object") {
var type = value.type;
if (typeof type === "string" &&
hasOwn.call(defCache, type)) {
var d = defCache[type];
if (d.finalized) {
return d;
}
}
}
return null;
};
var Dp = Def.prototype;
Dp.isSupertypeOf = function (that) {
if (that instanceof Def) {
if (this.finalized !== true ||
that.finalized !== true) {
throw new Error("");
}
return hasOwn.call(that.allSupertypes, this.typeName);
} else {
throw new Error(that + " is not a Def");
}
};
// Note that the list returned by this function is a copy of the internal
// supertypeList, *without* the typeName itself as the first element.
exports.getSupertypeNames = function (typeName) {
if (!hasOwn.call(defCache, typeName)) {
throw new Error("");
}
var d = defCache[typeName];
if (d.finalized !== true) {
throw new Error("");
}
return d.supertypeList.slice(1);
};
// Returns an object mapping from every known type in the defCache to the
// most specific supertype whose name is an own property of the candidates
// object.
exports.computeSupertypeLookupTable = function (candidates) {
var table = {};
var typeNames = Object.keys(defCache);
var typeNameCount = typeNames.length;
for (var i = 0; i < typeNameCount; ++i) {
var typeName = typeNames[i];
var d = defCache[typeName];
if (d.finalized !== true) {
throw new Error("" + typeName);
}
for (var j = 0; j < d.supertypeList.length; ++j) {
var superTypeName = d.supertypeList[j];
if (hasOwn.call(candidates, superTypeName)) {
table[typeName] = superTypeName;
break;
}
}
}
return table;
};
Dp.checkAllFields = function (value, deep) {
var allFields = this.allFields;
if (this.finalized !== true) {
throw new Error("" + this.typeName);
}
function checkFieldByName(name) {
var field = allFields[name];
var type = field.type;
var child = field.getValue(value);
return type.check(child, deep);
}
return isObject.check(value)
&& Object.keys(allFields).every(checkFieldByName);
};
Dp.check = function (value, deep) {
if (this.finalized !== true) {
throw new Error(
"prematurely checking unfinalized type " + this.typeName
);
}
// A Def type can only match an object value.
if (!isObject.check(value))
return false;
var vDef = Def.fromValue(value);
if (!vDef) {
// If we couldn't infer the Def associated with the given value,
// and we expected it to be a SourceLocation or a Position, it was
// probably just missing a "type" field (because Esprima does not
// assign a type property to such nodes). Be optimistic and let
// this.checkAllFields make the final decision.
if (this.typeName === "SourceLocation" ||
this.typeName === "Position") {
return this.checkAllFields(value, deep);
}
// Calling this.checkAllFields for any other type of node is both
// bad for performance and way too forgiving.
return false;
}
// If checking deeply and vDef === this, then we only need to call
// checkAllFields once. Calling checkAllFields is too strict when deep
// is false, because then we only care about this.isSupertypeOf(vDef).
if (deep && vDef === this)
return this.checkAllFields(value, deep);
// In most cases we rely exclusively on isSupertypeOf to make O(1)
// subtyping determinations. This suffices in most situations outside
// of unit tests, since interface conformance is checked whenever new
// instances are created using builder functions.
if (!this.isSupertypeOf(vDef))
return false;
// The exception is when deep is true; then, we recursively check all
// fields.
if (!deep)
return true;
// Use the more specific Def (vDef) to perform the deep check, but
// shallow-check fields defined by the less specific Def (this).
return vDef.checkAllFields(value, deep)
&& this.checkAllFields(value, false);
};
Dp.bases = function () {
var args = slice.call(arguments);
var bases = this.baseNames;
if (this.finalized) {
if (args.length !== bases.length) {
throw new Error("");
}
for (var i = 0; i < args.length; i++) {
if (args[i] !== bases[i]) {
throw new Error("");
}
}
return this;
}
args.forEach(function (baseName) {
isString.assert(baseName);
// This indexOf lookup may be O(n), but the typical number of base
// names is very small, and indexOf is a native Array method.
if (bases.indexOf(baseName) < 0)
bases.push(baseName);
});
return this; // For chaining.
};
// False by default until .build(...) is called on an instance.
Object.defineProperty(Dp, "buildable", {value: false});
var builders = {};
exports.builders = builders;
// This object is used as prototype for any node created by a builder.
var nodePrototype = {};
// Call this function to define a new method to be shared by all AST
// nodes. The replaced method (if any) is returned for easy wrapping.
exports.defineMethod = function (name, func) {
var old = nodePrototype[name];
// Pass undefined as func to delete nodePrototype[name].
if (isUndefined.check(func)) {
delete nodePrototype[name];
} else {
isFunction.assert(func);
Object.defineProperty(nodePrototype, name, {
enumerable: true, // For discoverability.
configurable: true, // For delete proto[name].
value: func
});
}
return old;
};
var isArrayOfString = isString.arrayOf();
// Calling the .build method of a Def simultaneously marks the type as
// buildable (by defining builders[getBuilderName(typeName)]) and
// specifies the order of arguments that should be passed to the builder
// function to create an instance of the type.
Dp.build = function (/* param1, param2, ... */) {
var self = this;
var newBuildParams = slice.call(arguments);
isArrayOfString.assert(newBuildParams);
// Calling Def.prototype.build multiple times has the effect of merely
// redefining this property.
Object.defineProperty(self, "buildParams", {
value: newBuildParams,
writable: false,
enumerable: false,
configurable: true
});
if (self.buildable) {
// If this Def is already buildable, update self.buildParams and
// continue using the old builder function.
return self;
}
// Every buildable type will have its "type" field filled in
// automatically. This includes types that are not subtypes of Node,
// like SourceLocation, but that seems harmless (TODO?).
self.field("type", String, function () { return self.typeName });
// Override Dp.buildable for this Def instance.
Object.defineProperty(self, "buildable", {value: true});
Object.defineProperty(builders, getBuilderName(self.typeName), {
enumerable: true,
value: function () {
var args = arguments;
var argc = args.length;
var built = Object.create(nodePrototype);
if (!self.finalized) {
throw new Error(
"attempting to instantiate unfinalized type " +
self.typeName
);
}
function add(param, i) {
if (hasOwn.call(built, param))
return;
var all = self.allFields;
if (!hasOwn.call(all, param)) {
throw new Error("" + param);
}
var field = all[param];
var type = field.type;
var value;
if (isNumber.check(i) && i < argc) {
value = args[i];
} else if (field.defaultFn) {
// Expose the partially-built object to the default
// function as its `this` object.
value = field.defaultFn.call(built);
} else {
var message = "no value or default function given for field " +
JSON.stringify(param) + " of " + self.typeName + "(" +
self.buildParams.map(function (name) {
return all[name];
}).join(", ") + ")";
throw new Error(message);
}
if (!type.check(value)) {
throw new Error(
shallowStringify(value) +
" does not match field " + field +
" of type " + self.typeName
);
}
// TODO Could attach getters and setters here to enforce
// dynamic type safety.
built[param] = value;
}
self.buildParams.forEach(function (param, i) {
add(param, i);
});
Object.keys(self.allFields).forEach(function (param) {
add(param); // Use the default value.
});
// Make sure that the "type" field was filled automatically.
if (built.type !== self.typeName) {
throw new Error("");
}
return built;
}
});
return self; // For chaining.
};
function getBuilderName(typeName) {
return typeName.replace(/^[A-Z]+/, function (upperCasePrefix) {
var len = upperCasePrefix.length;
switch (len) {
case 0: return "";
// If there's only one initial capital letter, just lower-case it.
case 1: return upperCasePrefix.toLowerCase();
default:
// If there's more than one initial capital letter, lower-case
// all but the last one, so that XMLDefaultDeclaration (for
// example) becomes xmlDefaultDeclaration.
return upperCasePrefix.slice(
0, len - 1).toLowerCase() +
upperCasePrefix.charAt(len - 1);
}
});
}
exports.getBuilderName = getBuilderName;
function getStatementBuilderName(typeName) {
typeName = getBuilderName(typeName);
return typeName.replace(/(Expression)?$/, "Statement");
}
exports.getStatementBuilderName = getStatementBuilderName;
// The reason fields are specified using .field(...) instead of an object
// literal syntax is somewhat subtle: the object literal syntax would
// support only one key and one value, but with .field(...) we can pass
// any number of arguments to specify the field.
Dp.field = function (name, type, defaultFn, hidden) {
if (this.finalized) {
console.error("Ignoring attempt to redefine field " +
JSON.stringify(name) + " of finalized type " +
JSON.stringify(this.typeName));
return this;
}
this.ownFields[name] = new Field(name, type, defaultFn, hidden);
return this; // For chaining.
};
var namedTypes = {};
exports.namedTypes = namedTypes;
// Like Object.keys, but aware of what fields each AST type should have.
function getFieldNames(object) {
var d = Def.fromValue(object);
if (d) {
return d.fieldNames.slice(0);
}
if ("type" in object) {
throw new Error(
"did not recognize object of type " +
JSON.stringify(object.type)
);
}
return Object.keys(object);
}
exports.getFieldNames = getFieldNames;
// Get the value of an object property, taking object.type and default
// functions into account.
function getFieldValue(object, fieldName) {
var d = Def.fromValue(object);
if (d) {
var field = d.allFields[fieldName];
if (field) {
return field.getValue(object);
}
}
return object && object[fieldName];
}
exports.getFieldValue = getFieldValue;
// Iterate over all defined fields of an object, including those missing
// or undefined, passing each field name and effective value (as returned
// by getFieldValue) to the callback. If the object has no corresponding
// Def, the callback will never be called.
exports.eachField = function (object, callback, context) {
getFieldNames(object).forEach(function (name) {
callback.call(this, name, getFieldValue(object, name));
}, context);
};
// Similar to eachField, except that iteration stops as soon as the
// callback returns a truthy value. Like Array.prototype.some, the final
// result is either true or false to indicates whether the callback
// returned true for any element or not.
exports.someField = function (object, callback, context) {
return getFieldNames(object).some(function (name) {
return callback.call(this, name, getFieldValue(object, name));
}, context);
};
// This property will be overridden as true by individual Def instances
// when they are finalized.
Object.defineProperty(Dp, "finalized", {value: false});
Dp.finalize = function () {
var self = this;
// It's not an error to finalize a type more than once, but only the
// first call to .finalize does anything.
if (!self.finalized) {
var allFields = self.allFields;
var allSupertypes = self.allSupertypes;
self.baseNames.forEach(function (name) {
var def = defCache[name];
if (def instanceof Def) {
def.finalize();
extend(allFields, def.allFields);
extend(allSupertypes, def.allSupertypes);
} else {
var message = "unknown supertype name " +
JSON.stringify(name) +
" for subtype " +
JSON.stringify(self.typeName);
throw new Error(message);
}
});
// TODO Warn if fields are overridden with incompatible types.
extend(allFields, self.ownFields);
allSupertypes[self.typeName] = self;
self.fieldNames.length = 0;
for (var fieldName in allFields) {
if (hasOwn.call(allFields, fieldName) &&
!allFields[fieldName].hidden) {
self.fieldNames.push(fieldName);
}
}
// Types are exported only once they have been finalized.
Object.defineProperty(namedTypes, self.typeName, {
enumerable: true,
value: self.type
});
Object.defineProperty(self, "finalized", {value: true});
// A linearization of the inheritance hierarchy.
populateSupertypeList(self.typeName, self.supertypeList);
if (self.buildable && self.supertypeList.lastIndexOf("Expression") >= 0) {
wrapExpressionBuilderWithStatement(self.typeName);
}
}
};
// Adds an additional builder for Expression subtypes
// that wraps the built Expression in an ExpressionStatements.
function wrapExpressionBuilderWithStatement(typeName) {
var wrapperName = getStatementBuilderName(typeName);
// skip if the builder already exists
if (builders[wrapperName]) return;
// the builder function to wrap with builders.ExpressionStatement
var wrapped = builders[getBuilderName(typeName)];
// skip if there is nothing to wrap
if (!wrapped) return;
builders[wrapperName] = function () {
return builders.expressionStatement(wrapped.apply(builders, arguments));
};
}
function populateSupertypeList(typeName, list) {
list.length = 0;
list.push(typeName);
var lastSeen = Object.create(null);
for (var pos = 0; pos < list.length; ++pos) {
typeName = list[pos];
var d = defCache[typeName];
if (d.finalized !== true) {
throw new Error("");
}
// If we saw typeName earlier in the breadth-first traversal,
// delete the last-seen occurrence.
if (hasOwn.call(lastSeen, typeName)) {
delete list[lastSeen[typeName]];
}
// Record the new index of the last-seen occurrence of typeName.
lastSeen[typeName] = pos;
// Enqueue the base names of this type.
list.push.apply(list, d.baseNames);
}
// Compaction loop to remove array holes.
for (var to = 0, from = to, len = list.length; from < len; ++from) {
if (hasOwn.call(list, from)) {
list[to++] = list[from];
}
}
list.length = to;
}
function extend(into, from) {
Object.keys(from).forEach(function (name) {
into[name] = from[name];
});
return into;
};
exports.finalize = function () {
Object.keys(defCache).forEach(function (name) {
defCache[name].finalize();
});
};
return exports;
};