Skip to content

Don’t try to create invalid type/symbol merges#3205

Merged
andrewbranch merged 1 commit intomicrosoft:mainfrom
andrewbranch:bug/3202
Mar 25, 2026
Merged

Don’t try to create invalid type/symbol merges#3205
andrewbranch merged 1 commit intomicrosoft:mainfrom
andrewbranch:bug/3202

Conversation

@andrewbranch
Copy link
Copy Markdown
Member

Fixes #3202, alternative to #3203.

This deletes a couple diffs but also adds one in a way that feels arbitrary if not correct. That said, I’m not totally confident this is the end of the bug tail from the new merging capabilities. I was a little surprised that combineTypeAndValueSymbols didn't go through the standard symbol merging functionality that issues diagnostics—I don’t feel like I understand why this code is special-cased.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Prevents the checker from attempting invalid type/value symbol merges by short-circuiting in combineValueAndTypeSymbols, and adds regression coverage around export-assignment merging scenarios.

Changes:

  • Update combineValueAndTypeSymbols to return early when the “type” symbol already contains a value symbol.
  • Add two new compiler test cases covering export assignment merging edge cases.
  • Update relevant test baselines to match the new symbol/type outputs.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated no comments.

Show a summary per file
File Description
testdata/tests/cases/compiler/exportAssignmentMerging9.ts New regression test for invalid export assignment + exported member merging case.
testdata/tests/cases/compiler/exportAssignmentMerging10.ts New regression test for value/type confusion around export = and namespace-style aliasing.
testdata/baselines/reference/submodule/compiler/jsxFragmentFactoryNoUnusedLocals.types.diff Removes an outdated baseline diff after baseline regeneration.
testdata/baselines/reference/submodule/compiler/jsxFragmentFactoryNoUnusedLocals.types Updates baseline output to reflect changed createElement symbol/type printing.
testdata/baselines/reference/submodule/compiler/es6ExportEqualsInterop.symbols.diff Adds baseline diff capturing updated symbol identity for a members.
testdata/baselines/reference/submodule/compiler/es6ExportEqualsInterop.symbols Updates baseline outputs for symbol identities (z4.aa4, etc.).
testdata/baselines/reference/submodule/compiler/aliasDoesNotDuplicateSignatures.symbols.diff Removes an outdated baseline diff after baseline regeneration.
testdata/baselines/reference/submodule/compiler/aliasDoesNotDuplicateSignatures.symbols Updates baseline output for symbol identity in alias/signature scenario.
testdata/baselines/reference/compiler/exportAssignmentMerging9.types New baseline for the added test case.
testdata/baselines/reference/compiler/exportAssignmentMerging9.symbols New baseline for the added test case.
testdata/baselines/reference/compiler/exportAssignmentMerging9.errors.txt New baseline errors for the added test case.
testdata/baselines/reference/compiler/exportAssignmentMerging10.types New baseline for the added test case.
testdata/baselines/reference/compiler/exportAssignmentMerging10.symbols New baseline for the added test case.
testdata/baselines/reference/compiler/exportAssignmentMerging10.errors.txt New baseline errors for the added test case.
internal/checker/checker.go Adds an early-return guard to avoid attempting invalid symbol merges.
Comments suppressed due to low confidence (1)

internal/checker/checker.go:1

  • This new early return changes the precedence rules in combineValueAndTypeSymbols: if typeSymbol unexpectedly has SymbolFlagsValue, the function returns it even when valueSymbol would otherwise be preferred (e.g., when valueSymbol also carries Type|Namespace). To make the behavior more robust/intentional, consider either (a) narrowing the guard to the specific condition you expect (e.g., only when typeSymbol is already the combined symbol you want to reuse), or (b) reordering the checks so an already-combined valueSymbol remains the preferred result. Adding a short comment explaining the invariant being relied on here would also help prevent future regressions.

@jakebailey
Copy link
Copy Markdown
Member

I tried to do what you were thinking re: reusing the merge code, but to get things passing without crashing, it didn't seem very helpful:

diff --git a/internal/checker/checker.go b/internal/checker/checker.go
index 5f348dca44..f2480b1977 100644
--- a/internal/checker/checker.go
+++ b/internal/checker/checker.go
@@ -14240,12 +14240,13 @@ func (c *Checker) getExternalModuleMember(node *ast.Node, specifier *ast.Node, d
 					}
 				}
 			}
-			symbol := symbolFromVariable
-			if symbolFromModule != nil {
+			var symbol *ast.Symbol
+			if symbolFromModule != nil && symbolFromVariable != nil && symbolFromModule != symbolFromVariable {
+				symbol = c.mergeExportEquals(symbolFromModule, symbolFromVariable)
+			} else if symbolFromModule != nil {
 				symbol = symbolFromModule
-				if symbolFromVariable != nil {
-					symbol = c.combineValueAndTypeSymbols(symbolFromVariable, symbolFromModule)
-				}
+			} else {
+				symbol = symbolFromVariable
 			}
 			if ast.IsImportOrExportSpecifier(specifier) && c.isOnlyImportableAsDefault(moduleSpecifier, moduleSymbol) && nameText != ast.InternalSymbolNameDefault {
 				c.error(name, diagnostics.Named_imports_from_a_JSON_file_into_an_ECMAScript_module_are_not_allowed_when_module_is_set_to_0, c.moduleKind.String())
@@ -14268,45 +14269,31 @@ func (c *Checker) getPropertyOfVariable(symbol *ast.Symbol, name string) *ast.Sy
 	return nil
 }
 
-// This function creates a synthetic symbol that combines the value side of one symbol with the
-// type/namespace side of another symbol. Consider this example:
-//
-//	declare module graphics {
-//	    interface Point {
-//	        x: number;
-//	        y: number;
-//	    }
-//	}
-//	declare var graphics: {
-//	    Point: new (x: number, y: number) => graphics.Point;
-//	}
-//	declare module "graphics" {
-//	    export = graphics;
-//	}
-//
-// An 'import { Point } from "graphics"' needs to create a symbol that combines the value side 'Point'
-// property with the type/namespace side interface 'Point'.
-func (c *Checker) combineValueAndTypeSymbols(valueSymbol *ast.Symbol, typeSymbol *ast.Symbol) *ast.Symbol {
-	if valueSymbol == c.unknownSymbol && typeSymbol == c.unknownSymbol {
+// mergeExportEquals combines a value symbol (from getPropertyOfType on an export= type) with
+// a type/namespace symbol (from module exports) for the same named import. This uses mergeSymbol
+// for proper ValueDeclaration handling (via SetValueDeclaration) and flag conflict detection,
+// while avoiding recordMergedSymbol which would corrupt the global resolution chain.
+func (c *Checker) mergeExportEquals(symbolFromModule *ast.Symbol, symbolFromVariable *ast.Symbol) *ast.Symbol {
+	if symbolFromModule == c.unknownSymbol && symbolFromVariable == c.unknownSymbol {
 		return c.unknownSymbol
 	}
-	if typeSymbol.Flags&ast.SymbolFlagsValue != 0 {
-		return typeSymbol
+	if symbolFromVariable.Flags&(ast.SymbolFlagsType|ast.SymbolFlagsNamespace) != 0 {
+		return symbolFromVariable
 	}
-	if valueSymbol.Flags&(ast.SymbolFlagsType|ast.SymbolFlagsNamespace) != 0 {
-		return valueSymbol
+	// When the type symbol already has value flags that conflict with the value symbol,
+	// prefer the type symbol — it has the richer declaration (e.g. a class with members).
+	if symbolFromModule.Flags&getExcludedSymbolFlags(symbolFromVariable.Flags) != 0 {
+		return symbolFromModule
 	}
-	result := c.newSymbol(valueSymbol.Flags|typeSymbol.Flags, valueSymbol.Name)
-	debug.Assert(len(valueSymbol.Declarations) > 0 || len(typeSymbol.Declarations) > 0)
-	result.Declarations = slices.Compact(slices.Concat(valueSymbol.Declarations, typeSymbol.Declarations))
-	result.Parent = valueSymbol.Parent
-	if result.Parent == nil {
-		result.Parent = typeSymbol.Parent
-	}
-	result.ValueDeclaration = valueSymbol.ValueDeclaration
-	result.Members = maps.Clone(typeSymbol.Members)
-	result.Exports = maps.Clone(valueSymbol.Exports)
-	return result
+	// Create a fresh transient target to avoid corrupting the original module symbol via
+	// recordMergedSymbol. mergeSymbol won't clone again since the target is already transient.
+	target := c.newSymbol(symbolFromModule.Flags, symbolFromModule.Name)
+	target.Declarations = symbolFromModule.Declarations[0:len(symbolFromModule.Declarations):len(symbolFromModule.Declarations)]
+	target.Parent = symbolFromModule.Parent
+	target.ValueDeclaration = symbolFromModule.ValueDeclaration
+	target.Members = maps.Clone(symbolFromModule.Members)
+	target.Exports = maps.Clone(symbolFromModule.Exports)
+	return c.mergeSymbol(target, symbolFromVariable, true /*unidirectional*/)
 }
 
 func (c *Checker) getExportOfModule(symbol *ast.Symbol, nameText string, specifier *ast.Node, dontResolveAlias bool) *ast.Symbol {

@andrewbranch andrewbranch added this pull request to the merge queue Mar 25, 2026
Merged via the queue into microsoft:main with commit 1a9e830 Mar 25, 2026
25 checks passed
@andrewbranch andrewbranch deleted the bug/3202 branch March 25, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Panic in 20260321.1 release

4 participants