Skip to content

Commit ce95d9c

Browse files
authored
Fix values and types merging in JS module exports (microsoft#37896)
* Fix values and types merging in JS module exports * Fix everything * Share `setValueDeclaration` between binder (local merge) and checker (cross-file merge) * Revert accidental changes to baselines * Update baseline from master merge
1 parent 1785d6c commit ce95d9c

25 files changed

+337
-57
lines changed

src/compiler/binder.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -320,16 +320,6 @@ namespace ts {
320320
}
321321
}
322322

323-
function setValueDeclaration(symbol: Symbol, node: Declaration): void {
324-
const { valueDeclaration } = symbol;
325-
if (!valueDeclaration ||
326-
(isAssignmentDeclaration(valueDeclaration) && !isAssignmentDeclaration(node)) ||
327-
(valueDeclaration.kind !== node.kind && isEffectiveModuleDeclaration(valueDeclaration))) {
328-
// other kinds of value declarations take precedence over modules and assignment declarations
329-
symbol.valueDeclaration = node;
330-
}
331-
}
332-
333323
// Should not be called on a declaration with a computed property name,
334324
// unless it is a well known Symbol.
335325
function getDeclarationName(node: Declaration): __String | undefined {

src/compiler/checker.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,12 +1115,8 @@ namespace ts {
11151115
target.constEnumOnlyModule = false;
11161116
}
11171117
target.flags |= source.flags;
1118-
if (source.valueDeclaration &&
1119-
(!target.valueDeclaration ||
1120-
isAssignmentDeclaration(target.valueDeclaration) && !isAssignmentDeclaration(source.valueDeclaration) ||
1121-
isEffectiveModuleDeclaration(target.valueDeclaration) && !isEffectiveModuleDeclaration(source.valueDeclaration))) {
1122-
// other kinds of value declarations take precedence over modules and assignment declarations
1123-
target.valueDeclaration = source.valueDeclaration;
1118+
if (source.valueDeclaration) {
1119+
setValueDeclaration(target, source.valueDeclaration);
11241120
}
11251121
addRange(target.declarations, source.declarations);
11261122
if (source.members) {
@@ -7724,11 +7720,38 @@ namespace ts {
77247720
resolvedSymbol.exports = createSymbolTable();
77257721
}
77267722
(resolvedSymbol || symbol).exports!.forEach((s, name) => {
7727-
if (members.has(name)) {
7728-
const exportedMember = exportedType.members.get(name)!;
7729-
const union = createSymbol(s.flags | exportedMember.flags, name);
7730-
union.type = getUnionType([getTypeOfSymbol(s), getTypeOfSymbol(exportedMember)]);
7731-
members.set(name, union);
7723+
const exportedMember = members.get(name)!;
7724+
if (exportedMember && exportedMember !== s) {
7725+
if (s.flags & SymbolFlags.Value) {
7726+
// If the member has an additional value-like declaration, union the types from the two declarations,
7727+
// but issue an error if they occurred in two different files. The purpose is to support a JS file with
7728+
// a pattern like:
7729+
//
7730+
// module.exports = { a: true };
7731+
// module.exports.a = 3;
7732+
//
7733+
// but we may have a JS file with `module.exports = { a: true }` along with a TypeScript module augmentation
7734+
// declaring an `export const a: number`. In that case, we issue a duplicate identifier error, because
7735+
// it's unclear what that's supposed to mean, so it's probably a mistake.
7736+
if (getSourceFileOfNode(s.valueDeclaration) !== getSourceFileOfNode(exportedMember.valueDeclaration)) {
7737+
const unescapedName = unescapeLeadingUnderscores(s.escapedName);
7738+
const exportedMemberName = tryCast(exportedMember.valueDeclaration, isNamedDeclaration)?.name || exportedMember.valueDeclaration;
7739+
addRelatedInfo(
7740+
error(s.valueDeclaration, Diagnostics.Duplicate_identifier_0, unescapedName),
7741+
createDiagnosticForNode(exportedMemberName, Diagnostics._0_was_also_declared_here, unescapedName));
7742+
addRelatedInfo(
7743+
error(exportedMemberName, Diagnostics.Duplicate_identifier_0, unescapedName),
7744+
createDiagnosticForNode(s.valueDeclaration, Diagnostics._0_was_also_declared_here, unescapedName));
7745+
}
7746+
const union = createSymbol(s.flags | exportedMember.flags, name);
7747+
union.type = getUnionType([getTypeOfSymbol(s), getTypeOfSymbol(exportedMember)]);
7748+
union.valueDeclaration = exportedMember.valueDeclaration;
7749+
union.declarations = concatenate(exportedMember.declarations, s.declarations);
7750+
members.set(name, union);
7751+
}
7752+
else {
7753+
members.set(name, mergeSymbol(s, exportedMember));
7754+
}
77327755
}
77337756
else {
77347757
members.set(name, s);

src/compiler/utilities.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2314,6 +2314,17 @@ namespace ts {
23142314
!!getJSDocTypeTag(expr.parent);
23152315
}
23162316

2317+
export function setValueDeclaration(symbol: Symbol, node: Declaration): void {
2318+
const { valueDeclaration } = symbol;
2319+
if (!valueDeclaration ||
2320+
!(node.flags & NodeFlags.Ambient && !(valueDeclaration.flags & NodeFlags.Ambient)) &&
2321+
(isAssignmentDeclaration(valueDeclaration) && !isAssignmentDeclaration(node)) ||
2322+
(valueDeclaration.kind !== node.kind && isEffectiveModuleDeclaration(valueDeclaration))) {
2323+
// other kinds of value declarations take precedence over modules and assignment declarations
2324+
symbol.valueDeclaration = node;
2325+
}
2326+
}
2327+
23172328
export function isFunctionSymbol(symbol: Symbol | undefined) {
23182329
if (!symbol || !symbol.valueDeclaration) {
23192330
return false;

tests/baselines/reference/jsDeclarationsClassExtendsVisibility.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ module.exports = Foo;
2525
>Foo : Symbol(Foo, Decl(cls.js, 4, 2))
2626

2727
module.exports.Strings = Strings;
28-
>module.exports.Strings : Symbol(Strings)
28+
>module.exports.Strings : Symbol(Strings, Decl(cls.js, 6, 21))
2929
>module.exports : Symbol(Strings, Decl(cls.js, 6, 21))
3030
>module : Symbol(module, Decl(cls.js, 5, 24))
3131
>exports : Symbol("tests/cases/conformance/jsdoc/declarations/cls", Decl(cls.js, 0, 0))

tests/baselines/reference/jsDeclarationsClassStatic.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ module.exports = Handler;
3434
>Handler : Symbol(Handler, Decl(source.js, 0, 0), Decl(source.js, 7, 1))
3535

3636
module.exports.Strings = Strings
37-
>module.exports.Strings : Symbol(Strings)
37+
>module.exports.Strings : Symbol(Strings, Decl(source.js, 14, 25))
3838
>module.exports : Symbol(Strings, Decl(source.js, 14, 25))
3939
>module : Symbol(module, Decl(source.js, 12, 1))
4040
>exports : Symbol("tests/cases/conformance/jsdoc/declarations/source", Decl(source.js, 0, 0))

tests/baselines/reference/jsDeclarationsCrossfileMerge.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module.exports = m.default;
1313
>default : Symbol(default, Decl(exporter.js, 0, 22))
1414

1515
module.exports.memberName = "thing";
16-
>module.exports.memberName : Symbol(memberName)
16+
>module.exports.memberName : Symbol(memberName, Decl(index.js, 2, 27))
1717
>module.exports : Symbol(memberName, Decl(index.js, 2, 27))
1818
>module : Symbol(module, Decl(index.js, 0, 32))
1919
>exports : Symbol("tests/cases/conformance/jsdoc/declarations/index", Decl(index.js, 0, 0))

tests/baselines/reference/jsDeclarationsExportAssignedClassExpressionAnonymousWithSub.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module.exports = class {
1818
}
1919
}
2020
module.exports.Sub = class {
21-
>module.exports.Sub : Symbol(Sub)
21+
>module.exports.Sub : Symbol(Sub, Decl(index.js, 7, 1))
2222
>module.exports : Symbol(Sub, Decl(index.js, 7, 1))
2323
>module : Symbol(module, Decl(index.js, 0, 0), Decl(index.js, 10, 27))
2424
>exports : Symbol("tests/cases/conformance/jsdoc/declarations/index", Decl(index.js, 0, 0))

tests/baselines/reference/jsDeclarationsExportAssignedClassExpressionShadowing.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ module.exports = class Q {
2727
}
2828
}
2929
module.exports.Another = Q;
30-
>module.exports.Another : Symbol(Another)
30+
>module.exports.Another : Symbol(Another, Decl(index.js, 10, 1))
3131
>module.exports : Symbol(Another, Decl(index.js, 10, 1))
3232
>module : Symbol(module, Decl(index.js, 5, 1))
3333
>exports : Symbol("tests/cases/conformance/jsdoc/declarations/index", Decl(index.js, 0, 0))

tests/baselines/reference/jsDeclarationsExportSubAssignments.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ module.exports = Foo;
1919
>Foo : Symbol(Foo, Decl(cls.js, 3, 2))
2020

2121
module.exports.Strings = Strings;
22-
>module.exports.Strings : Symbol(Strings)
22+
>module.exports.Strings : Symbol(Strings, Decl(cls.js, 5, 21))
2323
>module.exports : Symbol(Strings, Decl(cls.js, 5, 21))
2424
>module : Symbol(module, Decl(cls.js, 4, 12))
2525
>exports : Symbol("tests/cases/conformance/jsdoc/declarations/cls", Decl(cls.js, 0, 0))
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
=== /test.js ===
2+
class Abcde {
3+
>Abcde : Symbol(Abcde, Decl(test.js, 0, 0))
4+
5+
/** @type {string} */
6+
x;
7+
>x : Symbol(Abcde.x, Decl(test.js, 0, 13))
8+
}
9+
10+
module.exports = {
11+
>module.exports : Symbol("/test", Decl(test.js, 0, 0))
12+
>module : Symbol("/test.js", Decl(test.js, 3, 1), Decl(index.ts, 0, 31))
13+
>exports : Symbol("/test.js", Decl(test.js, 3, 1), Decl(index.ts, 0, 31))
14+
15+
Abcde
16+
>Abcde : Symbol(Abcde, Decl(test.js, 5, 18))
17+
18+
};
19+
20+
=== /index.ts ===
21+
import { Abcde } from "./test";
22+
>Abcde : Symbol(Abcde, Decl(index.ts, 0, 8))
23+
24+
declare module "./test" {
25+
>"./test" : Symbol("/test.js", Decl(test.js, 3, 1), Decl(index.ts, 0, 31))
26+
27+
interface Abcde { b: string }
28+
>Abcde : Symbol(Abcde, Decl(index.ts, 2, 25), Decl(test.js, 5, 18))
29+
>b : Symbol(Abcde.b, Decl(index.ts, 3, 19))
30+
}
31+
32+
new Abcde().x;
33+
>new Abcde().x : Symbol(Abcde.x, Decl(test.js, 0, 13))
34+
>Abcde : Symbol(Abcde, Decl(index.ts, 0, 8))
35+
>x : Symbol(Abcde.x, Decl(test.js, 0, 13))
36+
37+
// Bug: the type meaning from /test.js does not
38+
// propagate through the object literal export.
39+
const x: Abcde = { b: "" };
40+
>x : Symbol(x, Decl(index.ts, 10, 5))
41+
>Abcde : Symbol(Abcde, Decl(index.ts, 0, 8))
42+
>b : Symbol(b, Decl(index.ts, 10, 18))
43+

0 commit comments

Comments
 (0)