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 4 commits into
base: main
Choose a base branch
from
Open
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
29 changes: 27 additions & 2 deletions internal/ast/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ func IsClassElement(node *Node) bool {
return false
}

func isMethodOrAccessor(node *Node) bool {
func IsMethodOrAccessor(node *Node) bool {
switch node.Kind {
case KindMethodDeclaration, KindGetAccessor, KindSetAccessor:
return true
Expand All @@ -575,7 +575,7 @@ func isMethodOrAccessor(node *Node) bool {
}

func IsPrivateIdentifierClassElementDeclaration(node *Node) bool {
return (IsPropertyDeclaration(node) || isMethodOrAccessor(node)) && IsPrivateIdentifier(node.Name())
return (IsPropertyDeclaration(node) || IsMethodOrAccessor(node)) && IsPrivateIdentifier(node.Name())
}

func IsObjectLiteralOrClassExpressionMethodOrAccessor(node *Node) bool {
Expand Down Expand Up @@ -1467,6 +1467,18 @@ func getAssignedName(node *Node) *Node {
return nil
}

func IsStaticPropertyDeclaration(member *ClassElement) bool {
return IsPropertyDeclaration(member) && HasStaticModifier(member)
}

func IsStaticPropertyDeclarationOrClassStaticBlockDeclaration(element *ClassElement) bool {
return IsStaticPropertyDeclaration(element) || IsClassStaticBlockDeclaration(element)
}

func GetStaticPropertiesAndClassStaticBlock(node *ClassLikeDeclaration) []*Node {
return core.Filter(node.Members(), IsStaticPropertyDeclarationOrClassStaticBlockDeclaration)
}

type JSDeclarationKind int

const (
Expand Down Expand Up @@ -3853,3 +3865,16 @@ func GetRestIndicatorOfBindingOrAssignmentElement(bindingElement *Node) *Node {
}
return nil
}

func GetEffectiveBaseTypeNode(node *Node) *Node {
baseType := GetClassExtendsHeritageElement(node)
// !!! TODO: JSDoc support
// if (baseType && isInJSFile(node)) {
// // Prefer an @augments tag because it may have type parameters.
// const tag = getJSDocAugmentsTag(node);
// if (tag) {
// return tag.class;
// }
// }
return baseType
}
2 changes: 1 addition & 1 deletion internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -12213,7 +12213,7 @@ func (c *Checker) getBaseTypesIfUnrelated(leftType *Type, rightType *Type, isRel
func (c *Checker) checkAssignmentOperator(left *ast.Node, operator ast.Kind, right *ast.Node, leftType *Type, rightType *Type) {
if ast.IsAssignmentOperator(operator) {
// getters can be a subtype of setters, so to check for assignability we use the setter's type instead
if isCompoundAssignment(operator) && ast.IsPropertyAccessExpression(left) {
if IsCompoundAssignment(operator) && ast.IsPropertyAccessExpression(left) {
leftType = c.checkPropertyAccessExpression(left, CheckModeNormal, true /*writeOnly*/)
}
if c.checkReferenceExpression(left, diagnostics.The_left_hand_side_of_an_assignment_expression_must_be_a_variable_or_a_property_access, diagnostics.The_left_hand_side_of_an_assignment_expression_may_not_be_an_optional_property_access) {
Expand Down
2 changes: 1 addition & 1 deletion internal/checker/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func findInMap[K comparable, V any](m map[K]V, predicate func(V) bool) V {
return *new(V)
}

func isCompoundAssignment(token ast.Kind) bool {
func IsCompoundAssignment(token ast.Kind) bool {
return token >= ast.KindFirstCompoundAssignment && token <= ast.KindLastCompoundAssignment
}

Expand Down
12 changes: 12 additions & 0 deletions internal/printer/classfacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package printer

type ClassFacts int32

const (
ClassFactsClassWasDecorated = 1 << iota
ClassFactsNeedsClassConstructorReference
ClassFactsNeedsClassSuperReference
ClassFactsNeedsSubstitutionForThisInClassStaticField
ClassFactsWillHoistInitializersToConstructor
ClassFactsNone ClassFacts = 0
)
186 changes: 176 additions & 10 deletions internal/printer/emitcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,24 @@ import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/scanner"
)

// Stores side-table information used during transformation that can be read by the printer to customize emit
//
// NOTE: EmitContext is not guaranteed to be thread-safe.
type EmitContext struct {
Factory *NodeFactory // Required. The NodeFactory to use to create new nodes
autoGenerate map[*ast.MemberName]*AutoGenerateInfo
textSource map[*ast.StringLiteralNode]*ast.Node
original map[*ast.Node]*ast.Node
emitNodes core.LinkStore[*ast.Node, emitNode]
assignedName map[*ast.Node]*ast.Expression
classThis map[*ast.Node]*ast.IdentifierNode
varScopeStack core.Stack[*varScope]
letScopeStack core.Stack[*varScope]
emitHelpers collections.OrderedSet[*EmitHelper]
Factory *NodeFactory // Required. The NodeFactory to use to create new nodes
autoGenerate map[*ast.MemberName]*AutoGenerateInfo
textSource map[*ast.StringLiteralNode]*ast.Node
original map[*ast.Node]*ast.Node
emitNodes core.LinkStore[*ast.Node, emitNode]
assignedName map[*ast.Node]*ast.Expression
classThis map[*ast.Node]*ast.IdentifierNode
varScopeStack core.Stack[*varScope]
letScopeStack core.Stack[*varScope]
classScopeStack core.Stack[*classScope]
emitHelpers collections.OrderedSet[*EmitHelper]
}

type environmentFlags int
Expand Down Expand Up @@ -156,6 +158,24 @@ func (c *EmitContext) endAndMergeVariableEnvironment(statements []*ast.Statement
return c.mergeEnvironment(statements, c.EndVariableEnvironment())
}

// NOTE: This is the new implementation of `ClassLexicalEnvironment` in Strada
type classScope struct {
facts ClassFacts

classContainer *ast.ClassLikeDeclaration
className *ast.Node // used for prefixing generated variable names
// !!! weakSetName *ast.Node // used for brand check on private methods

// A mapping of generated private names to information needed for transformation.
generatedIdentifiers map[*ast.Node]PrivateIdentifierInfo
// A mapping of private names to information needed for transformation.
identifiers map[string]PrivateIdentifierInfo

// Tracks what computed name expressions originating from elided names must be inlined
// at the next execution site, in document order
pendingExpressions []*ast.Expression
}

// Adds a `var` declaration to the current VariableEnvironment
//
// NOTE: This is the equivalent of `transformContext.hoistVariableDeclaration` in Strada.
Expand Down Expand Up @@ -367,6 +387,59 @@ func (c *EmitContext) isHoistedVariableStatement(node *ast.Statement) bool {
core.Every(node.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes, isHoistedVariable)
}

func (c *EmitContext) StartClassLexicalEnvironment(node *ast.ClassLikeDeclaration) {
// classContainer
classContainer := node

// className
var className *ast.Node
name := ast.GetNameOfDeclaration(node)
if name != nil && ast.IsIdentifier(name) {
className = name
} else {
assignedName := c.AssignedName(node)
if assignedName != nil {
if ast.IsStringLiteral(assignedName) {
// If the class name was assigned from a string literal based on an Identifier, use the Identifier
// as the prefix.
if textSourceNode := c.textSource[assignedName]; textSourceNode != nil && ast.IsIdentifier(textSourceNode) {
className = textSourceNode
} else if scanner.IsIdentifierText(assignedName.Text(), core.LanguageVariantStandard) {
// If the class name was assigned from a string literal that is a valid identifier, create an
// identifier from it.
prefixName := c.Factory.NewIdentifier(assignedName.Text())
className = prefixName
}
}
}
}

// !!! Set WeakSet for private instance methods and accessor brand check

c.classScopeStack.Push(&classScope{
facts: ClassFactsNone,
classContainer: classContainer,
className: className,
})
}

func (c *EmitContext) EndClassLexicalEnvironment() {
c.classScopeStack.Pop()
}

func (c *EmitContext) GetClassContainer() *ast.ClassLikeDeclaration {
if c.classScopeStack.Len() == 0 {
return nil
}
scope := c.classScopeStack.Peek()
return scope.classContainer
}

func (c *EmitContext) GetClassFacts() ClassFacts {
scope := c.classScopeStack.Peek()
return scope.facts
}

//
// Name Generation
//
Expand Down Expand Up @@ -575,6 +648,14 @@ func (c *EmitContext) SourceMapRange(node *ast.Node) core.TextRange {
return node.Loc
}

// Sets `EFNoComments` on a node and removes any leading and trailing synthetic comments.
func (c *EmitContext) RemoveAllComments(node *ast.Node) {
c.AddEmitFlags(node, EFNoComments)
// !!! TODO: Also remove synthetic trailing/leading comments added by transforms
// emitNode.leadingComments = undefined;
// emitNode.trailingComments = undefined;
}

// Sets the range to use for a node when emitting source maps.
func (c *EmitContext) SetSourceMapRange(node *ast.Node, loc core.TextRange) {
emitNode := c.emitNodes.Get(node)
Expand Down Expand Up @@ -869,6 +950,91 @@ func (c *EmitContext) AddInitializationStatement(node *ast.Node) {
scope.initializationStatements = append(scope.initializationStatements, node)
}

func (c *EmitContext) AddPrivateIdentifierToEnvironment(node *ast.Node, name *ast.PrivateIdentifier) {
scope := c.classScopeStack.Peek()
scope.facts |= ClassFactsWillHoistInitializersToConstructor

previousInfo := c.GetPrivateIdentifierInfo(name.AsPrivateIdentifier())
isStatic := ast.HasStaticModifier(node)
isValid := (c.HasAutoGenerateInfo(name.AsNode()) || name.Text != "#constructor") && previousInfo == nil
if ast.IsAutoAccessorPropertyDeclaration(node) {
// !!!
} else if ast.IsPropertyDeclaration(node) {
if isStatic {
// !!!
} else {
className := scope.className
weakMapName := c.Factory.NewGeneratedNameForNodeEx(name.AsNode(), AutoGenerateOptions{
Prefix: "_" + className.Text() + "_",
})
c.AddVariableDeclaration(weakMapName)

c.setPrivateIdentifierInfo(name, NewPrivateIdentifierInstanceFieldInfo(
weakMapName.AsIdentifier(),
isValid,
))

scope.pendingExpressions = append(
scope.pendingExpressions,
c.Factory.NewAssignmentExpression(
weakMapName,
c.Factory.NewNewExpression(
c.Factory.NewIdentifier("WeakMap"),
nil, /*typeArguments*/
&ast.NodeList{},
),
),
)
}
} else if ast.IsMethodDeclaration(node) {
// !!!
} else if ast.IsGetAccessorDeclaration(node) {
// !!!
} else if ast.IsSetAccessorDeclaration(node) {
// !!!
}
}

func (c *EmitContext) setPrivateIdentifierInfo(name *ast.PrivateIdentifier, info PrivateIdentifierInfo) {
scope := c.classScopeStack.Peek()
if c.HasAutoGenerateInfo(name.AsNode()) {
if scope.generatedIdentifiers == nil {
scope.generatedIdentifiers = make(map[*ast.Node]PrivateIdentifierInfo)
}
scope.generatedIdentifiers[c.GetNodeForGeneratedName(name.AsNode())] = info
} else {
if scope.identifiers == nil {
scope.identifiers = make(map[string]PrivateIdentifierInfo)
}
scope.identifiers[name.Text] = info
}
}

// NOTE: This is the equivalent of `accessPrivateIdentifier` in Strada
func (c *EmitContext) GetPrivateIdentifierInfo(name *ast.PrivateIdentifier) PrivateIdentifierInfo {
if c.classScopeStack.Len() == 0 {
return nil
}
scope := c.classScopeStack.Peek()
if c.HasAutoGenerateInfo(name.AsNode()) {
if scope.generatedIdentifiers == nil {
return nil
} else {
return scope.generatedIdentifiers[c.GetNodeForGeneratedName(name.AsNode())]
}
} else {
if scope.identifiers == nil {
return nil
}
return scope.identifiers[name.Text]
}
}

func (c *EmitContext) GetPendingExpressions() []*ast.Expression {
scope := c.classScopeStack.Peek()
return scope.pendingExpressions
}

func (c *EmitContext) VisitFunctionBody(node *ast.BlockOrExpression, visitor *ast.NodeVisitor) *ast.BlockOrExpression {
// !!! c.resumeVariableEnvironment()
updated := visitor.VisitNode(node)
Expand Down
55 changes: 55 additions & 0 deletions internal/printer/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -690,3 +690,58 @@ func (f *NodeFactory) NewRewriteRelativeImportExtensionsHelper(firstArgument *as
ast.NodeFlagsNone,
)
}

// Allocate a new call expression to the `__classPrivateFieldGet` helper
func (f *NodeFactory) NewClassPrivateFieldGetHelper(receiver *ast.Expression, state *ast.Identifier, kind PrivateIdentifierKind, farg *ast.Identifier) *ast.Expression {
f.emitContext.RequestEmitHelper(classPrivateFieldGetHelper)
var arguments []*ast.Expression
if farg == nil {
arguments = []*ast.Expression{
receiver,
state.AsNode(),
f.NewStringLiteral(kind.String()),
}
} else {
arguments = []*ast.Expression{
receiver,
state.AsNode(),
f.NewStringLiteral(kind.String()),
farg.AsNode(),
}
}
return f.NewCallExpression(
f.NewUnscopedHelperName("__classPrivateFieldGet"),
nil, /*questionDotToken*/
nil, /*typeArguments*/
f.NewNodeList(arguments),
ast.NodeFlagsNone,
)
}

// Allocate a new call expression to the `__classPrivateFieldSet` helper
func (f *NodeFactory) NewClassPrivateFieldSetHelper(receiver *ast.Expression, state *ast.Identifier, value *ast.Expression, kind PrivateIdentifierKind, farg *ast.Identifier) *ast.Expression {
f.emitContext.RequestEmitHelper(classPrivateFieldSetHelper)
var arguments []*ast.Expression
if farg == nil {
arguments = []*ast.Expression{
receiver,
state.AsNode(),
value,
f.NewStringLiteral(kind.String()),
}
} else {
arguments = []*ast.Expression{
receiver,
state.AsNode(),
f.NewStringLiteral(kind.String()),
farg.AsNode(),
}
}
return f.NewCallExpression(
f.NewUnscopedHelperName("__classPrivateFieldSet"),
nil, /*questionDotToken*/
nil, /*typeArguments*/
f.NewNodeList(arguments),
ast.NodeFlagsNone,
)
}
23 changes: 23 additions & 0 deletions internal/printer/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,26 @@ var rewriteRelativeImportExtensionsHelper = &EmitHelper{
return path;
};`,
}

var classPrivateFieldGetHelper = &EmitHelper{
Name: "typescript:classPrivateFieldGet",
ImportName: "__classPrivateFieldGet",
Scoped: false,
Text: `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 classPrivateFieldSetHelper = &EmitHelper{
Name: "typescript:classPrivateFieldSet",
ImportName: "__classPrivateFieldSet",
Scoped: false,
Text: `var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};`,
}
Loading
Loading