From 3498195ae25475a376edf7149bff0e8b69eee002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Tue, 8 Oct 2019 18:50:03 +0200 Subject: [PATCH] Do not hoist jsx referencing a mutable binding (#10529) --- .../deopt-mutable-complex/input.mjs | 10 ++++++++++ .../deopt-mutable-complex/output.mjs | 10 ++++++++++ .../constant-elements/deopt-mutable/input.mjs | 6 ++++++ .../constant-elements/deopt-mutable/output.mjs | 5 +++++ packages/babel-traverse/src/path/lib/hoister.js | 14 ++++++++++++++ 5 files changed, 45 insertions(+) create mode 100644 packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable-complex/input.mjs create mode 100644 packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable-complex/output.mjs create mode 100644 packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable/input.mjs create mode 100644 packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable/output.mjs diff --git a/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable-complex/input.mjs b/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable-complex/input.mjs new file mode 100644 index 0000000000..d55eaf7cd0 --- /dev/null +++ b/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable-complex/input.mjs @@ -0,0 +1,10 @@ +let foo = 'hello'; + +const mutate = () => { + foo = 'goodbye'; +} + +export const Component = () => { + if (Math.random() > 0.5) mutate(); + return {foo}; +}; diff --git a/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable-complex/output.mjs b/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable-complex/output.mjs new file mode 100644 index 0000000000..83c8f67722 --- /dev/null +++ b/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable-complex/output.mjs @@ -0,0 +1,10 @@ +let foo = 'hello'; + +const mutate = () => { + foo = 'goodbye'; +}; + +export const Component = () => { + if (Math.random() > 0.5) mutate(); + return {foo}; +}; diff --git a/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable/input.mjs b/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable/input.mjs new file mode 100644 index 0000000000..1225d11e87 --- /dev/null +++ b/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable/input.mjs @@ -0,0 +1,6 @@ +let foo = 'hello'; + +export const Component = () => { + foo = 'goodbye'; + return {foo}; +}; diff --git a/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable/output.mjs b/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable/output.mjs new file mode 100644 index 0000000000..dd266a198d --- /dev/null +++ b/packages/babel-plugin-transform-react-constant-elements/test/fixtures/constant-elements/deopt-mutable/output.mjs @@ -0,0 +1,5 @@ +let foo = 'hello'; +export const Component = () => { + foo = 'goodbye'; + return {foo}; +}; diff --git a/packages/babel-traverse/src/path/lib/hoister.js b/packages/babel-traverse/src/path/lib/hoister.js index 2fd8339eaa..7e7f850346 100644 --- a/packages/babel-traverse/src/path/lib/hoister.js +++ b/packages/babel-traverse/src/path/lib/hoister.js @@ -32,6 +32,15 @@ const referenceVisitor = { const binding = path.scope.getBinding(path.node.name); if (!binding) return; + // we can handle reassignments only if they happen in the same scope as the declaration + for (const violation of binding.constantViolations) { + if (violation.scope !== binding.path.scope) { + state.mutableBinding = true; + path.stop(); + 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; @@ -46,6 +55,9 @@ export default class PathHoister { this.breakOnScopePaths = []; // Storage for bindings that may affect what path we can hoist to. this.bindings = {}; + // "true" if the current path contains a reference to a binding whose + // value can change and thus can't be safely hoisted. + this.mutableBinding = false; // Storage for eligible scopes. this.scopes = []; // Our original scope and path. @@ -195,6 +207,8 @@ export default class PathHoister { run() { this.path.traverse(referenceVisitor, this); + if (this.mutableBinding) return; + this.getCompatibleScopes(); const attachTo = this.getAttachmentPath();