229 lines
7.3 KiB
JavaScript
229 lines
7.3 KiB
JavaScript
import { react } from "babel-types";
|
|
import * as t from "babel-types";
|
|
|
|
const referenceVisitor = {
|
|
// This visitor looks for bindings to establish a topmost scope for hoisting.
|
|
ReferencedIdentifier(path, state) {
|
|
// Don't hoist regular JSX identifiers ('div', 'span', etc).
|
|
// We do have to consider member expressions for hoisting (e.g. `this.component`)
|
|
if (
|
|
path.isJSXIdentifier() &&
|
|
react.isCompatTag(path.node.name) &&
|
|
!path.parentPath.isJSXMemberExpression()
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// If the identifier refers to `this`, we need to break on the closest non-arrow scope.
|
|
if (path.node.name === "this") {
|
|
let scope = path.scope;
|
|
do {
|
|
if (scope.path.isFunction() && !scope.path.isArrowFunctionExpression()) break;
|
|
} while (scope = scope.parent);
|
|
if (scope) state.breakOnScopePaths.push(scope.path);
|
|
}
|
|
|
|
// direct references that we need to track to hoist this to the highest scope we can
|
|
const binding = path.scope.getBinding(path.node.name);
|
|
if (!binding) return;
|
|
|
|
// this binding isn't accessible from the parent scope so we can safely ignore it
|
|
// eg. it's in a closure etc
|
|
if (binding !== state.scope.getBinding(path.node.name)) return;
|
|
|
|
state.bindings[path.node.name] = binding;
|
|
},
|
|
};
|
|
|
|
export default class PathHoister {
|
|
constructor(path, scope) {
|
|
// Storage for scopes we can't hoist above.
|
|
this.breakOnScopePaths = [];
|
|
// Storage for bindings that may affect what path we can hoist to.
|
|
this.bindings = {};
|
|
// Storage for eligible scopes.
|
|
this.scopes = [];
|
|
// Our original scope and path.
|
|
this.scope = scope;
|
|
this.path = path;
|
|
// By default, we attach as far up as we can; but if we're trying
|
|
// to avoid referencing a binding, we may have to go after.
|
|
this.attachAfter = false;
|
|
}
|
|
|
|
// A scope is compatible if all required bindings are reachable.
|
|
isCompatibleScope(scope) {
|
|
for (const key in this.bindings) {
|
|
const binding = this.bindings[key];
|
|
if (!scope.bindingIdentifierEquals(key, binding.identifier)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Look through all scopes and push compatible ones.
|
|
getCompatibleScopes() {
|
|
let scope = this.path.scope;
|
|
do {
|
|
if (this.isCompatibleScope(scope)) {
|
|
this.scopes.push(scope);
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
// deopt: These scopes are set in the visitor on const violations
|
|
if (this.breakOnScopePaths.indexOf(scope.path) >= 0) {
|
|
break;
|
|
}
|
|
} while (scope = scope.parent);
|
|
}
|
|
|
|
getAttachmentPath() {
|
|
let path = this._getAttachmentPath();
|
|
if (!path) return;
|
|
|
|
let targetScope = path.scope;
|
|
|
|
// don't allow paths that have their own lexical environments to pollute
|
|
if (targetScope.path === path) {
|
|
targetScope = path.scope.parent;
|
|
}
|
|
|
|
// avoid hoisting to a scope that contains bindings that are executed after our attachment path
|
|
if (targetScope.path.isProgram() || targetScope.path.isFunction()) {
|
|
for (const name in this.bindings) {
|
|
// check binding is a direct child of this paths scope
|
|
if (!targetScope.hasOwnBinding(name)) continue;
|
|
|
|
const binding = this.bindings[name];
|
|
|
|
// allow parameter references and expressions in params (like destructuring rest)
|
|
if (binding.kind === "param" || binding.path.parentKey === "params") continue;
|
|
|
|
// For each binding, get its attachment parent. This gives us an idea of where we might
|
|
// introduce conflicts.
|
|
const bindingParentPath = this.getAttachmentParentForPath(binding.path);
|
|
|
|
// If the binding's attachment appears at or after our attachment point, then we move after it.
|
|
if (bindingParentPath.key >= path.key) {
|
|
this.attachAfter = true;
|
|
path = binding.path;
|
|
|
|
// We also move past any constant violations.
|
|
for (const violationPath of (binding.constantViolations: Array)) {
|
|
if (this.getAttachmentParentForPath(violationPath).key > path.key) {
|
|
path = violationPath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// We can't insert before/after a child of an export declaration, so move up
|
|
// to the declaration itself.
|
|
if (path.parentPath.isExportDeclaration()) {
|
|
path = path.parentPath;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
_getAttachmentPath() {
|
|
const scopes = this.scopes;
|
|
|
|
const scope = scopes.pop();
|
|
// deopt: no compatible scopes
|
|
if (!scope) return;
|
|
|
|
if (scope.path.isFunction()) {
|
|
if (this.hasOwnParamBindings(scope)) {
|
|
// deopt: should ignore this scope since it's ourselves
|
|
if (this.scope === scope) return;
|
|
|
|
// needs to be attached to the body
|
|
const bodies = scope.path.get("body").get("body");
|
|
for (let i = 0; i < bodies.length; i++) {
|
|
// Don't attach to something that's going to get hoisted,
|
|
// like a default parameter
|
|
if (bodies[i].node._blockHoist) continue;
|
|
return bodies[i];
|
|
}
|
|
// deopt: If here, no attachment path found
|
|
} else {
|
|
// doesn't need to be be attached to this scope
|
|
return this.getNextScopeAttachmentParent();
|
|
}
|
|
} else if (scope.path.isProgram()) {
|
|
return this.getNextScopeAttachmentParent();
|
|
}
|
|
}
|
|
|
|
getNextScopeAttachmentParent() {
|
|
const scope = this.scopes.pop();
|
|
if (scope) return this.getAttachmentParentForPath(scope.path);
|
|
}
|
|
|
|
// Find an attachment for this path.
|
|
getAttachmentParentForPath(path) {
|
|
do {
|
|
if (
|
|
// Beginning of the scope
|
|
!path.parentPath ||
|
|
// Has siblings and is a statement
|
|
(Array.isArray(path.container) && path.isStatement())
|
|
) {
|
|
return path;
|
|
}
|
|
} while ((path = path.parentPath));
|
|
}
|
|
|
|
// Returns true if a scope has param bindings.
|
|
hasOwnParamBindings(scope) {
|
|
for (const name in this.bindings) {
|
|
if (!scope.hasOwnBinding(name)) continue;
|
|
|
|
const binding = this.bindings[name];
|
|
// Ensure constant; without it we could place behind a reassignment
|
|
if (binding.kind === "param" && binding.constant) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
run() {
|
|
const node = this.path.node;
|
|
if (node._hoisted) return;
|
|
node._hoisted = true;
|
|
|
|
this.path.traverse(referenceVisitor, this);
|
|
|
|
this.getCompatibleScopes();
|
|
|
|
const attachTo = this.getAttachmentPath();
|
|
if (!attachTo) return;
|
|
|
|
// don't bother hoisting to the same function as this will cause multiple branches to be
|
|
// evaluated more than once leading to a bad optimisation
|
|
if (attachTo.getFunctionParent() === this.path.getFunctionParent()) return;
|
|
|
|
// generate declaration and insert it to our point
|
|
let uid = attachTo.scope.generateUidIdentifier("ref");
|
|
const declarator = t.variableDeclarator(uid, this.path.node);
|
|
|
|
const insertFn = this.attachAfter ? "insertAfter" : "insertBefore";
|
|
attachTo[insertFn]([
|
|
attachTo.isVariableDeclarator() ? declarator : t.variableDeclaration("var", [declarator]),
|
|
]);
|
|
|
|
const parent = this.path.parentPath;
|
|
if (parent.isJSXElement() && this.path.container === parent.node.children) {
|
|
// turning the `span` in `<div><span /></div>` to an expression so we need to wrap it with
|
|
// an expression container
|
|
uid = t.JSXExpressionContainer(uid);
|
|
}
|
|
|
|
this.path.replaceWith(uid);
|
|
}
|
|
}
|