Repository navigation
Downlevel static {} block emit rewrites this.prop inside a nested function () expression to the class alias _a.prop #64733
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
- This changed between versions 6.0.3 and 7.0 (introduced in Fix property access name substitution in decorator and class field transformers typescript-go#3147, commit dd25deb): tsc 6.0.3 emits
this.bar(); tsgo built frommainat aa81492 emits_a.bar(). - This is the behavior in every version I tried, and I reviewed the FAQ for entries about class static blocks and
this.
⏯ Playground Link
💻 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:
- When a class has a constructor alias in
tx.classAliases(_a = Foo) andclassFieldsTransformertransforms astatic {}block,tx.currentClassElementis set to theClassStaticBlockDeclaration. - Inside the
static {}block, a regularFunctionExpressionmasks outSubtreeContainsLexicalThis | SubtreeContainsLexicalSupervia^SubtreeExclusionsFunction. - When
visitvisits theFoo.prototype.toObject = function () { ... }statement (or theFunctionExpressionitself),node.SubtreeFacts() & (ast.SubtreeContainsClassFields | ast.SubtreeContainsLexicalThisOrSuper) == 0whilelen(tx.classAliases) > 0, sovisitdelegates totx.visitForSubstitution(node)without passing throughvisitFunctionExpressionOrDeclaration(leavingtx.currentClassElementset to the enclosing static block). - Inside
visitForSubstitution,tx.substitutionVisitortraverses into the function body and encountersthis.bar. Becausethis.baris aPropertyAccessExpressionwhose.Name()is an identifier,visitForSubstitutioncallsvisitPropertyAccessExpressionForSubstitution. visitPropertyAccessExpressionForSubstitution(added in Fix property access name substitution in decorator and class field transformers typescript-go#3147, dd25deb) visitsnode.Expressionwithtx.Visitor()(the fulltx.visitvisitor) instead of the active visitor (tx.substitutionVisitorwhen called fromvisitForSubstitution).- Because the
ThisKeywordnode itself hasSubtreeContainsLexicalThis,tx.visit(this)dispatches tovisitThisExpression, seestx.currentClassElementis still the static block, and replacesthiswith_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()
}
Activity