Skip to content

Fix incorrect renaming of arguments in non-reference positions in async downlevel#3910

Open
Copilot wants to merge 4 commits into
mainfrom
copilot/fix-tsgo-arguments-rename
Open

Fix incorrect renaming of arguments in non-reference positions in async downlevel#3910
Copilot wants to merge 4 commits into
mainfrom
copilot/fix-tsgo-arguments-rename

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 16, 2026

When targeting ES2015, the async transformer incorrectly substitutes arguments identifiers in non-reference positions with the lexical capture binding:

async function f() {
  const { arguments: args } = await { arguments: 42 };
  return args;
}

Emitted { arguments_1: args } instead of { arguments: args }, causing the destructured value to be undefined.

Similarly, with jsx: preserve, a JSX attribute named arguments (e.g., <div arguments={42} />) would be incorrectly substituted since JSX nodes survive to the async transformer when the JSX transform is skipped.

Root cause: The custom isNameOfPropertyAccessOrAssignment helper only guarded against substituting arguments in PropertyAccessExpression and PropertyAssignment name positions, missing other non-reference identifier positions like BindingElement.PropertyName, JsxAttribute names, and labels.

Fix: Replace the custom helper with the existing ast.IsIdentifierName(node) utility (which covers all non-reference identifier positions: property access, property assignment, binding element property name, JSX attributes, qualified names, import/export specifiers, etc.) plus ast.IsLabelName(node) for label positions. This also removes the now-unnecessary parentNode/currentNode tracking and descendInto method, since IsIdentifierName uses the node's original parse-tree parent directly.

In the async transformer's fallback visitor, `arguments` identifiers
used as property names in binding elements (e.g., `{ arguments: args }`)
were incorrectly being replaced with the lexical arguments binding
(e.g., `arguments_1`). This caused the destructuring to look up a
non-existent property, producing `undefined`.

Extended `isNameOfPropertyAccessOrAssignment` to also check if the
identifier is the `PropertyName` of a `BindingElement`, preventing
the incorrect substitution.

Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/129652c1-726d-40e0-a33c-ba6a86eb1050

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix tsgo incorrectly renaming arguments destructuring property Fix incorrect renaming of arguments destructuring property key in async downlevel May 16, 2026
Copilot AI requested a review from jakebailey May 16, 2026 16:49
}
if ast.IsBindingElement(parent) && parent.PropertyName() == node {
return true
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is pretty similar to IsIdentifierName so I checked what else we're missing.

Can we end up with a JSX attribute named arguments at this point if we have JSX preserved?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot investigate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes — with jsx: preserve, JSX nodes survive to the async transformer since the JSX transform is skipped. A <div arguments={42} /> would have the attribute name visited through fallbackVisitor and incorrectly substituted. Added the JsxAttribute check in 0f09058.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this code should probably be flipped, so that the caller can do:

case ast.KindIdentifier:
    if tx.lexicalArguments.binding != nil &&
        node.Text() == "arguments" &&
        !ast.IsIdentifierName(node) /* and not a label */ {
        ...
    }

And only substitute when the identifier is in a reference position?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot address

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refactored in 08ed2ea — replaced the custom isNameOfPropertyAccessOrAssignment with !ast.IsIdentifierName(node) && !ast.IsLabelName(node), which covers all the non-reference identifier positions. Also removed the now-dead parentNode/currentNode/descendInto tracking since IsIdentifierName uses the node's parse-tree parent directly.

With jsx: preserve, JSX nodes survive to the async transformer.
A JSX attribute named `arguments` (e.g., <div arguments={42} />)
would be incorrectly substituted. Add JsxAttribute to the check.

Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/4fbacced-1019-4504-9244-03adde27f7aa

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Copilot AI changed the title Fix incorrect renaming of arguments destructuring property key in async downlevel Fix incorrect renaming of arguments in non-reference positions in async downlevel May 18, 2026
@@ -966,9 +966,19 @@ func (tx *asyncTransformer) getOriginalIfFunctionLike(node *ast.Node) *ast.Node
}

func isNameOfPropertyAccessOrAssignment(parent *ast.Node, node *ast.Node) bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Basically the old codebase used to call resolver.isArgumentsLocalBinding(node) instead of this

…AccessOrAssignment

Replace the custom `isNameOfPropertyAccessOrAssignment` helper with the
existing `ast.IsIdentifierName` utility (which already covers all the
relevant non-reference positions) plus `ast.IsLabelName` for label
positions.

This also removes the now-unnecessary `parentNode`/`currentNode` tracking
and `descendInto` method, since `IsIdentifierName` uses the node's
original parse-tree parent directly.

Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/23a9ff97-5a55-47b8-86f7-e7642d1869f1

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Copilot AI requested a review from jakebailey May 18, 2026 16:18
Comment on lines +111 to +114
if tx.lexicalArguments.binding != nil &&
node.Text() == "arguments" &&
!ast.IsIdentifierName(node) &&
!ast.IsLabelName(node) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot Compare this to the way the old compiler worked in its transform; make sure you haven't forgotten any edge cases here, we might not have this 100% tested

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Compared with the old TS approach. The old code in checker.ts:50594-50605:

function isArgumentsLocalBinding(nodeIn: Identifier): boolean {
    // Note: does not handle isShorthandPropertyAssignment (and probably a few more)
    if (isGeneratedIdentifier(nodeIn)) return false;
    const node = getParseTreeNode(nodeIn, isIdentifier);
    if (!node) return false;
    const parent = node.parent;
    if (!parent) return false;
    const isPropertyName = (isPropertyAccessExpression(parent)
        || isPropertyAssignment(parent))
        && parent.name === node;
    return !isPropertyName && getReferencedValueSymbol(node) === argumentsSymbol;
}

The old code has a narrow syntactic guard (only PropertyAccess + PropertyAssignment) but is saved by getReferencedValueSymbol(node) === argumentsSymbol which does full name resolution—identifiers in non-reference positions won't resolve to the arguments symbol.

Our !ast.IsIdentifierName(node) && !ast.IsLabelName(node) is a broader syntactic guard covering PropertyAccess, PropertyAssignment, BindingElement.PropertyName, JsxAttribute, QualifiedName, ImportSpecifier.PropertyName, ExportSpecifier, JSX element tags, etc.—all positions where an identifier isn't a value reference.

Edge cases considered:

  • ShorthandPropertyAssignment ({ arguments }): Not covered by IsIdentifierName, so we substitute. This is correct—it's a value reference. Same behavior as old code (the old comment even acknowledges this gap).
  • Declaration names (const arguments): Not covered by IsIdentifierName, but unreachable—async functions are strict mode, where arguments can't be a binding name.
  • ExportSpecifier/ImportSpecifier: IsIdentifierName returns true, but these are module-top-level only, unreachable inside async function bodies.

I believe we're covering all reachable cases correctly.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes an async downlevel (ES2015 target) bug where arguments was incorrectly renamed to the lexical capture binding in non-reference identifier positions (e.g. destructuring property names and JSX attribute names when jsx: preserve leaves JSX nodes for the async transformer).

Changes:

  • Replace the custom isNameOfPropertyAccessOrAssignment guard with ast.IsIdentifierName(node) plus ast.IsLabelName(node) when deciding whether to substitute arguments.
  • Simplify traversal by removing the async transformer’s manual parentNode/currentNode tracking and descendInto helper.
  • Add compiler regression tests (with baselines) covering destructuring { arguments: ... } and JSX <div arguments={...} /> cases.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
testdata/tests/cases/compiler/asyncJsxArgumentsAttributeName.ts Adds a regression test ensuring JSX attribute name arguments is not renamed during async downlevel.
testdata/tests/cases/compiler/asyncDestructuringArgumentsProperty.ts Adds a regression test ensuring destructuring property name arguments is preserved under async downlevel.
testdata/baselines/reference/compiler/asyncJsxArgumentsAttributeName.types Baseline for type output of the new JSX regression test.
testdata/baselines/reference/compiler/asyncJsxArgumentsAttributeName.symbols Baseline for symbol output of the new JSX regression test.
testdata/baselines/reference/compiler/asyncJsxArgumentsAttributeName.js Baseline for emit output of the new JSX regression test.
testdata/baselines/reference/compiler/asyncDestructuringArgumentsProperty.types Baseline for type output of the destructuring regression test.
testdata/baselines/reference/compiler/asyncDestructuringArgumentsProperty.symbols Baseline for symbol output of the destructuring regression test.
testdata/baselines/reference/compiler/asyncDestructuringArgumentsProperty.js Baseline for emit output of the destructuring regression test.
internal/transformers/estransforms/async.go Fixes arguments substitution logic to avoid renaming in non-reference identifier positions using shared AST utilities.

return tx.fallbackNodeVisitor.VisitEachChild(node)
}

func (tx *asyncTransformer) descendInto(node *ast.Node) func() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why was this removed?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With the latest commit, this infra is entirely unreferenced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tsgo incorrectly renames arguments destructuring property when targeting ES2015

4 participants