Repository: microsoft/typescript-go
Pull request #521: Synthetic export for nested namespace, redux
Author: login: sandersn
Created: 2025-03-12T13:55:26Z | Merged: 2025-03-20T12:45:49Z

Redo of #412 since I had to remake my fork as public.
 
Add a synthetic modifier list and export modifier when parsing nested namespaces. This is very similar to the way I'm going to add support for @type, @param and @return next, so I think this is a good test case.

When adding the synthetic node, I mark it with NodeFlagsSynthesized but provided a realistic position -- not -1,-1 I think having a real position will make error reporting easier for JSDoc tags, although it doesn't help for export keywords on nested namespaces. 

In astnav, if the binary search encounters a synthetic node, it filters out all synthetics and tries the binary search again. Thanks to @rbuckton for that idea.

I also erased the synthetic exports in the type eraser. I'm not 100% sure this is the right transform, but it is a TS-only construct, and it's where I'll erase all the JS-specific nodes as well. (Those which aren't types themselves, and therefore *already* erased by the type eraser.)
 
Fixes https://github.com/microsoft/typescript-go/issues/309


Diff:
diff --git a/internal/ast/ast.go b/internal/ast/ast.go
index 5acec0a3015..17a9fb0bf36 100644
--- a/internal/ast/ast.go
+++ b/internal/ast/ast.go
@@ -442,9 +442,6 @@ func (n *Node) ModifierFlags() ModifierFlags {
 	if modifiers != nil {
 		return modifiers.ModifierFlags
 	}
-	if n.Flags&NodeFlagsNestedNamespace != 0 {
-		return ModifierFlagsExport
-	}
 	return ModifierFlagsNone
 }
 
@@ -3404,22 +3401,23 @@ type ModuleDeclaration struct {
 	ModifiersBase
 	LocalsContainerBase
 	BodyBase
-	name *ModuleName // ModuleName
+	name    *ModuleName // ModuleName
+	Keyword Kind        // KindModuleKeyword, KindNamespaceKeyword, KindGlobalKeyword (global augmentation)
 }
 
-func (f *NodeFactory) NewModuleDeclaration(modifiers *ModifierList, name *ModuleName, body *ModuleBody, flags NodeFlags) *Node {
+func (f *NodeFactory) NewModuleDeclaration(modifiers *ModifierList, keyword Kind, name *ModuleName, body *ModuleBody) *Node {
 	data := &ModuleDeclaration{}
 	data.modifiers = modifiers
+	data.Keyword = keyword
 	data.name = name
 	data.Body = body
 	node := newNode(KindModuleDeclaration, data, f.hooks)
-	node.Flags |= flags & (NodeFlagsNamespace | NodeFlagsNestedNamespace | NodeFlagsGlobalAugmentation)
 	return node
 }
 
-func (f *NodeFactory) UpdateModuleDeclaration(node *ModuleDeclaration, modifiers *ModifierList, name *ModuleName, body *ModuleBody) *Node {
-	if modifiers != node.modifiers || name != node.name || body != node.Body {
-		return updateNode(f.NewModuleDeclaration(modifiers, name, body, node.Flags), node.AsNode(), f.hooks)
+func (f *NodeFactory) UpdateModuleDeclaration(node *ModuleDeclaration, modifiers *ModifierList, keyword Kind, name *ModuleName, body *ModuleBody) *Node {
+	if modifiers != node.modifiers || keyword != node.Keyword || name != node.name || body != node.Body {
+		return updateNode(f.NewModuleDeclaration(modifiers, keyword, name, body), node.AsNode(), f.hooks)
 	}
 	return node.AsNode()
 }
@@ -3429,11 +3427,11 @@ func (node *ModuleDeclaration) ForEachChild(v Visitor) bool {
 }
 
 func (node *ModuleDeclaration) VisitEachChild(v *NodeVisitor) *Node {
-	return v.Factory.UpdateModuleDeclaration(node, v.visitModifiers(node.modifiers), v.visitNode(node.name), v.visitNode(node.Body))
+	return v.Factory.UpdateModuleDeclaration(node, v.visitModifiers(node.modifiers), node.Keyword, v.visitNode(node.name), v.visitNode(node.Body))
 }
 
 func (node *ModuleDeclaration) Clone(f *NodeFactory) *Node {
-	return cloneNode(f.NewModuleDeclaration(node.Modifiers(), node.Name(), node.Body, node.Flags), node.AsNode(), f.hooks)
+	return cloneNode(f.NewModuleDeclaration(node.Modifiers(), node.Keyword, node.Name(), node.Body), node.AsNode(), f.hooks)
 }
 
 func (node *ModuleDeclaration) Name() *DeclarationName {
diff --git a/internal/ast/nodeflags.go b/internal/ast/nodeflags.go
index 62e78910425..2feb4f2a525 100644
--- a/internal/ast/nodeflags.go
+++ b/internal/ast/nodeflags.go
@@ -7,25 +7,22 @@ const (
 	NodeFlagsLet                             NodeFlags = 1 << 0  // Variable declaration
 	NodeFlagsConst                           NodeFlags = 1 << 1  // Variable declaration
 	NodeFlagsUsing                           NodeFlags = 1 << 2  // Variable declaration
-	NodeFlagsNestedNamespace                 NodeFlags = 1 << 3  // Namespace declaration
+	NodeFlagsReparsed                        NodeFlags = 1 << 3  // Node was synthesized during parsing
 	NodeFlagsSynthesized                     NodeFlags = 1 << 4  // Node was synthesized during transformation
-	NodeFlagsNamespace                       NodeFlags = 1 << 5  // Namespace declaration
-	NodeFlagsOptionalChain                   NodeFlags = 1 << 6  // Chained MemberExpression rooted to a pseudo-OptionalExpression
-	NodeFlagsExportContext                   NodeFlags = 1 << 7  // Export context (initialized by binding)
-	NodeFlagsContainsThis                    NodeFlags = 1 << 8  // Interface contains references to "this"
-	NodeFlagsHasImplicitReturn               NodeFlags = 1 << 9  // If function implicitly returns on one of codepaths (initialized by binding)
-	NodeFlagsHasExplicitReturn               NodeFlags = 1 << 10 // If function has explicit reachable return on one of codepaths (initialized by binding)
-	NodeFlagsGlobalAugmentation              NodeFlags = 1 << 11 // Set if module declaration is an augmentation for the global scope
-	NodeFlagsHasAsyncFunctions               NodeFlags = 1 << 12 // If the file has async functions (initialized by binding)
-	NodeFlagsDisallowInContext               NodeFlags = 1 << 13 // If node was parsed in a context where 'in-expressions' are not allowed
-	NodeFlagsYieldContext                    NodeFlags = 1 << 14 // If node was parsed in the 'yield' context created when parsing a generator
-	NodeFlagsDecoratorContext                NodeFlags = 1 << 15 // If node was parsed as part of a decorator
-	NodeFlagsAwaitContext                    NodeFlags = 1 << 16 // If node was parsed in the 'await' context created when parsing an async function
-	NodeFlagsDisallowConditionalTypesContext NodeFlags = 1 << 17 // If node was parsed in a context where conditional types are not allowed
-	NodeFlagsThisNodeHasError                NodeFlags = 1 << 18 // If the parser encountered an error when parsing the code that created this node
-	NodeFlagsJavaScriptFile                  NodeFlags = 1 << 19 // If node was parsed in a JavaScript
-	NodeFlagsThisNodeOrAnySubNodesHasError   NodeFlags = 1 << 20 // If this node or any of its children had an error
-	NodeFlagsHasAggregatedChildData          NodeFlags = 1 << 21 // If we've computed data from children and cached it in this node
+	NodeFlagsOptionalChain                   NodeFlags = 1 << 5  // Chained MemberExpression rooted to a pseudo-OptionalExpression
+	NodeFlagsExportContext                   NodeFlags = 1 << 6  // Export context (initialized by binding)
+	NodeFlagsContainsThis                    NodeFlags = 1 << 7  // Interface contains references to "this"
+	NodeFlagsHasImplicitReturn               NodeFlags = 1 << 8  // If function implicitly returns on one of codepaths (initialized by binding)
+	NodeFlagsHasExplicitReturn               NodeFlags = 1 << 9  // If function has explicit reachable return on one of codepaths (initialized by binding)
+	NodeFlagsDisallowInContext               NodeFlags = 1 << 10 // If node was parsed in a context where 'in-expressions' are not allowed
+	NodeFlagsYieldContext                    NodeFlags = 1 << 11 // If node was parsed in the 'yield' context created when parsing a generator
+	NodeFlagsDecoratorContext                NodeFlags = 1 << 12 // If node was parsed as part of a decorator
+	NodeFlagsAwaitContext                    NodeFlags = 1 << 13 // If node was parsed in the 'await' context created when parsing an async function
+	NodeFlagsDisallowConditionalTypesContext NodeFlags = 1 << 14 // If node was parsed in a context where conditional types are not allowed
+	NodeFlagsThisNodeHasError                NodeFlags = 1 << 15 // If the parser encountered an error when parsing the code that created this node
+	NodeFlagsJavaScriptFile                  NodeFlags = 1 << 16 // If node was parsed in a JavaScript
+	NodeFlagsThisNodeOrAnySubNodesHasError   NodeFlags = 1 << 17 // If this node or any of its children had an error
+	NodeFlagsHasAggregatedChildData          NodeFlags = 1 << 18 // If we've computed data from children and cached it in this node
 
 	// These flags will be set when the parser encounters a dynamic import expression or 'import.meta' to avoid
 	// walking the tree if the flags are not set. However, these flags are just a approximation
@@ -36,22 +33,21 @@ const (
 	// removal, it is likely that users will add the import anyway.
 	// The advantage of this approach is its simplicity. For the case of batch compilation,
 	// we guarantee that users won't have to pay the price of walking the tree if a dynamic import isn't used.
-	NodeFlagsPossiblyContainsDynamicImport NodeFlags = 1 << 22
-	NodeFlagsPossiblyContainsImportMeta    NodeFlags = 1 << 23
+	NodeFlagsPossiblyContainsDynamicImport NodeFlags = 1 << 19
+	NodeFlagsPossiblyContainsImportMeta    NodeFlags = 1 << 20
 
-	NodeFlagsHasJSDoc        NodeFlags = 1 << 24 // If node has preceding JSDoc comment(s)
-	NodeFlagsJSDoc           NodeFlags = 1 << 25 // If node was parsed inside jsdoc
-	NodeFlagsAmbient         NodeFlags = 1 << 26 // If node was inside an ambient context -- a declaration file, or inside something with the `declare` modifier.
-	NodeFlagsInWithStatement NodeFlags = 1 << 27 // If any ancestor of node was the `statement` of a WithStatement (not the `expression`)
-	NodeFlagsJsonFile        NodeFlags = 1 << 28 // If node was parsed in a Json
-	NodeFlagsDeprecated      NodeFlags = 1 << 30 // If has '@deprecated' JSDoc tag
+	NodeFlagsHasJSDoc        NodeFlags = 1 << 21 // If node has preceding JSDoc comment(s)
+	NodeFlagsJSDoc           NodeFlags = 1 << 22 // If node was parsed inside jsdoc
+	NodeFlagsAmbient         NodeFlags = 1 << 23 // If node was inside an ambient context -- a declaration file, or inside something with the `declare` modifier.
+	NodeFlagsInWithStatement NodeFlags = 1 << 24 // If any ancestor of node was the `statement` of a WithStatement (not the `expression`)
+	NodeFlagsJsonFile        NodeFlags = 1 << 25 // If node was parsed in a Json
+	NodeFlagsDeprecated      NodeFlags = 1 << 26 // If has '@deprecated' JSDoc tag
 
 	NodeFlagsBlockScoped = NodeFlagsLet | NodeFlagsConst | NodeFlagsUsing
 	NodeFlagsConstant    = NodeFlagsConst | NodeFlagsUsing
 	NodeFlagsAwaitUsing  = NodeFlagsConst | NodeFlagsUsing // Variable declaration (NOTE: on a single node these flags would otherwise be mutually exclusive)
 
-	NodeFlagsReachabilityCheckFlags   = NodeFlagsHasImplicitReturn | NodeFlagsHasExplicitReturn
-	NodeFlagsReachabilityAndEmitFlags = NodeFlagsReachabilityCheckFlags | NodeFlagsHasAsyncFunctions
+	NodeFlagsReachabilityCheckFlags = NodeFlagsHasImplicitReturn | NodeFlagsHasExplicitReturn
 
 	// Parsing context flags
 	NodeFlagsContextFlags NodeFlags = NodeFlagsDisallowInContext | NodeFlagsDisallowConditionalTypesContext | NodeFlagsYieldContext | NodeFlagsDecoratorContext | NodeFlagsAwaitContext | NodeFlagsJavaScriptFile | NodeFlagsInWithStatement | NodeFlagsAmbient
@@ -65,6 +61,5 @@ const (
 	NodeFlagsPermanentlySetIncrementalFlags NodeFlags = NodeFlagsPossiblyContainsDynamicImport | NodeFlagsPossiblyContainsImportMeta
 
 	// The following flags repurpose other NodeFlags as different meanings for Identifier nodes
-	NodeFlagsIdentifierHasExtendedUnicodeEscape NodeFlags = NodeFlagsContainsThis      // Indicates whether the identifier contains an extended unicode escape sequence
-	NodeFlagsIdentifierIsInJSDocNamespace       NodeFlags = NodeFlagsHasAsyncFunctions // Indicates whether the identifier is part of a JSDoc namespace
+	NodeFlagsIdentifierHasExtendedUnicodeEscape NodeFlags = NodeFlagsContainsThis // Indicates whether the identifier contains an extended unicode escape sequence
 )
diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go
index e8262ce04f8..4374e8dea56 100644
--- a/internal/ast/utilities.go
+++ b/internal/ast/utilities.go
@@ -80,6 +80,17 @@ func PositionIsSynthesized(pos int) bool {
 	return pos < 0
 }
 
+func FindLastVisibleNode(nodes []*Node) *Node {
+	fromEnd := 1
+	for fromEnd <= len(nodes) && nodes[len(nodes)-fromEnd].Flags&NodeFlagsReparsed != 0 {
+		fromEnd++
+	}
+	if fromEnd <= len(nodes) {
+		return nodes[len(nodes)-fromEnd]
+	}
+	return nil
+}
+
 func NodeKindIs(node *Node, kinds ...Kind) bool {
 	return slices.Contains(kinds, node.Kind)
 }
@@ -1476,7 +1487,7 @@ func IsExportNamespaceAsDefaultDeclaration(node *Node) bool {
 }
 
 func IsGlobalScopeAugmentation(node *Node) bool {
-	return node.Flags&NodeFlagsGlobalAugmentation != 0
+	return IsModuleDeclaration(node) && node.AsModuleDeclaration().Keyword == KindGlobalKeyword
 }
 
 func IsModuleAugmentationExternal(node *Node) bool {
@@ -2187,10 +2198,6 @@ func getModuleInstanceStateWorker(node *Node, ancestors []*Node, visited map[Nod
 		return state
 	case KindModuleDeclaration:
 		return getModuleInstanceState(node, ancestors, visited)
-	case KindIdentifier:
-		if node.Flags&NodeFlagsIdentifierIsInJSDocNamespace != 0 {
-			return ModuleInstanceStateNonInstantiated
-		}
 	}
 	return ModuleInstanceStateInstantiated
 }
diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go
index 0c09257277b..6a1e7cd57fb 100644
--- a/internal/astnav/tokens.go
+++ b/internal/astnav/tokens.go
@@ -64,7 +64,7 @@ func getTokenAtPosition(
 	visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node {
 		// We can't abort visiting children, so once a match is found, we set `next`
 		// and do nothing on subsequent visits.
-		if node != nil && next == nil {
+		if node != nil && node.Flags&ast.NodeFlagsReparsed == 0 && next == nil {
 			switch testNode(node) {
 			case -1:
 				if !ast.IsJSDocKind(node.Kind) {
@@ -81,20 +81,6 @@ func getTokenAtPosition(
 		return node
 	}
 
-	visitNodes := func(nodes []*ast.Node) {
-		index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, node *ast.Node) int {
-			cmp := testNode(node)
-			if cmp < 0 {
-				left = node.End()
-			}
-			return cmp
-		})
-
-		if match {
-			next = nodes[index]
-		}
-	}
-
 	visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
 		if nodeList != nil && len(nodeList.Nodes) > 0 && next == nil {
 			if nodeList.End() == position && includePrecedingTokenAtEndPosition != nil {
@@ -103,7 +89,33 @@ func getTokenAtPosition(
 			} else if nodeList.End() <= position {
 				left = nodeList.End()
 			} else if nodeList.Pos() <= position {
-				visitNodes(nodeList.Nodes)
+				nodes := nodeList.Nodes
+				index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, node *ast.Node) int {
+					if node.Flags&ast.NodeFlagsReparsed != 0 {
+						return 0
+					}
+					cmp := testNode(node)
+					if cmp < 0 {
+						left = node.End()
+					}
+					return cmp
+				})
+				if match && nodes[index].Flags&ast.NodeFlagsReparsed != 0 {
+					// filter and search again
+					nodes = core.Filter(nodes, func(node *ast.Node) bool {
+						return node.Flags&ast.NodeFlagsReparsed == 0
+					})
+					index, match = core.BinarySearchUniqueFunc(nodes, func(middle int, node *ast.Node) int {
+						cmp := testNode(node)
+						if cmp < 0 {
+							left = node.End()
+						}
+						return cmp
+					})
+				}
+				if match {
+					next = nodes[index]
+				}
 			}
 		}
 		return nodeList
@@ -195,14 +207,18 @@ func findRightmostNode(node *ast.Node) *ast.Node {
 		VisitNode:  visitNode,
 		VisitToken: visitNode,
 		VisitNodes: func(nodeList *ast.NodeList, visitor *ast.NodeVisitor) *ast.NodeList {
-			if nodeList != nil && len(nodeList.Nodes) > 0 {
-				next = nodeList.Nodes[len(nodeList.Nodes)-1]
+			if nodeList != nil {
+				if rightmost := ast.FindLastVisibleNode(nodeList.Nodes); rightmost != nil {
+					next = rightmost
+				}
 			}
 			return nodeList
 		},
 		VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList {
-			if modifiers != nil && len(modifiers.Nodes) > 0 {
-				next = modifiers.Nodes[len(modifiers.Nodes)-1]
+			if modifiers != nil {
+				if rightmost := ast.FindLastVisibleNode(modifiers.Nodes); rightmost != nil {
+					next = rightmost
+				}
 			}
 			return modifiers
 		},
diff --git a/internal/binder/binder.go b/internal/binder/binder.go
index 55e6fff575b..dc4a64308ca 100644
--- a/internal/binder/binder.go
+++ b/internal/binder/binder.go
@@ -892,9 +892,6 @@ func (b *Binder) hasExportDeclarations(node *ast.Node) bool {
 }
 
 func (b *Binder) bindFunctionExpression(node *ast.Node) {
-	if !b.file.IsDeclarationFile && node.Flags&ast.NodeFlagsAmbient == 0 && isAsyncFunction(node) {
-		b.emitFlags |= ast.NodeFlagsHasAsyncFunctions
-	}
 	setFlowNode(node, b.currentFlow)
 	bindingName := ast.InternalSymbolNameFunction
 	if ast.IsFunctionExpression(node) && node.AsFunctionExpression().Name() != nil {
@@ -938,9 +935,6 @@ func (b *Binder) bindClassLikeDeclaration(node *ast.Node) {
 }
 
 func (b *Binder) bindPropertyOrMethodOrAccessor(node *ast.Node, symbolFlags ast.SymbolFlags, symbolExcludes ast.SymbolFlags) {
-	if !b.file.IsDeclarationFile && node.Flags&ast.NodeFlagsAmbient == 0 && isAsyncFunction(node) {
-		b.emitFlags |= ast.NodeFlagsHasAsyncFunctions
-	}
 	if b.currentFlow != nil && ast.IsObjectLiteralOrClassExpressionMethodOrAccessor(node) {
 		setFlowNode(node, b.currentFlow)
 	}
@@ -1065,9 +1059,6 @@ func (b *Binder) bindParameter(node *ast.Node) {
 }
 
 func (b *Binder) bindFunctionDeclaration(node *ast.Node) {
-	if !b.file.IsDeclarationFile && node.Flags&ast.NodeFlagsAmbient == 0 && isAsyncFunction(node) {
-		b.emitFlags |= ast.NodeFlagsHasAsyncFunctions
-	}
 	b.checkStrictModeFunctionName(node)
 	if b.inStrictMode {
 		b.checkStrictModeFunctionDeclaration(node)
@@ -1431,7 +1422,7 @@ func (b *Binder) bindContainer(node *ast.Node, containerFlags ContainerFlags) {
 		b.hasExplicitReturn = false
 		b.bindChildren(node)
 		// Reset all reachability check related flags on node (for incremental scenarios)
-		node.Flags &= ^ast.NodeFlagsReachabilityAndEmitFlags
+		node.Flags &= ^ast.NodeFlagsReachabilityCheckFlags
 		if b.currentFlow.Flags&ast.FlowFlagsUnreachable == 0 && containerFlags&ContainerFlagsIsFunctionLike != 0 {
 			bodyData := node.BodyData()
 			if bodyData != nil && ast.NodeIsPresent(bodyData.Body) {
diff --git a/internal/checker/checker.go b/internal/checker/checker.go
index 045fb6c9c50..fadb43f4644 100644
--- a/internal/checker/checker.go
+++ b/internal/checker/checker.go
@@ -3379,7 +3379,7 @@ func (c *Checker) getEffectiveDeclarationFlags(n *ast.Node, flagsToCheck ast.Mod
 	// because those flags have no useful semantics there.
 	if !ast.IsInterfaceDeclaration(n.Parent) && !ast.IsClassDeclaration(n.Parent) && !ast.IsClassExpression(n.Parent) && n.Flags&ast.NodeFlagsAmbient != 0 {
 		container := getEnclosingContainer(n)
-		if container != nil && container.Flags&ast.NodeFlagsExportContext != 0 && flags&ast.ModifierFlagsAmbient == 0 && !(ast.IsModuleBlock(n.Parent) && ast.IsModuleDeclaration(n.Parent.Parent) && ast.IsGlobalScopeAugmentation(n.Parent.Parent)) {
+		if container != nil && container.Flags&ast.NodeFlagsExportContext != 0 && flags&ast.ModifierFlagsAmbient == 0 && !(ast.IsModuleBlock(n.Parent) && ast.IsGlobalScopeAugmentation(n.Parent.Parent)) {
 			// It is nested in an ambient export context, which means it is automatically exported
 			flags |= ast.ModifierFlagsExport
 		}
@@ -4750,7 +4750,7 @@ func (c *Checker) checkModuleDeclaration(node *ast.Node) {
 	}
 	if ast.IsIdentifier(node.Name()) {
 		c.checkCollisionsForDeclarationName(node, node.Name())
-		if node.Flags&(ast.NodeFlagsNamespace|ast.NodeFlagsGlobalAugmentation) == 0 {
+		if node.AsModuleDeclaration().Keyword == ast.KindModuleKeyword {
 			tokenRange := getNonModifierTokenRangeOfNode(node)
 			c.suggestionDiagnostics.Add(ast.NewDiagnostic(ast.GetSourceFileOfNode(node), tokenRange, diagnostics.A_namespace_declaration_should_not_be_declared_using_the_module_keyword_Please_use_the_namespace_keyword_instead))
 		}
diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go
index 73aa77ea4fe..b0c8d025777 100644
--- a/internal/checker/utilities.go
+++ b/internal/checker/utilities.go
@@ -501,7 +501,7 @@ func getSourceFileOfModule(module *ast.Symbol) *ast.SourceFile {
 
 func getNonAugmentationDeclaration(symbol *ast.Symbol) *ast.Node {
 	return core.Find(symbol.Declarations, func(d *ast.Node) bool {
-		return !isExternalModuleAugmentation(d) && !(ast.IsModuleDeclaration(d) && ast.IsGlobalScopeAugmentation(d))
+		return !isExternalModuleAugmentation(d) && !ast.IsGlobalScopeAugmentation(d)
 	})
 }
 
@@ -1841,7 +1841,9 @@ func isModuleExportsAccessExpression(node *ast.Node) bool {
 func getNonModifierTokenRangeOfNode(node *ast.Node) core.TextRange {
 	pos := node.Pos()
 	if node.Modifiers() != nil {
-		pos = core.LastOrNil(node.Modifiers().Nodes).End()
+		if last := ast.FindLastVisibleNode(node.Modifiers().Nodes); last != nil {
+			pos = last.Pos()
+		}
 	}
 	return scanner.GetRangeOfTokenAtPosition(ast.GetSourceFileOfNode(node), pos)
 }
diff --git a/internal/parser/jsdoc.go b/internal/parser/jsdoc.go
index 0149988fa08..662b3137143 100644
--- a/internal/parser/jsdoc.go
+++ b/internal/parser/jsdoc.go
@@ -977,18 +977,11 @@ func (p *Parser) parseJSDocTypeNameWithNamespace(nested bool) *ast.Node {
 	typeNameOrNamespaceName := p.parseJSDocIdentifierName(nil)
 	if p.parseOptional(ast.KindDotToken) {
 		body := p.parseJSDocTypeNameWithNamespace(true)
-		var flags ast.NodeFlags
-		if nested {
-			flags = ast.NodeFlagsNestedNamespace
-		}
-		jsDocNamespaceNode := p.factory.NewModuleDeclaration(nil, typeNameOrNamespaceName, body, flags)
+		jsDocNamespaceNode := p.factory.NewModuleDeclaration(nil /*modifiers*/, ast.KindModuleKeyword, typeNameOrNamespaceName, body)
 		p.finishNode(jsDocNamespaceNode, start)
 		return jsDocNamespaceNode
 	}
 
-	if nested {
-		typeNameOrNamespaceName.Flags |= ast.NodeFlagsIdentifierIsInJSDocNamespace
-	}
 	return typeNameOrNamespaceName
 }
 
diff --git a/internal/parser/parser.go b/internal/parser/parser.go
index f6655e8f230..41506637e54 100644
--- a/internal/parser/parser.go
+++ b/internal/parser/parser.go
@@ -1984,29 +1984,29 @@ func (p *Parser) parseEnumDeclaration(pos int, hasJSDoc bool, modifiers *ast.Mod
 }
 
 func (p *Parser) parseModuleDeclaration(pos int, hasJSDoc bool, modifiers *ast.ModifierList) *ast.Statement {
-	var flags ast.NodeFlags
+	keyword := ast.KindModuleKeyword
 	if p.token == ast.KindGlobalKeyword {
 		// global augmentation
 		return p.parseAmbientExternalModuleDeclaration(pos, hasJSDoc, modifiers)
 	} else if p.parseOptional(ast.KindNamespaceKeyword) {
-		flags |= ast.NodeFlagsNamespace
+		keyword = ast.KindNamespaceKeyword
 	} else {
 		p.parseExpected(ast.KindModuleKeyword)
 		if p.token == ast.KindStringLiteral {
 			return p.parseAmbientExternalModuleDeclaration(pos, hasJSDoc, modifiers)
 		}
 	}
-	return p.parseModuleOrNamespaceDeclaration(pos, hasJSDoc, modifiers, flags)
+	return p.parseModuleOrNamespaceDeclaration(pos, hasJSDoc, modifiers, false /*nested*/, keyword)
 }
 
 func (p *Parser) parseAmbientExternalModuleDeclaration(pos int, hasJSDoc bool, modifiers *ast.ModifierList) *ast.Node {
-	var flags ast.NodeFlags
 	var name *ast.Node
+	keyword := ast.KindModuleKeyword
 	saveHasAwaitIdentifier := p.statementHasAwaitIdentifier
 	if p.token == ast.KindGlobalKeyword {
 		// parse 'global' as name of global scope augmentation
 		name = p.parseIdentifier()
-		flags |= ast.NodeFlagsGlobalAugmentation
+		keyword = ast.KindGlobalKeyword
 	} else {
 		// parse string literal
 		name = p.parseLiteralExpression(true /*intern*/)
@@ -2017,7 +2017,7 @@ func (p *Parser) parseAmbientExternalModuleDeclaration(pos int, hasJSDoc bool, m
 	} else {
 		p.parseSemicolon()
 	}
-	result := p.factory.NewModuleDeclaration(modifiers, name, body, flags)
+	result := p.factory.NewModuleDeclaration(modifiers, keyword, name, body)
 	p.finishNode(result, pos)
 	p.withJSDoc(result, hasJSDoc)
 	p.statementHasAwaitIdentifier = saveHasAwaitIdentifier
@@ -2038,24 +2038,27 @@ func (p *Parser) parseModuleBlock() *ast.Node {
 	return result
 }
 
-func (p *Parser) parseModuleOrNamespaceDeclaration(pos int, hasJSDoc bool, modifiers *ast.ModifierList, flags ast.NodeFlags) *ast.Node {
+func (p *Parser) parseModuleOrNamespaceDeclaration(pos int, hasJSDoc bool, modifiers *ast.ModifierList, nested bool, keyword ast.Kind) *ast.Node {
 	saveHasAwaitIdentifier := p.statementHasAwaitIdentifier
-	// If we are parsing a dotted namespace name, we want to
-	// propagate the 'Namespace' flag across the names if set.
-	namespaceFlag := flags & ast.NodeFlagsNamespace
 	var name *ast.Node
-	if flags&ast.NodeFlagsNestedNamespace != 0 {
+	if nested {
 		name = p.parseIdentifierName()
 	} else {
 		name = p.parseIdentifier()
 	}
 	var body *ast.Node
 	if p.parseOptional(ast.KindDotToken) {
-		body = p.parseModuleOrNamespaceDeclaration(p.nodePos(), false /*hasJSDoc*/, nil /*modifiers*/, ast.NodeFlagsNestedNamespace|namespaceFlag)
+		implicitExport := p.factory.NewModifier(ast.KindExportKeyword)
+		implicitExport.Loc = core.NewTextRange(p.nodePos(), p.nodePos())
+		implicitExport.Flags = ast.NodeFlagsReparsed
+		nodes := p.nodeSlicePool.NewSlice(1)
+		nodes[0] = implicitExport
+		implicitModifiers := p.newModifierList(implicitExport.Loc, nodes)
+		body = p.parseModuleOrNamespaceDeclaration(p.nodePos(), false /*hasJSDoc*/, implicitModifiers, true /*nested*/, keyword)
 	} else {
 		body = p.parseModuleBlock()
 	}
-	result := p.factory.NewModuleDeclaration(modifiers, name, body, flags)
+	result := p.factory.NewModuleDeclaration(modifiers, keyword, name, body)
 	p.finishNode(result, pos)
 	p.withJSDoc(result, hasJSDoc)
 	p.statementHasAwaitIdentifier = saveHasAwaitIdentifier
diff --git a/internal/printer/printer.go b/internal/printer/printer.go
index fea150b71cf..78bb47b212d 100644
--- a/internal/printer/printer.go
+++ b/internal/printer/printer.go
@@ -3335,8 +3335,8 @@ func (p *Printer) emitEnumDeclaration(node *ast.EnumDeclaration) {
 func (p *Printer) emitModuleDeclaration(node *ast.ModuleDeclaration) {
 	p.enterNode(node.AsNode())
 	p.emitModifierList(node.AsNode(), node.Modifiers(), false /*allowDecorators*/)
-	if node.Flags&ast.NodeFlagsGlobalAugmentation == 0 {
-		p.writeKeyword(core.IfElse(node.Flags&ast.NodeFlagsNamespace != 0, "namespace", "module"))
+	if node.Keyword != ast.KindGlobalKeyword {
+		p.writeKeyword(core.IfElse(node.Keyword == ast.KindNamespaceKeyword, "namespace", "module"))
 		p.writeSpace()
 	}
 	p.emitModuleName(node.Name())
diff --git a/internal/testutil/tsbaseline/type_symbol_baseline.go b/internal/testutil/tsbaseline/type_symbol_baseline.go
index 27b5e29d3d8..e3d773b78fb 100644
--- a/internal/testutil/tsbaseline/type_symbol_baseline.go
+++ b/internal/testutil/tsbaseline/type_symbol_baseline.go
@@ -342,7 +342,7 @@ func (walker *typeWriterWalker) writeTypeOrSymbol(node *ast.Node, isSymbolWalk b
 			!ast.IsBindingElement(node.Parent) &&
 			!ast.IsPropertyAccessOrQualifiedName(node.Parent) &&
 			!ast.IsLabelName(node) &&
-			!(ast.IsModuleDeclaration(node.Parent) && ast.IsGlobalScopeAugmentation(node.Parent)) &&
+			!ast.IsGlobalScopeAugmentation(node.Parent) &&
 			!ast.IsMetaProperty(node.Parent) &&
 			!isImportStatementName(node) &&
 			!isExportStatementName(node) &&
diff --git a/internal/transformers/runtimesyntax.go b/internal/transformers/runtimesyntax.go
index 22ffa38e541..9f84adef22e 100644
--- a/internal/transformers/runtimesyntax.go
+++ b/internal/transformers/runtimesyntax.go
@@ -145,7 +145,7 @@ func (tx *RuntimeSyntaxTransformer) isFirstDeclarationInScope(node *ast.Node) bo
 }
 
 func (tx *RuntimeSyntaxTransformer) isExportOfNamespace(node *ast.Node) bool {
-	return tx.currentNamespace != nil && (node.ModifierFlags()&ast.ModifierFlagsExport != 0 || node.Flags&ast.NodeFlagsNestedNamespace != 0)
+	return tx.currentNamespace != nil && node.ModifierFlags()&ast.ModifierFlagsExport != 0
 }
 
 func (tx *RuntimeSyntaxTransformer) isExportOfExternalModule(node *ast.Node) bool {


Reviews:
user: login: rbuckton
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:11:19Z
user: login: ahejlsberg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T18:04:12Z
user: login: rbuckton
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T18:26:44Z
user: login: sandersn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T13:27:22Z
user: login: rbuckton
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T00:45:40Z
user: login: sandersn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T15:17:52Z
user: login: sandersn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T15:37:15Z
user: login: sandersn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T15:52:53Z
user: login: rbuckton
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T21:55:24Z
user: login: sandersn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:01:30Z
user: login: rbuckton
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T20:46:45Z

Review comments:
diff_hunk: @@ -80,6 +80,17 @@ func PositionIsSynthesized(pos int) bool {
 	return pos < 0
 }
 
+func FindLastVisibleNode(nodes []*Node) *Node {
+	fromEnd := 1
+	for fromEnd <= len(nodes) && nodes[len(nodes)-fromEnd].Flags&NodeFlagsSynthesized != 0 {
path: internal/ast/utilities.go
user: login: rbuckton
body: I wonder if `Synthesized` is the wrong flag for this and we need something else like `Virtual` instead. We seem to be running out of space in `NodeFlags`, but it might be worth seeing if there are any flags we can reclaim or move. For example, `NodeFlagsNamespace`, `NodeFlagsNestedNamespace`, and `NodeFlagsGlobalAugmentation` only ever apply to a `ModuleDeclaration`, so it might make more sense to move those to a separate enum and field on `ModuleDeclaration` instead. We may not even need `NodeFlagsGlobalAugmentation` as that can be trivially inferred from the name of the module declaration (`global`).
created_at: 2025-03-12T16:02:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,10 +9,11 @@ import (
 type TypeEraserTransformer struct {
 	Transformer
 	compilerOptions *core.CompilerOptions
+	sourceFile      *ast.SourceFile
 }
 
-func NewTypeEraserTransformer(emitContext *printer.EmitContext, compilerOptions *core.CompilerOptions) *Transformer {
-	tx := &TypeEraserTransformer{compilerOptions: compilerOptions}
+func NewTypeEraserTransformer(emitContext *printer.EmitContext, sourceFile *ast.SourceFile, compilerOptions *core.CompilerOptions) *Transformer {
+	tx := &TypeEraserTransformer{compilerOptions: compilerOptions, sourceFile: sourceFile}
path: internal/transformers/typeeraser.go
user: login: rbuckton
body: This is normally picked up by capturing the source file in `visit` (or `visitSourceFile`) as it's likely that I will change transformers to be reused like they are in Strada (though changed to one per thread rather than one for all emit). See `runtimesyntax.go` or `commonjsmodule.go` for examples.
created_at: 2025-03-12T16:05:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2037,7 +2037,13 @@ func (p *Parser) parseModuleOrNamespaceDeclaration(pos int, hasJSDoc bool, modif
 	}
 	var body *ast.Node
 	if p.parseOptional(ast.KindDotToken) {
-		body = p.parseModuleOrNamespaceDeclaration(p.nodePos(), false /*hasJSDoc*/, nil /*modifiers*/, ast.NodeFlagsNestedNamespace|namespaceFlag)
+		syntheticExport := p.factory.NewModifier(ast.KindExportKeyword)
+		syntheticExport.Loc = core.NewTextRange(p.nodePos(), p.nodePos())
+		syntheticExport.Flags = ast.NodeFlagsSynthesized
+		nodes := p.nodeSlicePool.NewSlice(1)
+		nodes[0] = syntheticExport
+		syntheticModifiers := p.newModifierList(syntheticExport.Loc, nodes)
+		body = p.parseModuleOrNamespaceDeclaration(p.nodePos(), false /*hasJSDoc*/, syntheticModifiers, ast.NodeFlagsNestedNamespace|namespaceFlag)
path: internal/parser/parser.go
user: login: rbuckton
body: If we add a synthetic `export` keyword, can we do away with `NodeFlagsNestedNamespace`?
created_at: 2025-03-12T16:07:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -80,6 +80,17 @@ func PositionIsSynthesized(pos int) bool {
 	return pos < 0
 }
 
+func FindLastVisibleNode(nodes []*Node) *Node {
+	fromEnd := 1
+	for fromEnd <= len(nodes) && nodes[len(nodes)-fromEnd].Flags&NodeFlagsSynthesized != 0 {
path: internal/ast/utilities.go
user: login: ahejlsberg
body: A number of these flags were previously computed by the checker so emit could use them. I think a lot of them can be eliminated now.
created_at: 2025-03-12T18:04:11Z
author_association: MEMBER
diff_hunk: @@ -80,6 +80,17 @@ func PositionIsSynthesized(pos int) bool {
 	return pos < 0
 }
 
+func FindLastVisibleNode(nodes []*Node) *Node {
+	fromEnd := 1
+	for fromEnd <= len(nodes) && nodes[len(nodes)-fromEnd].Flags&NodeFlagsSynthesized != 0 {
path: internal/ast/utilities.go
user: login: rbuckton
body: We've discussed deprecating `module id {}` in favor of `namespace id {}` for a while, especially in light of the [Module Declarations Proposal](https://github.com/tc39/proposal-module-declarations). Once we've finished the port, we should give some thought to splitting `namespace id {}` out to `NamespaceDeclaration` and only supporting `module "string" {}` and `global {}` for `ModuleDeclaration`.
created_at: 2025-03-12T18:26:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2037,7 +2037,13 @@ func (p *Parser) parseModuleOrNamespaceDeclaration(pos int, hasJSDoc bool, modif
 	}
 	var body *ast.Node
 	if p.parseOptional(ast.KindDotToken) {
-		body = p.parseModuleOrNamespaceDeclaration(p.nodePos(), false /*hasJSDoc*/, nil /*modifiers*/, ast.NodeFlagsNestedNamespace|namespaceFlag)
+		syntheticExport := p.factory.NewModifier(ast.KindExportKeyword)
+		syntheticExport.Loc = core.NewTextRange(p.nodePos(), p.nodePos())
+		syntheticExport.Flags = ast.NodeFlagsSynthesized
+		nodes := p.nodeSlicePool.NewSlice(1)
+		nodes[0] = syntheticExport
+		syntheticModifiers := p.newModifierList(syntheticExport.Loc, nodes)
+		body = p.parseModuleOrNamespaceDeclaration(p.nodePos(), false /*hasJSDoc*/, syntheticModifiers, ast.NodeFlagsNestedNamespace|namespaceFlag)
path: internal/parser/parser.go
user: login: sandersn
body: It's even easier -- just check whether a namespace's parent is also a namespace (and that the parent's body is the namespace).
created_at: 2025-03-13T13:27:22Z
author_association: MEMBER
diff_hunk: @@ -3401,22 +3401,23 @@ type ModuleDeclaration struct {
 	ModifiersBase
 	LocalsContainerBase
 	BodyBase
-	name *ModuleName // ModuleName
+	name    *ModuleName // ModuleName
+	Keyword Kind        // KindModuleKeyword (deprecated), KindNamespaceKeyword, KindGlobalKeyword (global augmentation)
path: internal/ast/ast.go
user: login: rbuckton
body: `module` isn't what's deprecated as we are still going to use `module "string"` for ambient module declarations. Only `module id`/`module a.b` is deprecated.

I'm surprised `KindGlobalKeyword` is needed given we parse `global` as an identifier when parsing `declare global {}`. The information is somewhat redundant, but I suppose we might as well since we need to store `Keyword` anyway. Ideally, we wouldn't bother to parse `global` as an identifier and would just leave `name` as `nil`, though I'm sure that would break some code somewhere since we expect `ModuleDeclaration` to always have some kind of name.
created_at: 2025-03-14T23:35:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,25 +7,23 @@ const (
 	NodeFlagsLet                             NodeFlags = 1 << 0  // Variable declaration
 	NodeFlagsConst                           NodeFlags = 1 << 1  // Variable declaration
 	NodeFlagsUsing                           NodeFlags = 1 << 2  // Variable declaration
-	NodeFlagsNestedNamespace                 NodeFlags = 1 << 3  // Namespace declaration
+	NodeFlagsReparsed                        NodeFlags = 1 << 3  // Node was synthesized during parsing
 	NodeFlagsSynthesized                     NodeFlags = 1 << 4  // Node was synthesized during transformation
-	NodeFlagsNamespace                       NodeFlags = 1 << 5  // Namespace declaration
-	NodeFlagsOptionalChain                   NodeFlags = 1 << 6  // Chained MemberExpression rooted to a pseudo-OptionalExpression
-	NodeFlagsExportContext                   NodeFlags = 1 << 7  // Export context (initialized by binding)
-	NodeFlagsContainsThis                    NodeFlags = 1 << 8  // Interface contains references to "this"
-	NodeFlagsHasImplicitReturn               NodeFlags = 1 << 9  // If function implicitly returns on one of codepaths (initialized by binding)
-	NodeFlagsHasExplicitReturn               NodeFlags = 1 << 10 // If function has explicit reachable return on one of codepaths (initialized by binding)
-	NodeFlagsGlobalAugmentation              NodeFlags = 1 << 11 // Set if module declaration is an augmentation for the global scope
-	NodeFlagsHasAsyncFunctions               NodeFlags = 1 << 12 // If the file has async functions (initialized by binding)
-	NodeFlagsDisallowInContext               NodeFlags = 1 << 13 // If node was parsed in a context where 'in-expressions' are not allowed
-	NodeFlagsYieldContext                    NodeFlags = 1 << 14 // If node was parsed in the 'yield' context created when parsing a generator
-	NodeFlagsDecoratorContext                NodeFlags = 1 << 15 // If node was parsed as part of a decorator
-	NodeFlagsAwaitContext                    NodeFlags = 1 << 16 // If node was parsed in the 'await' context created when parsing an async function
-	NodeFlagsDisallowConditionalTypesContext NodeFlags = 1 << 17 // If node was parsed in a context where conditional types are not allowed
-	NodeFlagsThisNodeHasError                NodeFlags = 1 << 18 // If the parser encountered an error when parsing the code that created this node
-	NodeFlagsJavaScriptFile                  NodeFlags = 1 << 19 // If node was parsed in a JavaScript
-	NodeFlagsThisNodeOrAnySubNodesHasError   NodeFlags = 1 << 20 // If this node or any of its children had an error
-	NodeFlagsHasAggregatedChildData          NodeFlags = 1 << 21 // If we've computed data from children and cached it in this node
+	NodeFlagsOptionalChain                   NodeFlags = 1 << 5  // Chained MemberExpression rooted to a pseudo-OptionalExpression
+	NodeFlagsExportContext                   NodeFlags = 1 << 6  // Export context (initialized by binding)
+	NodeFlagsContainsThis                    NodeFlags = 1 << 7  // Interface contains references to "this"
+	NodeFlagsHasImplicitReturn               NodeFlags = 1 << 8  // If function implicitly returns on one of codepaths (initialized by binding)
+	NodeFlagsHasExplicitReturn               NodeFlags = 1 << 9  // If function has explicit reachable return on one of codepaths (initialized by binding)
+	NodeFlagsHasAsyncFunctions               NodeFlags = 1 << 10 // If the file has async functions (initialized by binding)
path: internal/ast/nodeflags.go
user: login: rbuckton
body: This flag isn't used anymore except as an alias to `NodeFlagsIdentifierIsInJSDocNamespace`, which itself is only used in JSDoc namespaces in the same way `NestedNamespace` was used. It's possible we could get rid of both to save more space in `NodeFlags`.
created_at: 2025-03-14T23:45:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,6 +100,10 @@ func (tx *TypeEraserTransformer) visit(node *ast.Node) *ast.Node {
 			// TypeScript module declarations are elided if they are not instantiated or have no body
 			return nil
 		}
+		if ast.IsModuleDeclaration(tx.parentNode) && tx.parentNode.AsModuleDeclaration().Body == node {
path: internal/transformers/typeeraser.go
user: login: rbuckton
body: Is this necessary? I would think this would be handled in runtimesyntax.go.
created_at: 2025-03-15T00:41:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,7 +145,9 @@ func (tx *RuntimeSyntaxTransformer) isFirstDeclarationInScope(node *ast.Node) bo
 }
 
 func (tx *RuntimeSyntaxTransformer) isExportOfNamespace(node *ast.Node) bool {
-	return tx.currentNamespace != nil && (node.ModifierFlags()&ast.ModifierFlagsExport != 0 || node.Flags&ast.NodeFlagsNestedNamespace != 0)
+	return tx.currentNamespace != nil &&
+		(node.ModifierFlags()&ast.ModifierFlagsExport != 0 ||
+			ast.IsModuleDeclaration(node) && ast.IsModuleDeclaration(tx.parentNode) && tx.parentNode.AsModuleDeclaration().Body == node)
path: internal/transformers/runtimesyntax.go
user: login: rbuckton
body: Is this line necessary assuming we don't drop the synthetic `export` in typeeraser.go?
created_at: 2025-03-15T00:42:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3401,22 +3401,23 @@ type ModuleDeclaration struct {
 	ModifiersBase
 	LocalsContainerBase
 	BodyBase
-	name *ModuleName // ModuleName
+	name    *ModuleName // ModuleName
+	Keyword Kind        // KindModuleKeyword (deprecated), KindNamespaceKeyword, KindGlobalKeyword (global augmentation)
path: internal/ast/ast.go
user: login: sandersn
body: OK, I'll remove the deprecated note.
created_at: 2025-03-17T15:17:52Z
author_association: MEMBER
diff_hunk: @@ -7,25 +7,23 @@ const (
 	NodeFlagsLet                             NodeFlags = 1 << 0  // Variable declaration
 	NodeFlagsConst                           NodeFlags = 1 << 1  // Variable declaration
 	NodeFlagsUsing                           NodeFlags = 1 << 2  // Variable declaration
-	NodeFlagsNestedNamespace                 NodeFlags = 1 << 3  // Namespace declaration
+	NodeFlagsReparsed                        NodeFlags = 1 << 3  // Node was synthesized during parsing
 	NodeFlagsSynthesized                     NodeFlags = 1 << 4  // Node was synthesized during transformation
-	NodeFlagsNamespace                       NodeFlags = 1 << 5  // Namespace declaration
-	NodeFlagsOptionalChain                   NodeFlags = 1 << 6  // Chained MemberExpression rooted to a pseudo-OptionalExpression
-	NodeFlagsExportContext                   NodeFlags = 1 << 7  // Export context (initialized by binding)
-	NodeFlagsContainsThis                    NodeFlags = 1 << 8  // Interface contains references to "this"
-	NodeFlagsHasImplicitReturn               NodeFlags = 1 << 9  // If function implicitly returns on one of codepaths (initialized by binding)
-	NodeFlagsHasExplicitReturn               NodeFlags = 1 << 10 // If function has explicit reachable return on one of codepaths (initialized by binding)
-	NodeFlagsGlobalAugmentation              NodeFlags = 1 << 11 // Set if module declaration is an augmentation for the global scope
-	NodeFlagsHasAsyncFunctions               NodeFlags = 1 << 12 // If the file has async functions (initialized by binding)
-	NodeFlagsDisallowInContext               NodeFlags = 1 << 13 // If node was parsed in a context where 'in-expressions' are not allowed
-	NodeFlagsYieldContext                    NodeFlags = 1 << 14 // If node was parsed in the 'yield' context created when parsing a generator
-	NodeFlagsDecoratorContext                NodeFlags = 1 << 15 // If node was parsed as part of a decorator
-	NodeFlagsAwaitContext                    NodeFlags = 1 << 16 // If node was parsed in the 'await' context created when parsing an async function
-	NodeFlagsDisallowConditionalTypesContext NodeFlags = 1 << 17 // If node was parsed in a context where conditional types are not allowed
-	NodeFlagsThisNodeHasError                NodeFlags = 1 << 18 // If the parser encountered an error when parsing the code that created this node
-	NodeFlagsJavaScriptFile                  NodeFlags = 1 << 19 // If node was parsed in a JavaScript
-	NodeFlagsThisNodeOrAnySubNodesHasError   NodeFlags = 1 << 20 // If this node or any of its children had an error
-	NodeFlagsHasAggregatedChildData          NodeFlags = 1 << 21 // If we've computed data from children and cached it in this node
+	NodeFlagsOptionalChain                   NodeFlags = 1 << 5  // Chained MemberExpression rooted to a pseudo-OptionalExpression
+	NodeFlagsExportContext                   NodeFlags = 1 << 6  // Export context (initialized by binding)
+	NodeFlagsContainsThis                    NodeFlags = 1 << 7  // Interface contains references to "this"
+	NodeFlagsHasImplicitReturn               NodeFlags = 1 << 8  // If function implicitly returns on one of codepaths (initialized by binding)
+	NodeFlagsHasExplicitReturn               NodeFlags = 1 << 9  // If function has explicit reachable return on one of codepaths (initialized by binding)
+	NodeFlagsHasAsyncFunctions               NodeFlags = 1 << 10 // If the file has async functions (initialized by binding)
path: internal/ast/nodeflags.go
user: login: sandersn
body: When I merged from main this morning, I see it used 3 times as itself in the binder to mark async function expressions/declarations and method declarations. But the emitter never checks the flag. Both the binder and emitter are complete, right? If so, it's safe to remove them.

Actually, `typedef` and `callback` are no longer going to support nested names, since that was a hardly-used Closure feature. So all of the flags can be removed.
created_at: 2025-03-17T15:37:15Z
author_association: MEMBER
diff_hunk: @@ -145,7 +145,9 @@ func (tx *RuntimeSyntaxTransformer) isFirstDeclarationInScope(node *ast.Node) bo
 }
 
 func (tx *RuntimeSyntaxTransformer) isExportOfNamespace(node *ast.Node) bool {
-	return tx.currentNamespace != nil && (node.ModifierFlags()&ast.ModifierFlagsExport != 0 || node.Flags&ast.NodeFlagsNestedNamespace != 0)
+	return tx.currentNamespace != nil &&
+		(node.ModifierFlags()&ast.ModifierFlagsExport != 0 ||
+			ast.IsModuleDeclaration(node) && ast.IsModuleDeclaration(tx.parentNode) && tx.parentNode.AsModuleDeclaration().Body == node)
path: internal/transformers/runtimesyntax.go
user: login: sandersn
body: not for semantics. I thought that the emit might be different if we don't transform `namespace X { Y { Z { } }` back into `namespace X.Y.Z { }`, but that would be type emit -- and it might not matter if we changed that.
created_at: 2025-03-17T15:49:19Z
author_association: MEMBER
diff_hunk: @@ -100,6 +100,10 @@ func (tx *TypeEraserTransformer) visit(node *ast.Node) *ast.Node {
 			// TypeScript module declarations are elided if they are not instantiated or have no body
 			return nil
 		}
+		if ast.IsModuleDeclaration(tx.parentNode) && tx.parentNode.AsModuleDeclaration().Body == node {
path: internal/transformers/typeeraser.go
user: login: sandersn
body: I wasn't sure whether nested namespaces were "type erased" syntax or runtime syntax. Putting it in the type eraser would, I guessed, make sure that it was back to the Strada-expected form both for .js emit and .d.ts emit.

Like I said in the runtimesyntax comment, maybe it's better to expect the .d.ts emit to change (or the .d.ts emitter to change). What do you think?
created_at: 2025-03-17T15:52:49Z
author_association: MEMBER
diff_hunk: @@ -100,6 +100,10 @@ func (tx *TypeEraserTransformer) visit(node *ast.Node) *ast.Node {
 			// TypeScript module declarations are elided if they are not instantiated or have no body
 			return nil
 		}
+		if ast.IsModuleDeclaration(tx.parentNode) && tx.parentNode.AsModuleDeclaration().Body == node {
path: internal/transformers/typeeraser.go
user: login: rbuckton
body: for `export namespace a.b ...`? I think I would expect our .d.ts to continue emit `export namespace a.b ...`.
created_at: 2025-03-17T21:55:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,6 +100,10 @@ func (tx *TypeEraserTransformer) visit(node *ast.Node) *ast.Node {
 			// TypeScript module declarations are elided if they are not instantiated or have no body
 			return nil
 		}
+		if ast.IsModuleDeclaration(tx.parentNode) && tx.parentNode.AsModuleDeclaration().Body == node {
path: internal/transformers/typeeraser.go
user: login: sandersn
body: hmm. OK. I can remove the changes in both typeeraser and runtimesyntax then.
created_at: 2025-03-18T15:01:30Z
author_association: MEMBER

PR comments:
user: login: sandersn
created_at: 2025-03-12T21:12:42Z
body: I've got the 3 namespace flags down to just FlagsNamespace, which can just be a bool. But I'm not sure how to reduce it further since the keyword token isn't saved during parsing.
author_association: MEMBER
user: login: rbuckton
created_at: 2025-03-12T21:17:12Z
body: I think a `bool` on `ModuleDeclaration` is probably good enough. A `Token` might be better as there's less work necessary to recover position information for source maps/comments.
author_association: CONTRIBUTOR
user: login: sandersn
created_at: 2025-03-12T22:31:14Z
body: ah, didn't see this until I pushed a commit that switches to Kind, not Token. Turns out that `namespace global {` and `global {` are only distinguished by the flag, which means that I need a 3-way distinction that I encoded in the keyword token's kind.

Actually, `global { }` *also* uses "global" as its Name, parsing it as an identifier. I think it's safer to save only the kind for the keyword instead of having two nodes for the same span.

Edit: Also it complicates nested namespaces, which end up reusing the same `namespace` token for all the generated namespaces.
author_association: MEMBER