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

test: add parameter default test to renamer #10055

Closed
wants to merge 13 commits into from

Conversation

JLHwung
Copy link
Contributor

@JLHwung JLHwung commented Jun 5, 2019

Q                       A
Fixed Issues? Reveal potential issue
Patch: Bug Fix? Determined after further discussion
Major: Breaking Change? No
Minor: New Feature? No
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes? No
License MIT

This is a follow-up to #10053 (review)

Expected:

  • Renamer should not rename the bindings in the function default parameter if it is borrowed from parent scope
  • Renamer should rename the bindings in the function default parameter if it is borrowed from the param binding in the function scope.

Actual:

  • Renamer renames the bindings in the function default parameter even if it is borrowed from parent scope
  • Renamer renames the bindings in the function default parameter when it is borrowed from the param binding in the function scope.

On the test example, we have renamer to rename a to z on FunctionDeclaration visitor.
In the first case,

let a = "outside";
function f(g = () => a) {
  let a = "inside";
  return g();
}

The Renamer also rename () => a to () => z. This behavior is wrong since a is defined on global scope.

In the second case,

function h(a, g = () => a) {
  return g();
}

The renamer should also () => a to () => z since a is a param binding now.

If we agree on the correctness of my proposed behavior, then this PR reveals a bug in renamer and should be fixed in later release.

Note that CI will break since the master does not conform to this behavior.

@nicolo-ribaudo
Copy link
Member

I think that, in order to fix this, we need to change this line:
https://github.com/babel/babel/blob/master/packages/babel-traverse/src/scope/lib/renamer.js#L119

instead of running renameVisitor, we should first check where the binding is defined.
If the current scope is a function scope and the binding is not defined inside it's parameters, then we should only traverse its body instead of the whole declaration.

@JLHwung
Copy link
Contributor Author

JLHwung commented Jun 7, 2019

Hi Nicolo,

Thanks for your reply and I have drafted a WIP commit. However, after I add another test case

let a = "outside";

function j(g = a) {
  return g;
}

it successfully fails my WIP draft commit though the commit works good on those two testcase. I would work on this test case tomorrow. And I believe this case would also improve code coverage on the transform-parameters. I would sync it to #10053 later.

@JLHwung JLHwung force-pushed the renamer-parameter-default branch from fa11361 to 07bd17e Compare June 9, 2019 02:01
@babel-bot
Copy link
Collaborator

babel-bot commented Jun 9, 2019

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

@JLHwung
Copy link
Contributor Author

JLHwung commented Jun 9, 2019

Hi Nicolo, I have polished my work and it should be ready to review.

On this example,

let a = "outside";

function j(g = a) {
  return g;
}

I think it is expected since the binding scope is not always same with the scope that invokes renamer. On this case, there is no a binding inside the Function scope, so it will rename a to z in the whole program.

const params = path.get("params");
for (let i = 0; i < params.length; i++) {
const param = params[i];
if (param.isAssignmentPattern()) {
Copy link
Member

Choose a reason for hiding this comment

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

This is not enough; someone could also do this: (which is currently broken anyway)

let name;

function fn({ [name]: value }) {
  let name;
}

or

let name;

function fn({ key = name }) {
  let name;
}

if (!state.paramDefaultOuterBinding) {
if (
right.isIdentifier() &&
right.node.body.name === oldName &&
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
right.node.body.name === oldName &&
right.node.name === oldName &&

Identifiers don't have body, right?
Also, this should have been caught by a 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.

Ahh, good catch. Fixed and testcase is added.

if (
right.isIdentifier() &&
right.node.body.name === oldName &&
!isSafeBinding(scope, right.node.body)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
!isSafeBinding(scope, right.node.body)
!isSafeBinding(scope, right.node)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 840a26e.

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Jun 9, 2019

Please write a few more tests, to ensure that everything works 🙂


I think it is expected since the binding scope is not always same with the scope that invokes renamer. On this case, there is no a binding inside the Function scope, so it will rename a to z in the whole program.

Yeah, it is ok 👍

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

JLHwung commented Jun 17, 2019

Hi Nicolo,

If the current scope is a function scope and the binding is not defined inside it's parameters, then we should only traverse its body instead of the whole declaration.

I have crafted some examples and it turns out this "body/whole" approach would not cover some edge cases. In principal, what's missing here is that babel-traverse has not modeled the implicit intermediate scope for function parameters.

Examples of different categories

isolated scope

On the following example,

let a = "outside"
f(g = () => a) {
  let a = "inside";
}

() => a is a new scope, but the path.scope.bindingIdentifierEquals("a", state.binding.identifier) falsely returns true since path.scope.getBinding() returns let a = "inside" binding. If the implicit scope is modeled, () => a would not have binding in g = () => a scope and will pass on to the outer scope. Note that this is a separate bug in scope.getBinding and the "body/whole" approach does not fix it.

same scope

Besides new scope, another category is expressions in various binding patterns. For example

// computed property in `ObjectBindingPattern`
function l([{
  [a]: g
}]) {}

// expression in `KeyedBindingInitializer`
function k([{
  g = a
}]) {
  let a = "inside";
  return g;
}

These expressions do not introduce a new scope. So the renameVisistor will treat it as same as any other ReferencedIdentifier. If the parameter scope is modeled, the Scope visitor will kick in and check if could be skipped.

nested binding patterns

Perhaps the most complicated examples comes from nested binding patterns. On this example

function p(a, g = (a = a) => a) {
  g(a);
}

There are two a bindings in function p. One is param binding of p, the other is param binding of default parameter of g. When scope.getBinding() called on (a = a) => a, it returns the latter binding so the Scope visitor skip the whole path even though there is an = a binding, whose identifier exactly the param binding a of p.

The "body/whole" assumes that if a is defined as parameter in scope p, it should be safe to replace all bindings given that those in a new scope, who has different binding of a, should be skipped. However, the binding patterns can easily break across the scopes.

New approach

So today I have tried a different approach other than "body/whole" to solve these. For those in separated scope, I patched the scope.getBinding so that it can returns correct binding when it is in function parameter scope.

For those expressions in the same scope, I added logic in ReferencedIdentifier visitor to check ancestry if it is in function parameter scope.

For the third cases, I think we should should do a recursive revised renameVisitor in the Scope visitor to pick up those possible nested binding patterns. It will be triggered only when the new scope is inside the parameter scope. It is a rough idea and I suggest to move this to a separate PR.

I have pushed a working edition of my new approach, it is still proof-of-concept and, of course need refactoring before ready to merge. I would like to discuss this idea before I go too far.

Thank you for your patience.

@JLHwung
Copy link
Contributor Author

JLHwung commented Jun 28, 2019

Ping @nicolo-ribaudo. :)

@nicolo-ribaudo nicolo-ribaudo self-requested a review June 29, 2019 12:43
@@ -45,6 +45,20 @@ function gatherNodeParts(node: Object, parts: Array) {
}
}

function isScopeInFunctionParameter(scope) {
//todo: check arrow function expression or any util method?
if (scope.parent && scope.parent.block.type === "FunctionDeclaration") {
Copy link
Member

Choose a reason for hiding this comment

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

This could be any function kind. It should be t.isFunction(scope.parent.block)

//todo: check arrow function expression or any util method?
if (scope.parent && scope.parent.block.type === "FunctionDeclaration") {
const fdPath = scope.path.findParent(
path => path.type === "FunctionDeclaration",
Copy link
Member

Choose a reason for hiding this comment

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

Also here

);
if (fdPath) {
return !!fdPath.findParent(path => {
return fdPath.node.params.includes(path.node);
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 not sure that I understand this. If path.node must be inside the parameters of fdPath, why are we searching it in the parents of fdPath?

if (!scope) {
break;
}
} while (true);
Copy link
Member

Choose a reason for hiding this comment

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

Can we keep this as while ((scope = scope.parent))?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, I have refactored this part.

(path.type === "ObjectProperty" && path.node.computed)
);
});
const fdPath = path.findParent(path => path.type === "FunctionDeclaration");
Copy link
Member

Choose a reason for hiding this comment

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

  1. We should only compute this is computableNode has been found
  2. Can we share lines 12-17 with the same code in the isScopeInFunctionParameter function?

function isInFunctionParameterScope(path) {
const computableNode = path.findParent(path => {
return (
path.type === "AssignmentPattern" ||
Copy link
Member

Choose a reason for hiding this comment

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

WDYT about using path.parentPath.isAssignmentPattern({ right: path.node }) instead?
I don't think that it would change anything, but the left of an assignment isn't computable anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

WDYT about using path.parentPath.isAssignmentPattern({ right: path.node }) instead?

Good point. I have refactored this part and feel horrible on what I was writing a month ago.

while (
p.parentPath &&
!p.parentPath.isAssignmentPattern({ right: p.node }) &&
!p.isObjectProperty({ computed: true })
Copy link
Member

Choose a reason for hiding this comment

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

We should only stop if we were inside the key, not if we were inside the "value"/pattern of a computed property.

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 reworked on #10912 to get rid of the spaghetti. 😝

@JLHwung
Copy link
Contributor Author

JLHwung commented Dec 23, 2019

Closing this PR in favor of #10912.

@JLHwung JLHwung closed this Dec 23, 2019
@JLHwung JLHwung deleted the renamer-parameter-default branch December 23, 2019 14:27
@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 24, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 24, 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 pkg: traverse (scope) 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.

None yet

3 participants