Skip to content

Fix typedef binding with CJS exports= #826

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 6 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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions internal/api/encoder/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ func getChildrenPropertyMask(node *ast.Node) uint8 {
case ast.KindNamespaceExportDeclaration:
n := node.AsNamespaceExportDeclaration()
return (boolToByte(n.Modifiers() != nil) << 0) | (boolToByte(n.Name() != nil) << 1)
case ast.KindExportDeclaration:
case ast.KindExportDeclaration, ast.KindJSExportDeclaration:
n := node.AsExportDeclaration()
return (boolToByte(n.Modifiers() != nil) << 0) | (boolToByte(n.ExportClause != nil) << 1) | (boolToByte(n.ModuleSpecifier != nil) << 2) | (boolToByte(n.Attributes != nil) << 3)
case ast.KindExportSpecifier:
Expand Down Expand Up @@ -733,7 +733,7 @@ func getNodeDefinedData(node *ast.Node) uint32 {
case ast.KindExportAssignment:
n := node.AsExportAssignment()
return uint32(boolToByte(n.IsExportEquals)) << 24
case ast.KindExportDeclaration:
case ast.KindExportDeclaration, ast.KindJSExportDeclaration:
n := node.AsExportDeclaration()
return uint32(boolToByte(n.IsTypeOnly)) << 24
case ast.KindBlock:
Expand Down
22 changes: 17 additions & 5 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ func (n *Node) ModuleSpecifier() *Expression {
switch n.Kind {
case KindImportDeclaration:
return n.AsImportDeclaration().ModuleSpecifier
case KindExportDeclaration:
case KindExportDeclaration, KindJSExportDeclaration:
return n.AsExportDeclaration().ModuleSpecifier
case KindJSDocImportTag:
return n.AsJSDocImportTag().ModuleSpecifier
Expand Down Expand Up @@ -4482,19 +4482,27 @@ type ExportDeclaration struct {
Attributes *ImportAttributesNode // ImportAttributesNode. Optional
}

func (f *NodeFactory) NewExportDeclaration(modifiers *ModifierList, isTypeOnly bool, exportClause *NamedExportBindings, moduleSpecifier *Expression, attributes *ImportAttributesNode) *Node {
func (f *NodeFactory) newExportOrJSExportDeclaration(kind Kind, modifiers *ModifierList, isTypeOnly bool, exportClause *NamedExportBindings, moduleSpecifier *Expression, attributes *ImportAttributesNode) *Node {
data := &ExportDeclaration{}
data.modifiers = modifiers
data.IsTypeOnly = isTypeOnly
data.ExportClause = exportClause
data.ModuleSpecifier = moduleSpecifier
data.Attributes = attributes
return f.newNode(KindExportDeclaration, data)
return f.newNode(kind, data)
}

func (f *NodeFactory) NewExportDeclaration(modifiers *ModifierList, isTypeOnly bool, exportClause *NamedExportBindings, moduleSpecifier *Expression, attributes *ImportAttributesNode) *Node {
return f.newExportOrJSExportDeclaration(KindExportDeclaration, modifiers, isTypeOnly, exportClause, moduleSpecifier, attributes)
}

func (f *NodeFactory) NewJSExportDeclaration(moduleSpecifier *Expression) *Node {
return f.newExportOrJSExportDeclaration(KindJSExportDeclaration, nil /*modifiers*/, false /*isTypeOnly*/, nil /*exportClause*/, moduleSpecifier, nil /*attributes*/)
}

func (f *NodeFactory) UpdateExportDeclaration(node *ExportDeclaration, modifiers *ModifierList, isTypeOnly bool, exportClause *NamedExportBindings, moduleSpecifier *Expression, attributes *ImportAttributesNode) *Node {
if modifiers != node.modifiers || exportClause != node.ExportClause || moduleSpecifier != node.ModuleSpecifier || attributes != node.Attributes {
return updateNode(f.NewExportDeclaration(modifiers, isTypeOnly, exportClause, moduleSpecifier, attributes), node.AsNode(), f.hooks)
return updateNode(f.newExportOrJSExportDeclaration(node.Kind, modifiers, isTypeOnly, exportClause, moduleSpecifier, attributes), node.AsNode(), f.hooks)
}
return node.AsNode()
}
Expand All @@ -4508,7 +4516,7 @@ func (node *ExportDeclaration) VisitEachChild(v *NodeVisitor) *Node {
}

func (node *ExportDeclaration) Clone(f NodeFactoryCoercible) *Node {
return cloneNode(f.AsNodeFactory().NewExportDeclaration(node.Modifiers(), node.IsTypeOnly, node.ExportClause, node.ModuleSpecifier, node.Attributes), node.AsNode(), f.AsNodeFactory().hooks)
return cloneNode(f.AsNodeFactory().newExportOrJSExportDeclaration(node.Kind, node.Modifiers(), node.IsTypeOnly, node.ExportClause, node.ModuleSpecifier, node.Attributes), node.AsNode(), f.AsNodeFactory().hooks)
}

func (node *ExportDeclaration) computeSubtreeFacts() SubtreeFacts {
Expand All @@ -4523,6 +4531,10 @@ func IsExportDeclaration(node *Node) bool {
return node.Kind == KindExportDeclaration
}

func IsJSExportDeclaration(node *Node) bool {
return node.Kind == KindJSExportDeclaration
}

// NamespaceExport

type NamespaceExport struct {
Expand Down
1 change: 1 addition & 0 deletions internal/ast/kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ const (
// Reparsed JS nodes
KindJSTypeAliasDeclaration
KindJSExportAssignment
KindJSExportDeclaration
KindCommonJSExport
// Transformation nodes
KindNotEmittedStatement
Expand Down
17 changes: 9 additions & 8 deletions internal/ast/kind_stringer_generated.go

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions internal/ast/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ func isDeclarationStatementKind(kind Kind) bool {
KindImportDeclaration,
KindImportEqualsDeclaration,
KindExportDeclaration,
KindJSExportDeclaration,
KindExportAssignment,
KindJSExportAssignment,
KindNamespaceExportDeclaration:
Expand Down Expand Up @@ -1677,7 +1678,7 @@ func GetExternalModuleName(node *Node) *Expression {
switch node.Kind {
case KindImportDeclaration:
return node.AsImportDeclaration().ModuleSpecifier
case KindExportDeclaration:
case KindExportDeclaration, KindJSExportDeclaration:
return node.AsExportDeclaration().ModuleSpecifier
case KindJSDocImportTag:
return node.AsJSDocImportTag().ModuleSpecifier
Expand All @@ -1703,7 +1704,7 @@ func GetImportAttributes(node *Node) *Node {
switch node.Kind {
case KindImportDeclaration:
return node.AsImportDeclaration().Attributes
case KindExportDeclaration:
case KindExportDeclaration, KindJSExportDeclaration:
return node.AsExportDeclaration().Attributes
}
panic("Unhandled case in getImportAttributes")
Expand Down Expand Up @@ -2055,7 +2056,8 @@ func GetMeaningFromDeclaration(node *Node) SemanticMeaning {
KindImportDeclaration,
KindExportAssignment,
KindJSExportAssignment,
KindExportDeclaration:
KindExportDeclaration,
KindJSExportDeclaration:
return SemanticMeaningAll

// An external module can be a Value
Expand Down Expand Up @@ -2346,7 +2348,7 @@ func GetNamespaceDeclarationNode(node *Node) *Node {
}
case KindImportEqualsDeclaration:
return node
case KindExportDeclaration:
case KindExportDeclaration, KindJSExportDeclaration:
exportClause := node.AsExportDeclaration().ExportClause
if exportClause != nil && IsNamespaceExport(exportClause) {
return exportClause
Expand Down
36 changes: 33 additions & 3 deletions internal/binder/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type Binder struct {
flowNodePool core.Pool[ast.FlowNode]
flowListPool core.Pool[ast.FlowList]
singleDeclarationsPool core.Pool[*ast.Node]
delayedTypeAliases []*ast.Node
}

type ActiveLabel struct {
Expand Down Expand Up @@ -124,9 +125,32 @@ func bindSourceFile(file *ast.SourceFile, options *core.SourceFileAffectingCompi
b.bind(file.AsNode())
file.SymbolCount = b.symbolCount
file.ClassifiableNames = b.classifiableNames
b.delayedBindJSDocTypedefTag()
})
}

// top-level typedef binding is delayed because it changes based on whether `module.exports = x` is bound
func (b *Binder) delayedBindJSDocTypedefTag() {
if b.delayedTypeAliases == nil {
return
}
if b.file.Symbol != nil {
if exportEq := b.file.Symbol.Exports[ast.InternalSymbolNameExportEquals]; exportEq != nil && b.file.CommonJSModuleIndicator != nil {
for _, node := range b.delayedTypeAliases {
b.declareSymbol(ast.GetSymbolTable(&exportEq.Exports), exportEq /*parent*/, node, ast.SymbolFlagsTypeAlias, ast.SymbolFlagsTypeAliasExcludes)
Copy link
Member

@weswigham weswigham Apr 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works for one level of indirection to a local namespace-y thing, but what about an aliased namespacey thing? Eg

const mod = require("./mod.js")
/**
* @typedef {string} T
*/
module.exports = mod

or, abusing a ts construct to do it locally,

namespace ns {
  export namespace inner {}
}
import mod = ns.inner
/**
* @typedef {string} T
*/
module.exports = mod

mod's symbol will be an alias, and this will just patch the typedefs onto the alias symbol, and not the alias symbol's ultimate target, which, dollars to donuts, means it's going to effectively go missing without extra lookup logic in the checker, similar to what we used to have. 🥹

Copy link
Member Author

@sandersn sandersn Apr 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even simpler, this also reproduces the problem.

function f() { }
/** @typedef {string} T */
module.exports = f

b.declareSymbol(ast.GetLocals(b.file.AsNode()), b.file.Symbol, node, ast.SymbolFlagsTypeAlias, ast.SymbolFlagsTypeAliasExcludes)
}
return
}
}
// bind normally
b.container = b.file.AsNode()
b.blockScopeContainer = b.file.AsNode()
for _, node := range b.delayedTypeAliases {
b.bindBlockScopedDeclaration(node, ast.SymbolFlagsTypeAlias, ast.SymbolFlagsTypeAliasExcludes)
}
}

func (b *Binder) newSymbol(flags ast.SymbolFlags, name string) *ast.Symbol {
b.symbolCount++
result := b.symbolPool.New()
Expand Down Expand Up @@ -352,7 +376,7 @@ func (b *Binder) getDeclarationName(node *ast.Node) string {
return ast.InternalSymbolNameNew
case ast.KindIndexSignature:
return ast.InternalSymbolNameIndex
case ast.KindExportDeclaration:
case ast.KindExportDeclaration, ast.KindJSExportDeclaration:
return ast.InternalSymbolNameExportStar
case ast.KindSourceFile:
return ast.InternalSymbolNameExportEquals
Expand Down Expand Up @@ -686,8 +710,14 @@ func (b *Binder) bind(node *ast.Node) bool {
b.bindBlockScopedDeclaration(node, ast.SymbolFlagsInterface, ast.SymbolFlagsInterfaceExcludes)
case ast.KindCallExpression:
b.bindCallExpression(node)
case ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration:
case ast.KindTypeAliasDeclaration:
b.bindBlockScopedDeclaration(node, ast.SymbolFlagsTypeAlias, ast.SymbolFlagsTypeAliasExcludes)
case ast.KindJSTypeAliasDeclaration:
if b.file.AsNode() == b.container {
b.delayedTypeAliases = append(b.delayedTypeAliases, node)
} else {
b.bindBlockScopedDeclaration(node, ast.SymbolFlagsTypeAlias, ast.SymbolFlagsTypeAliasExcludes)
}
case ast.KindEnumDeclaration:
b.bindEnumDeclaration(node)
case ast.KindModuleDeclaration:
Expand All @@ -698,7 +728,7 @@ func (b *Binder) bind(node *ast.Node) bool {
b.bindNamespaceExportDeclaration(node)
case ast.KindImportClause:
b.bindImportClause(node)
case ast.KindExportDeclaration:
case ast.KindExportDeclaration, ast.KindJSExportDeclaration:
b.bindExportDeclaration(node)
case ast.KindExportAssignment, ast.KindJSExportAssignment:
b.bindExportAssignment(node)
Expand Down
2 changes: 2 additions & 0 deletions internal/binder/referenceresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ func (r *referenceResolver) isTypeOnlyAliasDeclaration(symbol *ast.Symbol) bool
switch node.Kind {
case ast.KindImportEqualsDeclaration, ast.KindExportDeclaration:
return node.IsTypeOnly()
case ast.KindJSExportDeclaration:
return false
case ast.KindImportClause, ast.KindImportSpecifier, ast.KindExportSpecifier:
if node.IsTypeOnly() {
return true
Expand Down
Loading
Loading