Skip to content

Commit 9952925

Browse files
committed
Freshness may be gone, but we still need refinement filtering on mutable location assignment
1 parent bf68b75 commit 9952925

19 files changed

Lines changed: 2215 additions & 15 deletions

‎tsc/internal/checker/checker.go‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17377,7 +17377,10 @@ func (c *Checker) getWidenedLiteralTypeForInitializer(declaration *ast.Node, t *
1737717377
if c.getCombinedNodeFlagsCached(declaration)&ast.NodeFlagsConstant != 0 || isDeclarationReadonly(declaration) {
1737817378
return t
1737917379
}
17380-
return c.getWidenedLiteralType(t)
17380+
if initializer := declaration.Initializer(); initializer != nil && isTypeAssertion(ast.SkipOuterExpressions(initializer, ast.OEKParentheses|ast.OEKNonNullAssertions|ast.OEKSatisfies)) {
17381+
return c.getWidenedLiteralType(t)
17382+
}
17383+
return c.getWidenedNegatedLiteralType(c.getWidenedLiteralType(t))
1738117384
}
1738217385

1738317386
func (c *Checker) getTypeOfFuncClassEnumModule(symbol *ast.Symbol) *Type {
@@ -21230,7 +21233,7 @@ func (c *Checker) checkIfExpressionRefinesParameter(fn *ast.Node, expr *ast.Node
2123021233
antecedent = &ast.FlowNode{Flags: ast.FlowFlagsStart}
2123121234
}
2123221235
trueCondition := &ast.FlowNode{Flags: ast.FlowFlagsTrueCondition, Node: expr, Antecedent: antecedent}
21233-
trueType := c.getFlowTypeOfReferenceEx(param.Name(), initType, initType, fn, trueCondition)
21236+
trueType := c.getWidenedNegatedLiteralType(c.getFlowTypeOfReferenceEx(param.Name(), initType, initType, fn, trueCondition))
2123421237
if trueType == initType {
2123521238
return nil
2123621239
}

‎tsc/internal/checker/checker_test.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ func BenchmarkCheckFreshProgram(b *testing.B) {
171171
program.BindSourceFiles()
172172
c, _ := checker.NewChecker(program, nil)
173173
for _, file := range program.GetSourceFiles() {
174-
assert.Equal(b, len(c.GetDiagnostics(ctx, file)), 0)
174+
diagnostics := c.GetDiagnostics(ctx, file)
175+
assert.Check(b, len(diagnostics) == 0, "%s: %v", file.FileName(), diagnostics)
175176
}
176177
assert.Equal(b, len(c.GetGlobalDiagnostics()), 0)
177178
}

‎tsc/internal/checker/flow.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,14 @@ func (c *Checker) getTypeAtFlowAssignment(f *FlowState, flow *ast.FlowNode) Flow
240240
flowType := c.getTypeAtFlowNode(f, flow.Antecedent)
241241
return c.newFlowType(c.getBaseTypeOfLiteralType(flowType.t), flowType.incomplete)
242242
}
243+
if c.maybeTypeOfKind(f.declaredType, TypeFlagsNullable) {
244+
flowType := c.getTypeAtFlowNode(f, flow.Antecedent)
245+
t := c.filterType(f.declaredType, func(t *Type) bool {
246+
return t.flags&TypeFlagsNullable == 0 ||
247+
c.hasTypeFacts(flowType.t, core.IfElse(t.flags&TypeFlagsUndefined != 0, TypeFactsEQUndefined, TypeFactsEQNull))
248+
})
249+
return c.newFlowType(t, flowType.incomplete)
250+
}
243251
return FlowType{t: f.declaredType}
244252
}
245253
if f.declaredType == c.autoType || f.declaredType == c.autoArrayType {

‎tsc/internal/checker/negated.go‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,24 @@ func containsNegatedType(t *Type) bool {
5959
return false
6060
}
6161

62+
// Literal exclusions must not make inferred mutable locations narrower than their primitive types.
63+
func (c *Checker) getWidenedNegatedLiteralType(t *Type) *Type {
64+
switch {
65+
case t.flags&TypeFlagsNegated != 0:
66+
if t.AsNegatedType().baseType.flags&TypeFlagsLiteral != 0 {
67+
return c.unknownType
68+
}
69+
case t.flags&TypeFlagsUnion != 0:
70+
return c.mapType(t, c.getWidenedNegatedLiteralType)
71+
case t.flags&TypeFlagsIntersection != 0:
72+
types := core.SameMap(t.Types(), c.getWidenedNegatedLiteralType)
73+
if !slices.Equal(types, t.Types()) {
74+
return c.getIntersectionType(types)
75+
}
76+
}
77+
return t
78+
}
79+
6280
// removeComplementaryNegatedTypes replaces 'Base & not C' with 'Base' when another branch covers
6381
// 'Base & C'. That branch is retained unless it is also contained in Base. Removing one negation
6482
// at a time preserves shared negative factors and allows successive reductions, for example
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
compoundAssignmentPreservesNullableNarrowing.ts(99,5): error TS18049: 'value' is possibly 'null' or 'undefined'.
2+
compoundAssignmentPreservesNullableNarrowing.ts(100,5): error TS18049: 'value' is possibly 'null' or 'undefined'.
3+
compoundAssignmentPreservesNullableNarrowing.ts(101,11): error TS2322: Type 'number | null | undefined' is not assignable to type 'number'.
4+
Type 'undefined' is not assignable to type 'number'.
5+
compoundAssignmentPreservesNullableNarrowing.ts(107,9): error TS18048: 'value' is possibly 'undefined'.
6+
compoundAssignmentPreservesNullableNarrowing.ts(109,15): error TS2322: Type 'number | undefined' is not assignable to type 'number'.
7+
Type 'undefined' is not assignable to type 'number'.
8+
compoundAssignmentPreservesNullableNarrowing.ts(117,9): error TS18047: 'value' is possibly 'null'.
9+
compoundAssignmentPreservesNullableNarrowing.ts(119,15): error TS2322: Type 'number | null' is not assignable to type 'number'.
10+
Type 'null' is not assignable to type 'number'.
11+
12+
13+
==== compoundAssignmentPreservesNullableNarrowing.ts (7 errors) ====
14+
enum Flags {
15+
None = 0,
16+
First = 1,
17+
Second = 2,
18+
}
19+
20+
export function enumFlags(value: Flags | undefined) {
21+
if (value === undefined) {
22+
value = Flags.None;
23+
value |= Flags.First;
24+
value |= Flags.Second;
25+
}
26+
const result: Flags = value;
27+
return value;
28+
}
29+
30+
export function numeric(value: number | null | undefined) {
31+
if (value != null) {
32+
value -= 1;
33+
value += 1;
34+
value *= 2;
35+
value /= 2;
36+
value %= 2;
37+
value **= 2;
38+
value <<= 1;
39+
value >>= 1;
40+
value >>>= 1;
41+
value |= 1;
42+
value &= 1;
43+
value ^= 1;
44+
const result: number = value;
45+
return value;
46+
}
47+
throw new Error();
48+
}
49+
50+
export function updates(value: number | null | undefined) {
51+
if (value != null) {
52+
value++;
53+
++value;
54+
value--;
55+
--value;
56+
const result: number = value;
57+
return value;
58+
}
59+
throw new Error();
60+
}
61+
62+
export function bigint(value: bigint | null | undefined) {
63+
if (value != null) {
64+
value *= 2n;
65+
value++;
66+
const result: bigint = value;
67+
return value;
68+
}
69+
throw new Error();
70+
}
71+
72+
export function text(value: string | null | undefined) {
73+
if (value != null) {
74+
value += "suffix";
75+
const result: string = value;
76+
return value;
77+
}
78+
throw new Error();
79+
}
80+
81+
export function properties(holder: { value?: number | null }) {
82+
if (holder.value != null) {
83+
holder.value -= 1;
84+
holder["value"]++;
85+
const result: number = holder.value;
86+
return holder["value"];
87+
}
88+
throw new Error();
89+
}
90+
91+
export function loop(value: number | null | undefined) {
92+
if (value != null) {
93+
while (value !== 0) {
94+
value -= 1;
95+
}
96+
const result: 0 = value;
97+
return value;
98+
}
99+
throw new Error();
100+
}
101+
102+
export function mutableFlags(value: number, flags: number[]) {
103+
if (value === 0) return "";
104+
let remainingFlags: number = value;
105+
for (const flag of flags) {
106+
remainingFlags &= ~flag;
107+
}
108+
return remainingFlags === 0 ? "all" : "some";
109+
}
110+
111+
export function unchecked(value: number | null | undefined) {
112+
value -= 1;
113+
~~~~~
114+
!!! error TS18049: 'value' is possibly 'null' or 'undefined'.
115+
value++;
116+
~~~~~
117+
!!! error TS18049: 'value' is possibly 'null' or 'undefined'.
118+
const result: number = value;
119+
~~~~~~
120+
!!! error TS2322: Type 'number | null | undefined' is not assignable to type 'number'.
121+
!!! error TS2322: Type 'undefined' is not assignable to type 'number'.
122+
return value;
123+
}
124+
125+
export function checkedOnlyNull(value: number | null | undefined) {
126+
if (value !== null) {
127+
value -= 1;
128+
~~~~~
129+
!!! error TS18048: 'value' is possibly 'undefined'.
130+
const result: number | undefined = value;
131+
const invalid: number = value;
132+
~~~~~~~
133+
!!! error TS2322: Type 'number | undefined' is not assignable to type 'number'.
134+
!!! error TS2322: Type 'undefined' is not assignable to type 'number'.
135+
return value;
136+
}
137+
throw new Error();
138+
}
139+
140+
export function checkedOnlyUndefined(value: number | null | undefined) {
141+
if (value !== undefined) {
142+
value -= 1;
143+
~~~~~
144+
!!! error TS18047: 'value' is possibly 'null'.
145+
const result: number | null = value;
146+
const invalid: number = value;
147+
~~~~~~~
148+
!!! error TS2322: Type 'number | null' is not assignable to type 'number'.
149+
!!! error TS2322: Type 'null' is not assignable to type 'number'.
150+
return value;
151+
}
152+
throw new Error();
153+
}
154+

0 commit comments

Comments
 (0)