-
Notifications
You must be signed in to change notification settings - Fork 680
Port class fields transformer #1542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Port class fields transformer #1542
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements the class fields transformer functionality, porting significant TypeScript class transformation logic to handle private fields, instance properties, and class static blocks. It brings the Go implementation closer to TypeScript's behavior for class field transformations.
- Adds comprehensive class fields transformation infrastructure including private identifier handling, class lexical environments, and field initialization logic
- Moves utility functions to appropriate modules and adds new helper functions for private identifier access patterns
- Reduces differences in the
privateNameHashCharName
test baseline, demonstrating improved compatibility
Reviewed Changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 15 comments.
Show a summary per file
File | Description |
---|---|
testdata/baselines/reference/submodule/conformance/privateNameHashCharName.js.diff |
Removes diff content showing test now produces expected output |
testdata/baselines/reference/submodule/conformance/privateNameHashCharName.js |
Updates baseline to match TypeScript's expected private field transformation output |
internal/transformers/utilities.go |
Adds IsSimpleInlineableExpression utility function |
internal/transformers/moduletransforms/utilities.go |
Removes duplicate utility function, now using shared version |
internal/transformers/moduletransforms/commonjsmodule.go |
Updates to use shared IsSimpleInlineableExpression function |
internal/transformers/estransforms/classthis.go |
Adds helper function for detecting class-this assignments |
internal/transformers/estransforms/classfields.go |
Major implementation of class fields transformer with private identifier handling |
internal/transformers/declarations/util.go |
Removes duplicate utility function, now using shared AST version |
internal/transformers/declarations/transform.go |
Updates to use shared utility functions and remove duplicate code |
internal/printer/privateidentifierinfo.go |
Adds comprehensive private identifier information types and interfaces |
internal/printer/helpers.go |
Adds class private field get helper for runtime support |
internal/printer/factory.go |
Adds factory methods for internal names and private field access helpers |
internal/printer/emitcontext.go |
Adds utility methods for comment removal and text source tracking |
internal/core/stack.go |
Adds FindFromTop method for stack searching |
internal/ast/utilities.go |
Adds utility functions for class elements and static property handling |
func NewPrivateIdentifierAccessorInfo(brandCheckIdentifier *ast.Identifier, getterName *ast.Identifier, setterName *ast.Identifier, isValid bool, isStatc bool) *PrivateIdentifierAccessorInfo { | ||
return &PrivateIdentifierAccessorInfo{ | ||
kind: PrivateIdentifierKindAccessor, | ||
PrivateIdentifierInfoBase: PrivateIdentifierInfoBase{ | ||
brandCheckIdentifier: brandCheckIdentifier, | ||
isStatic: isStatc, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parameter name 'isStatc' appears to be a typo. It should be 'isStatic' to match the field name and other similar parameters.
func NewPrivateIdentifierAccessorInfo(brandCheckIdentifier *ast.Identifier, getterName *ast.Identifier, setterName *ast.Identifier, isValid bool, isStatc bool) *PrivateIdentifierAccessorInfo { | |
return &PrivateIdentifierAccessorInfo{ | |
kind: PrivateIdentifierKindAccessor, | |
PrivateIdentifierInfoBase: PrivateIdentifierInfoBase{ | |
brandCheckIdentifier: brandCheckIdentifier, | |
isStatic: isStatc, | |
func NewPrivateIdentifierAccessorInfo(brandCheckIdentifier *ast.Identifier, getterName *ast.Identifier, setterName *ast.Identifier, isValid bool, isStatic bool) *PrivateIdentifierAccessorInfo { | |
return &PrivateIdentifierAccessorInfo{ | |
kind: PrivateIdentifierKindAccessor, | |
PrivateIdentifierInfoBase: PrivateIdentifierInfoBase{ | |
brandCheckIdentifier: brandCheckIdentifier, | |
isStatic: isStatic, |
Copilot uses AI. Check for mistakes.
func NewPrivateIdentifierMethodInfo(brandCheckIdentifier *ast.Identifier, methodName *ast.Identifier, isValid bool, isStatc bool) *PrivateIdentifierMethodInfo { | ||
return &PrivateIdentifierMethodInfo{ | ||
kind: PrivateIdentifierKindMethod, | ||
PrivateIdentifierInfoBase: PrivateIdentifierInfoBase{ | ||
brandCheckIdentifier: brandCheckIdentifier, | ||
isStatic: isStatc, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parameter name 'isStatc' appears to be a typo. It should be 'isStatic' to match the field name and other similar parameters.
func NewPrivateIdentifierMethodInfo(brandCheckIdentifier *ast.Identifier, methodName *ast.Identifier, isValid bool, isStatc bool) *PrivateIdentifierMethodInfo { | |
return &PrivateIdentifierMethodInfo{ | |
kind: PrivateIdentifierKindMethod, | |
PrivateIdentifierInfoBase: PrivateIdentifierInfoBase{ | |
brandCheckIdentifier: brandCheckIdentifier, | |
isStatic: isStatc, | |
func NewPrivateIdentifierMethodInfo(brandCheckIdentifier *ast.Identifier, methodName *ast.Identifier, isValid bool, isStatic bool) *PrivateIdentifierMethodInfo { | |
return &PrivateIdentifierMethodInfo{ | |
kind: PrivateIdentifierKindMethod, | |
PrivateIdentifierInfoBase: PrivateIdentifierInfoBase{ | |
brandCheckIdentifier: brandCheckIdentifier, | |
isStatic: isStatic, |
Copilot uses AI. Check for mistakes.
// Used for `super` references in static initializers. | ||
superClassReference *ast.IdentifierNode | ||
|
||
privateEnvrionment *PrivateEnvironment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Field name 'privateEnvrionment' appears to be a typo. It should be 'privateEnvironment' for consistency with the type name.
privateEnvrionment *PrivateEnvironment | |
privateEnvironment *PrivateEnvironment |
Copilot uses AI. Check for mistakes.
Identifiers map[string]printer.PrivateIdentifierInfo | ||
} | ||
|
||
type classLexicalEnvrionment struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type name 'classLexicalEnvrionment' appears to be a typo. It should be 'classLexicalEnvironment' for consistency with standard spelling.
type classLexicalEnvrionment struct { | |
type classLexicalEnvironment struct { |
Copilot uses AI. Check for mistakes.
currentClassContainer *ast.ClassLikeDeclaration | ||
currentClassElement *ast.ClassElement | ||
|
||
classScopeStack core.Stack[*classLexicalEnvrionment] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type reference 'classLexicalEnvrionment' appears to be a typo. It should be 'classLexicalEnvironment' for consistency with standard spelling.
classScopeStack core.Stack[*classLexicalEnvrionment] | |
classScopeStack core.Stack[*classLexicalEnvironment] |
Copilot uses AI. Check for mistakes.
tx.classScopeStack.Pop() | ||
} | ||
|
||
func (tx *classFieldsTransformer) getClassLexicalEnvironment() *classLexicalEnvrionment { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return type 'classLexicalEnvrionment' appears to be a typo. It should be 'classLexicalEnvironment' for consistency with standard spelling.
Copilot uses AI. Check for mistakes.
|
||
func (tx *classFieldsTransformer) getPrivateEnvironment() *PrivateEnvironment { | ||
classScope := tx.classScopeStack.Peek() | ||
if classScope.privateEnvrionment == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Field access 'privateEnvrionment' appears to be a typo. It should be 'privateEnvironment' for consistency with the field name.
Copilot uses AI. Check for mistakes.
func (tx *classFieldsTransformer) getPrivateEnvironment() *PrivateEnvironment { | ||
classScope := tx.classScopeStack.Peek() | ||
if classScope.privateEnvrionment == nil { | ||
classScope.privateEnvrionment = &PrivateEnvironment{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Field access 'privateEnvrionment' appears to be a typo. It should be 'privateEnvironment' for consistency with the field name.
Copilot uses AI. Check for mistakes.
func (tx *classFieldsTransformer) getClassLexicalEnvironment() *classLexicalEnvrionment { | ||
return tx.classScopeStack.Peek() | ||
} | ||
|
||
func (tx *classFieldsTransformer) getPrivateEnvironment() *PrivateEnvironment { | ||
classScope := tx.classScopeStack.Peek() | ||
if classScope.privateEnvrionment == nil { | ||
classScope.privateEnvrionment = &PrivateEnvironment{} | ||
} | ||
return classScope.privateEnvrionment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Field access 'privateEnvrionment' appears to be a typo. It should be 'privateEnvironment' for consistency with the field name.
func (tx *classFieldsTransformer) getClassLexicalEnvironment() *classLexicalEnvrionment { | |
return tx.classScopeStack.Peek() | |
} | |
func (tx *classFieldsTransformer) getPrivateEnvironment() *PrivateEnvironment { | |
classScope := tx.classScopeStack.Peek() | |
if classScope.privateEnvrionment == nil { | |
classScope.privateEnvrionment = &PrivateEnvironment{} | |
} | |
return classScope.privateEnvrionment | |
func (tx *classFieldsTransformer) getClassLexicalEnvironment() *classLexicalEnvironment { | |
return tx.classScopeStack.Peek() | |
} | |
func (tx *classFieldsTransformer) getPrivateEnvironment() *PrivateEnvironment { | |
classScope := tx.classScopeStack.Peek() | |
if classScope.privateEnvironment == nil { | |
classScope.privateEnvironment = &PrivateEnvironment{} | |
} | |
return classScope.privateEnvironment |
Copilot uses AI. Check for mistakes.
// !!! | ||
var info printer.PrivateIdentifierInfo | ||
tx.classScopeStack.FindFromTop(func(item *classLexicalEnvrionment) bool { | ||
privateEnv := item.privateEnvrionment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Field access 'privateEnvrionment' appears to be a typo. It should be 'privateEnvironment' for consistency with the field name.
privateEnv := item.privateEnvrionment | |
privateEnv := item.privateEnvironment |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's ambitious to take this on - this is one of the largest transforms in the TS codebase. Correspondingly, it's kinda difficult to judge how "right" this is without all of it in place. I can say "great, one less diff" - but with tests failing with unaccepted diffs it's a 🤷♂️ if it's actually net-positive, even for just the currently claimed logic. Still, aside from the spelling errors copilot picked up on, I see a few other fixable issues:
- The substitution pass is no more (intentionally). Anything referencing them needs to be reimplemented with direct transformations. Usually not a problem, but it's not a direct port of the substitution logic like I partially see here.
- Closures. Closures need to be avoided, generally - we used them liberally in TS, but in the
go
codebase they've proven to generally have an outsized performance cost. Most, if not all closures, and auto-closing function/method references, need to be replaced with either static function refs or captured a single time on transform creation. Or just eliminated entirely. - This falls more into the "open questions" category - in the TS repo, we unconditionally execute a single class fields transform that has a bunch of internal switches for param properties,get/set semantics, auto accessors, private name support, static private name support, etc. While I did stub out a single empty transform, it might ultimately make more sense to have each of those sub-features all in separate transformers for the sake of being able to modularly compose them sensibly (moving the configuration mostly out of the transformer logic and into initial transformer selection). We've broken down other transforms by reasonably logical feature in this way already, and some functionality has already moved, as noted, to other transformers, like parameter properties. I'd probably prefer more subdivision here, but it's a bit of extra work. Doing so might make piecemeal implementations like this a bit easier to digest one bit at a time, though.
Hey @weswigham ,thanks for the detailed feedback! As I mentioned earlier, I only realized during implementation just how complex this transformer is — far beyond my initial expectation. As the code size grew, I started to suspect I might be heading in the wrong direction. My initial intent was to fix a single test case and then immediately pause, using this PR as a way to “test the waters” and invite feedback from existing contributors so we could adjust the approach early. Even if this PR doesn’t get merged, I completely understand. I really appreciate the suggestions and review. I’ll take some time to get a deeper feel for the current design philosophy, and then explore whether it’s possible to narrow this down to a well-scoped subdivision so that any contribution stays aligned and ensures we’re on the same page. |
Summary
This PR ports the
classFieldsTransformer
from Strada into the current codebase.Details
The motivation for this change originates from this issue. However, after digging into the implementation, it became clear that the scope of work is significantly more complex than initially expected.
At this stage, the PR fixes the transformation logic for the
privateNameHashCharName
test case. Please check 78458b3Before proceeding further, I'd like to confirm that this direction aligns with project goals and that this contribution is welcome. Once confirmed, I plan to continue addressing the remaining related test cases.
Test
This PR reduces the test result difference between the Strada and Corsa, brining them closer to parity.