Fix property access name substitution in decorator and class field transformers#3147
Fix property access name substitution in decorator and class field transformers#3147
Conversation
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 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>
Good call — confirmed the same bug exists in the class fields transformer ( Added tests across all three transformer paths:
|
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
…cess in transformers and checker Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
There was a problem hiding this comment.
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
.nameof aPropertyAccessExpressionin 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.
internal/checker/checker.go
Outdated
| if ast.IsIdentifier(node) && ast.IsRightSideOfPropertyAccess(node) { | ||
| keyType := c.getLiteralTypeFromPropertyName(node) |
There was a problem hiding this comment.
Added node.Parent != nil guard before ast.IsRightSideOfPropertyAccess(node) in getIndexSignaturesAtLocation to prevent a potential nil dereference. See 1d02f5b.
testdata/tests/cases/compiler/legacyDecoratorsEnumAccessSameNameAsClass.ts
Show resolved
Hide resolved
testdata/tests/cases/compiler/esDecoratorsPropertyAccessSameNameAsClass.ts
Show resolved
Hide resolved
testdata/tests/cases/compiler/classFieldsPropertyAccessSameNameAsClass.ts
Show resolved
Hide resolved
testdata/tests/cases/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.ts
Show resolved
Hide resolved
…sRightSideOfPropertyAccess Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
There was a problem hiding this comment.
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.IsRightSideOfPropertyAccessin checker utilities and add a nil-safety guard ingetIndexSignaturesAtLocation.
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.
testdata/tests/cases/compiler/classFieldsPrivatePropertyAccessSameNameAsClass.ts
Show resolved
Hide resolved
testdata/tests/cases/compiler/legacyDecoratorsEnumAccessSameNameAsClass.ts
Show resolved
Hide resolved
testdata/tests/cases/compiler/esDecoratorsPropertyAccessSameNameAsClass.ts
Show resolved
Hide resolved
testdata/tests/cases/compiler/classFieldsPropertyAccessSameNameAsClass.ts
Show resolved
Hide resolved
| 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Reverted all parent-access checks and checker changes. Now using explicit PropertyAccessExpression visitor functions:
- legacydecorators.go: Added
KindPropertyAccessExpressioncase →visitPropertyAccessExpressionthat visits only the expression, not the name - classfields.go: Handle
PropertyAccessExpressionwith identifier names before the identifier early-out; updatedvisitPropertyAccessExpressionfallthrough to usevisitPropertyAccessExpressionForSubstitutionfor identifier names while preservingVisitEachChildfor private identifiers
See a15fecc.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
Legacy decorator and class fields transformers incorrectly rename property access names when the name matches a class with an alias. For example,
MyEnum.Fooinside a decorated classFooemits asMyEnum.Foo_1, andobj.Quxinside a class expressionclass Quxwith static fields emits asobj._a.Cause:
visitIdentifierin bothlegacydecorators.goandclassfields.gosubstitutes all identifiers resolving to the class, including property access names. In TypeScript,onSubstituteNodeonly fires forEmitHint.Expression, which excludes the.nameofPropertyAccessExpression. The Go port uses a visitor instead, so it lacks that distinction.Fix: Add explicit
PropertyAccessExpressionvisitor functions that visit the expression child but skip visiting theNamechild, preventing property access names from reachingvisitIdentifierand being incorrectly substituted. This avoids relying on parent access checks, which are unreliable in transforms since previous transforms may not set parents.visitPropertyAccessExpressioninlegacydecorators.gothat visits only the expression, not the namevisitPropertyAccessExpressionForSubstitutioninclassfields.goand updatedvisitPropertyAccessExpressionto use it for identifier names while preservingVisitEachChildfor private identifier namesPropertyAccessExpressionwith identifier names before the identifier early-out inclassfields.goto prevent name substitution in non-class-field subtreeslegacyDecoratorsEnumAccessSameNameAsClass.ts— enum access, object property access, nested access, static member access, self-referenceclassFieldsPropertyAccessSameNameAsClass.ts— class declarations + class expression with self-reference at target es2020classFieldsPrivatePropertyAccessSameNameAsClass.ts— private fields with class expression at target es2020esDecoratorsPropertyAccessSameNameAsClass.ts— ES decorators at target es2020Original prompt
🔒 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.