Skip to content

Fix property access name substitution in decorator and class field transformers#3147

Open
Copilot wants to merge 12 commits intomainfrom
copilot/add-test-case-for-enum-rename-issue
Open

Fix property access name substitution in decorator and class field transformers#3147
Copilot wants to merge 12 commits intomainfrom
copilot/add-test-case-for-enum-rename-issue

Conversation

Copy link
Contributor

Copilot AI commented Mar 18, 2026

Legacy decorator and class fields transformers incorrectly rename property access names when the name matches a class with an alias. For example, MyEnum.Foo inside a decorated class Foo emits as MyEnum.Foo_1, and obj.Qux inside a class expression class Qux with static fields emits as obj._a.

// @experimentalDecorators: true
@myDecorator
export class Foo {
    type = MyEnum.Foo;       // emitted as MyEnum.Foo_1 ❌
    getType() {
        return MyEnum.Foo;   // emitted as MyEnum.Foo_1 ❌
    }
}

Cause: visitIdentifier in both legacydecorators.go and classfields.go substitutes all identifiers resolving to the class, including property access names. In TypeScript, onSubstituteNode only fires for EmitHint.Expression, which excludes the .name of PropertyAccessExpression. The Go port uses a visitor instead, so it lacks that distinction.

Fix: Add explicit PropertyAccessExpression visitor functions that visit the expression child but skip visiting the Name child, preventing property access names from reaching visitIdentifier and being incorrectly substituted. This avoids relying on parent access checks, which are unreliable in transforms since previous transforms may not set parents.

  • Added visitPropertyAccessExpression in legacydecorators.go that visits only the expression, not the name
  • Added visitPropertyAccessExpressionForSubstitution in classfields.go and updated visitPropertyAccessExpression to use it for identifier names while preserving VisitEachChild for private identifier names
  • Handled PropertyAccessExpression with identifier names before the identifier early-out in classfields.go to prevent name substitution in non-class-field subtrees
  • Added comprehensive test cases across all affected transformer paths:
    • legacyDecoratorsEnumAccessSameNameAsClass.ts — enum access, object property access, nested access, static member access, self-reference
    • classFieldsPropertyAccessSameNameAsClass.ts — class declarations + class expression with self-reference at target es2020
    • classFieldsPrivatePropertyAccessSameNameAsClass.ts — private fields with class expression at target es2020
    • esDecoratorsPropertyAccessSameNameAsClass.ts — ES decorators at target es2020
Original prompt

This section details on the original issue you should resolve

<issue_title>experimentalDecorators renames enum accesses with same name as class</issue_title>
<issue_description>Given:

// @experimentalDecorators: true
export enum MyEnum {
    Foo = "FooValue",
    Bar = "BarValue",
}
 
function myDecorator(target: any) {
    return target;
}
 
@myDecorator
export class Foo {
    type: MyEnum = MyEnum.Foo;
 
    getType(): MyEnum {
        return this.type || MyEnum.Foo;
    }
}

We emit:

let Foo = Foo_1 = class Foo {
    type = MyEnum.Foo_1;
    getType() {
        return this.type || MyEnum.Foo_1;
    }
};

Incorrectly renaming Foo to Foo_1. But, obviously MyEnum.Foo is not Foo itself.</issue_description>

<agent_instructions>Create a test case FIRST! Accept the baselines, and commit that. Then fix the issue.</agent_instructions>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Copilot AI and others added 2 commits March 18, 2026 17:57
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
…lass

Skip identifier substitution when the identifier is the name part of a
PropertyAccessExpression (e.g., MyEnum.Foo). In TypeScript, the
substituteExpression is only invoked for EmitHint.Expression, which
excludes member names in property accesses. In Go, since substitution is
done via the visitor, we need to explicitly skip identifiers that are
property access names.

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix enum access renaming issue in experimental decorators Fix experimentalDecorators renaming enum accesses with same name as class Mar 18, 2026
Copilot AI requested a review from jakebailey March 18, 2026 18:00
@jakebailey
Copy link
Member

@copilot I can imagine this bug happening all over the place, for ES decorators, class fields, etc etc. Can you write a slew of tests that could break now that we don't do this via substitution?

- Expanded legacy decorators test with more property access scenarios
  (object access, nested access, static member, self-reference)
- Added class fields test with class expressions at target es2020 that
  trigger temp alias creation and demonstrate the bug
- Added class fields + private fields test at target es2020
- Added ES decorators test at target es2020
- Fixed the same property access name substitution bug in classfields.go
  visitIdentifier (confirmed with class expression tests)

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 18, 2026

@copilot I can imagine this bug happening all over the place, for ES decorators, class fields, etc etc. Can you write a slew of tests that could break now that we don't do this via sub...

Good call — confirmed the same bug exists in the class fields transformer (classfields.go). Fixed it in bd33135.

Added tests across all three transformer paths:

  • Legacy decorators: expanded test with enum access, object property access, nested access, static member access, and self-reference scenarios
  • Class fields (classFieldsPropertyAccessSameNameAsClass): includes a class expression with self-reference + static fields at target es2020, which creates a temp alias and demonstrated obj2.Qux being incorrectly emitted as obj2._a
  • Class fields + private fields (classFieldsPrivatePropertyAccessSameNameAsClass): same pattern with private fields at target es2020
  • ES decorators (esDecoratorsPropertyAccessSameNameAsClass): at target es2020 to exercise both ES decorator and class fields transformers

Copilot AI changed the title Fix experimentalDecorators renaming enum accesses with same name as class Fix property access name substitution in decorator and class field transformers Mar 18, 2026
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Copilot AI requested a review from jakebailey March 18, 2026 18:40
…cess in transformers and checker

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Copilot AI requested a review from jakebailey March 18, 2026 19:00
@jakebailey jakebailey marked this pull request as ready for review March 18, 2026 19:02
Copilot AI review requested due to automatic review settings March 18, 2026 19:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an identifier-substitution bug in the Go port’s legacy decorators + class fields transformers where property-access names (e.g. MyEnum.Foo’s Foo) could be incorrectly substituted when they resolve to a class alias, aligning behavior with TypeScript’s emit-time substitution rules.

Changes:

  • Skip class-alias substitution when an identifier is the .name of a PropertyAccessExpression in legacy decorators and class fields transformers.
  • Refactor checker helpers to reuse existing AST access-expression predicates.
  • Add new compiler regression tests and accepted baselines covering legacy decorators, ES decorators, and class fields/private fields scenarios.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
testdata/tests/cases/compiler/legacyDecoratorsEnumAccessSameNameAsClass.ts New legacy-decorators regression covering enum/member/property access name substitution behavior.
testdata/tests/cases/compiler/esDecoratorsPropertyAccessSameNameAsClass.ts New ES-decorators regression covering property access name substitution behavior.
testdata/tests/cases/compiler/classFieldsPropertyAccessSameNameAsClass.ts New class-fields regression including class expression + static field lowering path.
testdata/tests/cases/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.ts New class-fields regression for private fields + aliasing behavior.
testdata/baselines/reference/compiler/legacyDecoratorsEnumAccessSameNameAsClass.types Accepted baseline output for the new legacy-decorators test (types).
testdata/baselines/reference/compiler/legacyDecoratorsEnumAccessSameNameAsClass.symbols Accepted baseline output for the new legacy-decorators test (symbols).
testdata/baselines/reference/compiler/legacyDecoratorsEnumAccessSameNameAsClass.js Accepted baseline output for the new legacy-decorators test (JS emit).
testdata/baselines/reference/compiler/esDecoratorsPropertyAccessSameNameAsClass.types Accepted baseline output for the new ES-decorators test (types).
testdata/baselines/reference/compiler/esDecoratorsPropertyAccessSameNameAsClass.symbols Accepted baseline output for the new ES-decorators test (symbols).
testdata/baselines/reference/compiler/esDecoratorsPropertyAccessSameNameAsClass.js Accepted baseline output for the new ES-decorators test (JS emit).
testdata/baselines/reference/compiler/classFieldsPropertyAccessSameNameAsClass.types Accepted baseline output for the new class-fields test (types).
testdata/baselines/reference/compiler/classFieldsPropertyAccessSameNameAsClass.symbols Accepted baseline output for the new class-fields test (symbols).
testdata/baselines/reference/compiler/classFieldsPropertyAccessSameNameAsClass.js Accepted baseline output for the new class-fields test (JS emit).
testdata/baselines/reference/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.types Accepted baseline output for the new private-fields test (types).
testdata/baselines/reference/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.symbols Accepted baseline output for the new private-fields test (symbols).
testdata/baselines/reference/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.js Accepted baseline output for the new private-fields test (JS emit).
internal/transformers/tstransforms/legacydecorators.go Skip substitution for property-access names while resolving class-alias references.
internal/transformers/estransforms/classfields.go Skip substitution for property-access names in the class-fields alias substitution path.
internal/checker/utilities.go Refactor access-expression helper to reuse AST predicates.
internal/checker/checker.go Refactor index-signature lookup to use ast.IsRightSideOfPropertyAccess.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 30730 to 30731
if ast.IsIdentifier(node) && ast.IsRightSideOfPropertyAccess(node) {
keyType := c.getLiteralTypeFromPropertyName(node)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added node.Parent != nil guard before ast.IsRightSideOfPropertyAccess(node) in getIndexSignaturesAtLocation to prevent a potential nil dereference. See 1d02f5b.

…sRightSideOfPropertyAccess

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes incorrect class-alias substitution where identifiers used as the name in property access expressions (e.g. MyEnum.Foo) were being treated as standalone references and renamed during decorator/class-field transforms in the TypeScript-Go port.

Changes:

  • Add new compiler regression tests (legacy decorators, ES decorators, and class fields/private fields) covering property access scenarios that must not be substituted.
  • Update legacy decorators and class fields transformers to skip substitution when an identifier is the right side of a property access.
  • Reuse ast.IsRightSideOfPropertyAccess in checker utilities and add a nil-safety guard in getIndexSignaturesAtLocation.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
testdata/tests/cases/compiler/legacyDecoratorsEnumAccessSameNameAsClass.ts New legacy-decorators regression test reproducing enum-member access rename issue.
testdata/tests/cases/compiler/esDecoratorsPropertyAccessSameNameAsClass.ts New ES-decorators regression test for property/enum member access vs class aliasing.
testdata/tests/cases/compiler/classFieldsPropertyAccessSameNameAsClass.ts New class-fields regression test for aliasing and property access names.
testdata/tests/cases/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.ts New private-fields regression test ensuring property access names aren’t substituted.
testdata/baselines/reference/compiler/legacyDecoratorsEnumAccessSameNameAsClass.types Baseline updates for the new legacy-decorators test.
testdata/baselines/reference/compiler/legacyDecoratorsEnumAccessSameNameAsClass.symbols Symbol baseline updates for the new legacy-decorators test.
testdata/baselines/reference/compiler/legacyDecoratorsEnumAccessSameNameAsClass.js Emit baseline showing corrected output for legacy decorators transform.
testdata/baselines/reference/compiler/esDecoratorsPropertyAccessSameNameAsClass.types Baseline updates for the new ES-decorators test.
testdata/baselines/reference/compiler/esDecoratorsPropertyAccessSameNameAsClass.symbols Symbol baseline updates for the new ES-decorators test.
testdata/baselines/reference/compiler/esDecoratorsPropertyAccessSameNameAsClass.js Emit baseline for ES decorators ensuring property names remain stable.
testdata/baselines/reference/compiler/classFieldsPropertyAccessSameNameAsClass.types Baseline updates for the class-fields regression test.
testdata/baselines/reference/compiler/classFieldsPropertyAccessSameNameAsClass.symbols Symbol baseline updates for the class-fields regression test.
testdata/baselines/reference/compiler/classFieldsPropertyAccessSameNameAsClass.js Emit baseline confirming property access names aren’t renamed in class-fields transform.
testdata/baselines/reference/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.types Baseline updates for the private-fields regression test.
testdata/baselines/reference/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.symbols Symbol baseline updates for the private-fields regression test.
testdata/baselines/reference/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.js Emit baseline confirming property access names aren’t renamed with private fields.
internal/transformers/tstransforms/legacydecorators.go Skip substitution for identifiers that are property-access names in legacy decorators transformer.
internal/transformers/estransforms/classfields.go Skip substitution for identifiers that are property-access names in class fields transformer.
internal/checker/utilities.go Refactor to reuse shared AST helper for “right side of access expression” checks.
internal/checker/checker.go Add nil-guard before calling ast.IsRightSideOfPropertyAccess in index-signature lookup.

You can also share your feedback on Copilot code review. Take the survey.

@jakebailey jakebailey requested a review from weswigham March 19, 2026 21:13
func (tx *classFieldsTransformer) visitIdentifier(node *ast.Identifier) *ast.Node {
declaration := tx.resolver.GetReferencedValueDeclaration(tx.EmitContext().MostOriginal(node.AsNode()))
original := tx.EmitContext().MostOriginal(node.AsNode())
if original.Parent != nil && ast.IsRightSideOfPropertyAccess(original) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parent accesses in transforms are a nonstarter - it's rng if this gets hit or not, as a previous transform running will block this logic by parents not getting set. Fortunately, disabling substitutions on the Names of property accesses is really easy - just add an explicit PropertyAccessExpression visitor func and don't visit the Name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Original, no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can change it, but I assumed this was somewhat fair game.

In any case, @copilot revert all changes and try doing explicit visiting of PropertyAccessExpressions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted all parent-access checks and checker changes. Now using explicit PropertyAccessExpression visitor functions:

  • legacydecorators.go: Added KindPropertyAccessExpression case → visitPropertyAccessExpression that visits only the expression, not the name
  • classfields.go: Handle PropertyAccessExpression with identifier names before the identifier early-out; updated visitPropertyAccessExpression fallthrough to use visitPropertyAccessExpressionForSubstitution for identifier names while preserving VisitEachChild for private identifiers

See a15fecc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, moving the bail check to after the .Original does make the parent check not crash, it's true, but does just make it not do the right thing - it doesn't respect the current, transformed shape of the AST.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't satisfied with what it did, so made an explicit visitor for this in classfields.

The decorators pass doesn't need a special separate visitor, I think.

…sitor to skip name substitution

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
@jakebailey jakebailey requested a review from weswigham March 20, 2026 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

experimentalDecorators renames enum accesses with same name as class

4 participants