// Type description parser // // Type description JSON files (such as ecma5.json and browser.json) // are used to // // A) describe types that come from native code // // B) to cheaply load the types for big libraries, or libraries that // can't be inferred well (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS return exports.init = mod; if (typeof define == "function" && define.amd) // AMD return define({init: mod}); tern.def = {init: mod}; })(function(exports, infer) { "use strict"; function hop(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } var TypeParser = exports.TypeParser = function(spec, start, base, forceNew) { this.pos = start || 0; this.spec = spec; this.base = base; this.forceNew = forceNew; }; function unwrapType(type, self, args) { return type.call ? type(self, args) : type; } function extractProp(type, prop) { if (prop == "!ret") { if (type.retval) return type.retval; var rv = new infer.AVal; type.propagate(new infer.IsCallee(infer.ANull, [], null, rv)); return rv; } else { return type.getProp(prop); } } function computedFunc(name, args, retType, generator) { return function(self, cArgs) { var realArgs = []; for (var i = 0; i < args.length; i++) realArgs.push(unwrapType(args[i], self, cArgs)); return new infer.Fn(name, infer.ANull, realArgs, unwrapType(retType, self, cArgs), generator); }; } function computedUnion(types) { return function(self, args) { var union = new infer.AVal; for (var i = 0; i < types.length; i++) unwrapType(types[i], self, args).propagate(union); union.maxWeight = 1e5; return union; }; } function computedArray(inner) { return function(self, args) { return new infer.Arr(inner(self, args)); }; } function computedTuple(types) { return function(self, args) { return new infer.Arr(types.map(function(tp) { return unwrapType(tp, self, args) })) } } TypeParser.prototype = { eat: function(str) { if (str.length == 1 ? this.spec.charAt(this.pos) == str : this.spec.indexOf(str, this.pos) == this.pos) { this.pos += str.length; return true; } }, word: function(re) { var word = "", ch; re = re || /[\w$]/; while ((ch = this.spec.charAt(this.pos)) && re.test(ch)) { word += ch; ++this.pos; } return word; }, error: function() { throw new Error("Unrecognized type spec: " + this.spec + " (at " + this.pos + ")"); }, parseFnType: function(comp, name, top, generator) { var args = [], names = [], computed = false; if (!this.eat(")")) for (var i = 0; ; ++i) { var colon = this.spec.indexOf(": ", this.pos), argname; if (colon != -1) { argname = this.spec.slice(this.pos, colon); if (/^[$\w?]+$/.test(argname)) this.pos = colon + 2; else argname = null; } names.push(argname); var argType = this.parseType(comp); if (argType.call) computed = true; args.push(argType); if (!this.eat(", ")) { this.eat(")") || this.error(); break; } } var retType, computeRet, computeRetStart, fn; if (this.eat(" -> ")) { var retStart = this.pos; retType = this.parseType(true); if (retType.call && !computed) { computeRet = retType; retType = infer.ANull; computeRetStart = retStart; } } else { retType = infer.ANull; } if (computed) return computedFunc(name, args, retType, generator); if (top && (fn = this.base)) infer.Fn.call(this.base, name, infer.ANull, args, names, retType, generator); else fn = new infer.Fn(name, infer.ANull, args, names, retType, generator); if (computeRet) fn.computeRet = computeRet; if (computeRetStart != null) fn.computeRetSource = this.spec.slice(computeRetStart, this.pos); return fn; }, parseType: function(comp, name, top) { var main = this.parseTypeMaybeProp(comp, name, top); if (!this.eat("|")) return main; var types = [main], computed = main.call; for (;;) { var next = this.parseTypeMaybeProp(comp, name, top); types.push(next); if (next.call) computed = true; if (!this.eat("|")) break; } if (computed) return computedUnion(types); var union = new infer.AVal; for (var i = 0; i < types.length; i++) types[i].propagate(union); union.maxWeight = 1e5; return union; }, parseTypeMaybeProp: function(comp, name, top) { var result = this.parseTypeInner(comp, name, top); while (comp && this.eat(".")) result = this.extendWithProp(result); return result; }, extendWithProp: function(base) { var propName = this.word(/[\w<>$!:]/) || this.error(); if (base.apply) return function(self, args) { return extractProp(base(self, args), propName); }; return extractProp(base, propName); }, parseTypeInner: function(comp, name, top) { var gen if (this.eat("fn(") || (gen = this.eat("fn*("))) { return this.parseFnType(comp, name, top, gen); } else if (this.eat("[")) { var inner = this.parseType(comp), types, computed = inner.call while (this.eat(", ")) { if (!types) types = [inner] var next = this.parseType(comp) types.push(next) computed = computed || next.call } this.eat("]") || this.error() if (computed) return types ? computedTuple(types) : computedArray(inner) if (top && this.base) { infer.Arr.call(this.base, types || inner) return this.base } return new infer.Arr(types || inner) } else if (this.eat("+")) { var path = this.word(/[\w$<>\.:!]/) var base = infer.cx().localDefs[path + ".prototype"] if (!base) { var base = parsePath(path); if (!(base instanceof infer.Obj)) return base; var proto = descendProps(base, ["prototype"]) if (proto && (proto = proto.getObjType())) base = proto } if (comp && this.eat("[")) return this.parsePoly(base); if (top && this.forceNew) return new infer.Obj(base); return infer.getInstance(base); } else if (this.eat(":")) { name = this.word(/[\w$\.]/) return infer.getSymbol(name) } else if (comp && this.eat("!")) { var arg = this.word(/\d/); if (arg) { arg = Number(arg); return function(_self, args) {return args[arg] || infer.ANull;}; } else if (this.eat("this")) { return function(self) {return self;}; } else if (this.eat("custom:")) { var fname = this.word(/[\w$]/); return customFunctions[fname] || function() { return infer.ANull; }; } else { return this.fromWord("!" + this.word(/[\w$<>\.!:]/)); } } else if (this.eat("?")) { return infer.ANull; } else { return this.fromWord(this.word(/[\w$<>\.!:`]/)); } }, fromWord: function(spec) { var cx = infer.cx(); switch (spec) { case "number": return cx.num; case "string": return cx.str; case "bool": return cx.bool; case "": return cx.topScope; } if (cx.localDefs && spec in cx.localDefs) return cx.localDefs[spec]; return parsePath(spec); }, parsePoly: function(base) { var propName = "", match; if (match = this.spec.slice(this.pos).match(/^\s*([\w$:]+)\s*=\s*/)) { propName = match[1]; this.pos += match[0].length; } var value = this.parseType(true); if (!this.eat("]")) this.error(); if (value.call) return function(self, args) { var instance = new infer.Obj(base); value(self, args).propagate(instance.defProp(propName)); return instance; }; var instance = new infer.Obj(base); value.propagate(instance.defProp(propName)); return instance; } }; function parseType(spec, name, base, forceNew) { var type = new TypeParser(spec, null, base, forceNew).parseType(false, name, true); if (/^fn\(/.test(spec)) for (var i = 0; i < type.args.length; ++i) (function(i) { var arg = type.args[i]; if (arg instanceof infer.Fn && arg.args && arg.args.length) addEffect(type, function(_self, fArgs) { var fArg = fArgs[i]; if (fArg) fArg.propagate(new infer.IsCallee(infer.cx().topScope, arg.args, null, infer.ANull)); }); })(i); return type; } function addEffect(fn, handler, replaceRet) { var oldCmp = fn.computeRet, rv = fn.retval; fn.computeRet = function(self, args, argNodes) { var handled = handler(self, args, argNodes); var old = oldCmp ? oldCmp(self, args, argNodes) : rv; return replaceRet ? handled : old; }; } var parseEffect = exports.parseEffect = function(effect, fn) { var m; if (effect.indexOf("propagate ") == 0) { var p = new TypeParser(effect, 10); var origin = p.parseType(true); if (!p.eat(" ")) p.error(); var target = p.parseType(true); addEffect(fn, function(self, args) { unwrapType(origin, self, args).propagate(unwrapType(target, self, args)); }); } else if (effect.indexOf("call ") == 0) { var andRet = effect.indexOf("and return ", 5) == 5; var p = new TypeParser(effect, andRet ? 16 : 5); var getCallee = p.parseType(true), getSelf = null, getArgs = []; if (p.eat(" this=")) getSelf = p.parseType(true); while (p.eat(" ")) getArgs.push(p.parseType(true)); addEffect(fn, function(self, args) { var callee = unwrapType(getCallee, self, args); var slf = getSelf ? unwrapType(getSelf, self, args) : infer.ANull, as = []; for (var i = 0; i < getArgs.length; ++i) as.push(unwrapType(getArgs[i], self, args)); var result = andRet ? new infer.AVal : infer.ANull; callee.propagate(new infer.IsCallee(slf, as, null, result)); return result; }, andRet); } else if (m = effect.match(/^custom (\S+)\s*(.*)/)) { var customFunc = customFunctions[m[1]]; if (customFunc) addEffect(fn, m[2] ? customFunc(m[2]) : customFunc); } else if (effect.indexOf("copy ") == 0) { var p = new TypeParser(effect, 5); var getFrom = p.parseType(true); p.eat(" "); var getTo = p.parseType(true); addEffect(fn, function(self, args) { var from = unwrapType(getFrom, self, args), to = unwrapType(getTo, self, args); from.forAllProps(function(prop, val, local) { if (local && prop != "") to.propagate(new infer.DefProp(prop, val)); }); }); } else { throw new Error("Unknown effect type: " + effect); } }; var currentTopScope; var parsePath = exports.parsePath = function(path, scope) { var cx = infer.cx(), cached = cx.paths[path], origPath = path; if (cached != null) return cached; cx.paths[path] = infer.ANull; var base = scope || currentTopScope || cx.topScope; if (cx.localDefs) for (var name in cx.localDefs) { if (path.indexOf(name) == 0) { if (path == name) return cx.paths[path] = cx.localDefs[path]; if (path.charAt(name.length) == ".") { base = cx.localDefs[name]; path = path.slice(name.length + 1); break; } } } var result = descendProps(base, path.split(".")) // Uncomment this to get feedback on your poorly written .json files // if (result == infer.ANull) console.error("bad path: " + origPath + " (" + cx.curOrigin + ")") cx.paths[origPath] = result == infer.ANull ? null : result return result } function descendProps(base, parts) { for (var i = 0; i < parts.length && base != infer.ANull; ++i) { var prop = parts[i]; if (prop.charAt(0) == "!") { if (prop == "!proto") { base = (base instanceof infer.Obj && base.proto) || infer.ANull; } else { var fn = base.getFunctionType(); if (!fn) { base = infer.ANull; } else if (prop == "!ret") { base = fn.retval && fn.retval.getType(false) || infer.ANull; } else { var arg = fn.args && fn.args[Number(prop.slice(1))]; base = (arg && arg.getType(false)) || infer.ANull; } } } else if (base instanceof infer.Obj) { var propVal = (prop == "prototype" && base instanceof infer.Fn) ? base.getProp(prop) : base.props[prop]; if (!propVal || propVal.isEmpty()) base = infer.ANull; else base = propVal.types[0]; } } return base; } function emptyObj(ctor) { var empty = Object.create(ctor.prototype); empty.props = Object.create(null); empty.isShell = true; return empty; } function isSimpleAnnotation(spec) { if (!spec["!type"] || /^(fn\(|\[)/.test(spec["!type"])) return false; for (var prop in spec) if (prop != "!type" && prop != "!doc" && prop != "!url" && prop != "!span" && prop != "!data") return false; return true; } function passOne(base, spec, path) { if (!base) { var tp = spec["!type"]; if (tp) { if (/^fn\(/.test(tp)) base = emptyObj(infer.Fn); else if (tp.charAt(0) == "[") base = emptyObj(infer.Arr); else throw new Error("Invalid !type spec: " + tp); } else if (spec["!stdProto"]) { base = infer.cx().protos[spec["!stdProto"]]; } else { base = emptyObj(infer.Obj); } base.name = path; } for (var name in spec) if (hop(spec, name) && name.charCodeAt(0) != 33) { var inner = spec[name]; if (typeof inner == "string" || isSimpleAnnotation(inner)) continue; var prop = base.defProp(name); passOne(prop.getObjType(), inner, path ? path + "." + name : name).propagate(prop); } return base; } function passTwo(base, spec, path) { if (base.isShell) { delete base.isShell; var tp = spec["!type"]; if (tp) { parseType(tp, path, base); } else { var proto = spec["!proto"] && parseType(spec["!proto"]); infer.Obj.call(base, proto instanceof infer.Obj ? proto : true, path); } } var effects = spec["!effects"]; if (effects && base instanceof infer.Fn) for (var i = 0; i < effects.length; ++i) parseEffect(effects[i], base); copyInfo(spec, base); for (var name in spec) if (hop(spec, name) && name.charCodeAt(0) != 33) { var inner = spec[name], known = base.defProp(name), innerPath = path ? path + "." + name : name; if (typeof inner == "string") { if (known.isEmpty()) parseType(inner, innerPath).propagate(known); } else { if (!isSimpleAnnotation(inner)) passTwo(known.getObjType(), inner, innerPath); else if (known.isEmpty()) parseType(inner["!type"], innerPath, null, true).propagate(known); else continue; if (inner["!doc"]) known.doc = inner["!doc"]; if (inner["!url"]) known.url = inner["!url"]; if (inner["!span"]) known.span = inner["!span"]; } } return base; } function copyInfo(spec, type) { if (spec["!doc"]) type.doc = spec["!doc"]; if (spec["!url"]) type.url = spec["!url"]; if (spec["!span"]) type.span = spec["!span"]; if (spec["!data"]) type.metaData = spec["!data"]; } function doLoadEnvironment(data, scope) { var cx = infer.cx(), server = cx.parent infer.addOrigin(cx.curOrigin = data["!name"] || "env#" + cx.origins.length); cx.localDefs = cx.definitions[cx.curOrigin] = Object.create(null); if (server) server.signal("preLoadDef", data) passOne(scope, data); var def = data["!define"]; if (def) { for (var name in def) { var spec = def[name]; cx.localDefs[name] = typeof spec == "string" ? parsePath(spec) : passOne(null, spec, name); } for (var name in def) { var spec = def[name]; if (typeof spec != "string") passTwo(cx.localDefs[name], def[name], name); } } passTwo(scope, data); if (server) server.signal("postLoadDef", data) cx.curOrigin = cx.localDefs = null; } exports.load = function(data, scope) { if (!scope) scope = infer.cx().topScope; var oldScope = currentTopScope; currentTopScope = scope; try { doLoadEnvironment(data, scope); } finally { currentTopScope = oldScope; } }; exports.parse = function(data, origin, path) { var cx = infer.cx(); if (origin) { cx.origin = origin; cx.localDefs = cx.definitions[origin]; } try { if (typeof data == "string") return parseType(data, path); else return passTwo(passOne(null, data, path), data, path); } finally { if (origin) cx.origin = cx.localDefs = null; } }; // Used to register custom logic for more involved effect or type // computation. var customFunctions = Object.create(null); infer.registerFunction = function(name, f) { customFunctions[name] = f; }; var IsCreated = infer.constraint({ construct: function(created, target, spec) { this.created = created; this.target = target; this.spec = spec; }, addType: function(tp) { if (tp instanceof infer.Obj && this.created++ < 5) { var derived = new infer.Obj(tp), spec = this.spec; if (spec instanceof infer.AVal) spec = spec.getObjType(false); if (spec instanceof infer.Obj) for (var prop in spec.props) { var cur = spec.props[prop].types[0]; var p = derived.defProp(prop); if (cur && cur instanceof infer.Obj && cur.props.value) { var vtp = cur.props.value.getType(false); if (vtp) p.addType(vtp); } } this.target.addType(derived); } } }); infer.registerFunction("Object_create", function(_self, args, argNodes) { if (argNodes && argNodes.length && argNodes[0].type == "Literal" && argNodes[0].value == null) return new infer.Obj(); var result = new infer.AVal; if (args[0]) args[0].propagate(new IsCreated(0, result, args[1])); return result; }); var PropSpec = infer.constraint({ construct: function(target) { this.target = target; }, addType: function(tp) { if (!(tp instanceof infer.Obj)) return; if (tp.hasProp("value")) tp.getProp("value").propagate(this.target); else if (tp.hasProp("get")) tp.getProp("get").propagate(new infer.IsCallee(infer.ANull, [], null, this.target)); } }); infer.registerFunction("Object_defineProperty", function(_self, args, argNodes) { if (argNodes && argNodes.length >= 3 && argNodes[1].type == "Literal" && typeof argNodes[1].value == "string") { var obj = args[0], connect = new infer.AVal; obj.propagate(new infer.DefProp(argNodes[1].value, connect, argNodes[1])); args[2].propagate(new PropSpec(connect)); } return infer.ANull; }); infer.registerFunction("Object_defineProperties", function(_self, args, argNodes) { if (args.length >= 2) { var obj = args[0]; args[1].forAllProps(function(prop, val, local) { if (!local) return; var connect = new infer.AVal; obj.propagate(new infer.DefProp(prop, connect, argNodes && argNodes[1])); val.propagate(new PropSpec(connect)); }); } return infer.ANull; }); var IsBound = infer.constraint({ construct: function(self, args, target) { this.self = self; this.args = args; this.target = target; }, addType: function(tp) { if (!(tp instanceof infer.Fn)) return; this.target.addType(new infer.Fn(tp.name, infer.ANull, tp.args.slice(this.args.length), tp.argNames.slice(this.args.length), tp.retval, tp.generator)); this.self.propagate(tp.self); for (var i = 0; i < Math.min(tp.args.length, this.args.length); ++i) this.args[i].propagate(tp.args[i]); } }); infer.registerFunction("Function_bind", function(self, args) { if (!args.length) return infer.ANull; var result = new infer.AVal; self.propagate(new IsBound(args[0], args.slice(1), result)); return result; }); infer.registerFunction("Array_ctor", function(_self, args) { var arr = new infer.Arr; if (args.length != 1 || !args[0].hasType(infer.cx().num)) { var content = arr.getProp(""); for (var i = 0; i < args.length; ++i) args[i].propagate(content); } return arr; }); infer.registerFunction("Promise_ctor", function(_self, args, argNodes) { var defs6 = infer.cx().definitions.ecma6 if (!defs6 || args.length < 1) return infer.ANull; var self = new infer.Obj(defs6["Promise.prototype"]); var valProp = self.defProp(":t", argNodes && argNodes[0]); var valArg = new infer.AVal; valArg.propagate(valProp); var exec = new infer.Fn("execute", infer.ANull, [valArg], ["value"], infer.ANull); var reject = defs6.Promise_reject; args[0].propagate(new infer.IsCallee(infer.ANull, [exec, reject], null, infer.ANull)); return self; }); var PromiseResolvesTo = infer.constraint({ construct: function(output) { this.output = output; }, addType: function(tp) { if (tp.constructor == infer.Obj && tp.name == "Promise" && tp.hasProp(":t")) tp.getProp(":t").propagate(this.output); else tp.propagate(this.output); } }); var WG_PROMISE_KEEP_VALUE = 50; infer.registerFunction("Promise_then", function(self, args, argNodes) { var fn = args.length && args[0].getFunctionType(); var defs6 = infer.cx().definitions.ecma6 if (!fn || !defs6) return self; var result = new infer.Obj(defs6["Promise.prototype"]); var value = result.defProp(":t", argNodes && argNodes[0]), ty; if (fn.retval.isEmpty() && (ty = self.getType()) instanceof infer.Obj && ty.hasProp(":t")) ty.getProp(":t").propagate(value, WG_PROMISE_KEEP_VALUE); fn.retval.propagate(new PromiseResolvesTo(value)); return result; }); infer.registerFunction("getOwnPropertySymbols", function(_self, args) { if (!args.length) return infer.ANull var result = new infer.AVal args[0].forAllProps(function(prop, _val, local) { if (local && prop.charAt(0) == ":") result.addType(infer.getSymbol(prop.slice(1))) }) return result }) infer.registerFunction("getSymbol", function(_self, _args, argNodes) { if (argNodes.length && argNodes[0].type == "Literal" && typeof argNodes[0].value == "string") return infer.getSymbol(argNodes[0].value) else return infer.ANull }) return exports; });