Skip to content
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
4 changes: 2 additions & 2 deletions tsc/internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -2417,7 +2417,7 @@ type SourceFile struct {
CheckJsDirective *CheckJsDirective
NodeCount int
TextCount int
CommonJSModuleIndicator *Node
IsCommonJSModule bool
// If this is the SourceFile itself, then this module was "forced"
// to be an external module (previously "true").
ExternalModuleIndicator *Node
Expand Down Expand Up @@ -2738,7 +2738,7 @@ func (node *SourceFile) copyFrom(other *SourceFile) {
node.ReferencedFiles = other.ReferencedFiles
node.TypeReferenceDirectives = other.TypeReferenceDirectives
node.LibReferenceDirectives = other.LibReferenceDirectives
node.CommonJSModuleIndicator = other.CommonJSModuleIndicator
node.IsCommonJSModule = other.IsCommonJSModule
node.ExternalModuleIndicator = other.ExternalModuleIndicator
node.Flags |= other.Flags
}
Expand Down
4 changes: 2 additions & 2 deletions tsc/internal/ast/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -1737,12 +1737,12 @@ func IsExternalModule(file *SourceFile) bool {
}

func IsExternalOrCommonJSModule(file *SourceFile) bool {
return file.ExternalModuleIndicator != nil || file.CommonJSModuleIndicator != nil
return file.ExternalModuleIndicator != nil || file.IsCommonJSModule
}

// TODO: Should we deprecate `IsExternalOrCommonJSModule` in favor of this function?
func IsEffectiveExternalModule(node *SourceFile, compilerOptions *core.CompilerOptions) bool {
return IsExternalModule(node) || (isCommonJSContainingModuleKind(compilerOptions.GetEmitModuleKind()) && node.CommonJSModuleIndicator != nil)
return IsExternalModule(node) || (isCommonJSContainingModuleKind(compilerOptions.GetEmitModuleKind()) && node.IsCommonJSModule)
}

func isCommonJSContainingModuleKind(kind core.ModuleKind) bool {
Expand Down
82 changes: 47 additions & 35 deletions tsc/internal/binder/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,33 +52,34 @@ type Binder struct {
bindFunc func(*ast.Node) bool
unreachableFlow *ast.FlowNode

container *ast.Node
thisContainer *ast.Node
blockScopeContainer *ast.Node
lastContainer *ast.Node
currentFlow *ast.FlowNode
currentBreakTarget *ast.FlowLabel
currentContinueTarget *ast.FlowLabel
currentReturnTarget *ast.FlowLabel
currentTrueTarget *ast.FlowLabel
currentFalseTarget *ast.FlowLabel
currentExceptionTarget *ast.FlowLabel
preSwitchCaseFlow *ast.FlowNode
activeLabelList *ActiveLabel
emitFlags ast.NodeFlags
seenThisKeyword bool
hasExplicitReturn bool
hasFlowEffects bool
inAssignmentPattern bool
seenParseError bool
symbolCount int
notConstEnumOnlyModules collections.Set[*ast.Symbol]
symbolWithDataArena core.Arena[ast.SymbolWithData]
symbolExtraArena ast.SymbolExtraArena
flowNodeArena core.Arena[ast.FlowNode]
flowListArena core.Arena[ast.FlowList]
singleDeclarationsArena core.Arena[*ast.Node]
expandoAssignments []ExpandoAssignmentInfo
container *ast.Node
thisContainer *ast.Node
blockScopeContainer *ast.Node
lastContainer *ast.Node
currentFlow *ast.FlowNode
currentBreakTarget *ast.FlowLabel
currentContinueTarget *ast.FlowLabel
currentReturnTarget *ast.FlowLabel
currentTrueTarget *ast.FlowLabel
currentFalseTarget *ast.FlowLabel
currentExceptionTarget *ast.FlowLabel
preSwitchCaseFlow *ast.FlowNode
activeLabelList *ActiveLabel
emitFlags ast.NodeFlags
seenThisKeyword bool
hasExplicitReturn bool
hasFlowEffects bool
inAssignmentPattern bool
seenParseError bool
seenCommonJSModuleIndicator bool
symbolCount int
notConstEnumOnlyModules collections.Set[*ast.Symbol]
symbolWithDataArena core.Arena[ast.SymbolWithData]
symbolExtraArena ast.SymbolExtraArena
flowNodeArena core.Arena[ast.FlowNode]
flowListArena core.Arena[ast.FlowList]
singleDeclarationsArena core.Arena[*ast.Node]
expandoAssignments []ExpandoAssignmentInfo
}

type ActiveLabel struct {
Expand Down Expand Up @@ -758,7 +759,7 @@ func (b *Binder) bindPropertyWorker(node *ast.Node) {

func (b *Binder) bindSourceFileIfExternalModule() {
b.setExportContextFlag(b.file.AsNode())
if ast.IsExternalOrCommonJSModule(b.file) {
if b.isExternalOrCommonJSModule() {
b.bindSourceFileAsExternalModule()
} else if ast.IsJsonSourceFile(b.file) {
b.bindSourceFileAsExternalModule()
Expand Down Expand Up @@ -927,24 +928,34 @@ func (b *Binder) bindFunctionExpression(node *ast.Node) {
func (b *Binder) bindCallExpression(node *ast.Node) {
// We're only inspecting call expressions to detect CommonJS modules, so we can skip
// this check if we've already seen the module indicator
if b.file.CommonJSModuleIndicator == nil && ast.IsRequireCall(node, false /*requireStringLiteralLikeArgument*/) {
if !b.seenCommonJSModuleIndicator && ast.IsRequireCall(node, false /*requireStringLiteralLikeArgument*/) {
b.setCommonJSModuleIndicator(node)
}
}

// setCommonJSModuleIndicator reports whether node's file is a CommonJS module, which the
// parser has already determined. The file symbol is created at the first CommonJS construct
// in the walk, rather than up front, to preserve symbol creation order.
func (b *Binder) setCommonJSModuleIndicator(node *ast.Node) bool {
if b.file.ExternalModuleIndicator != nil && b.file.ExternalModuleIndicator != b.file.AsNode() {
if !b.file.IsCommonJSModule {
debug.Assert(b.file.ExternalModuleIndicator != nil && b.file.ExternalModuleIndicator != b.file.AsNode(), "parser missed a CommonJS module indicator")
return false
}
if b.file.CommonJSModuleIndicator == nil {
b.file.CommonJSModuleIndicator = node
if !b.seenCommonJSModuleIndicator {
b.seenCommonJSModuleIndicator = true
if b.file.ExternalModuleIndicator == nil {
b.bindSourceFileAsExternalModule()
}
}
return true
}

// isExternalOrCommonJSModule is like ast.IsExternalOrCommonJSModule, but a CommonJS file only
// counts as a module once the walk has reached its first CommonJS construct.
func (b *Binder) isExternalOrCommonJSModule() bool {
return b.file.ExternalModuleIndicator != nil || b.seenCommonJSModuleIndicator
}

func (b *Binder) bindClassLikeDeclaration(node *ast.Node) {
name := node.Name()
switch node.Kind {
Expand Down Expand Up @@ -1248,7 +1259,7 @@ func (b *Binder) bindBlockScopedDeclaration(node *ast.Node, symbolFlags ast.Symb
case ast.KindModuleDeclaration:
b.declareModuleMember(node, symbolFlags, symbolExcludes)
case ast.KindSourceFile:
if ast.IsExternalOrCommonJSModule(b.container.AsSourceFile()) {
if b.isExternalOrCommonJSModule() {
b.declareModuleMember(node, symbolFlags, symbolExcludes)
break
}
Expand Down Expand Up @@ -1612,12 +1623,13 @@ func (b *Binder) bindContainer(node *ast.Node, containerFlags ContainerFlags) {
b.bindBlockScopedDeclaration(statement, ast.SymbolFlagsTypeAlias, ast.SymbolFlagsTypeAliasExcludes)
}
}
if b.file.CommonJSModuleIndicator != nil {
debug.Assert(b.seenCommonJSModuleIndicator == b.file.IsCommonJSModule, "binder missed a CommonJS module indicator")
if b.seenCommonJSModuleIndicator {
b.declareCommonJSVariable("module")
b.declareCommonJSVariable("exports")
}
}
if ast.IsSourceFile(node) && ast.IsExternalOrCommonJSModule(node.AsSourceFile()) || ast.IsAmbientModule(node) {
if ast.IsSourceFile(node) && b.isExternalOrCommonJSModule() || ast.IsAmbientModule(node) {
b.bindCommonJSTypeExports(node.Symbol())
}
b.container = saveContainer
Expand Down
2 changes: 1 addition & 1 deletion tsc/internal/binder/nameresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ loop:
}
if name != ast.InternalSymbolNameDefault {
if result = r.lookup(moduleExports, name, meaning&ast.SymbolFlagsModuleMember); result != nil {
if ast.IsSourceFile(location) && location.AsSourceFile().CommonJSModuleIndicator != nil && result.Flags()&ast.SymbolFlagsType == 0 {
if ast.IsSourceFile(location) && location.AsSourceFile().IsCommonJSModule && result.Flags()&ast.SymbolFlagsType == 0 {
result = nil
} else {
break loop
Expand Down
4 changes: 2 additions & 2 deletions tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -12309,7 +12309,7 @@ func (c *Checker) getContextualThisParameterType(fn *ast.Node) *Type {
// Don't contextually type `this` as `exports` in `exports.Point = function(x, y) { this.x = x; this.y = y; }`
if inJs && ast.IsIdentifier(expression) {
sourceFile := ast.GetSourceFileOfNode(parent)
if sourceFile.CommonJSModuleIndicator != nil && c.getResolvedSymbol(expression).Flags()&ast.SymbolFlagsModuleExports != 0 {
if sourceFile.IsCommonJSModule && c.getResolvedSymbol(expression).Flags()&ast.SymbolFlagsModuleExports != 0 {
return nil
}
}
Expand Down Expand Up @@ -17341,7 +17341,7 @@ func (c *Checker) getTypeOfFuncClassEnumModuleWorker(symbol *ast.Symbol) *Type {
if symbol.Flags()&ast.SymbolFlagsModule != 0 && isShorthandAmbientModuleSymbol(symbol) {
return c.anyType
} else if symbol.Flags()&ast.SymbolFlagsValueModule != 0 && symbol.ValueDeclaration() != nil &&
ast.IsSourceFile(symbol.ValueDeclaration()) && symbol.ValueDeclaration().AsSourceFile().CommonJSModuleIndicator != nil {
ast.IsSourceFile(symbol.ValueDeclaration()) && symbol.ValueDeclaration().AsSourceFile().IsCommonJSModule {
resolvedModule := c.resolveExternalModuleSymbol(symbol, false /*dontResolveAlias*/)
if resolvedModule != symbol {
return c.getTypeOfSymbol(resolvedModule)
Expand Down
2 changes: 1 addition & 1 deletion tsc/internal/checker/emitresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func (r *EmitResolver) PrecalculateDeclarationEmitVisibility(file *ast.SourceFil

func isCommonJSModuleExports(node *ast.Node) bool {
if ast.IsBinaryExpression(node) && ast.IsExpressionStatement(node.Parent) && ast.IsSourceFile(node.Parent.Parent) &&
node.Parent.Parent.AsSourceFile().CommonJSModuleIndicator != nil {
node.Parent.Parent.AsSourceFile().IsCommonJSModule {
switch ast.GetAssignmentDeclarationKind(node) {
case ast.JSDeclarationKindModuleExports, ast.JSDeclarationKindExportsProperty:
return true
Expand Down
26 changes: 26 additions & 0 deletions tsc/internal/compiler/program_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,3 +621,29 @@ func BenchmarkNewProgram(b *testing.B) {
}
})
}

func TestUpdateProgramCommonJSFile(t *testing.T) {
t.Parallel()
files := map[string]any{
"/src/tsconfig.json": `{"compilerOptions":{"allowJs":true,"noLib":true},"files":["index.js"]}`,
"/src/index.js": "exports.a = 1;",
}
fs := vfstest.FromMap(files, tspath.CaseSensitive)
config, diagnostics := tsoptions.GetParsedCommandLineOfConfigFile("/src/tsconfig.json", nil, nil, tsoptions.NewDefaultParseConfigHost(fs), nil)
assert.Equal(t, len(diagnostics), 0)
program := NewProgram(ProgramOptions{Config: config, Host: NewCompilerHost(fs, "/", nil, nil, nil)})
program.BindSourceFiles()

update := func(content string) bool {
files["/src/index.js"] = content
host := NewCompilerHost(vfstest.FromMap(files, tspath.CaseSensitive), "/", nil, nil, nil)
var reused bool
program, _, reused = program.UpdateProgram("/src/index.js", host, nil, nil)
program.BindSourceFiles()
return reused
}

assert.Assert(t, update("exports.a = 2;"), "editing a CommonJS file should reuse the program")
assert.Assert(t, update("var a = 2;") == false, "a CommonJS file becoming a script should not reuse the program")
assert.Assert(t, update("exports.a = 2;") == false, "a script becoming a CommonJS file should not reuse the program")
}
2 changes: 1 addition & 1 deletion tsc/internal/ls/autoimport/fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -922,7 +922,7 @@ func detectSyntax(file *ast.SourceFile, options *core.CompilerOptions) fileSynta
// ExternalModuleIndicator may be the source file itself rather than a real
// statement, so we look at Imports() for actual import/export declarations.
func detectSyntaxIndicators(file *ast.SourceFile, options *core.CompilerOptions) (hasESM bool, hasCJS bool) {
hasCJS = file.CommonJSModuleIndicator != nil
hasCJS = file.IsCommonJSModule
if options.GetEmitModuleDetectionKind() != core.ModuleDetectionKindForce {
// ExternalModuleIndicator is reliable when moduleDetection is not "force"
hasESM = file.ExternalModuleIndicator != nil
Expand Down
58 changes: 56 additions & 2 deletions tsc/internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ type Parser struct {
currentParent *ast.Node
setParentFromContext ast.Visitor
reparsedClones []*ast.Node

hasCommonJSConstruct bool
jsdocHasCommonJSConstruct bool
}

func newParser() *Parser {
Expand Down Expand Up @@ -346,6 +349,8 @@ type ParserState struct {
reparsedClonesLen int
statementHasAwaitIdentifier bool
hasParseError bool
hasCommonJSConstruct bool
jsdocHasCommonJSConstruct bool
}

func (p *Parser) mark() ParserState {
Expand All @@ -358,6 +363,8 @@ func (p *Parser) mark() ParserState {
reparsedClonesLen: len(p.reparsedClones),
statementHasAwaitIdentifier: p.statementHasAwaitIdentifier,
hasParseError: p.hasParseError,
hasCommonJSConstruct: p.hasCommonJSConstruct,
jsdocHasCommonJSConstruct: p.jsdocHasCommonJSConstruct,
}
}

Expand All @@ -371,6 +378,8 @@ func (p *Parser) rewind(state ParserState) {
p.reparsedClones = p.reparsedClones[0:state.reparsedClonesLen]
p.statementHasAwaitIdentifier = state.statementHasAwaitIdentifier
p.hasParseError = state.hasParseError
p.hasCommonJSConstruct = state.hasCommonJSConstruct
p.jsdocHasCommonJSConstruct = state.jsdocHasCommonJSConstruct
}

func (p *Parser) lookAhead(callback func(p *Parser) bool) bool {
Expand Down Expand Up @@ -451,6 +460,10 @@ func (p *Parser) parseSourceFileWorker() *ast.SourceFile {
if !result.IsDeclarationFile && result.ExternalModuleIndicator != nil && len(p.possibleAwaitSpans) > 0 {
reparse := p.finishNode(p.reparseTopLevelAwait(result), pos)
if node != reparse {
// Reparsing can change which CommonJS constructs exist, so recompute from the final tree.
if p.contextFlags&ast.NodeFlagsJavaScriptFile != 0 {
p.hasCommonJSConstruct = containsCommonJSModuleIndicator(reparse)
}
result = reparse.AsSourceFile()
p.finishSourceFile(result, isDeclarationFile)
}
Expand Down Expand Up @@ -483,6 +496,47 @@ func (p *Parser) finishSourceFile(result *ast.SourceFile, isDeclarationFile bool
slices.SortFunc(p.reparsedClones, ast.CompareNodePositions)
result.ReparsedClones = slices.Clone(p.reparsedClones)
ast.SetExternalModuleIndicator(result, p.opts.ExternalModuleIndicatorOptions)
// CommonJS constructs only make a file a module when it has no ES module syntax.
if result.ExternalModuleIndicator == nil || result.ExternalModuleIndicator == result.AsNode() {
if !p.hasCommonJSConstruct && p.jsdocHasCommonJSConstruct {
// Only reparsed JSDoc clones that survive into the tree are bound.
p.hasCommonJSConstruct = containsCommonJSModuleIndicator(result.AsNode())
}
result.IsCommonJSModule = p.hasCommonJSConstruct
} else {
result.IsCommonJSModule = false
}
}

// checkCommonJSModuleIndicator records whether a JavaScript file contains a CommonJS construct
// (a require call, or an assignment or Object.defineProperty on exports or module.exports).
// The binder binds the same constructs as CommonJS exports. Nodes parsed within JSDoc are
// only bound if reparsed, so for those we just note that the final tree needs checking.
func (p *Parser) checkCommonJSModuleIndicator(node *ast.Node) *ast.Node {
if !p.hasCommonJSConstruct && node.Flags&ast.NodeFlagsJavaScriptFile != 0 && isCommonJSModuleIndicator(node) {
if node.Flags&ast.NodeFlagsJSDoc == 0 {
p.hasCommonJSConstruct = true
} else {
p.jsdocHasCommonJSConstruct = true
}
}
return node
}

func containsCommonJSModuleIndicator(node *ast.Node) bool {
var visit func(*ast.Node) bool
visit = func(n *ast.Node) bool {
return isCommonJSModuleIndicator(n) || n.ForEachChild(visit)
}
return visit(node)
}

func isCommonJSModuleIndicator(node *ast.Node) bool {
switch ast.GetAssignmentDeclarationKind(node) {
case ast.JSDeclarationKindModuleExports, ast.JSDeclarationKindExportsProperty, ast.JSDeclarationKindObjectDefinePropertyExports:
return true
}
return ast.IsRequireCall(node, false /*requireStringLiteralLikeArgument*/)
}

func (p *Parser) createJSDocCache() map[*ast.Node][]*ast.Node {
Expand Down Expand Up @@ -4740,7 +4794,7 @@ func (p *Parser) makeAsExpression(left *ast.Expression, right *ast.TypeNode) *as
}

func (p *Parser) makeBinaryExpression(left *ast.Expression, operatorToken *ast.Node, right *ast.Expression, pos int) *ast.Node {
return p.finishNode(p.factory.NewBinaryExpression(nil /*modifiers*/, left, nil /*typeNode*/, operatorToken, right), pos)
return p.checkCommonJSModuleIndicator(p.finishNode(p.factory.NewBinaryExpression(nil /*modifiers*/, left, nil /*typeNode*/, operatorToken, right), pos))
}

func (p *Parser) parseUnaryExpressionOrHigher() *ast.Expression {
Expand Down Expand Up @@ -5556,7 +5610,7 @@ func (p *Parser) parseCallExpressionRest(pos int, expression *ast.Expression) *a
inner := expression
argumentList := p.parseArgumentList()
isOptionalChain := questionDotToken != nil || p.tryReparseOptionalChain(expression)
expression = p.checkJSSyntax(p.finishNode(p.factory.NewCallExpression(expression, questionDotToken, typeArguments, argumentList, core.IfElse(isOptionalChain, ast.NodeFlagsOptionalChain, ast.NodeFlagsNone)), pos))
expression = p.checkCommonJSModuleIndicator(p.checkJSSyntax(p.finishNode(p.factory.NewCallExpression(expression, questionDotToken, typeArguments, argumentList, core.IfElse(isOptionalChain, ast.NodeFlagsOptionalChain, ast.NodeFlagsNone)), pos)))
p.unparseExpressionWithTypeArguments(inner, typeArguments, expression)
continue
}
Expand Down
Loading
Loading