-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Description
💻
- Would you like to work on a fix?
How are you using Babel?
Programmatic API (babel.transform, babel.parse)
Input code
// WORKING_DIR_ROOT/test/custom_tests/remove.js
const traverse = require("../../packages/babel-traverse/lib").default;
const t = require("../../packages/babel-types/lib");
const generate = require("../../packages/babel-generator/lib");
const { parse } = require("../../packages/babel-parser");
const script = `
{
let a = 33;
}
let a = 42;
`
const AST = parse(script);
let found = false;
traverse(AST, {
/**
*
* @param {NodePath<t.VariableDeclarator>} path
* @returns
*/
VariableDeclarator(path){
if(!found && path.toString() === "a = 33"){
found = true;
path.remove();
return;
} else if(path.toString() === "a = 42") {
const binding = path.scope.getBinding("a");
// binding is undefined
console.log(binding);
// can't access path of undefined
console.log(binding.path.toString());
}
}
});Configuration file name
No response
Configuration
No response
Current and expected behavior
The test script is comprised of a BlockStatement containing one VariableDeclaration with a VariableDeclarator named a having any type of value. Outside of the BlockStatement we have another VariableDeclaration, just as the last one, with a VariableDeclarator having the same name.
What babel is doing is that when we remove the VariableDeclarator (NOT VariableDeclaration) from inside the BlockStatement makes it so that both Bindings of a (the one from the scope of the BlockStatement, and also the one from the parent scope) are deleted.
The expected behavior would be that only the Binding of a in the BlockStatement's scope to be deleted, but that doesn't happen
Environment
System:
OS: Windows 10 10.0.19045
Binaries:
Node: 20.14.0 - C:\Program Files\nodejs\node.EXE
Yarn: 4.2.2 - C:\Program Files\nodejs\yarn.CMD
npm: 10.7.0 - C:\Program Files\nodejs\npm.CMD
Possible solution
The problem arises in the packages/babel-traverse/src/path/removal.ts > remove function, more specifically, when calling the this._callRemovalHooks() function. By the point the _callRemovalHooks function is called, the Binding of a is already removed, keep this in mind.
The first hook:
function (self: NodePath, parent: NodePath) {
const removeParent =
// while (NODE);
// removing the test of a while/switch, we can either just remove it entirely *or* turn the
// `test` into `true` unlikely that the latter will ever be what's wanted so we just remove
// the loop to avoid infinite recursion
(self.key === "test" && (parent.isWhile() || parent.isSwitchCase())) ||
// export NODE;
// just remove a declaration for an export as this is no longer valid
(self.key === "declaration" && parent.isExportDeclaration()) ||
// label: NODE
// stray labeled statement with no body
(self.key === "body" && parent.isLabeledStatement()) ||
// let NODE;
// remove an entire declaration if there are no declarators left
(self.listKey === "declarations" &&
parent.isVariableDeclaration() &&
parent.node.declarations.length === 1) ||
// NODE;
// remove the entire expression statement if there's no expression
(self.key === "expression" && parent.isExpressionStatement());
if (removeParent) {
parent.remove();
return true;
}
}is the one we are going down on, which in turn calls parent.remove(), which is the same remove function that was previously mentioned.
Now, the problem lies when we call this._removeFromScope(), that is because the call to getBindingIdentifiers returns the bindings that we already removed.
Additional context
No response