Don’t try to create invalid type/symbol merges#3205
Merged
andrewbranch merged 1 commit intomicrosoft:mainfrom Mar 25, 2026
Merged
Don’t try to create invalid type/symbol merges#3205andrewbranch merged 1 commit intomicrosoft:mainfrom
andrewbranch merged 1 commit intomicrosoft:mainfrom
Conversation
Contributor
There was a problem hiding this comment.
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
combineValueAndTypeSymbolsto 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.a → a4, 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: iftypeSymbolunexpectedly hasSymbolFlagsValue, the function returns it even whenvalueSymbolwould otherwise be preferred (e.g., whenvalueSymbolalso carriesType|Namespace). To make the behavior more robust/intentional, consider either (a) narrowing the guard to the specific condition you expect (e.g., only whentypeSymbolis already the combined symbol you want to reuse), or (b) reordering the checks so an already-combinedvalueSymbolremains the preferred result. Adding a short comment explaining the invariant being relied on here would also help prevent future regressions.
ahejlsberg
approved these changes
Mar 25, 2026
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 { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
combineTypeAndValueSymbolsdidn't go through the standard symbol merging functionality that issues diagnostics—I don’t feel like I understand why this code is special-cased.