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

Check shadow variable to identifier in default parameters #10053

Merged
merged 1 commit into from Dec 13, 2019

Conversation

JLHwung
Copy link
Contributor

@JLHwung JLHwung commented Jun 4, 2019

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

When there is a variable declaration inside the function body, which shares its name to a referenced identifier in default parameter expression, the function body should be wrapped into iife, otherwise the binding in default parameter scope will be shadowed by function body.

On the example,

// scope P
let x = "outside";
function outer(a = () => x // scope B) { // scope A
  let x = "inside";
  return a();
}
outer();

As is illustrated, there are 3 scopes in this program. Variable x in scope B will borrow from scope P. However, since there is x declared in scope A, we should wrap body in scope A to IIFE, otherwise the transformed code will have x in scope B borrowed from scope A, which is incorrect.

Here we check if it is a safe binding with respect to the function scope. Note that unlike the path scope changing within the traverse, this scope reference is invariant and always refers to the function declaration scope (in our example, it is scope A).

When there is a variable declaration inside the function body, which shares its name to a referenced identifier in default parameter expression, the function body should be wrapped into iife, otherwise the binding in default parameter scope will be shadowed by function body.
@babel-bot
Copy link
Collaborator

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

1 similar comment
@babel-bot
Copy link
Collaborator

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

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.

I think that a better approach would be to rename the inner x binding using path.scope.rename("x"), to avoid the IIFE:

let x = "outside";
function outer(a = () => x) {
  let x = "inside";
  return a();
}


let x = "outside";
function outer(a = () => x) {
  let x = "inside";
  return a();
}
outer();

// -->

var x = "outside";

function outer() {
  var a = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {
    return x;
  };
  var _x = "inside";
  return a();
}

outer();

If it turns out to be difficult/buggy, I'm ok with the behavior implemented by this PR since this is a very rare edge case.

@nicolo-ribaudo nicolo-ribaudo added the PR: Bug Fix 🐛 A type of pull request used for our changelog categories label Jun 4, 2019
@nicolo-ribaudo
Copy link
Member

Related: #2471

@JLHwung
Copy link
Contributor Author

JLHwung commented Jun 4, 2019

Hi Nicolò,

Thanks for the quick response. The issue 2471 is still reproducible on master. I would try to look into this in couple of days. At first glance, renaming the inner binding looks better as it prevents from IIFE performance penalties.

Where could I know all the possible values and the meaning of binding.kind? It would greatly help list every binding cases and evaluate whether it is safe to rename.

@JLHwung
Copy link
Contributor Author

JLHwung commented Jun 5, 2019

Hi Nicolò,

I have tried to use path.scope.rename but it seems that the renamer will also rename the referenced identifier borrowed from parent scope. I have added extra test to showcase this potential issue. (PR is here) The renamer approach is blocked by this issue.

Though issue #2471 is related, I would suggest move things forward and try to fix them on different PR. Any thoughts?

if (node.name === "eval" || !isSafeBinding(scope, node)) {
if (
node.name === "eval" ||
!isSafeBinding(scope, node) ||
Copy link
Contributor Author

@JLHwung JLHwung Dec 12, 2019

Choose a reason for hiding this comment

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

This should be considered a workaround hotfix because the ReferencedIdentifier in params initializers should point to the parent of the function scope. Since path.scope is not accurate here, we have to do a hotfix and check state.scope again.

The ideal fix should construct a separate scope for parameter initializer.

Copy link
Contributor Author

@JLHwung JLHwung left a comment

Choose a reason for hiding this comment

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

@nicolo-ribaudo nicolo-ribaudo merged commit bffa415 into babel:master Dec 13, 2019
@nicolo-ribaudo nicolo-ribaudo deleted the fix-9947 branch December 13, 2019 13:39
@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 Mar 14, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 14, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
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
3 participants