add a path hoisting mechanism that will hoist a node to it's highest compatible scope, a compatible scope is considered to be one where all references inside can be resolved to, also adds an optimisation.react.constantElements transformer that uses this to much success facebook/react#3226

This commit is contained in:
Sebastian McKenzie
2015-03-30 00:16:19 +11:00
parent 6a698f7ae4
commit 52c6fe2bc1
18 changed files with 263 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ export default {
"spec.blockScopedFunctions": require("./spec/block-scoped-functions"),
"optimisation.react.constantElements": require("./optimisation/react.constant-elements"),
reactCompat: require("./other/react-compat"),
react: require("./other/react"),

View File

@@ -0,0 +1,42 @@
import * as react from "../../helpers/react";
export var metadata = {
optional: true
};
var immutabilityVisitor = {
enter(node, parent, scope, state) {
var stop = () => {
state.isImmutable = false;
this.stop();
};
if (this.isJSXClosingElement()) {
this.skip();
return;
}
if (this.isJSXIdentifier({ name: "ref" }) && this.parentPath.isJSXAttribute({ name: node })) {
return stop();
}
if (this.isJSXIdentifier() || this.isIdentifier() || this.isJSXMemberExpression()) {
return;
}
if (!this.isImmutable()) stop();
}
};
export function JSXElement(node, parent, scope, file) {
if (node._ignoreConstant) return;
var state = { isImmutable: true };
this.traverse(immutabilityVisitor, state);
this.skip();
if (state.isImmutable) {
this.hoist();
node._ignoreConstant = true;
}
}

View File

@@ -0,0 +1,106 @@
import * as react from "../../transformation/helpers/react";
import * as t from "../../types";
var referenceVisitor = {
enter(node, parent, scope, state) {
if (this.isJSXIdentifier() && react.isCompatTag(node.name)) {
return;
}
if (this.isJSXIdentifier() || this.isIdentifier()) {
// direct references that we need to track to hoist this to the highest scope we can
if (this.isReferenced()) {
var bindingInfo = scope.getBinding(node.name);
if (bindingInfo && bindingInfo.constant) {
state.bindings[node.name] = bindingInfo;
} else {
scope.dump();
state.foundIncompatible = true;
this.stop();
}
}
}
}
};
export default class PathHoister {
constructor(path) {
this.foundIncompatible = false;
this.bindings = {};
this.scopes = [];
this.path = path;
}
isCompatibleScope(scope) {
for (var key in this.bindings) {
var binding = this.bindings[key];
if (!scope.bindingIdentifierEquals(key, binding.identifier)) {
return false;
}
}
return true;
}
getCompatibleScopes() {
var checkScope = this.path.scope;
do {
if (this.isCompatibleScope(checkScope)) {
this.scopes.push(checkScope);
} else {
break;
}
} while(checkScope = checkScope.parent);
}
getAttachmentPath() {
var scopes = this.scopes;
var scope = scopes.pop();
if (scope.path.isFunction()) {
if (this.hasNonParamBindings()) {
// can't be attached to this scope
return this.getNextScopeStatementParent();
} else {
// needs to be attached to the body
return scope.path.get("body").get("body")[0];
}
} else if (scope.path.isProgram()) {
return this.getNextScopeStatementParent();
}
}
getNextScopeStatementParent() {
var scope = this.scopes.pop();
if (scope) return scope.path.getStatementParent();
}
hasNonParamBindings() {
for (var name in this.bindings) {
var binding = this.bindings[name];
if (binding.kind !== "param") return true;
}
return false;
}
run() {
this.path.traverse(referenceVisitor, this);
if (this.foundIncompatible) return;
this.getCompatibleScopes();
var path = this.getAttachmentPath();
if (!path) return;
var uid = path.scope.generateUidIdentifier("ref");
path.insertBefore([
t.variableDeclaration("var", [
t.variableDeclarator(uid, this.path.node)
])
]);
this.path.replaceWith(uid);
}
}

View File

@@ -635,6 +635,14 @@ export default class TraversalPath {
traverse(opts, state) {
traverse(this.node, opts, this.scope, state, this);
/**
* Description
*/
hoist() {
var hoister = new PathHoister(this);
return hoister.run();
}
/**

View File

@@ -156,11 +156,11 @@ export function isSpecifierDefault(specifier: Object): boolean {
export function isScope(node: Object, parent: Object): boolean {
if (t.isBlockStatement(node)) {
if (t.isLoop(parent.block, { body: node })) {
if (t.isLoop(parent, { body: node })) {
return false;
}
if (t.isFunction(parent.block, { body: node })) {
if (t.isFunction(parent, { body: node })) {
return false;
}
}