Skip to content

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

chengcyber
Copy link
Contributor

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 78458b3

Before 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.

@Copilot Copilot AI review requested due to automatic review settings August 8, 2025 09:06
Copy link
Contributor

@Copilot 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

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

Comment on lines +73 to +78
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,
Copy link
Preview

Copilot AI Aug 8, 2025

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.

Suggested change
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.

Comment on lines +106 to +111
func NewPrivateIdentifierMethodInfo(brandCheckIdentifier *ast.Identifier, methodName *ast.Identifier, isValid bool, isStatc bool) *PrivateIdentifierMethodInfo {
return &PrivateIdentifierMethodInfo{
kind: PrivateIdentifierKindMethod,
PrivateIdentifierInfoBase: PrivateIdentifierInfoBase{
brandCheckIdentifier: brandCheckIdentifier,
isStatic: isStatc,
Copy link
Preview

Copilot AI Aug 8, 2025

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.

Suggested change
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
Copy link
Preview

Copilot AI Aug 8, 2025

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.

Suggested change
privateEnvrionment *PrivateEnvironment
privateEnvironment *PrivateEnvironment

Copilot uses AI. Check for mistakes.

Identifiers map[string]printer.PrivateIdentifierInfo
}

type classLexicalEnvrionment struct {
Copy link
Preview

Copilot AI Aug 8, 2025

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.

Suggested change
type classLexicalEnvrionment struct {
type classLexicalEnvironment struct {

Copilot uses AI. Check for mistakes.

currentClassContainer *ast.ClassLikeDeclaration
currentClassElement *ast.ClassElement

classScopeStack core.Stack[*classLexicalEnvrionment]
Copy link
Preview

Copilot AI Aug 8, 2025

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.

Suggested change
classScopeStack core.Stack[*classLexicalEnvrionment]
classScopeStack core.Stack[*classLexicalEnvironment]

Copilot uses AI. Check for mistakes.

tx.classScopeStack.Pop()
}

func (tx *classFieldsTransformer) getClassLexicalEnvironment() *classLexicalEnvrionment {
Copy link
Preview

Copilot AI Aug 8, 2025

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 {
Copy link
Preview

Copilot AI Aug 8, 2025

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{}
Copy link
Preview

Copilot AI Aug 8, 2025

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.

Comment on lines +588 to +597
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
Copy link
Preview

Copilot AI Aug 8, 2025

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.

Suggested change
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
Copy link
Preview

Copilot AI Aug 8, 2025

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.

Suggested change
privateEnv := item.privateEnvrionment
privateEnv := item.privateEnvironment

Copilot uses AI. Check for mistakes.

Copy link
Member

@weswigham weswigham left a 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:

  1. 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.
  2. 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.
  3. 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.

@chengcyber
Copy link
Contributor Author

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.

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.

2 participants