Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 4 additions & 22 deletions internal/transformers/estransforms/async.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ type asyncTransformer struct {
enclosingFunctionParameterNames *collections.Set[string]
lexicalArguments lexicalArgumentsInfo

parentNode *ast.Node
currentNode *ast.Node

asyncBodyVisitor *ast.NodeVisitor
fallbackNodeVisitor *ast.NodeVisitor
}
Expand Down Expand Up @@ -111,31 +108,22 @@ func (tx *asyncTransformer) fallbackVisitor(node *ast.Node) *ast.Node {
ast.KindVariableDeclaration:
// fall through to visitEachChild
case ast.KindIdentifier:
if tx.lexicalArguments.binding != nil && node.Text() == "arguments" && !isNameOfPropertyAccessOrAssignment(tx.parentNode, node) {
if tx.lexicalArguments.binding != nil &&
node.Text() == "arguments" &&
!ast.IsIdentifierName(node) &&
!ast.IsLabelName(node) {
Comment on lines +111 to +114
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.

tx.lexicalArguments.used = true
return tx.lexicalArguments.binding
}
}
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

savedParent := tx.parentNode
tx.parentNode = tx.currentNode
tx.currentNode = node
return func() { tx.currentNode = tx.parentNode; tx.parentNode = savedParent }
}

func (tx *asyncTransformer) visitFallback(node *ast.Node) *ast.Node {
cleanup := tx.descendInto(node)
defer cleanup()
return tx.fallbackVisitor(node)
}

func (tx *asyncTransformer) visit(node *ast.Node) *ast.Node {
cleanup := tx.descendInto(node)
defer cleanup()

if node.SubtreeFacts()&(ast.SubtreeContainsAnyAwait|ast.SubtreeContainsAwait) == 0 {
return tx.fallbackVisitor(node)
}
Expand Down Expand Up @@ -965,12 +953,6 @@ func (tx *asyncTransformer) getOriginalIfFunctionLike(node *ast.Node) *ast.Node
return node
}

func isNameOfPropertyAccessOrAssignment(parent *ast.Node, node *ast.Node) bool {
return parent != nil &&
(ast.IsPropertyAccessExpression(parent) || ast.IsPropertyAssignment(parent)) &&
parent.Name() == node
}

// isSimpleParameterList checks if every parameter has no initializer and an Identifier name.
func isSimpleParameterList(params []*ast.Node) bool {
for _, param := range params {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//// [tests/cases/compiler/asyncDestructuringArgumentsProperty.ts] ////

//// [asyncDestructuringArgumentsProperty.ts]
async function f() {
const { arguments: args } = await { arguments: 42 };
return args;
}


//// [asyncDestructuringArgumentsProperty.js]
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
function f() {
return __awaiter(this, void 0, void 0, function* () {
const { arguments: args } = yield { arguments: 42 };
return args;
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//// [tests/cases/compiler/asyncDestructuringArgumentsProperty.ts] ////

=== asyncDestructuringArgumentsProperty.ts ===
async function f() {
>f : Symbol(f, Decl(asyncDestructuringArgumentsProperty.ts, 0, 0))

const { arguments: args } = await { arguments: 42 };
>arguments : Symbol(arguments, Decl(asyncDestructuringArgumentsProperty.ts, 1, 37))
>args : Symbol(args, Decl(asyncDestructuringArgumentsProperty.ts, 1, 9))
>arguments : Symbol(arguments, Decl(asyncDestructuringArgumentsProperty.ts, 1, 37))

return args;
>args : Symbol(args, Decl(asyncDestructuringArgumentsProperty.ts, 1, 9))
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//// [tests/cases/compiler/asyncDestructuringArgumentsProperty.ts] ////

=== asyncDestructuringArgumentsProperty.ts ===
async function f() {
>f : () => Promise<number>

const { arguments: args } = await { arguments: 42 };
>arguments : any
>args : number
>await { arguments: 42 } : { arguments: number; }
>{ arguments: 42 } : { arguments: number; }
>arguments : number
>42 : 42

return args;
>args : number
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//// [tests/cases/compiler/asyncJsxArgumentsAttributeName.ts] ////

//// [test.tsx]
declare namespace JSX {
interface IntrinsicElements { div: any; }
}

async function f() {
return <div arguments={42} />;
}


//// [test.jsx]
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
function f() {
return __awaiter(this, void 0, void 0, function* () {
return <div arguments={42}/>;
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//// [tests/cases/compiler/asyncJsxArgumentsAttributeName.ts] ////

=== test.tsx ===
declare namespace JSX {
>JSX : Symbol(JSX, Decl(test.tsx, 0, 0))

interface IntrinsicElements { div: any; }
>IntrinsicElements : Symbol(IntrinsicElements, Decl(test.tsx, 0, 23))
>div : Symbol(IntrinsicElements.div, Decl(test.tsx, 1, 31))
}

async function f() {
>f : Symbol(f, Decl(test.tsx, 2, 1))

return <div arguments={42} />;
>div : Symbol(JSX.IntrinsicElements.div, Decl(test.tsx, 1, 31))
>arguments : Symbol(arguments, Decl(test.tsx, 5, 13))
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//// [tests/cases/compiler/asyncJsxArgumentsAttributeName.ts] ////

=== test.tsx ===
declare namespace JSX {
interface IntrinsicElements { div: any; }
>div : any
}

async function f() {
>f : () => Promise<any>

return <div arguments={42} />;
><div arguments={42} /> : error
>div : any
>arguments : number
>42 : 42
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @target: es2015

async function f() {
const { arguments: args } = await { arguments: 42 };
return args;
}
11 changes: 11 additions & 0 deletions testdata/tests/cases/compiler/asyncJsxArgumentsAttributeName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @target: es2015
// @jsx: preserve

// @filename: test.tsx
declare namespace JSX {
interface IntrinsicElements { div: any; }
}

async function f() {
return <div arguments={42} />;
}
Loading