Skip to content

Commit ce95d9c

Browse files
authored
Fix values and types merging in JS module exports (#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+
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
=== /test.js ===
2+
class Abcde {
3+
>Abcde : Abcde
4+
5+
/** @type {string} */
6+
x;
7+
>x : string
8+
}
9+
10+
module.exports = {
11+
>module.exports = { Abcde} : { Abcde: typeof Abcde; }
12+
>module.exports : { Abcde: typeof Abcde; }
13+
>module : { "\"/test\"": { Abcde: typeof Abcde; }; }
14+
>exports : { Abcde: typeof Abcde; }
15+
>{ Abcde} : { Abcde: typeof Abcde; }
16+
17+
Abcde
18+
>Abcde : typeof Abcde
19+
20+
};
21+
22+
=== /index.ts ===
23+
import { Abcde } from "./test";
24+
>Abcde : typeof Abcde
25+
26+
declare module "./test" {
27+
>"./test" : { Abcde: typeof Abcde; }
28+
29+
interface Abcde { b: string }
30+
>b : string
31+
}
32+
33+
new Abcde().x;
34+
>new Abcde().x : string
35+
>new Abcde() : Abcde
36+
>Abcde : typeof Abcde
37+
>x : string
38+
39+
// Bug: the type meaning from /test.js does not
40+
// propagate through the object literal export.
41+
const x: Abcde = { b: "" };
42+
>x : Abcde
43+
>{ b: "" } : { b: string; }
44+
>b : string
45+
>"" : ""
46+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/index.ts(4,16): error TS2300: Duplicate identifier 'a'.
2+
/index.ts(7,3): error TS2339: Property 'toFixed' does not exist on type 'string'.
3+
/test.js(2,3): error TS2300: Duplicate identifier 'a'.
4+
5+
6+
==== /test.js (1 errors) ====
7+
module.exports = {
8+
a: "ok"
9+
~
10+
!!! error TS2300: Duplicate identifier 'a'.
11+
!!! related TS6203 /index.ts:4:16: 'a' was also declared here.
12+
};
13+
14+
==== /index.ts (2 errors) ====
15+
import { a } from "./test";
16+
17+
declare module "./test" {
18+
export const a: number;
19+
~
20+
!!! error TS2300: Duplicate identifier 'a'.
21+
!!! related TS6203 /test.js:2:3: 'a' was also declared here.
22+
}
23+
24+
a.toFixed();
25+
~~~~~~~
26+
!!! error TS2339: Property 'toFixed' does not exist on type 'string'.
27+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
=== /test.js ===
2+
module.exports = {
3+
>module.exports : Symbol("/test", Decl(test.js, 0, 0))
4+
>module : Symbol("/test.js", Decl(test.js, 0, 0), Decl(index.ts, 0, 27))
5+
>exports : Symbol("/test.js", Decl(test.js, 0, 0), Decl(index.ts, 0, 27))
6+
7+
a: "ok"
8+
>a : Symbol(a, Decl(test.js, 0, 18))
9+
10+
};
11+
12+
=== /index.ts ===
13+
import { a } from "./test";
14+
>a : Symbol(a, Decl(index.ts, 0, 8))
15+
16+
declare module "./test" {
17+
>"./test" : Symbol("/test.js", Decl(test.js, 0, 0), Decl(index.ts, 0, 27))
18+
19+
export const a: number;
20+
>a : Symbol(a, Decl(index.ts, 3, 14))
21+
}
22+
23+
a.toFixed();
24+
>a : Symbol(a, Decl(index.ts, 0, 8))
25+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
=== /test.js ===
2+
module.exports = {
3+
>module.exports = { a: "ok"} : { a: string | number; }
4+
>module.exports : { a: string | number; }
5+
>module : { "\"/test\"": { a: string | number; }; }
6+
>exports : { a: string | number; }
7+
>{ a: "ok"} : { a: string; }
8+
9+
a: "ok"
10+
>a : string
11+
>"ok" : "ok"
12+
13+
};
14+
15+
=== /index.ts ===
16+
import { a } from "./test";
17+
>a : string
18+
19+
declare module "./test" {
20+
>"./test" : { a: string | number; }
21+
22+
export const a: number;
23+
>a : number
24+
}
25+
26+
a.toFixed();
27+
>a.toFixed() : any
28+
>a.toFixed : any
29+
>a : string
30+
>toFixed : any
31+

tests/baselines/reference/moduleExportAlias2.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ exports = module.exports = C
3737
>C : Symbol(C, Decl(semver.js, 2, 22))
3838

3939
exports.f = n => n + 1
40-
>exports.f : Symbol(f)
40+
>exports.f : Symbol(f, Decl(semver.js, 1, 28))
4141
>exports : Symbol(f, Decl(semver.js, 1, 28))
4242
>f : Symbol(f, Decl(semver.js, 1, 28))
4343
>n : Symbol(n, Decl(semver.js, 2, 11))

tests/baselines/reference/moduleExportAlias4.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module.exports = class C {}
1212
>C : Symbol(C, Decl(bug24024.js, 2, 16))
1313

1414
module.exports.D = class D { }
15-
>module.exports.D : Symbol(D)
15+
>module.exports.D : Symbol(D, Decl(bug24024.js, 2, 27))
1616
>module.exports : Symbol(D, Decl(bug24024.js, 2, 27))
1717
>module : Symbol(module, Decl(bug24024.js, 1, 31))
1818
>exports : Symbol("tests/cases/conformance/salsa/bug24024", Decl(bug24024.js, 0, 0))

tests/baselines/reference/moduleExportAliasImported.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
=== tests/cases/conformance/salsa/bug28014.js ===
22
exports.version = 1
3-
>exports.version : Symbol(version)
3+
>exports.version : Symbol(version, Decl(bug28014.js, 0, 0))
44
>exports : Symbol(version, Decl(bug28014.js, 0, 0))
55
>version : Symbol(version, Decl(bug28014.js, 0, 0))
66

tests/baselines/reference/moduleExportPropertyAssignmentDefault.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module.exports = axios // both assignments should be ok
99
>axios : Symbol(axios, Decl(axios.js, 0, 3))
1010

1111
module.exports.default = axios
12-
>module.exports.default : Symbol(default)
12+
>module.exports.default : Symbol(default, Decl(axios.js, 1, 22))
1313
>module.exports : Symbol(default, Decl(axios.js, 1, 22))
1414
>module : Symbol(module, Decl(axios.js, 0, 14))
1515
>exports : Symbol("tests/cases/conformance/salsa/axios", Decl(axios.js, 0, 0))

tests/baselines/reference/moduleExportWithExportPropertyAssignment3.symbols

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ mod1.justExport.toFixed()
1313
>toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --))
1414

1515
mod1.bothBefore.toFixed() // error, 'toFixed' not on 'string | number'
16-
>mod1.bothBefore : Symbol(bothBefore)
16+
>mod1.bothBefore : Symbol(bothBefore, Decl(mod1.js, 3, 18), Decl(mod1.js, 0, 0))
1717
>mod1 : Symbol(mod1, Decl(a.js, 1, 3))
18-
>bothBefore : Symbol(bothBefore)
18+
>bothBefore : Symbol(bothBefore, Decl(mod1.js, 3, 18), Decl(mod1.js, 0, 0))
1919

2020
mod1.bothAfter.toFixed() // error, 'toFixed' not on 'string | number'
21-
>mod1.bothAfter : Symbol(bothAfter)
21+
>mod1.bothAfter : Symbol(bothAfter, Decl(mod1.js, 4, 18), Decl(mod1.js, 6, 1))
2222
>mod1 : Symbol(mod1, Decl(a.js, 1, 3))
23-
>bothAfter : Symbol(bothAfter)
23+
>bothAfter : Symbol(bothAfter, Decl(mod1.js, 4, 18), Decl(mod1.js, 6, 1))
2424

2525
mod1.justProperty.length
2626
>mod1.justProperty.length : Symbol(String.length, Decl(lib.es5.d.ts, --, --))
@@ -41,7 +41,7 @@ declare function require(name: string): any;
4141
=== tests/cases/conformance/salsa/mod1.js ===
4242
/// <reference path='./requires.d.ts' />
4343
module.exports.bothBefore = 'string'
44-
>module.exports.bothBefore : Symbol(bothBefore)
44+
>module.exports.bothBefore : Symbol(bothBefore, Decl(mod1.js, 3, 18), Decl(mod1.js, 0, 0))
4545
>module.exports : Symbol(bothBefore, Decl(mod1.js, 0, 0))
4646
>module : Symbol(module, Decl(mod1.js, 0, 0))
4747
>exports : Symbol("tests/cases/conformance/salsa/mod1", Decl(mod1.js, 0, 0))
@@ -62,7 +62,7 @@ module.exports = {
6262
>bothAfter : Symbol(bothAfter, Decl(mod1.js, 4, 18))
6363
}
6464
module.exports.bothAfter = 'string'
65-
>module.exports.bothAfter : Symbol(bothAfter)
65+
>module.exports.bothAfter : Symbol(bothAfter, Decl(mod1.js, 4, 18), Decl(mod1.js, 6, 1))
6666
>module.exports : Symbol(bothAfter, Decl(mod1.js, 6, 1))
6767
>module : Symbol(module, Decl(mod1.js, 0, 0))
6868
>exports : Symbol("tests/cases/conformance/salsa/mod1", Decl(mod1.js, 0, 0))

0 commit comments

Comments
 (0)