separate binding logic from scope to a binding class, move binding type resolution to the path so it can be used on any expression - #653

This commit is contained in:
Sebastian McKenzie 2015-03-13 01:08:46 +11:00
parent 40a111abbf
commit b44ba25d11
12 changed files with 372 additions and 225 deletions

View File

@ -0,0 +1 @@
for (var KEY = 0; KEY < ARR.length; KEY++) BODY;

View File

@ -432,7 +432,7 @@ class DestructuringTransformer {
} else {
arrayRef = this.scope.generateUidBasedOnNode(arrayRef);
this.nodes.push(this.buildVariableDeclaration(arrayRef, toArray));
this.scope.assignTypeGeneric(arrayRef.name, "Array");
this.getBinding(arrayRef.name).assignTypeGeneric("Array");
}
//

View File

@ -5,6 +5,12 @@ import * as t from "../../../types";
export var check = t.isForOfStatement;
export function ForOfStatement(node, parent, scope, file) {
if (this.get("right").isTypeGeneric("Array")) {
return array(node, scope, file);
}
//
var callback = spec;
if (file.isLoose("es6.forOf")) callback = loose;
@ -36,6 +42,44 @@ export function ForOfStatement(node, parent, scope, file) {
}
}
var array = function (node, scope, file) {
var nodes = [];
var right = node.right;
if (!t.isIdentifier(right) || !scope.hasBinding(right.name)) {
var uid = scope.generateUidIdentifier("arr");
nodes.push(t.variableDeclaration("var", [
t.variableDeclarator(uid, right)
]));
right = uid;
}
var iterationKey = scope.generateUidIdentifier("i");
var loop = util.template("for-of-array", {
BODY: node.body,
KEY: iterationKey,
ARR: right
});
t.inherits(loop, node);
t.ensureBlock(loop);
var iterationValue = t.memberExpression(right, iterationKey, true);
var left = node.left;
if (t.isVariableDeclaration(left)) {
left.declarations[0].init = iterationValue;
loop.body.body.unshift(left);
} else {
loop.body.body.unshift(t.expressionStatement(t.assignmentExpression("=", left, iterationValue)));
}
nodes.push(loop);
return nodes;
};
var loose = function (node, parent, scope, file) {
var left = node.left;
var declar, id;

View File

@ -127,16 +127,14 @@ exports.Function = function (node, parent, scope, file) {
);
}
scope.assignTypeGeneric(rest.name, "Array");
var loop = util.template("rest", {
ARGUMENTS: argsId,
ARRAY_KEY: arrKey,
ARRAY_LEN: arrLen,
START: start,
ARRAY: rest,
KEY: key,
LEN: len,
START: start,
ARRAY: rest,
KEY: key,
LEN: len
});
loop._blockHoist = node.params.length + 1;
node.body.body.unshift(loop);

View File

@ -142,7 +142,7 @@ class TailCallTransformer {
hasDeopt() {
// check if the ownerId has been reassigned, if it has then it's not safe to
// perform optimisations
var ownerIdInfo = this.scope.getBindingInfo(this.ownerId.name);
var ownerIdInfo = this.scope.getBinding(this.ownerId.name);
return ownerIdInfo && ownerIdInfo.reassigned;
}

View File

@ -20,7 +20,6 @@ export default {
"playground.objectGetterMemoization": require("./playground/object-getter-memoization"),
reactCompat: require("./other/react-compat"),
flow: require("./other/flow"),
react: require("./other/react"),
_modules: require("./internal/modules"),
@ -110,5 +109,6 @@ export default {
"utility.inlineExpressions": require("./utility/inline-expressions"),
"utility.deadCodeElimination": require("./utility/dead-code-elimination"),
flow: require("./other/flow"),
_cleanUp: require("./internal/cleanup")
};

View File

@ -0,0 +1,92 @@
import * as t from "../types";
export default class Binding {
constructor({ identifier, scope, path, kind }) {
this.identifier = identifier;
this.reassigned = false;
this.scope = scope;
this.path = path;
this.kind = kind;
}
/**
* Description
*/
setTypeAnnotation() {
var typeInfo = this.path.getTypeAnnotation();
this.typeAnnotationInferred = typeInfo.inferred;
this.typeAnnotation = typeInfo.annotation;
}
/**
* Description
*/
isTypeGeneric(): boolean {
return this.path.isTypeGeneric(...arguments);
}
/**
* Description
*/
assignTypeGeneric(type: Object, params?) {
var typeParams = null;
if (params) params = t.typeParameterInstantiation(params);
this.assignType(t.genericTypeAnnotation(t.identifier(type), typeParams));
}
/**
* Description
*/
assignType(type: Object) {
this.typeAnnotation = type;
}
/**
* Description
*/
reassign() {
this.reassigned = true;
if (this.typeAnnotationInferred) {
// destroy the inferred typeAnnotation
this.typeAnnotation = null;
}
}
/**
* Description
*/
getValueIfImmutable() {
// can't guarantee this value is the same
if (this.reassigned) return;
var node = this.path.node;
if (t.isVariableDeclarator(node)) {
if (t.isIdentifier(node.id)) {
node = node.init;
} else {
// otherwise it's probably a destructuring like:
// var { foo } = "foo";
return;
}
}
if (t.isImmutable(node)) {
return node;
}
}
/**
* Description
*/
isCompatibleWithType(newType): boolean {
return false;
}
}

View File

@ -42,7 +42,6 @@ traverse.node = function (node, opts, scope, state, parentPath) {
function clearNode(node) {
node._declarations = null;
node.extendedRange = null;
node._scopeInfo = null;
node._paths = null;
node.tokens = null;
node.range = null;

View File

@ -1,3 +1,7 @@
import isBoolean from "lodash/lang/isBoolean";
import isNumber from "lodash/lang/isNumber";
import isRegExp from "lodash/lang/isRegExp";
import isString from "lodash/lang/isString";
import traverse from "./index";
import includes from "lodash/collection/includes";
import Scope from "./scope";
@ -200,7 +204,104 @@ export default class TraversalPath {
}
get(key) {
return TraversalPath.get(this, this.context, this.node, this.node, key);
var node = this.node;
var container = node[key];
if (Array.isArray(container)) {
return container.map((_, i) => {
return TraversalPath.get(this, this.context, node, container, i);
});
} else {
return TraversalPath.get(this, this.context, node, node, key);
}
}
has(key) {
return !!this.node[key];
}
getTypeAnnotation(): Object {
if (this.typeInfo) {
return this.typeInfo;
}
var info = this.typeInfo = {
inferred: false,
annotation: null
};
var type = this.node.typeAnnotation;
if (!type) {
info.inferred = true;
type = this.inferType(this);
}
if (type) {
if (t.isTypeAnnotation(type)) type = type.typeAnnotation;
info.annotation = type;
}
return info;
}
resolve(): ?TraversalPath {
if (this.isVariableDeclarator()) {
return this.get("init").resolve();
} else if (this.isIdentifier()) {
var binding = this.scope.getBinding(this.node.name);
if (!binding) return;
if (binding.path === this) {
return this;
} else {
return binding.path.resolve();;
}
} else if (this.isMemberExpression()) {
var targetKey = t.toComputedKey(this.node);
if (!t.isLiteral(targetKey)) return;
var targetName = targetKey.value;
var target = this.get("object").resolve();
if (!target || !target.isObjectExpression()) return;
var props = target.get("properties");
for (var i = 0; i < props.length; i++) {
var prop = props[i];
if (!prop.isProperty()) continue;
var key = prop.get("key");
if (key.isIdentifier({ name: targetName }) || key.isLiteral({ value: targetName })) {
return prop.get("value");
}
}
} else {
return this;
}
}
inferType(path: TraversalPath) {
path = path.resolve();
if (!path) return;
if (path.isRestElement() || path.isArrayExpression()) {
return t.genericTypeAnnotation(t.identifier("Array"));
}
if (path.isObjectExpression()) {
return t.genericTypeAnnotation(t.identifier("Object"));
}
if (path.isLiteral()) {
var value = path.node.value;
if (isString(value)) return t.stringTypeAnnotation();
if (isNumber(value)) return t.numberTypeAnnotation();
if (isBoolean(value)) return t.booleanTypeAnnotation();
}
if (path.isCallExpression()) {
var callee = path.get("callee").resolve();
if (callee && callee.isFunction()) return callee.node.returnType;
}
}
isScope() {
@ -215,13 +316,40 @@ export default class TraversalPath {
return t.isReferenced(this.node, this.parent);
}
isBlockScoped() {
return t.isBlockScoped(this.node);
}
isVar() {
return t.isVar(this.node);
}
isScope() {
return t.isScope(this.node, this.parent);
}
isTypeGeneric(genericName: string, hasTypeParameters?): boolean {
var type = this.getTypeAnnotation().annotation;
if (!type) return false;
if (!t.isGenericTypeAnnotation(type) || !t.isIdentifier(type.id, { name: genericName })) {
return false;
}
if (hasTypeParameters && !type.typeParameters) {
return false;
}
return true;
}
getBindingIdentifiers() {
return t.getBindingIdentifiers(this.node);
}
traverse(opts, state) {
traverse(this.node, opts, this.scope, state);
}
}
for (var i = 0; i < t.TYPES.length; i++) {

View File

@ -2,6 +2,7 @@ import includes from "lodash/collection/includes";
import traverse from "./index";
import defaults from "lodash/object/defaults";
import * as messages from "../messages";
import Binding from "./binding";
import globals from "globals";
import flatten from "lodash/array/flatten";
import extend from "lodash/object/extend";
@ -12,27 +13,27 @@ import * as t from "../types";
var functionVariableVisitor = {
enter(node, parent, scope, state) {
if (t.isFor(node)) {
each(t.FOR_INIT_KEYS, function (key) {
var declar = node[key];
if (t.isVar(declar)) state.scope.registerBinding("var", declar);
each(t.FOR_INIT_KEYS, (key) => {
var declar = this.get(key);
if (declar.isVar()) state.scope.registerBinding("var", declar);
});
}
// this block is a function so we'll stop since none of the variables
// declared within are accessible
if (t.isFunction(node)) return this.skip();
if (this.isFunction()) return this.skip();
// function identifier doesn't belong to this scope
if (state.blockId && node === state.blockId) return;
// delegate block scope handling to the `blockVariableVisitor`
if (t.isBlockScoped(node)) return;
if (this.isBlockScoped()) return;
// this will be hit again once we traverse into it after this iteration
if (t.isExportDeclaration(node) && t.isDeclaration(node.declaration)) return;
if (this.isExportDeclaration() && t.isDeclaration(node.declaration)) return;
// we've ran into a declaration!
if (t.isDeclaration(node)) state.scope.registerDeclaration(node);
if (this.isDeclaration()) state.scope.registerDeclaration(this);
}
};
@ -42,16 +43,20 @@ var programReferenceVisitor = {
state.addGlobal(node);
} else if (t.isLabeledStatement(node)) {
state.addGlobal(node);
} else if (t.isAssignmentExpression(node) || t.isUpdateExpression(node) || (t.isUnaryExpression(node) && node.operator === "delete")) {
scope.registerBindingReassignment(node);
} else if (t.isAssignmentExpression(node)) {
scope.registerBindingReassignment(this.get("left"), this.get("right"));
} else if (t.isUpdateExpression(node)) {
// TODO
} else if (t.isUnaryExpression(node) && node.operator === "delete") {
scope.registerBindingReassignment(this.get("left"), null);
}
}
};
var blockVariableVisitor = {
enter(node, parent, scope, state) {
if (t.isFunctionDeclaration(node) || t.isBlockScoped(node)) {
state.registerDeclaration(node);
if (this.isFunctionDeclaration() || this.isBlockScoped()) {
state.registerDeclaration(this);
} else if (t.isScope(node, parent)) {
this.skip();
}
@ -112,9 +117,7 @@ export default class Scope {
*/
generateUidIdentifier(name: string) {
var id = t.identifier(this.generateUid(name));
this.getFunctionParent().registerBinding("uid", id);
return id;
return t.identifier(this.generateUid(name));
}
/**
@ -129,7 +132,8 @@ export default class Scope {
do {
uid = this._generateUid(name, i);
i++;
} while (this.hasBinding(uid) || this.hasGlobal(uid));
} while (this.hasBinding(uid) || this.hasGlobal(uid) || this.hasUid(uid));
this.uids[uid] = true;
return uid;
}
@ -139,6 +143,19 @@ export default class Scope {
return `_${id}`;
}
/**
* Description
*/
hasUid(name): boolean {
var scope = this;
do {
if (scope.uids[name]) return true;
scope = scope.parent;
} while (scope);
return false;
}
/*
* Description
*/
@ -217,7 +234,7 @@ export default class Scope {
rename(oldName: string, newName: string) {
newName ||= this.generateUidIdentifier(oldName).name;
var info = this.getBindingInfo(oldName);
var info = this.getBinding(oldName);
if (!info) return;
var binding = info.identifier;
@ -246,102 +263,6 @@ export default class Scope {
binding.name = newName;
}
/**
* Description
*/
inferType(node: Object) {
var target;
if (t.isVariableDeclarator(node)) {
target = node.init;
}
if (t.isArrayExpression(target)) {
return t.genericTypeAnnotation(t.identifier("Array"));
}
if (t.isObjectExpression(target)) {
return;
}
if (t.isLiteral(target)) {
return;
}
if (t.isCallExpression(target) && t.isIdentifier(target.callee)) {
var funcInfo = this.getBindingInfo(target.callee.name);
if (funcInfo) {
var funcNode = funcInfo.node;
return !funcInfo.reassigned && t.isFunction(funcNode) && node.returnType;
}
}
if (t.isIdentifier(target)) {
return;
}
}
/**
* Description
*/
isTypeGeneric(name: string, genericName: string) {
var info = this.getBindingInfo(name);
if (!info) return false;
var type = info.typeAnnotation;
return t.isGenericTypeAnnotation(type) && t.isIdentifier(type.id, { name: genericName });
}
/**
* Description
*/
assignTypeGeneric(name: string, type: Object) {
this.assignType(name, t.genericTypeAnnotation(t.identifier(type)));
}
/**
* Description
*/
assignType(name: string, type: Object) {
var info = this.getBindingInfo(name);
if (!info) return;
info.typeAnnotation = type;
}
/**
* Description
*/
getTypeAnnotation(id: Object, node: Object): Object {
var info = {
annotation: null,
inferred: false
};
var type;
if (id.typeAnnotation) {
type = id.typeAnnotation;
}
if (!type) {
info.inferred = true;
type = this.inferType(node);
}
if (type) {
if (t.isTypeAnnotation(type)) type = type.typeAnnotation;
info.annotation = type;
}
return info;
}
/**
* Description
*/
@ -349,8 +270,9 @@ export default class Scope {
toArray(node: Object, i?: number) {
var file = this.file;
if (t.isIdentifier(node) && this.isTypeGeneric(node.name, "Array")) {
return node;
if (t.isIdentifier(node)) {
var binding = this.getBinding(node.name);
if (binding && binding.isTypeGeneric("Array")) return node;
}
if (t.isArrayExpression(node)) {
@ -376,33 +298,21 @@ export default class Scope {
* Description
*/
refreshDeclaration(node: Object) {
if (t.isBlockScoped(node)) {
this.getBlockParent().registerDeclaration(node);
} else if (t.isVariableDeclaration(node, { kind: "var" })) {
this.getFunctionParent().registerDeclaration(node);
} else if (node === this.block) {
this.recrawl();
}
}
/**
* Description
*/
registerDeclaration(node: Object) {
registerDeclaration(path: TraversalPath) {
var node = path.node;
if (t.isFunctionDeclaration(node)) {
this.registerBinding("hoisted", node);
this.registerBinding("hoisted", path);
} else if (t.isVariableDeclaration(node)) {
for (var i = 0; i < node.declarations.length; i++) {
this.registerBinding(node.kind, node.declarations[i]);
var declarations = path.get("declarations");
for (var i = 0; i < declarations.length; i++) {
this.registerBinding(node.kind, declarations[i]);
}
} else if (t.isClassDeclaration(node)) {
this.registerBinding("let", node);
this.registerBinding("let", path);
} else if (t.isImportDeclaration(node) || t.isExportDeclaration(node)) {
this.registerBinding("module", node);
this.registerBinding("module", path);
} else {
this.registerBinding("unknown", node);
this.registerBinding("unknown", path);
}
}
@ -410,18 +320,16 @@ export default class Scope {
* Description
*/
registerBindingReassignment(node: Object) {
var ids = t.getBindingIdentifiers(node);
registerBindingReassignment(left: TraversalPath, right: TraversalPath) {
var ids = left.getBindingIdentifiers();
for (var name in ids) {
var info = this.getBindingInfo(name);
if (info) {
info.reassigned = true;
if (info.typeAnnotationInferred) {
// destroy the inferred typeAnnotation
info.typeAnnotation = null;
}
var binding = this.getBinding(name);
if (!binding) continue;
if (right) {
var rightType = right.typeAnnotation;
if (rightType && binding.isCompatibleWithType(rightType)) continue;
}
binding.reassign();
}
}
@ -429,27 +337,22 @@ export default class Scope {
* Description
*/
registerBinding(kind: string, node: Object) {
registerBinding(kind: string, path: TraversalPath) {
if (!kind) throw new ReferenceError("no `kind`");
var ids = t.getBindingIdentifiers(node);
var ids = path.getBindingIdentifiers();
for (var name in ids) {
var id = ids[name];
this.checkBlockScopedCollisions(kind, name, id);
var typeInfo = this.getTypeAnnotation(id, node);
this.bindings[name] = {
typeAnnotationInferred: typeInfo.inferred,
typeAnnotation: typeInfo.annotation,
reassigned: false,
identifier: id,
scope: this,
node: node,
kind: kind
};
this.bindings[name] = new Binding({
identifier: id,
scope: this,
path: path,
kind: kind
});
}
}
@ -489,91 +392,92 @@ export default class Scope {
*/
crawl() {
var block = this.block;
var path = this.path;
var i;
//
var info = this.path.getData("scopeInfo");
var info = path.getData("scopeInfo");
if (info) {
extend(this, info);
return;
}
info = this.path.setData("scopeInfo", {
info = path.setData("scopeInfo", {
bindings: object(),
globals: object()
globals: object(),
uids: object()
});
extend(this, info);
// ForStatement - left, init
if (t.isLoop(block)) {
if (path.isLoop()) {
for (i = 0; i < t.FOR_INIT_KEYS.length; i++) {
var node = block[t.FOR_INIT_KEYS[i]];
if (t.isBlockScoped(node)) this.registerBinding("let", node);
var node = path.get(t.FOR_INIT_KEYS[i]);
if (node.isBlockScoped()) this.registerBinding("let", node);
}
if (t.isBlockStatement(block.body)) {
block = block.body;
}
var body = path.get("body");
if (body.isBlockStatement()) path = path.get("body");
}
// FunctionExpression - id
if (t.isFunctionExpression(block) && block.id) {
if (!t.isProperty(this.parentBlock, { method: true })) {
this.registerBinding("var", block.id);
if (path.isFunctionExpression() && path.has("id")) {
if (!t.isProperty(path.parent, { method: true })) {
this.registerBinding("var", path.get("id"));
}
}
// Class
if (t.isClass(block) && block.id) {
this.registerBinding("var", block.id);
if (path.isClass() && path.has("id")) {
this.registerBinding("var", path.get("id"));
}
// Function - params, rest
if (t.isFunction(block)) {
for (i = 0; i < block.params.length; i++) {
this.registerBinding("param", block.params[i]);
if (path.isFunction()) {
var params = path.get("params");
for (i = 0; i < params.length; i++) {
this.registerBinding("param", params[i]);
}
this.traverse(block.body, blockVariableVisitor, this);
this.traverse(path.get("body"), blockVariableVisitor, this);
}
// Program, BlockStatement, Function - let variables
if (t.isBlockStatement(block) || t.isProgram(block)) {
this.traverse(block, blockVariableVisitor, this);
if (path.isBlockStatement() || path.isProgram()) {
this.traverse(path.node, blockVariableVisitor, this);
}
// CatchClause - param
if (t.isCatchClause(block)) {
this.registerBinding("let", block.param);
if (path.isCatchClause()) {
this.registerBinding("let", path.get("param"));
}
// ComprehensionExpression - blocks
if (t.isComprehensionExpression(block)) {
this.registerBinding("let", block);
if (path.isComprehensionExpression()) {
this.registerBinding("let", path);
}
// Program, Function - var variables
if (t.isProgram(block) || t.isFunction(block)) {
this.traverse(block, functionVariableVisitor, {
blockId: block.id,
if (path.isProgram() || path.isFunction()) {
this.traverse(path.node, functionVariableVisitor, {
blockId: path.get("id").node,
scope: this
});
}
// Program
if (t.isProgram(block)) {
this.traverse(block, programReferenceVisitor, this);
if (path.isProgram()) {
this.traverse(path.node, programReferenceVisitor, this);
}
}
@ -674,7 +578,7 @@ export default class Scope {
* Description
*/
getBindingInfo(name: string) {
getBinding(name: string) {
var scope = this;
do {
@ -696,7 +600,7 @@ export default class Scope {
*/
getBindingIdentifier(name: string) {
var info = this.getBindingInfo(name);
var info = this.getBinding(name);
return info && info.identifier;
}
@ -722,29 +626,11 @@ export default class Scope {
*/
getImmutableBindingValue(name: string) {
return this._immutableBindingInfoToValue(this.getBindingInfo(name));
return this._immutableBindingInfoToValue(this.getBinding(name));
}
_immutableBindingInfoToValue(info) {
if (!info) return;
// can't guarantee this value is the same
if (info.reassigned) return;
var node = info.node;
if (t.isVariableDeclarator(node)) {
if (t.isIdentifier(node.id)) {
node = node.init;
} else {
// otherwise it's probably a destructuring like:
// var { foo } = "foo";
return;
}
}
if (t.isImmutable(node)) {
return node;
}
_immutableBindingInfoToValue(binding) {
if (binding) return binding.getValueIfImmutable();
}
/**
@ -789,7 +675,7 @@ export default class Scope {
*/
removeBinding(name: string) {
var info = this.getBindingInfo(name);
var info = this.getBinding(name);
if (info) info.scope.removeOwnBinding(name);
}
}

View File

@ -10,7 +10,7 @@ import * as t from "./index";
* Description
*/
export function toComputedKey(node: Object, key: Object = node.key): Object {
export function toComputedKey(node: Object, key: Object = node.key || node.property): Object {
if (!node.computed) {
if (t.isIdentifier(key)) key = t.literal(key.name);
}

View File

@ -281,7 +281,6 @@ export function inheritsComments(child: Object, parent: Object): Object {
export function inherits(child: Object, parent: Object): Object {
child._declarations = parent._declarations;
child._scopeInfo = parent._scopeInfo;
child.range = parent.range;
child.start = parent.start;
child.loc = parent.loc;