-
-
Notifications
You must be signed in to change notification settings - Fork 219
Helper evaluate path #645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Helper evaluate path #645
Changes from all commits
03d1e63
8998845
0002669
110a1ec
b51b904
ad9be49
81e364a
c9a8059
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,37 @@ | ||
| "use strict"; | ||
|
|
||
| module.exports = function evaluate(path) { | ||
| if (path.isReferencedIdentifier()) { | ||
| return evaluateIdentifier(path); | ||
| } | ||
|
|
||
| const state = { | ||
| confident: true | ||
| }; | ||
|
|
||
| // prepare | ||
| path.traverse({ | ||
| Scope(scopePath) { | ||
| scopePath.skip(); | ||
| }, | ||
| ReferencedIdentifier(idPath) { | ||
| const binding = idPath.scope.getBinding(idPath.node.name); | ||
| // don't deopt globals | ||
| // let babel take care of it | ||
| if (!binding) return; | ||
|
|
||
| const evalResult = evaluateIdentifier(idPath); | ||
| if (!evalResult.confident) { | ||
| state.confident = evalResult.confident; | ||
| state.deoptPath = evalResult.deoptPath; | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| if (!state.confident) { | ||
| return state; | ||
| } | ||
|
|
||
| try { | ||
| return path.evaluate(); | ||
| } catch (e) { | ||
|
|
@@ -8,3 +41,166 @@ module.exports = function evaluate(path) { | |
| }; | ||
| } | ||
| }; | ||
|
|
||
| // Original Source: | ||
| // https://github.com/babel/babel/blob/master/packages/babel-traverse/src/path/evaluation.js | ||
| // modified for Babili use | ||
| function evaluateIdentifier(path) { | ||
| if (!path.isReferencedIdentifier()) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check can be removed.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will help in catching bugs where the refId path is replaced with something else. This has been an issue in DCE+mangle previously. |
||
| throw new Error(`Expected ReferencedIdentifier. Got ${path.type}`); | ||
| } | ||
|
|
||
| const { node } = path; | ||
|
|
||
| const binding = path.scope.getBinding(node.name); | ||
|
|
||
| if (!binding) { | ||
| return deopt(path); | ||
| } | ||
|
|
||
| if (binding.constantViolations.length > 0) { | ||
| return deopt(binding.path); | ||
| } | ||
|
|
||
| // referenced in a different scope - deopt | ||
| if (shouldDeoptBasedOnScope(binding, path)) { | ||
| return deopt(path); | ||
| } | ||
|
|
||
| // let/var/const referenced before init | ||
| // or "var" referenced in an outer scope | ||
| const flowEvalResult = evaluateBasedOnControlFlow(binding, path); | ||
|
|
||
| if (flowEvalResult.confident) { | ||
| return flowEvalResult; | ||
| } | ||
|
|
||
| if (flowEvalResult.shouldDeopt) { | ||
| return deopt(path); | ||
| } | ||
|
|
||
| return path.evaluate(); | ||
| } | ||
|
|
||
| // check if referenced in a different fn scope | ||
| // we can't determine if this function is called sync or async | ||
| // if the binding is in program scope | ||
| // all it's references inside a different function should be deopted | ||
| function shouldDeoptBasedOnScope(binding, refPath) { | ||
| if (binding.scope.path.isProgram() && refPath.scope !== binding.scope) { | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| function evaluateBasedOnControlFlow(binding, refPath) { | ||
| if (binding.kind === "var") { | ||
| // early-exit | ||
| const declaration = binding.path.parentPath; | ||
| if ( | ||
| declaration.parentPath.isIfStatement() || | ||
| declaration.parentPath.isLoop() || | ||
| declaration.parentPath.isSwitchCase() | ||
| ) { | ||
| return { shouldDeopt: true }; | ||
| } | ||
|
|
||
| let blockParent = binding.path.scope.getBlockParent().path; | ||
| const fnParent = binding.path.getFunctionParent(); | ||
|
|
||
| if (blockParent === fnParent) { | ||
| if (!fnParent.isProgram()) blockParent = blockParent.get("body"); | ||
| } | ||
|
|
||
| // detect Usage Outside Init Scope | ||
| if (!blockParent.get("body").some(stmt => stmt.isAncestor(refPath))) { | ||
| return { shouldDeopt: true }; | ||
| } | ||
|
|
||
| // Detect usage before init | ||
| const stmts = fnParent.isProgram() | ||
| ? fnParent.get("body") | ||
| : fnParent.get("body").get("body"); | ||
|
|
||
| const compareResult = compareBindingAndReference({ | ||
| binding, | ||
| refPath, | ||
| stmts | ||
| }); | ||
|
|
||
| if (compareResult.reference && compareResult.binding) { | ||
| if ( | ||
| compareResult.reference.scope === "current" && | ||
| compareResult.reference.idx < compareResult.binding.idx | ||
| ) { | ||
| return { confident: true, value: void 0 }; | ||
| } | ||
|
|
||
| return { shouldDeopt: true }; | ||
| } | ||
| } else if (binding.kind === "let" || binding.kind === "const") { | ||
| // binding.path is the declarator | ||
| const declarator = binding.path; | ||
| let scopePath = declarator.scope.path; | ||
| if (scopePath.isFunction()) { | ||
| scopePath = scopePath.get("body"); | ||
| } | ||
|
|
||
| // Detect Usage before Init | ||
| const stmts = scopePath.get("body"); | ||
|
|
||
| const compareResult = compareBindingAndReference({ | ||
| binding, | ||
| refPath, | ||
| stmts | ||
| }); | ||
|
|
||
| if (compareResult.reference && compareResult.binding) { | ||
| if ( | ||
| compareResult.reference.scope === "current" && | ||
| compareResult.reference.idx < compareResult.binding.idx | ||
| ) { | ||
| throw new Error( | ||
| `ReferenceError: Used ${refPath.node.name}: ` + | ||
| `${binding.kind} binding before declaration` | ||
| ); | ||
| } | ||
| if (compareResult.reference.scope === "other") { | ||
| return { shouldDeopt: true }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return { confident: false, shouldDeopt: false }; | ||
| } | ||
|
|
||
| function compareBindingAndReference({ binding, refPath, stmts }) { | ||
| const state = { | ||
| binding: null, | ||
| reference: null | ||
| }; | ||
|
|
||
| for (const [idx, stmt] of stmts.entries()) { | ||
| if (stmt.isAncestor(binding.path)) { | ||
| state.binding = { idx }; | ||
| } | ||
| for (const ref of binding.referencePaths) { | ||
| if (ref === refPath && stmt.isAncestor(ref)) { | ||
| state.reference = { | ||
| idx, | ||
| scope: binding.path.scope === ref.scope ? "current" : "other" | ||
| }; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return state; | ||
| } | ||
|
|
||
| function deopt(deoptPath) { | ||
| return { | ||
| confident: false, | ||
| deoptPath | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can remove this as well. evaluate identifier takes care of this already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is required as well. For babel's evaluate to take care of
globals. Here we ignore globals. In evaluatePath we depot it. If a referencedId makes it to evaluateId, we have to deopt instead of simply ignoring it. Though that code looks like it's never reached, it will be useful for detecting side-effects from other transformations.