package transformers import ( "slices" "strconv" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/core" ) // FlattenLevel controls how deeply binding/assignment patterns are decomposed. type FlattenLevel int const ( FlattenLevelAll FlattenLevel = iota // Fully decompose all patterns into individual assignments/bindings FlattenLevelObjectRest // Only decompose patterns containing object rest elements ) // CreateAssignmentCallback is a callback used to create custom assignment expressions during destructuring flattening. // When provided, the target will always be an Identifier, and the callback can wrap the assignment with additional logic // (e.g., export expressions in CJS modules or namespace member assignments). type CreateAssignmentCallback func(name *ast.IdentifierNode, value *ast.Expression, location *core.TextRange) *ast.Expression // FlattenDestructuringAssignment flattens a destructuring assignment expression into a sequence of // individual property/element access assignments. Supports custom assignment callbacks for module // export or namespace member expressions. func FlattenDestructuringAssignment( tx *Transformer, node *ast.Node, // VariableDeclaration | DestructuringAssignment needsValue bool, level FlattenLevel, createAssignmentCallback CreateAssignmentCallback, ) *ast.Expression { f := newFlattener(tx, level) f.createAssignmentCallback = createAssignmentCallback f.hoistTempVariables = true // Assignment mode callbacks f.emitBindingOrAssignment = (*flattener).emitAssignment f.createArrayBindingOrAssignmentPattern = (*flattener).createArrayAssignmentPattern f.createObjectBindingOrAssignmentPattern = (*flattener).createObjectAssignmentPattern f.createArrayBindingOrAssignmentElement = (*flattener).createArrayAssignmentElement return f.flattenDestructuringAssignment(node, needsValue) } // pendingDecl tracks a pending variable declaration during binding flattening. type pendingDecl struct { pendingExpressions []*ast.Node name *ast.Node value *ast.Node location core.TextRange original *ast.Node } // FlattenDestructuringBinding flattens a binding pattern in a variable declaration or parameter // into individual variable declarations. Returns a single VariableDeclaration, a SyntaxList of // declarations, or nil. func FlattenDestructuringBinding( tx *Transformer, node *ast.Node, // VariableDeclaration | ParameterDeclaration | BindingElement rval *ast.Node, level FlattenLevel, hoistTempVariables bool, skipInitializer bool, ) *ast.Node { f := newFlattener(tx, level) f.hoistTempVariables = hoistTempVariables // Binding mode callbacks f.emitBindingOrAssignment = (*flattener).emitBinding f.createArrayBindingOrAssignmentPattern = (*flattener).createArrayBindingPattern f.createObjectBindingOrAssignmentPattern = (*flattener).createObjectBindingPattern f.createArrayBindingOrAssignmentElement = (*flattener).createArrayBindingElement return f.flattenDestructuringBinding(node, rval, skipInitializer) } // flattener encapsulates the state and logic for flattening destructuring patterns. // It is equivalent to TypeScript's FlattenContext in destructuring.ts. type flattener struct { tx *Transformer level FlattenLevel createAssignmentCallback CreateAssignmentCallback // State expressions []*ast.Node declarations []pendingDecl hasTransformedPriorElement bool hoistTempVariables bool // Mode callbacks (set by FlattenDestructuringAssignment or FlattenDestructuringBinding) emitBindingOrAssignment func(f *flattener, target *ast.Node, value *ast.Node, location core.TextRange, original *ast.Node) createArrayBindingOrAssignmentPattern func(f *flattener, elements []*ast.Node) *ast.Node createObjectBindingOrAssignmentPattern func(f *flattener, elements []*ast.Node) *ast.Node createArrayBindingOrAssignmentElement func(f *flattener, expr *ast.Node) *ast.Node } func newFlattener(tx *Transformer, level FlattenLevel) *flattener { return &flattener{ tx: tx, level: level, } } // --- Assignment mode callbacks --- func (f *flattener) createArrayAssignmentPattern(elements []*ast.Node) *ast.Node { return f.tx.Factory().NewArrayLiteralExpression(f.tx.Factory().NewNodeList(elements), false) } func (f *flattener) createObjectAssignmentPattern(elements []*ast.Node) *ast.Node { return f.tx.Factory().NewObjectLiteralExpression(f.tx.Factory().NewNodeList(elements), false) } func (f *flattener) createArrayAssignmentElement(expr *ast.Node) *ast.Node { return expr } func (f *flattener) emitAssignment(target *ast.Node, value *ast.Node, location core.TextRange, original *ast.Node) { var expression *ast.Expression if f.createAssignmentCallback != nil && ast.IsIdentifier(target) { expression = f.createAssignmentCallback(target, value, &location) } else { expression = f.tx.Factory().NewAssignmentExpression(f.tx.Visitor().VisitNode(target), value) expression.Loc = location } f.tx.EmitContext().SetOriginal(expression, original) f.emitExpression(expression) } // --- Binding mode callbacks --- func (f *flattener) createArrayBindingPattern(elements []*ast.Node) *ast.Node { return f.tx.Factory().NewBindingPattern(ast.KindArrayBindingPattern, f.tx.Factory().NewNodeList(elements)) } func (f *flattener) createObjectBindingPattern(elements []*ast.Node) *ast.Node { return f.tx.Factory().NewBindingPattern(ast.KindObjectBindingPattern, f.tx.Factory().NewNodeList(elements)) } func (f *flattener) createArrayBindingElement(expr *ast.Node) *ast.Node { return f.tx.Factory().NewBindingElement(nil, nil, expr, nil) } func (f *flattener) emitBinding(target *ast.Node, value *ast.Node, location core.TextRange, original *ast.Node) { if len(f.expressions) > 0 { value = f.tx.Factory().InlineExpressions(append(f.expressions, value)) f.expressions = nil } f.declarations = append(f.declarations, pendingDecl{ name: target, value: value, location: location, original: original, }) } // --- Shared helpers --- func (f *flattener) emitExpression(expr *ast.Node) { f.expressions = append(f.expressions, expr) } func (f *flattener) ensureIdentifier(value *ast.Node, reuseIdentifierExpressions bool, location core.TextRange) *ast.Node { if reuseIdentifierExpressions && ast.IsIdentifier(value) { return value } temp := f.tx.Factory().NewTempVariable() if f.hoistTempVariables { f.tx.EmitContext().AddVariableDeclaration(temp) assign := f.tx.Factory().NewAssignmentExpression(temp, value) assign.Loc = location f.emitExpression(assign) } else { f.emitBindingOrAssignment(f, temp, value, location, nil) } return temp } func (f *flattener) createDefaultValueCheck(value *ast.Expression, defaultValue *ast.Expression, location core.TextRange) *ast.Node { value = f.ensureIdentifier(value, true, location) return f.tx.Factory().NewConditionalExpression( f.tx.Factory().NewTypeCheck(value, "undefined"), f.tx.Factory().NewToken(ast.KindQuestionToken), defaultValue, f.tx.Factory().NewToken(ast.KindColonToken), value, ) } func (f *flattener) createDestructuringPropertyAccess(value *ast.Node, propertyName *ast.Node) *ast.Node { if ast.IsComputedPropertyName(propertyName) { argumentExpression := f.ensureIdentifier(f.tx.Visitor().VisitNode(propertyName.Expression()), false, propertyName.Loc) return f.tx.Factory().NewElementAccessExpression(value, nil, argumentExpression, ast.NodeFlagsNone) } else if ast.IsStringOrNumericLiteralLike(propertyName) || ast.IsBigIntLiteral(propertyName) { argumentExpression := propertyName.Clone(f.tx.Factory()) return f.tx.Factory().NewElementAccessExpression(value, nil, argumentExpression, ast.NodeFlagsNone) } else { name := f.tx.Factory().NewIdentifier(propertyName.Text()) return f.tx.Factory().NewPropertyAccessExpression(value, nil, name, ast.NodeFlagsNone) } } // --- Entry points --- func (f *flattener) flattenDestructuringAssignment(node *ast.Node, needsValue bool) *ast.Expression { location := node.Loc var value *ast.Node if ast.IsDestructuringAssignment(node) { value = node.AsBinaryExpression().Right for ast.IsEmptyArrayLiteral(node.AsBinaryExpression().Left) || ast.IsEmptyObjectLiteral(node.AsBinaryExpression().Left) { if ast.IsDestructuringAssignment(value) { node = value location = node.Loc value = node.AsBinaryExpression().Right } else { return f.tx.Visitor().VisitNode(value) } } } if value != nil { value = f.tx.Visitor().VisitNode(value) if ast.IsIdentifier(value) && BindingOrAssignmentElementAssignsToName(node, value.Text()) || BindingOrAssignmentElementContainsNonLiteralComputedName(node) { value = f.ensureIdentifier(value, false, location) } else if needsValue { value = f.ensureIdentifier(value, true, location) } else if ast.NodeIsSynthesized(node) { location = value.Loc } } f.flattenBindingOrAssignmentElement(node, value, location, ast.IsDestructuringAssignment(node)) if value != nil && needsValue { if len(f.expressions) == 0 { return value } f.expressions = append(f.expressions, value) } res := f.tx.Factory().InlineExpressions(f.expressions) if res != nil { return res } return f.tx.Factory().NewOmittedExpression() } func (f *flattener) flattenDestructuringBinding(node *ast.Node, rval *ast.Node, skipInitializer bool) *ast.Node { if ast.IsVariableDeclaration(node) { initializer := GetInitializerOfBindingOrAssignmentElement(node) if initializer != nil && (ast.IsIdentifier(initializer) && BindingOrAssignmentElementAssignsToName(node, initializer.Text()) || BindingOrAssignmentElementContainsNonLiteralComputedName(node)) { initializer = f.ensureIdentifier(f.tx.Visitor().VisitNode(initializer), false, initializer.Loc) node = f.tx.Factory().UpdateVariableDeclaration(node.AsVariableDeclaration(), node.Name(), nil, nil, initializer) } } f.flattenBindingOrAssignmentElement(node, rval, node.Loc, skipInitializer) if len(f.expressions) > 0 { temp := f.tx.Factory().NewTempVariable() if f.hoistTempVariables { value := f.tx.Factory().InlineExpressions(f.expressions) f.expressions = nil f.emitBindingOrAssignment(f, temp, value, core.TextRange{}, nil) } else { f.tx.EmitContext().AddVariableDeclaration(temp) last := &f.declarations[len(f.declarations)-1] last.pendingExpressions = append(last.pendingExpressions, f.tx.Factory().NewAssignmentExpression(temp, last.value)) last.pendingExpressions = append(last.pendingExpressions, f.expressions...) last.value = temp } } decls := make([]*ast.Node, 0, len(f.declarations)) for _, pending := range f.declarations { expr := pending.value if len(pending.pendingExpressions) > 0 { expr = f.tx.Factory().InlineExpressions(append(pending.pendingExpressions, pending.value)) } decl := f.tx.Factory().NewVariableDeclaration(pending.name, nil, nil, expr) decl.Loc = pending.location if pending.original != nil { f.tx.EmitContext().SetOriginal(decl, pending.original) } decls = append(decls, decl) } if len(decls) == 1 { return decls[0] } if len(decls) == 0 { return nil } return f.tx.Factory().NewSyntaxList(decls) } // --- Core flattening --- func (f *flattener) flattenBindingOrAssignmentElement(element *ast.Node, value *ast.Node, location core.TextRange, skipInitializer bool) { bindingTarget := ast.GetTargetOfBindingOrAssignmentElement(element) if bindingTarget == nil { return } if !skipInitializer { initializer := f.tx.Visitor().VisitNode(GetInitializerOfBindingOrAssignmentElement(element)) if initializer != nil { if value != nil { value = f.createDefaultValueCheck(value, initializer, location) if !IsSimpleCopiableExpression(initializer) && (ast.IsBindingPattern(bindingTarget) || ast.IsAssignmentPattern(bindingTarget)) { value = f.ensureIdentifier(value, true, location) } } else { value = initializer } } else if value == nil { value = f.tx.Factory().NewVoidZeroExpression() } } if isObjectBindingOrAssignmentPattern(bindingTarget) { f.flattenObjectBindingOrAssignmentPattern(element, bindingTarget, value, location) } else if isArrayBindingOrAssignmentPattern(bindingTarget) { f.flattenArrayBindingOrAssignmentPattern(element, bindingTarget, value, location) } else { f.emitBindingOrAssignment(f, bindingTarget, value, location, element) } } func (f *flattener) flattenObjectBindingOrAssignmentPattern(parent *ast.Node, pattern *ast.Node, value *ast.Node, location core.TextRange) { elements := ast.GetElementsOfBindingOrAssignmentPattern(pattern) numElements := len(elements) if numElements != 1 { reuseIdentifierExpressions := !ast.IsDeclarationBindingElement(parent) || numElements != 0 value = f.ensureIdentifier(value, reuseIdentifierExpressions, location) } var bindingElements []*ast.Node var computedTempVariables []*ast.Node for i, element := range elements { if ast.GetRestIndicatorOfBindingOrAssignmentElement(element) == nil { propertyName := ast.TryGetPropertyNameOfBindingOrAssignmentElement(element) if f.level >= FlattenLevelObjectRest && element.SubtreeFacts()&(ast.SubtreeContainsRestOrSpread|ast.SubtreeContainsObjectRestOrSpread) == 0 && ast.GetTargetOfBindingOrAssignmentElement(element).SubtreeFacts()&(ast.SubtreeContainsRestOrSpread|ast.SubtreeContainsObjectRestOrSpread) == 0 && !ast.IsComputedPropertyName(propertyName) { bindingElements = append(bindingElements, f.tx.Visitor().VisitNode(element)) } else { if len(bindingElements) > 0 { f.emitBindingOrAssignment(f, f.createObjectBindingOrAssignmentPattern(f, bindingElements), value, location, pattern) bindingElements = nil } rhsValue := f.createDestructuringPropertyAccess(value, propertyName) if ast.IsComputedPropertyName(propertyName) { computedTempVariables = append(computedTempVariables, rhsValue.AsElementAccessExpression().ArgumentExpression) } f.flattenBindingOrAssignmentElement(element, rhsValue, element.Loc, false) } } else if i == numElements-1 { if len(bindingElements) > 0 { f.emitBindingOrAssignment(f, f.createObjectBindingOrAssignmentPattern(f, bindingElements), value, location, pattern) bindingElements = nil } rhsValue := f.tx.Factory().NewRestHelper(value, elements, computedTempVariables, pattern.Loc) f.flattenBindingOrAssignmentElement(element, rhsValue, element.Loc, false) } } if len(bindingElements) > 0 { f.emitBindingOrAssignment(f, f.createObjectBindingOrAssignmentPattern(f, bindingElements), value, location, pattern) } } type restIdElemPair struct { id *ast.Node element *ast.Node } func (f *flattener) flattenArrayBindingOrAssignmentPattern(parent *ast.Node, pattern *ast.Node, value *ast.Node, location core.TextRange) { elements := ast.GetElementsOfBindingOrAssignmentPattern(pattern) numElements := len(elements) if numElements != 1 && (f.level < FlattenLevelObjectRest || numElements == 0) || core.Every(elements, ast.IsOmittedExpression) { reuseIdentifierExpressions := !ast.IsDeclarationBindingElement(parent) || numElements != 0 value = f.ensureIdentifier(value, reuseIdentifierExpressions, location) } var bindingElements []*ast.Node var restContainingElements []restIdElemPair for i, element := range elements { if f.level >= FlattenLevelObjectRest { if element.SubtreeFacts()&ast.SubtreeContainsObjectRestOrSpread != 0 || f.hasTransformedPriorElement && !isSimpleBindingOrAssignmentElement(element) { f.hasTransformedPriorElement = true temp := f.tx.Factory().NewTempVariable() if f.hoistTempVariables { f.tx.EmitContext().AddVariableDeclaration(temp) } restContainingElements = append(restContainingElements, restIdElemPair{temp, element}) bindingElements = append(bindingElements, f.createArrayBindingOrAssignmentElement(f, temp)) } else { bindingElements = append(bindingElements, element) } } else if ast.IsOmittedExpression(element) { continue } else if ast.GetRestIndicatorOfBindingOrAssignmentElement(element) == nil { rhsValue := f.tx.Factory().NewElementAccessExpression(value, nil, f.tx.Factory().NewNumericLiteral(strconv.Itoa(i), ast.TokenFlagsNone), ast.NodeFlagsNone) f.flattenBindingOrAssignmentElement(element, rhsValue, element.Loc, false) } else if i == numElements-1 { rhsValue := f.tx.Factory().NewArraySliceCall(value, i) f.flattenBindingOrAssignmentElement(element, rhsValue, element.Loc, false) } } if len(bindingElements) > 0 { f.emitBindingOrAssignment(f, f.createArrayBindingOrAssignmentPattern(f, bindingElements), value, location, pattern) } if len(restContainingElements) > 0 { for _, pair := range restContainingElements { f.flattenBindingOrAssignmentElement(pair.element, pair.id, pair.element.Loc, false) } } } // --- Exported helper functions --- // BindingOrAssignmentElementAssignsToName checks if any target in a binding/assignment pattern assigns to the given name. func BindingOrAssignmentElementAssignsToName(element *ast.Node, name string) bool { target := ast.GetTargetOfBindingOrAssignmentElement(element) if target == nil { return false } if ast.IsBindingPattern(target) || ast.IsAssignmentPattern(target) { return bindingOrAssignmentPatternAssignsToName(target, name) } else if ast.IsIdentifier(target) { return target.Text() == name } return false } func bindingOrAssignmentPatternAssignsToName(pattern *ast.Node, name string) bool { elements := ast.GetElementsOfBindingOrAssignmentPattern(pattern) for _, element := range elements { if BindingOrAssignmentElementAssignsToName(element, name) { return true } } return false } // BindingOrAssignmentElementContainsNonLiteralComputedName checks if any element has a non-literal computed property name. func BindingOrAssignmentElementContainsNonLiteralComputedName(element *ast.Node) bool { propertyName := ast.TryGetPropertyNameOfBindingOrAssignmentElement(element) if propertyName != nil && ast.IsComputedPropertyName(propertyName) && !ast.IsLiteralExpression(propertyName.Expression()) { return true } target := ast.GetTargetOfBindingOrAssignmentElement(element) return target != nil && (ast.IsBindingPattern(target) || ast.IsAssignmentPattern(target)) && bindingOrAssignmentPatternContainsNonLiteralComputedName(target) } func bindingOrAssignmentPatternContainsNonLiteralComputedName(pattern *ast.Node) bool { elements := ast.GetElementsOfBindingOrAssignmentPattern(pattern) return slices.ContainsFunc(elements, BindingOrAssignmentElementContainsNonLiteralComputedName) } // GetInitializerOfBindingOrAssignmentElement returns the initializer/default value of a binding or assignment element. func GetInitializerOfBindingOrAssignmentElement(bindingElement *ast.Node) *ast.Node { if bindingElement == nil { return nil } if ast.IsDeclarationBindingElement(bindingElement) { return bindingElement.Initializer() } if ast.IsPropertyAssignment(bindingElement) { initializer := bindingElement.Initializer() if ast.IsAssignmentExpression(initializer, true) { return initializer.AsBinaryExpression().Right } return nil } if ast.IsShorthandPropertyAssignment(bindingElement) { return bindingElement.AsShorthandPropertyAssignment().ObjectAssignmentInitializer } if ast.IsAssignmentExpression(bindingElement, true) { return bindingElement.AsBinaryExpression().Right } if ast.IsSpreadElement(bindingElement) { return GetInitializerOfBindingOrAssignmentElement(bindingElement.Expression()) } return nil } func isObjectBindingOrAssignmentPattern(node *ast.Node) bool { return node != nil && (node.Kind == ast.KindObjectBindingPattern || node.Kind == ast.KindObjectLiteralExpression) } func isArrayBindingOrAssignmentPattern(node *ast.Node) bool { return node != nil && (node.Kind == ast.KindArrayBindingPattern || node.Kind == ast.KindArrayLiteralExpression) } func isSimpleBindingOrAssignmentElement(element *ast.Node) bool { target := ast.GetTargetOfBindingOrAssignmentElement(element) if target == nil || ast.IsOmittedExpression(target) { return true } propertyName := ast.TryGetPropertyNameOfBindingOrAssignmentElement(element) if propertyName != nil && !ast.IsPropertyNameLiteral(propertyName) { return false } initializer := GetInitializerOfBindingOrAssignmentElement(element) if initializer != nil && !IsSimpleInlineableExpression(initializer) { return false } if ast.IsBindingPattern(target) || ast.IsAssignmentPattern(target) { return core.Every(ast.GetElementsOfBindingOrAssignmentPattern(target), isSimpleBindingOrAssignmentElement) } return ast.IsIdentifier(target) }