Skip to content
Merged
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
48 changes: 41 additions & 7 deletions internal/transformers/estransforms/classfields.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ type classFieldsTransformer struct {
accessorFieldResultVisitor *ast.NodeVisitor
arrayAssignmentElementVisitor *ast.NodeVisitor
objectAssignmentElementVisitor *ast.NodeVisitor
substitutionVisitor *ast.NodeVisitor

// Pre-bound callbacks to avoid repeated closure allocation.
isAnonymousClassNeedingAssignedName func(*anonymousFunctionDefinition) bool
Expand Down Expand Up @@ -183,6 +184,7 @@ func newClassFieldsTransformer(opts *transformers.TransformOptions) *transformer
tx.accessorFieldResultVisitor = tx.EmitContext().NewNodeVisitor(tx.visitAccessorFieldResult)
tx.arrayAssignmentElementVisitor = tx.EmitContext().NewNodeVisitor(tx.visitArrayAssignmentElement)
tx.objectAssignmentElementVisitor = tx.EmitContext().NewNodeVisitor(tx.visitObjectAssignmentElement)
tx.substitutionVisitor = tx.EmitContext().NewNodeVisitor(tx.visitForSubstitution)
tx.isAnonymousClassNeedingAssignedName = tx.isAnonymousClassNeedingAssignedNameWorker

return result
Expand Down Expand Up @@ -241,19 +243,27 @@ func (tx *classFieldsTransformer) visitModifier(node *ast.Node) *ast.Node {
return nil
}

// visit is the main visitor.
func (tx *classFieldsTransformer) visit(node *ast.Node) *ast.Node {
// Strada's onSubstituteNode runs on ALL emitted nodes regardless of transform flags.
// Since we substitute eagerly, we must check for identifiers needing alias substitution
// even in subtrees with no class field transforms.
if node.Kind == ast.KindIdentifier && len(tx.classAliases) > 0 {
// visitForSubstitution visits nodes solely for class alias substitution in subtrees
// that don't contain class field or lexical this/super transforms. It substitutes
// identifiers that reference class declarations with their aliases, while skipping
// the .Name() of PropertyAccessExpressions since Strada's onSubstituteNode only
// fires for EmitHint.Expression, which excludes property access names.
func (tx *classFieldsTransformer) visitForSubstitution(node *ast.Node) *ast.Node {
if node.Kind == ast.KindIdentifier {
return tx.visitIdentifier(node.AsIdentifier())
}
if node.Kind == ast.KindPropertyAccessExpression && ast.IsIdentifier(node.AsPropertyAccessExpression().Name()) {
return tx.visitPropertyAccessExpressionForSubstitution(node.AsPropertyAccessExpression())
}
return tx.substitutionVisitor.VisitEachChild(node)
}

// visit is the main visitor.
func (tx *classFieldsTransformer) visit(node *ast.Node) *ast.Node {
if node.SubtreeFacts()&(ast.SubtreeContainsClassFields|ast.SubtreeContainsLexicalThisOrSuper) == 0 {
if tx.currentClassContainer != nil && len(tx.classAliases) > 0 {
// Continue visiting for alias substitution even in non-class-field subtrees.
return tx.Visitor().VisitEachChild(node)
return tx.visitForSubstitution(node)
}
return node
}
Expand Down Expand Up @@ -1064,9 +1074,28 @@ func (tx *classFieldsTransformer) visitPropertyAccessExpression(node *ast.Proper
return superProperty
}
}
// Visit only the expression, not the name (when it's a regular identifier), to prevent
// substitution of property names. Strada's onSubstituteNode only fires for
// EmitHint.Expression, which excludes the .name of PropertyAccessExpression.
// 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.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)
if expression != node.Expression {
return tx.Factory().UpdatePropertyAccessExpression(node, expression, node.QuestionDotToken, node.Name())
}
return node.AsNode()
}

func (tx *classFieldsTransformer) visitElementAccessExpression(node *ast.ElementAccessExpression) *ast.Node {
if tx.shouldTransformSuperInStaticInitializers && tx.currentClassElement != nil &&
ast.IsSuperProperty(node.AsNode()) &&
Expand Down Expand Up @@ -1645,6 +1674,11 @@ func (tx *classFieldsTransformer) memberContainsConstructorReference(member *ast
return true
}
}
// For PropertyAccessExpression, only check the expression, not the name.
// The .Name() is a property access name, not a value reference to the class.
if ast.IsPropertyAccessExpression(n) {
return check(n.Expression())
}
return n.ForEachChild(check)
}
// Check only the body/initializer of the member, not the name (which may be
Expand Down
18 changes: 18 additions & 0 deletions internal/transformers/tstransforms/legacydecorators.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func (tx *LegacyDecoratorsTransformer) visit(node *ast.Node) *ast.Node {
switch node.Kind {
case ast.KindIdentifier:
return tx.visitIdentifier(node.AsIdentifier())
case ast.KindPropertyAccessExpression:
return tx.visitPropertyAccessExpression(node.AsPropertyAccessExpression())
case ast.KindDecorator:
// Decorators are elided. They will be emitted as part of `visitClassDeclaration`.
return nil
Expand Down Expand Up @@ -78,6 +80,17 @@ func (tx *LegacyDecoratorsTransformer) visitIdentifier(node *ast.Identifier) *as
return node.AsNode()
}

func (tx *LegacyDecoratorsTransformer) visitPropertyAccessExpression(node *ast.PropertyAccessExpression) *ast.Node {
// Visit the expression but not the name, since property access names should not be substituted.
// Strada's onSubstituteNode only fires for EmitHint.Expression, which excludes the
// .name of PropertyAccessExpression.
expression := tx.Visitor().VisitNode(node.Expression)
if expression != node.Expression {
return tx.Factory().UpdatePropertyAccessExpression(node, expression, node.QuestionDotToken, node.Name())
}
return node.AsNode()
}

func elideNodes(f *printer.NodeFactory, nodes *ast.NodeList) *ast.NodeList {
if nodes == nil {
return nil
Expand Down Expand Up @@ -503,6 +516,11 @@ func (tx *LegacyDecoratorsTransformer) hasInternalStaticReference(node *ast.Clas
if ast.IsIdentifier(n) && tx.referenceResolver.GetReferencedValueDeclaration(tx.EmitContext().MostOriginal(n)) == classNode {
return true
}
// For PropertyAccessExpression, only check the expression, not the name.
// The .Name() is a property access name, not a value reference to the class.
if ast.IsPropertyAccessExpression(n) {
return isOrContainsStaticSelfReference(n.Expression())
}
return n.ForEachChild(isOrContainsStaticSelfReference)
}
for _, member := range node.Members.Nodes {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//// [tests/cases/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.ts] ////

//// [classFieldsPrivatePropertyAccessSameNameAsClass.ts]
export enum MyEnum {
Foo = "FooValue",
Bar = "BarValue",
}

// Private fields on a class trigger class alias creation at target < ES2022.
// Enum member access with same name as class should not be renamed.
export class Foo {
#private = 1;
static instance = new Foo();
type: MyEnum = MyEnum.Foo;

getType(): MyEnum {
return this.type || MyEnum.Foo;
}

getPrivate() {
return this.#private;
}
}

// Property access with same name as class in static context
declare const obj: { Bar: number };

export class Bar {
#data = 0;
static ref = new Bar();
static prop = obj.Bar;
}

// Class expression with private fields and self-reference
declare const obj2: { Baz: string };

const MyClass = class Baz {
#field = 1;
static instance = new Baz();
prop = obj2.Baz;
}


//// [classFieldsPrivatePropertyAccessSameNameAsClass.js]
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _Foo_private, _Bar_data, _Baz_field, _a;
export var MyEnum;
(function (MyEnum) {
MyEnum["Foo"] = "FooValue";
MyEnum["Bar"] = "BarValue";
})(MyEnum || (MyEnum = {}));
// Private fields on a class trigger class alias creation at target < ES2022.
// Enum member access with same name as class should not be renamed.
export class Foo {
constructor() {
_Foo_private.set(this, 1);
this.type = MyEnum.Foo;
}
getType() {
return this.type || MyEnum.Foo;
}
getPrivate() {
return __classPrivateFieldGet(this, _Foo_private, "f");
}
}
_Foo_private = new WeakMap();
Foo.instance = new Foo();
export class Bar {
constructor() {
_Bar_data.set(this, 0);
}
}
_Bar_data = new WeakMap();
Bar.ref = new Bar();
Bar.prop = obj.Bar;
const MyClass = (_a = class Baz {
constructor() {
_Baz_field.set(this, 1);
this.prop = obj2.Baz;
}
},
_Baz_field = new WeakMap(),
_a.instance = new _a(),
_a);
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//// [tests/cases/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.ts] ////

=== classFieldsPrivatePropertyAccessSameNameAsClass.ts ===
export enum MyEnum {
>MyEnum : Symbol(MyEnum, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 0, 0))

Foo = "FooValue",
>Foo : Symbol(MyEnum.Foo, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 0, 20))

Bar = "BarValue",
>Bar : Symbol(MyEnum.Bar, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 1, 21))
}

// Private fields on a class trigger class alias creation at target < ES2022.
// Enum member access with same name as class should not be renamed.
export class Foo {
>Foo : Symbol(Foo, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 3, 1))

#private = 1;
>#private : Symbol(Foo.#private, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 7, 18))

static instance = new Foo();
>instance : Symbol(Foo.instance, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 8, 17))
>Foo : Symbol(Foo, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 3, 1))

type: MyEnum = MyEnum.Foo;
>type : Symbol(Foo.type, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 9, 32))
>MyEnum : Symbol(MyEnum, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 0, 0))
>MyEnum.Foo : Symbol(MyEnum.Foo, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 0, 20))
>MyEnum : Symbol(MyEnum, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 0, 0))
>Foo : Symbol(MyEnum.Foo, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 0, 20))

getType(): MyEnum {
>getType : Symbol(Foo.getType, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 10, 30))
>MyEnum : Symbol(MyEnum, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 0, 0))

return this.type || MyEnum.Foo;
>this.type : Symbol(Foo.type, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 9, 32))
>this : Symbol(Foo, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 3, 1))
>type : Symbol(Foo.type, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 9, 32))
>MyEnum.Foo : Symbol(MyEnum.Foo, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 0, 20))
>MyEnum : Symbol(MyEnum, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 0, 0))
>Foo : Symbol(MyEnum.Foo, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 0, 20))
}

getPrivate() {
>getPrivate : Symbol(Foo.getPrivate, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 14, 5))

return this.#private;
>this.#private : Symbol(Foo.#private, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 7, 18))
>this : Symbol(Foo, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 3, 1))
}
}

// Property access with same name as class in static context
declare const obj: { Bar: number };
>obj : Symbol(obj, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 22, 13))
>Bar : Symbol(Bar, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 22, 20))

export class Bar {
>Bar : Symbol(Bar, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 22, 35))

#data = 0;
>#data : Symbol(Bar.#data, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 24, 18))

static ref = new Bar();
>ref : Symbol(Bar.ref, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 25, 14))
>Bar : Symbol(Bar, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 22, 35))

static prop = obj.Bar;
>prop : Symbol(Bar.prop, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 26, 27))
>obj.Bar : Symbol(Bar, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 22, 20))
>obj : Symbol(obj, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 22, 13))
>Bar : Symbol(Bar, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 22, 20))
}

// Class expression with private fields and self-reference
declare const obj2: { Baz: string };
>obj2 : Symbol(obj2, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 31, 13))
>Baz : Symbol(Baz, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 31, 21))

const MyClass = class Baz {
>MyClass : Symbol(MyClass, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 33, 5))
>Baz : Symbol(Baz, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 33, 15))

#field = 1;
>#field : Symbol(Baz.#field, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 33, 27))

static instance = new Baz();
>instance : Symbol(Baz.instance, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 34, 15))
>Baz : Symbol(Baz, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 33, 15))

prop = obj2.Baz;
>prop : Symbol(Baz.prop, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 35, 32))
>obj2.Baz : Symbol(Baz, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 31, 21))
>obj2 : Symbol(obj2, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 31, 13))
>Baz : Symbol(Baz, Decl(classFieldsPrivatePropertyAccessSameNameAsClass.ts, 31, 21))
}

Loading
Loading