Skip to content

Add NodePath#getAssignmentIdentifiers#16551

Merged
nicolo-ribaudo merged 5 commits intobabel:mainfrom
JLHwung:add-get-assignment-identifiers
Jul 26, 2024
Merged

Add NodePath#getAssignmentIdentifiers#16551
nicolo-ribaudo merged 5 commits intobabel:mainfrom
JLHwung:add-get-assignment-identifiers

Conversation

@JLHwung
Copy link
Contributor

@JLHwung JLHwung commented Jun 5, 2024

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

In this PR we introduce a new getAssignmentIdentifiers method to extract identifiers allowed in the left hand side. Currently this method is a subset of the getBindingIdentifiers, however in Babel 8 we will remove the assignment LHS support for getBindingIdentifiers, as those are not binding identifiers per spec. This goal will be addressed in a new PR.

In Babel 8, getBindingIdentifiers will only return identifiers that will introduce new bindings, while getAssignmentIdentifiers only returns identifiers that is reassigned.

@JLHwung JLHwung added the PR: New Feature 🚀 A type of pull request used for our changelog categories label Jun 5, 2024
const ids = path.getBindingIdentifiers();
const ids = process.env.BABEL_8_BREAKING
? path.getAssignmentIdentifiers()
: path.getBindingIdentifiers();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change is deferred to Babel 8 because plugins might be used with an old Babel core that depends on a legacy Babel traverse.

@babel-bot
Copy link
Collaborator

babel-bot commented Jun 5, 2024

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

@nicolo-ribaudo nicolo-ribaudo added this to the v7.25.0 milestone Jun 6, 2024
@ehoogeveen-medweb
Copy link

In this PR we introduce a new getAssignmentIdentifiers method to extract identifiers allowed in the left hand side.

In Babel 8, [...] getAssignmentIdentifiers only returns identifiers that is reassigned.

To be clear, will this method return identifiers that are assigned to, or identifiers that can be assigned to (but don't create a new binding)? If it's the latter, is this affected by non-strict mode (where assigning to an undeclared variable creates a property on the global object)?

@JLHwung
Copy link
Contributor Author

JLHwung commented Jun 7, 2024

To be clear, will this method return identifiers that are assigned to, or identifiers that can be assigned to (but don't create a new binding)? If it's the latter, is this affected by non-strict mode (where assigning to an undeclared variable creates a property on the global object)?

Good catch. This method is supposed to return those that are assigned and it does not take into account non-strict mode. I overlooked the non-strict mode behaviour of assignment expressions. I believe this is why getBindingIdentifiers currently handles assignment expressions as well.

However, getBindingIdentifiers also covers update expressions and unary expressions, e.g. delete x, which mutates the value referenced by the identifiers but never introduce new bindings.

The map getBindingIdentifiers.keys also determines the virtual type BindingIdentifier, and here is known mismatch between Babel's type BindingIdentifier / ReferencedIdentifier and the spec production BindingIdentifier / IdentifierReference. Note: Babel's BindingIdentifier and ReferencedIdentifier also cover some TS/Flow types, which are omitted for clarity.

Babel ECMA262 (without Annex B)
BindingIdentifier BindingIdentifier + LabelIdentifier + IdentifierReference from AssignmentPattern from LeftHandSideExpression (LHS) + IdentifierReference from LHS from UpdateExpression and UnaryExpression
ReferencedIdentifier IdentifierReference - IdentifierReference from AssignmentPattern from LeftHandSideExpression (LHS)

As we can see,

  • the Babel BindingIdentifier covers binding identifier as well as most mutated identifier references, however, it does not cover x in for (x of []);
  • the Babel ReferencedIdentifier covers mostly immutable references, however, it does cover x in x++
  • the Babel BindingIdentifier set intersects with ReferencedIdentifier, e.g. x in x++ can be matched by both BindingIdentifier and ReferencedIdentifier
  • The union of BindingIdentifier and ReferencedIdentifier is BindingIdentifier + LabelIdentifier + IdentifierReference

If we introduce getAssignmentIdentifiers without updating these virtual types, it might be even more confusing as a Babel "BindingIdentifier", e.g. IdentifierReference in an assignment LHS, will not be returned from getBindingIdentifiers.

@JLHwung
Copy link
Contributor Author

JLHwung commented Jun 7, 2024

Here are some use cases for Babel's "BindingIdentifier" and "ReferencedIdentifier":

  1. transform-parameters deoptimize ...args when args is shadowed or mutated using BindingIdentifier
    /**
    * Deopt on use of a binding identifier with the same name as our rest param.
    *
    * See https://github.com/babel/babel/issues/2091
    */
    BindingIdentifier(path, state) {
    if (referencesRest(path, state)) {
    state.deopted = true;
    }
    },
    };
  2. transform-reserved-words renames invalid ES3 identifiers using the union BindingIdentifier | ReferencedIdentifier
    "BindingIdentifier|ReferencedIdentifier"(path: NodePath<t.Identifier>) {
    if (!t.isValidES3Identifier(path.node.name)) {
    path.scope.rename(path.node.name);
    }
  3. transform-class-properties renames the class binding references by the new class name using ReferencedIdentifier
    const innerReferencesVisitor: Visitor<ReplaceInnerBindingReferenceState> = {
    ReferencedIdentifier(path, state) {
    if (
    path.scope.bindingIdentifierEquals(path.node.name, state.innerBinding)
    ) {
    state.needsClassRef = true;
    path.node.name = state.thisRef.name;
    }
    },
    };
  4. traverse collects whether arguments are referenced using isReferencedIdentifier:
    Identifier(child, { argumentsPaths }) {
    if (!child.isReferencedIdentifier({ name: "arguments" })) return;
  5. traverse remove bindings from the scope
    export function _removeFromScope(this: NodePath) {
    const bindings = getBindingIdentifiers(this.node, false, false, true);
    Object.keys(bindings).forEach(name => this.scope.removeBinding(name));
    }

If we are to align current Babel definition to the spec notation, we will have to adjust case 1 and 4 since they now cover different AST structures. The case 2 will not be affected because the union set is not changed unless we want to separate LabelIdentifier from the BindingIdentifier. The case 3 is probably fine, too as the inner class binding is immutable. The case 5 need no changes and we can further remove the newBindingsOnly option.

I think the case 1 justifies the requirement of getAssignmentIdentifiers, although we can query similar information from binding.constantViolations, the AST query will return the up-to-date information, otherwise we will have to do a scope crawl to ensure that the constantViolations is correct.

One of technical challenges that may block alignment is identifiers within a pattern. To determine whether such an identifier is a reference, we have to walk up to the parent of the toplevel object/array pattern, but the current isBindingIdentifier only have access to up to 2 levels of ancestors. We could do the walk before calling the query and pass down something like patternParent, but then the babel-types isBindingIdentifier and isReferecedIdentifier check will be more expensive and no longer context-free.

Copy link
Member

@liuxingbaoyu liuxingbaoyu left a comment

Choose a reason for hiding this comment

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

Thank you for your detailed explanation!

@nicolo-ribaudo nicolo-ribaudo added the PR: Ready to be Merged A pull request with already two approvals, but waiting for the next minor release label Jun 29, 2024
@nicolo-ribaudo nicolo-ribaudo force-pushed the add-get-assignment-identifiers branch from 09bff4f to 2a5a022 Compare July 25, 2024 18:44
@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 Oct 26, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 26, 2024
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 PR: New Feature 🚀 A type of pull request used for our changelog categories PR: Ready to be Merged A pull request with already two approvals, but waiting for the next minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants