Skip to content

Downlevel static {} block emit rewrites this.prop inside a nested function () expression to the class alias _a.prop #64733

Closed

Description

🔎 Search Terms

visitPropertyAccessExpressionForSubstitution, visitForSubstitution, substitutionVisitor, classfields, "static block" function this, "_a.prototype", "class static block" this alias, "static {" this "_a", "static block" this "_a", static function this "_a"

🕗 Version & Regression Information

⏯ Playground Link

https://tsgo.sxzz.dev/#eNplkE9rwzAMxb+K0KmDENYeU7rT2LWwa9JD4iitS2IHSe5WSr/77GQpHQPjP+8962f5hh0WZYlDbV2ughnS9+hZwfS1CHx4D7fKAbQUBSZQv2/OZLSA1Qvs3sCFoSHepkhTc9SmNACTBnawnpx7mkRrtWbx9WRlO28jIx/Zq9frSPkCgB10wRm13sEqpYsUfNR/EJKVT+jfcvcFGQceshJVjHedPeZn8S42OFWo0PhhtD3xfkwMqbBYalcoytZokpQDZYusNR9pkisk2bxu1hU+zMG3oafZHJmE+EL/7E8S34cEnINNcG18w1Nu/uh6ifzh+6DvluebrZX4kudWDxkaLDB2eImLs8eT9td4+sKiq3uh+w+Fg50Q

💻 Code

// tsconfig: "target": "es2021"
export class Foo {
  declare toObject: () => number;
  bar() {
    return 1;
  }
  static {
    this;
    Foo.prototype.toObject = function (this: Foo) {
      return this.bar();
    };
  }
}

🙁 Actual behavior

When downleveling the static {} block (target <= es2021), tsgo rewrites this.bar() inside the regular function expression to _a.bar():

var _a;
export class Foo {
    bar() {
        return 1;
    }
}
_a = Foo;
(() => {
    _a;
    _a.prototype.toObject = function () {
        return _a.bar();
    };
})();

At runtime, calling new Foo().toObject() throws TypeError: _a.bar is not a function because _a is the class constructor Foo, not the instance receiver this.

🙂 Expected behavior

this.bar() inside the regular function expression is preserved, as tsc 6.0.3 emits:

var _a;
export class Foo {
    bar() {
        return 1;
    }
}
_a = Foo;
(() => {
    _a;
    _a.prototype.toObject = function () {
        return this.bar();
    };
})();

Additional information about the issue

In tsc/internal/transformers/estransforms/classfields.go:

  1. When a class has a constructor alias in tx.classAliases (_a = Foo) and classFieldsTransformer transforms a static {} block, tx.currentClassElement is set to the ClassStaticBlockDeclaration.
  2. Inside the static {} block, a regular FunctionExpression masks out SubtreeContainsLexicalThis | SubtreeContainsLexicalSuper via ^SubtreeExclusionsFunction.
  3. When visit visits the Foo.prototype.toObject = function () { ... } statement (or the FunctionExpression itself), node.SubtreeFacts() & (ast.SubtreeContainsClassFields | ast.SubtreeContainsLexicalThisOrSuper) == 0 while len(tx.classAliases) > 0, so visit delegates to tx.visitForSubstitution(node) without passing through visitFunctionExpressionOrDeclaration (leaving tx.currentClassElement set to the enclosing static block).
  4. Inside visitForSubstitution, tx.substitutionVisitor traverses into the function body and encounters this.bar. Because this.bar is a PropertyAccessExpression whose .Name() is an identifier, visitForSubstitution calls visitPropertyAccessExpressionForSubstitution.
  5. visitPropertyAccessExpressionForSubstitution (added in Fix property access name substitution in decorator and class field transformers typescript-go#3147, dd25deb) visits node.Expression with tx.Visitor() (the full tx.visit visitor) instead of the active visitor (tx.substitutionVisitor when called from visitForSubstitution).
  6. Because the ThisKeyword node itself has SubtreeContainsLexicalThis, tx.visit(this) dispatches to visitThisExpression, sees tx.currentClassElement is still the static block, and replaces this with _a.

Passing the active visitor into visitPropertyAccessExpressionForSubstitution (tx.substitutionVisitor from visitForSubstitution, tx.Visitor() from visitPropertyAccessExpression) fixes the issue:

--- a/tsc/internal/transformers/estransforms/classfields.go
+++ b/tsc/internal/transformers/estransforms/classfields.go
@@ -268,7 +268,7 @@ func (tx *classFieldsTransformer) visitForSubstitution(node *ast.Node) *ast.Node
 		return tx.visitIdentifier(node.AsIdentifier())
 	}
 	if node.Kind == ast.KindPropertyAccessExpression && ast.IsIdentifier(node.AsPropertyAccessExpression().Name()) {
-		return tx.visitPropertyAccessExpressionForSubstitution(node.AsPropertyAccessExpression())
+		return tx.visitPropertyAccessExpressionForSubstitution(node.AsPropertyAccessExpression(), tx.substitutionVisitor)
 	}
 	return tx.substitutionVisitor.VisitEachChild(node)
 }
@@ -1098,17 +1098,17 @@ func (tx *classFieldsTransformer) visitPropertyAccessExpression(node *ast.Proper
 	// Private identifier names are still visited through VisitEachChild so they can be
 	// transformed by visitPrivateIdentifier.
 	if ast.IsIdentifier(node.Name()) {
-		return tx.visitPropertyAccessExpressionForSubstitution(node)
+		return tx.visitPropertyAccessExpressionForSubstitution(node, tx.Visitor())
 	}
 	return tx.Visitor().VisitEachChild(node.AsNode())
 }
 
 // visitPropertyAccessExpressionForSubstitution visits only the expression of a PropertyAccessExpression,
 // leaving the name unchanged. This prevents the name from being treated as a standalone identifier
 // reference and incorrectly substituted with a class alias.
-func (tx *classFieldsTransformer) visitPropertyAccessExpressionForSubstitution(node *ast.PropertyAccessExpression) *ast.Node {
-	expression := tx.Visitor().VisitNode(node.Expression)
+func (tx *classFieldsTransformer) visitPropertyAccessExpressionForSubstitution(node *ast.PropertyAccessExpression, visitor *ast.NodeVisitor) *ast.Node {
+	expression := visitor.VisitNode(node.Expression)
 	if expression != node.Expression {
 		return tx.Factory().UpdatePropertyAccessExpression(node, expression, node.QuestionDotToken, node.Name(), node.Flags)
 	}
 	return node.AsNode()
 }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions