Conversation
|
Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/51643/ |
| } | ||
|
|
||
| export function _removeFromScope(this: NodePath) { | ||
| const bindings = this.getBindingIdentifiers(); |
There was a problem hiding this comment.
I think ideally this issue should have been fixed in getBindingIdentifiers. An assignment expression does not introduce new bindings and therefore it should not contain any binding identifiers: In the spec the AssignmentPattern production is resolved to IdentifierReference:
https://tc39.es/ecma262/#sec-destructuring-assignment
However, practically the getBindingIdentifiers is used to retrieve identifier references / binding identifiers in a destructuring pattern so unless we come up with a getBindingAndReferenceIdentifiers, we may have to live with that.
The assignment expression is registered as a constant violation in the binding info, so to fix this issue, we can check whether this is an AssignmentExpression, if so,
instead of removing the binding, we should check the binding info and remove the registered constantViolations.
In other words, in your example posted in #14429.
var traverse = require("@babel/traverse").default;
var parser = require("@babel/parser");
var ast = parser.parse('var a=1;a=2;');
traverse(ast, {
AssignmentExpression(path) {
console.log(path.scope.getBinding("a"));
path.remove();
console.log(path.scope.getBinding("a"));
}
});The expected output after removing the assignment should be
Binding {identifier: Node, scope: Scope, path: NodePath, kind: 'var', constantViolations: Array(0), …}
And we should also add a new test case for ForXStatement:
var a = 1;
for (a of []) {} // <-- removing this should not remove the bindingThere was a problem hiding this comment.
Thanks for your great point! It should work now!
e9a0fd0 to
b1379bb
Compare
|
Does this also fix #14413? |
|
It seems not. The two tests you proposed at #14413 (comment) now pass, but #14413 problem still exists. #14413 (comment) This test also seems to be fixed edited: Oops, I forgot it was a separate repository https://github.com/facebook/regenerator We need to wait for it to have this pr to test..... |
|
New discoveries! Because I think this is also a bug, so in 5c70514 removed code reserved for compatibility. But now when I'm researching the issue mentioned by @nicolo-ribaudo ,I see what this code does. Please see #2101 And the reason for #14413 is also this code. Please share your opinions! |
|
Are there any updates? Can it be merged first? Although it still has bugs in its handling of uids, there is currently no better solution, and the previous bug was more serious. Before this, the uids policy was eager deletion, now it becomes passive deletion. It used to affect all plugins using We can also add the uids strategy to |
JLHwung
left a comment
There was a problem hiding this comment.
The incremental scope info update algorithm presented in this PR has O(NBP) complexity, where N is number of nodes in the this NodePath, B is the number of resolvable bindings in current scope and P is max(#referencePath, #constantViolations).
It can be slow when this is a large AST tree. We can improve the algorithm to O(DBP), where D is the maximum depth of NodePath under the closest Program NodePath.
- Construct a set NR from the ancestry NodePaths of this up to the closest Program node.
- Construct a set R including only this
- Construct a list SA from this.scope and its ancestry scopes.
- For each scope S in SA
4.1. For every binding B of this.scope
4.1.1 Remove binding if S === this.scope and B.identifier === this.node
4.1.2 for each cv of B.constantViolations
if pathIsThisDescendant(cv, NR, R) returns true, then remove cv
4.1.3 for each rp of B.referencePaths
if pathIsThisDescendant(rp, NR, R) returns true, then remove rp
The subroutine pathIsThisDescendant(path, NR, R):
- Construct an empty list p
- Loop
2.1 let parent = path.parentPath
2.2 if NR has parent then
add p to NR
return false
2.3 if R has parent then
add p to R
return true
2.4 add parent to p
2.5 assign parent to path
|
Amazing algorithm! This will improve performance a lot!
It looks like the bindings registered in the child nodes are being missed here, maybe we should have NodePath for each binding. registerBinding(
kind: Binding["kind"],
path: NodePath,
bindingPath: NodePath = path,
) { |
Good catch. In that case we can perform
In this case |
|
@liuxingbaoyu would you like me to take a crack at implementing @JLHwung's algorithm? I've read through this change and understand his comments. I really want to get scope updates during replacement fixed so that my |
|
I've actually implemented it, but seem to be running into weird performance issues when testing in my personal project. |
|
I'm confused by this:
Isn't this equivalent to
Seems like you meant to go each binding in SA and see if the binding path is a descendant of this path? Depending on the amount of bindings in ancestor scopes vs the amount of descendants of this path, it might be more efficient to traverse all identifier descendants of this path and see if a given identifier path is a binding path in SA. |
|
@jedwards1211 Thank you for your efforts on tackling the issue.
Yes, I think 4.1 should have been "For every binding B of S", so they add up to: For each scope S in SA 4.1. For every binding B of S 4.1.1 Remove binding B if S === this.scope and B.identifier === this.node 4.1.2 for each cv of B.constantViolations if pathIsThisDescendant(cv, NR, R) returns true, then remove cv from B.constantViolations 4.1.3 for each rp of B.referencePaths if pathIsThisDescendant(rp, NR, R) returns true, then remove rp from B.referencePaths If
Yes if AST node is small and the removed node is deeply nested. Note that the check "if a given identifier path is a binding path in SA" still requires going through ancestry scopes and a linear search within its bindings. Besides Presumably we don't know the size of ast node without traversing. It could be as small as an identifier, or as big as a React component. I guess either would be fine, after all it should be still faster than |
|
@JLHwung gotcha about the linear search through ancestry scopes in that check.
Edit: nevermind, now I understand the constant violations and reference paths could exist in any descendant scope, even if the binding is above a function scope Also, if the path is the root of its own scope, we can exclude that scope from the list that we're checking right? |
|
To be honest I can't remember.😰 |
|
And if I understand correctly, instead of (in which case it would be pointless to iterate bindings of ancestor scopes) It should be Right? (Is this the case where a |
|
I guess yes. |
|
Definitely need to test the fact that const { parse } = require('@babel/parser')
const traverse = require('@babel/traverse').default
const ast = parse(`
if (true) {
var a
let b
}
`)
traverse(ast, {
BlockStatement(path) {
const aBinding = path.scope.getBinding('a')
const bBinding = path.scope.getBinding('b')
console.log(aBinding.scope === bBinding.scope) // false
},
}) |
Unfortunately not, as I'm finding while debugging failed tests... const [a, [{b, ...c}], {d, ...e}, [{ f, ...g}, {h: [i, {j, ...k}] }]] = x;For all of the bindings here, For example, when I think @liuxingbaoyu is right that having a I don't have any experience with the const violations and reference paths yet. I assume a const violation path always points to an |
|
I guess I could traverse the removed node to build a set of all identifier nodes in it, and use that to determine if each binding is a descendant of the removed node... I wouldn't have to traverse inside nested functions since nothing inside them could bind at a higher scope. |
|
Okay here's my plan:
If const violation paths don't necessarily point to identifiers, I'll have to also build a set of identifier path inside the current node and use the Thanks to the set DN, step 2 is just O(number of ancestor scopes * max(number of bindings/const violations/reference paths per scope)). Maybe for Babel 8 we could make a breaking change to require registering bindings with a path to the identifier instead of the node? |
Presumably so, since current citations are not always reliable.
But maybe there are identifiers that refer to outside bindings?
Yes, that's probably the only good way. But we need to make sure the path is always reliable first, and I'm not sure about that at the moment.😭 |
|
Oh that'strue about references. Maybe |
|
This is starting to seem like an intractable mess :( There are problems like this: const f = ({ a = get(), b, c, ...z }) => {
const v = b + 3;
};First Now there are no bindings for I think having And even then I worry that the way scope is maintained is too inconsistent. For example: if (container) {
container.push(declar); // this doesn't crawl and update scope bindings
} else {
parentPath.ensureBlock();
parentPath.get("body").unshiftContainer("body", declar); // but this does
} |
|
Wait though...if it's true what I'm saying about the plugin inserting the new declaration before removing the function param, it should cause duplicate binding errors...need to dig harder into what's really going on... |
|
I noticed that you are writing
Are they the same node object? Maybe you can modify
You're right, it would be more difficult. |
|
Okay no this plugin code is definitely flawed: if (paramPath.isObjectPattern() && hasRestElement(paramPath)) {
const uid = parentPath.scope.generateUidIdentifier("ref");
const declar = t.variableDeclaration("let", [
t.variableDeclarator(paramPath.node, uid),
]);
if (container) {
container.push(declar);
} else {
parentPath.ensureBlock();
parentPath.get("body").unshiftContainer("body", declar);
}
paramPath.replaceWith(t.cloneNode(uid));
}Between the const f = ({ a = get(), b, c, ...z } /* about to get replaced with _ref */) => {
const { a = get(), b, c, ...z } = _ref
const v = b + 3;
};I don't know why duplicate declaration errors aren't getting triggered. But if there's any hope of managing scope properly, one of the cardinal rules has to be to never temporarily insert duplicate bindings into the AST...instead plugins need to remove or replace the old bindings first... |
The binding seems to need to be registered manually, if the plugin doesn't register it manually, it won't be detected. |
|
@liuxingbaoyu well |
It seems so, kind of weird. |
Yeah that's what I'm leaning toward now. |
|
@JLHwung @liuxingbaoyu one good way to catch a lot of existing bugs would be to have all the tests compare the final bindings of the scopes to ones built from scratch by recrawling the final AST |
Yes, babel is so free that there are countless practical use cases.
Yes, that is correct, currently we have a simple test to make sure the binding is not lost. There are probably many 3rd party plugins that don't ensure this though, so how to start making changes can be a headache.😓 |
|
I meant more in terms of dangling bindings that should have gotten removed. But I've realized, most of babel's transpiling doesn't need to remove any bindings on scope. It mostly just needs to introduce intermediary variables. So I guess that's why there hasn't been a great need for a more systematic way of managing removal. I just realized another thing none of us has considered - if a binding that shadows another gets removed, we'd also need to transfer its const violations and reference paths to the un-shadowed binding |
Yes, actually converting JS to a compatible version doesn't require a particularly robust analysis system. I don't know if you have come across
Amazing idea, I really hadn't thought of this. Unfortunately this may also have compatibility implications. |
Uh oh!
There was an error while loading. Please reload this page.