Fix incorrect renaming of arguments in non-reference positions in async downlevel#3910
Fix incorrect renaming of arguments in non-reference positions in async downlevel#3910Copilot wants to merge 4 commits into
arguments in non-reference positions in async downlevel#3910Conversation
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>
arguments destructuring property key in async downlevel
| } | ||
| if ast.IsBindingElement(parent) && parent.PropertyName() == node { | ||
| return true | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>
arguments destructuring property key in async downlevelarguments in non-reference positions in async downlevel
| @@ -966,9 +966,19 @@ func (tx *asyncTransformer) getOriginalIfFunctionLike(node *ast.Node) *ast.Node | |||
| } | |||
|
|
|||
| func isNameOfPropertyAccessOrAssignment(parent *ast.Node, node *ast.Node) bool { | |||
There was a problem hiding this comment.
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>
| if tx.lexicalArguments.binding != nil && | ||
| node.Text() == "arguments" && | ||
| !ast.IsIdentifierName(node) && | ||
| !ast.IsLabelName(node) { |
There was a problem hiding this comment.
@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
There was a problem hiding this comment.
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 byIsIdentifierName, 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 byIsIdentifierName, but unreachable—async functions are strict mode, whereargumentscan't be a binding name. - ExportSpecifier/ImportSpecifier:
IsIdentifierNamereturns true, but these are module-top-level only, unreachable inside async function bodies.
I believe we're covering all reachable cases correctly.
There was a problem hiding this comment.
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
isNameOfPropertyAccessOrAssignmentguard withast.IsIdentifierName(node)plusast.IsLabelName(node)when deciding whether to substitutearguments. - Simplify traversal by removing the async transformer’s manual
parentNode/currentNodetracking anddescendIntohelper. - 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() { |
There was a problem hiding this comment.
Why was this removed?
There was a problem hiding this comment.
With the latest commit, this infra is entirely unreferenced
When targeting ES2015, the async transformer incorrectly substitutes
argumentsidentifiers in non-reference positions with the lexical capture binding:Emitted
{ arguments_1: args }instead of{ arguments: args }, causing the destructured value to beundefined.Similarly, with
jsx: preserve, a JSX attribute namedarguments(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
isNameOfPropertyAccessOrAssignmenthelper only guarded against substitutingargumentsinPropertyAccessExpressionandPropertyAssignmentname positions, missing other non-reference identifier positions likeBindingElement.PropertyName,JsxAttributenames, 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.) plusast.IsLabelName(node)for label positions. This also removes the now-unnecessaryparentNode/currentNodetracking anddescendIntomethod, sinceIsIdentifierNameuses the node's original parse-tree parent directly.