Repository: babel/babel (TypeScript)

Issue:
[Bug]: variable declarator remove bug
### 💻

- [x] Would you like to work on a fix?

### How are you using Babel?

Programmatic API (`babel.transform`, `babel.parse`)

### Input code

```js
// 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`](https://github.com/babel/babel/blob/main/packages/babel-traverse/src/path/removal.ts#L9) function, more specifically, when calling the [`this._callRemovalHooks()`](https://github.com/babel/babel/blob/main/packages/babel-traverse/src/path/removal.ts#L32) function. By the point the `_callRemovalHooks` function is called, the Binding of `a` is already removed, keep this in mind.

The first hook:

```js
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()`](https://github.com/babel/babel/blob/main/packages/babel-traverse/src/path/removal.ts#L14), that is because the call to [`getBindingIdentifiers`](https://github.com/babel/babel/blob/main/packages/babel-traverse/src/path/removal.ts#L28) returns the bindings that we already removed.

### Additional context

_No response_


Hints:
Hey @nerodesu017! We really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we're a limited number of volunteers, so it's possible this won't be addressed swiftly.

If you need any help, or just have general Babel or JavaScript questions, we have a vibrant [Slack community](https://babeljs.slack.com) that typically always has someone willing to help. You can sign-up [here](https://slack.babeljs.io/) for an invite.

Patch:
diff --git a/packages/babel-traverse/src/path/removal.ts b/packages/babel-traverse/src/path/removal.ts
--- a/packages/babel-traverse/src/path/removal.ts
+++ b/packages/babel-traverse/src/path/removal.ts
@@ -11,15 +11,16 @@ export function remove(this: NodePath) {
   _assertUnremoved.call(this);
 
   this.resync();
-  if (!this.opts?.noScope) {
-    _removeFromScope.call(this);
-  }
 
   if (_callRemovalHooks.call(this)) {
     _markRemoved.call(this);
     return;
   }
 
+  if (!this.opts?.noScope) {
+    _removeFromScope.call(this);
+  }
+
   this.shareCommentsWithSiblings();
   _remove.call(this);
   _markRemoved.call(this);


Test patch:
diff --git a/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/input.js b/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/input.js
new file mode 100644
--- /dev/null
+++ b/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/input.js
@@ -0,0 +1,4 @@
+{
+  let a = 33;
+}
+let a = 42;
\ No newline at end of file
diff --git a/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/options.json b/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/options.json
new file mode 100644
--- /dev/null
+++ b/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/options.json
@@ -0,0 +1,3 @@
+{
+  "plugins": ["./plugin"]
+}
diff --git a/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/output.js b/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/output.js
new file mode 100644
--- /dev/null
+++ b/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/output.js
@@ -0,0 +1,4 @@
+{}
+let a = 42;
+/*// a is defined when visiting a=33? true*/
+/*// a is defined when visiting a=42? true*/
diff --git a/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/plugin.js b/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/plugin.js
new file mode 100644
--- /dev/null
+++ b/packages/babel-traverse/test/fixtures/scope/remove-nested-let-path/plugin.js
@@ -0,0 +1,17 @@
+module.exports = function ({ types }) {
+  return {
+    visitor: {
+      VariableDeclarator(path) {
+        const { value } = path.node.init;
+
+        if (value === 33) path.remove();
+
+        types.addComment(
+          path.scope.getProgramParent().path.node,
+          "trailing",
+          `// a is defined when visiting a=${value}? ${path.scope.hasBinding("a")}`
+        );
+      },
+    },
+  };
+};
diff --git a/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/input.js b/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/input.js
new file mode 100644
--- /dev/null
+++ b/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/input.js
@@ -0,0 +1,3 @@
+{
+  var a = 1;
+}
diff --git a/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/options.json b/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/options.json
new file mode 100644
--- /dev/null
+++ b/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/options.json
@@ -0,0 +1,3 @@
+{
+  "plugins": ["./plugin"]
+}
diff --git a/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/output.js b/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/output.js
new file mode 100644
--- /dev/null
+++ b/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/output.js
@@ -0,0 +1 @@
+{}
diff --git a/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/plugin.js b/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/plugin.js
new file mode 100644
--- /dev/null
+++ b/packages/babel-traverse/test/fixtures/scope/remove-var-in-block/plugin.js
@@ -0,0 +1,10 @@
+module.exports = function () {
+  return {
+    visitor: {
+      VariableDeclarator(path) {
+        path.remove();
+        expect(path.scope.getBinding("a")).toBeUndefined();
+      },
+    },
+  };
+};