Skip to content

Commit b481dd4

Browse files
authored
More precise property-overwritten-by-spread errors (#37192)
* More precise property-overwritten-by-spread errors Trying to do this check in getSpreadType just doesn't have enough information, so I moved it to checkObjectLiteral, which is a better place for issuing errors anyway. Unfortunately, the approach is kind of expensive in that it 1. creates a new map for each property and 2. iterates over all properties of the spread type, even if it's a union. I have some ideas to improve (1) that might work out. I'm not sure how bad (2) is since we're going to iterate over all properties of all constituents of a union. Fixes #36779 * another test and rename
1 parent 3046a54 commit b481dd4

7 files changed

+143
-18
lines changed

src/compiler/checker.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13175,7 +13175,7 @@ namespace ts {
1317513175
* this function should be called in a left folding style, with left = previous result of getSpreadType
1317613176
* and right = the new element to be spread.
1317713177
*/
13178-
function getSpreadType(left: Type, right: Type, symbol: Symbol | undefined, objectFlags: ObjectFlags, readonly: boolean, isParentTypeNullable?: boolean): Type {
13178+
function getSpreadType(left: Type, right: Type, symbol: Symbol | undefined, objectFlags: ObjectFlags, readonly: boolean): Type {
1317913179
if (left.flags & TypeFlags.Any || right.flags & TypeFlags.Any) {
1318013180
return anyType;
1318113181
}
@@ -13191,16 +13191,16 @@ namespace ts {
1319113191
if (left.flags & TypeFlags.Union) {
1319213192
const merged = tryMergeUnionOfObjectTypeAndEmptyObject(left as UnionType, readonly);
1319313193
if (merged) {
13194-
return getSpreadType(merged, right, symbol, objectFlags, readonly, isParentTypeNullable);
13194+
return getSpreadType(merged, right, symbol, objectFlags, readonly);
1319513195
}
13196-
return mapType(left, t => getSpreadType(t, right, symbol, objectFlags, readonly, isParentTypeNullable));
13196+
return mapType(left, t => getSpreadType(t, right, symbol, objectFlags, readonly));
1319713197
}
1319813198
if (right.flags & TypeFlags.Union) {
1319913199
const merged = tryMergeUnionOfObjectTypeAndEmptyObject(right as UnionType, readonly);
1320013200
if (merged) {
13201-
return getSpreadType(left, merged, symbol, objectFlags, readonly, maybeTypeOfKind(right, TypeFlags.Nullable));
13201+
return getSpreadType(left, merged, symbol, objectFlags, readonly);
1320213202
}
13203-
return mapType(right, t => getSpreadType(left, t, symbol, objectFlags, readonly, maybeTypeOfKind(right, TypeFlags.Nullable)));
13203+
return mapType(right, t => getSpreadType(left, t, symbol, objectFlags, readonly));
1320413204
}
1320513205
if (right.flags & (TypeFlags.BooleanLike | TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.EnumLike | TypeFlags.NonPrimitive | TypeFlags.Index)) {
1320613206
return left;
@@ -13264,14 +13264,6 @@ namespace ts {
1326413264
result.nameType = getSymbolLinks(leftProp).nameType;
1326513265
members.set(leftProp.escapedName, result);
1326613266
}
13267-
else if (strictNullChecks &&
13268-
!isParentTypeNullable &&
13269-
symbol &&
13270-
!isFromSpreadAssignment(leftProp, symbol) &&
13271-
isFromSpreadAssignment(rightProp, symbol) &&
13272-
!maybeTypeOfKind(rightType, TypeFlags.Nullable)) {
13273-
error(leftProp.valueDeclaration, Diagnostics._0_is_specified_more_than_once_so_this_usage_will_be_overwritten, unescapeLeadingUnderscores(leftProp.escapedName));
13274-
}
1327513267
}
1327613268
else {
1327713269
members.set(leftProp.escapedName, getSpreadSymbol(leftProp, readonly));
@@ -16802,10 +16794,6 @@ namespace ts {
1680216794
return match === -1 || discriminable.indexOf(/*searchElement*/ true, match + 1) !== -1 ? defaultValue : target.types[match];
1680316795
}
1680416796

16805-
function isFromSpreadAssignment(prop: Symbol, container: Symbol) {
16806-
return prop.valueDeclaration?.parent !== container.valueDeclaration;
16807-
}
16808-
1680916797
/**
1681016798
* A type is 'weak' if it is an object type with at least one optional property
1681116799
* and no required properties, call/construct signatures or index signatures
@@ -22566,6 +22554,7 @@ namespace ts {
2256622554
checkGrammarObjectLiteralExpression(node, inDestructuringPattern);
2256722555

2256822556
let propertiesTable: SymbolTable;
22557+
const allPropertiesTable = createSymbolTable();
2256922558
let propertiesArray: Symbol[] = [];
2257022559
let spread: Type = emptyObjectType;
2257122560

@@ -22647,6 +22636,7 @@ namespace ts {
2264722636
prop.type = type;
2264822637
prop.target = member;
2264922638
member = prop;
22639+
allPropertiesTable.set(prop.escapedName, prop);
2265022640
}
2265122641
else if (memberDecl.kind === SyntaxKind.SpreadAssignment) {
2265222642
if (languageVersion < ScriptTarget.ES2015) {
@@ -22664,6 +22654,16 @@ namespace ts {
2266422654
error(memberDecl, Diagnostics.Spread_types_may_only_be_created_from_object_types);
2266522655
return errorType;
2266622656
}
22657+
for (const right of getPropertiesOfType(type)) {
22658+
const rightType = getTypeOfSymbol(right);
22659+
const left = allPropertiesTable.get(right.escapedName);
22660+
if (strictNullChecks &&
22661+
left &&
22662+
!maybeTypeOfKind(rightType, TypeFlags.Nullable)) {
22663+
error(left.valueDeclaration, Diagnostics._0_is_specified_more_than_once_so_this_usage_will_be_overwritten, unescapeLeadingUnderscores(left.escapedName));
22664+
}
22665+
}
22666+
2266722667
spread = getSpreadType(spread, type, node.symbol, objectFlags, inConstContext);
2266822668
offset = i + 1;
2266922669
continue;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
tests/cases/conformance/types/spread/objectSpreadSetonlyAccessor.ts(2,34): error TS2783: 'foo' is specified more than once, so this usage will be overwritten.
2+
3+
4+
==== tests/cases/conformance/types/spread/objectSpreadSetonlyAccessor.ts (1 errors) ====
5+
const o1: { foo: number, bar: undefined } = { foo: 1, ... { set bar(_v: number) { } } }
6+
const o2: { foo: undefined } = { foo: 1, ... { set foo(_v: number) { } } }
7+
~~~~~~
8+
!!! error TS2783: 'foo' is specified more than once, so this usage will be overwritten.
9+

tests/baselines/reference/spreadOverwritesPropertyStrict.errors.txt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
tests/cases/conformance/types/spread/spreadOverwritesPropertyStrict.ts(3,17): error TS2783: 'b' is specified more than once, so this usage will be overwritten.
22
tests/cases/conformance/types/spread/spreadOverwritesPropertyStrict.ts(15,14): error TS2783: 'x' is specified more than once, so this usage will be overwritten.
3+
tests/cases/conformance/types/spread/spreadOverwritesPropertyStrict.ts(24,14): error TS2783: 'command' is specified more than once, so this usage will be overwritten.
34

45

5-
==== tests/cases/conformance/types/spread/spreadOverwritesPropertyStrict.ts (2 errors) ====
6+
==== tests/cases/conformance/types/spread/spreadOverwritesPropertyStrict.ts (3 errors) ====
67
declare var ab: { a: number, b: number };
78
declare var abq: { a: number, b?: number };
89
var unused1 = { b: 1, ...ab } // error
@@ -23,4 +24,15 @@ tests/cases/conformance/types/spread/spreadOverwritesPropertyStrict.ts(15,14): e
2324
~~~~
2425
!!! error TS2783: 'x' is specified more than once, so this usage will be overwritten.
2526
}
27+
function i(b: boolean, t: { command: string, ok: string }) {
28+
return { command: "hi", ...(b ? t : {}) } // ok
29+
}
30+
function j() {
31+
return { ...{ command: "hi" } , ...{ command: "bye" } } // ok
32+
}
33+
function k(t: { command: string, ok: string }) {
34+
return { command: "hi", ...{ spoiler: true }, spoiler2: true, ...t } // error
35+
~~~~~~~~~~~~~
36+
!!! error TS2783: 'command' is specified more than once, so this usage will be overwritten.
37+
}
2638

tests/baselines/reference/spreadOverwritesPropertyStrict.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ function f(obj: { x: number } | undefined) {
1515
function h(obj: { x: number } | { x: string }) {
1616
return { x: 1, ...obj } // error
1717
}
18+
function i(b: boolean, t: { command: string, ok: string }) {
19+
return { command: "hi", ...(b ? t : {}) } // ok
20+
}
21+
function j() {
22+
return { ...{ command: "hi" } , ...{ command: "bye" } } // ok
23+
}
24+
function k(t: { command: string, ok: string }) {
25+
return { command: "hi", ...{ spoiler: true }, spoiler2: true, ...t } // error
26+
}
1827

1928

2029
//// [spreadOverwritesPropertyStrict.js]
@@ -44,3 +53,12 @@ function f(obj) {
4453
function h(obj) {
4554
return __assign({ x: 1 }, obj); // error
4655
}
56+
function i(b, t) {
57+
return __assign({ command: "hi" }, (b ? t : {})); // ok
58+
}
59+
function j() {
60+
return __assign({ command: "hi" }, { command: "bye" }); // ok
61+
}
62+
function k(t) {
63+
return __assign(__assign(__assign({ command: "hi" }, { spoiler: true }), { spoiler2: true }), t); // error
64+
}

tests/baselines/reference/spreadOverwritesPropertyStrict.symbols

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,35 @@ function h(obj: { x: number } | { x: string }) {
6262
>x : Symbol(x, Decl(spreadOverwritesPropertyStrict.ts, 14, 12))
6363
>obj : Symbol(obj, Decl(spreadOverwritesPropertyStrict.ts, 13, 11))
6464
}
65+
function i(b: boolean, t: { command: string, ok: string }) {
66+
>i : Symbol(i, Decl(spreadOverwritesPropertyStrict.ts, 15, 1))
67+
>b : Symbol(b, Decl(spreadOverwritesPropertyStrict.ts, 16, 11))
68+
>t : Symbol(t, Decl(spreadOverwritesPropertyStrict.ts, 16, 22))
69+
>command : Symbol(command, Decl(spreadOverwritesPropertyStrict.ts, 16, 27))
70+
>ok : Symbol(ok, Decl(spreadOverwritesPropertyStrict.ts, 16, 44))
71+
72+
return { command: "hi", ...(b ? t : {}) } // ok
73+
>command : Symbol(command, Decl(spreadOverwritesPropertyStrict.ts, 17, 12))
74+
>b : Symbol(b, Decl(spreadOverwritesPropertyStrict.ts, 16, 11))
75+
>t : Symbol(t, Decl(spreadOverwritesPropertyStrict.ts, 16, 22))
76+
}
77+
function j() {
78+
>j : Symbol(j, Decl(spreadOverwritesPropertyStrict.ts, 18, 1))
79+
80+
return { ...{ command: "hi" } , ...{ command: "bye" } } // ok
81+
>command : Symbol(command, Decl(spreadOverwritesPropertyStrict.ts, 20, 17))
82+
>command : Symbol(command, Decl(spreadOverwritesPropertyStrict.ts, 20, 40))
83+
}
84+
function k(t: { command: string, ok: string }) {
85+
>k : Symbol(k, Decl(spreadOverwritesPropertyStrict.ts, 21, 1))
86+
>t : Symbol(t, Decl(spreadOverwritesPropertyStrict.ts, 22, 11))
87+
>command : Symbol(command, Decl(spreadOverwritesPropertyStrict.ts, 22, 15))
88+
>ok : Symbol(ok, Decl(spreadOverwritesPropertyStrict.ts, 22, 32))
89+
90+
return { command: "hi", ...{ spoiler: true }, spoiler2: true, ...t } // error
91+
>command : Symbol(command, Decl(spreadOverwritesPropertyStrict.ts, 23, 12))
92+
>spoiler : Symbol(spoiler, Decl(spreadOverwritesPropertyStrict.ts, 23, 32))
93+
>spoiler2 : Symbol(spoiler2, Decl(spreadOverwritesPropertyStrict.ts, 23, 49))
94+
>t : Symbol(t, Decl(spreadOverwritesPropertyStrict.ts, 22, 11))
95+
}
6596

tests/baselines/reference/spreadOverwritesPropertyStrict.types

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,50 @@ function h(obj: { x: number } | { x: string }) {
7777
>1 : 1
7878
>obj : { x: number; } | { x: string; }
7979
}
80+
function i(b: boolean, t: { command: string, ok: string }) {
81+
>i : (b: boolean, t: { command: string; ok: string; }) => { command: string; ok: string; } | { command: string; }
82+
>b : boolean
83+
>t : { command: string; ok: string; }
84+
>command : string
85+
>ok : string
86+
87+
return { command: "hi", ...(b ? t : {}) } // ok
88+
>{ command: "hi", ...(b ? t : {}) } : { command: string; ok: string; } | { command: string; }
89+
>command : string
90+
>"hi" : "hi"
91+
>(b ? t : {}) : { command: string; ok: string; } | {}
92+
>b ? t : {} : { command: string; ok: string; } | {}
93+
>b : boolean
94+
>t : { command: string; ok: string; }
95+
>{} : {}
96+
}
97+
function j() {
98+
>j : () => { command: string; }
99+
100+
return { ...{ command: "hi" } , ...{ command: "bye" } } // ok
101+
>{ ...{ command: "hi" } , ...{ command: "bye" } } : { command: string; }
102+
>{ command: "hi" } : { command: string; }
103+
>command : string
104+
>"hi" : "hi"
105+
>{ command: "bye" } : { command: string; }
106+
>command : string
107+
>"bye" : "bye"
108+
}
109+
function k(t: { command: string, ok: string }) {
110+
>k : (t: { command: string; ok: string; }) => { command: string; ok: string; spoiler2: boolean; spoiler: boolean; }
111+
>t : { command: string; ok: string; }
112+
>command : string
113+
>ok : string
114+
115+
return { command: "hi", ...{ spoiler: true }, spoiler2: true, ...t } // error
116+
>{ command: "hi", ...{ spoiler: true }, spoiler2: true, ...t } : { command: string; ok: string; spoiler2: boolean; spoiler: boolean; }
117+
>command : string
118+
>"hi" : "hi"
119+
>{ spoiler: true } : { spoiler: boolean; }
120+
>spoiler : boolean
121+
>true : true
122+
>spoiler2 : boolean
123+
>true : true
124+
>t : { command: string; ok: string; }
125+
}
80126

tests/cases/conformance/types/spread/spreadOverwritesPropertyStrict.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,12 @@ function f(obj: { x: number } | undefined) {
1515
function h(obj: { x: number } | { x: string }) {
1616
return { x: 1, ...obj } // error
1717
}
18+
function i(b: boolean, t: { command: string, ok: string }) {
19+
return { command: "hi", ...(b ? t : {}) } // ok
20+
}
21+
function j() {
22+
return { ...{ command: "hi" } , ...{ command: "bye" } } // ok
23+
}
24+
function k(t: { command: string, ok: string }) {
25+
return { command: "hi", ...{ spoiler: true }, spoiler2: true, ...t } // error
26+
}

0 commit comments

Comments
 (0)