Justin Ridgewell 28ae47a174 Stop mutating nodes (#5963)
* Stop mutating nodes

* Update tests

* linting
2017-07-18 13:24:07 -04:00

77 lines
2.2 KiB
JavaScript

export default function({ types: t }) {
const HOISTED = new WeakSet();
const immutabilityVisitor = {
enter(path, state) {
const stop = () => {
state.isImmutable = false;
path.stop();
};
if (path.isJSXClosingElement()) {
path.skip();
return;
}
// Elements with refs are not safe to hoist.
if (
path.isJSXIdentifier({ name: "ref" }) &&
path.parentPath.isJSXAttribute({ name: path.node })
) {
return stop();
}
// Ignore identifiers & JSX expressions.
if (
path.isJSXIdentifier() ||
path.isIdentifier() ||
path.isJSXMemberExpression()
) {
return;
}
if (!path.isImmutable()) {
// If it's not immutable, it may still be a pure expression, such as string concatenation.
// It is still safe to hoist that, so long as its result is immutable.
// If not, it is not safe to replace as mutable values (like objects) could be mutated after render.
// https://github.com/facebook/react/issues/3226
if (path.isPure()) {
const expressionResult = path.evaluate();
if (expressionResult.confident) {
// We know the result; check its mutability.
const { value } = expressionResult;
const isMutable =
(value && typeof value === "object") ||
typeof value === "function";
if (!isMutable) {
// It evaluated to an immutable value, so we can hoist it.
return;
}
} else if (t.isIdentifier(expressionResult.deopt)) {
// It's safe to hoist here if the deopt reason is an identifier (e.g. func param).
// The hoister will take care of how high up it can be hoisted.
return;
}
}
stop();
}
},
};
return {
visitor: {
JSXElement(path) {
if (HOISTED.has(path.node)) return;
HOISTED.add(path.node);
const state = { isImmutable: true };
path.traverse(immutabilityVisitor, state);
if (state.isImmutable) {
path.hoist();
}
},
},
};
}