Skip to content
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

[systemjs] Fix nested let/const shadowing imported bindings #14057

Merged

Conversation

The-x-Theorist
Copy link
Contributor

@The-x-Theorist The-x-Theorist commented Dec 15, 2021

Q                       A
Fixed Issues? Fixes #13888
Patch: Bug Fix?
Major: Breaking Change?
Minor: New Feature?
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes?
License MIT

Variables will be renamed in the hoisting operation, and variables declared inside blockStatment will be different, hence they won't collide with any other identifier.

@babel-bot
Copy link
Collaborator

babel-bot commented Dec 15, 2021

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/51497/

@nicolo-ribaudo nicolo-ribaudo added area: modules PR: Bug Fix 🐛 A type of pull request used for our changelog categories labels Dec 21, 2021
Comment on lines 44 to 45
declar.parentPath.parentPath.isBlockStatement() &&
declar.parent.kind !== "var"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we can hoist this check outside of the for loop:

const needsRename = path.node.kind !== "var" && path.parentPath.isBlockStatement()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think we can just edit the Scope visitor above to check kind === "let" || kind === "const".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I will change that.

@@ -7,4 +7,4 @@ expect(
bar;
}
).toBe("foo");
expect(bar).toBe("foo");
// expect(bar).toBe("foo");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior should not change.

for (const declar of declarations) {
firstId = declar.node.id;
if (needsRename) {
declar.scope.rename(declar.node.id.name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TS warning here is correct; I think this breaks, for example, with const { x } = {}. You probably need to do

if (needsRename) {
  for (const name of Object.keys(declar.getBindingIdentifiers()) {
    declar.scope.rename(name);
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I will do that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add a test for this!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I will add the test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it's working now.

nicolo-ribaudo
nicolo-ribaudo previously approved these changes Jan 5, 2022
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

execute: function () {
if (true) {
({
x: _x
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why const { x } = { x: 1 } is also transformed? I would expect _x is registered in the same scope of the local x declaration:

if (true) {
  const { x: _x } = { x: 1 };
  console.log(_x);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hoisted vars are renamed, So I think that's the reason for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should we change in this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been thinking about this, and the proper fix probably is in the systemjs plugin itself.

We should not hoist all variables (var and let/const), but only var and top-level let/const. We can do this by:

  1. In the big loop over the program body (in the Program.exit visitor), we convert all the top-level let/const bindings to var. This is safe, they are not in nested blocks.
  2. When calling hoistVariables, we should remove the third parameter (null) so that it only hoists var and not also let/const.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For (1), be careful because export let x = 1; and export { x }; let x = 1; are handled in two different code paths and both should still work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If third parameter is removed from the hoistVariable it removes all the _export calls from the outputs, and doesn't transform the input?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we will still need to hoist these export bindings and set up the metadata from which the _export calls are built. I suggest we copy the code from the hoistVariable to the systemjs transform, and implement the custom hoisting logic mentioned in #14057 (comment). Or we can add a new option in the helper, though I lean to the first option since the hoisting logic here is not reused by other plugins.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised that the current helper doesn't work, because we only need _export for things that become var in step 1 🤔
Do you mind pushing the current (non working) code you have?

Copy link
Contributor

@JLHwung JLHwung Mar 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a working branch. We can infer the behaviour of hoistVariable helper from the implementation:

Scope(path: NodePath, state: State) {
if (state.kind === "let") path.skip();
},
FunctionParent(path: NodePath) {
path.skip();
},
VariableDeclaration(path: NodePath<t.VariableDeclaration>, state: State) {
if (state.kind && path.node.kind !== state.kind) return;

The third variable kind controls two behaviours: 1) Whether we should check a BlockParent to see if it contains to-be-hoisted variables, and 2) whether we should hoist the let-kind variable declarations.

In systemjs transform, what we want to achieve is to hoist var variables within block parent (so kind has to be "let"), but leave let-kind declarations within block parent untouched.

  • If we set kind to "var", only var-kind declarations will be hoisted, the top level let-kinds are not.
  • If we set kind to null, all declarations will be hoisted unless nested within FunctionParent. But we want to preserve let within block parent
  • If we set kind to "let", only let-kind declarations will be hoisted

Therefore we still need to extend the hoistVariable helper behaviour.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@The-x-Theorist I hope you don't mind, I pushed two commits to your branch.

14e9cfd (#14057) is how I was thinking to fix the problem, as described in #14057 (comment):

  1. It changes all the top-level let/const to var
  2. It only hoists vars

The updated tests look good, but it probably needs a few new tests to make sure that both const x = 1; export { x } and export const x = 1; work (unless we already have these tests).

@nicolo-ribaudo nicolo-ribaudo dismissed their stale review March 8, 2022 17:10

I'll re-review this

// Convert top-level variable declarations to "var",
// because they must be hoisted
declar.node.kind = "var";
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A top level var declaration can be nested within for statement:

for (var x = 1;false;);
export { x }

or it could be within a do expression.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They already have var, there is no need to manually change them.

@@ -38,8 +38,16 @@ const visitor = {
> = path.get("declarations");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hoistVariable visitor should be merged with environmentVisitor, too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets do it in another PR since it's separate from this bug 👍

@nicolo-ribaudo
Copy link
Member

@The-x-Theorist I reverted your hoistVariable changes but I still kept your tests since they ensure that we fixed the bug. Thank you!

@nicolo-ribaudo nicolo-ribaudo changed the title Fix: modules-systemjs transformation [systemjs] Fix nested let/const shadowing imported bindings Mar 17, 2022
@nicolo-ribaudo nicolo-ribaudo merged commit 410c9ac into babel:main Mar 17, 2022
@The-x-Theorist The-x-Theorist deleted the modules-systemjs-transformation branch March 23, 2022 10:22
@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Jun 23, 2022
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 23, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: modules outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: Bug Fix 🐛 A type of pull request used for our changelog categories
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: plugin-transform-modules-systemjs transformation is unsafe
4 participants