Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Latest commit

 

History

History
History
1409 lines (1251 loc) · 62.6 KB

File metadata and controls

1409 lines (1251 loc) · 62.6 KB
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
using SharpTS.Parsing;
namespace SharpTS.TypeSystem;
/// <summary>
/// Type guard analysis for control-flow based type narrowing.
/// </summary>
public partial class TypeChecker
{
private (string? VarName, TypeInfo? NarrowedType, TypeInfo? ExcludedType) AnalyzeTypeGuard(Expr condition)
{
// Pattern 1: typeof x === "string" or typeof x == "string"
if (condition is Expr.Binary bin &&
bin.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin.Left is Expr.Unary { Operator.Type: TokenType.TYPEOF, Right: Expr.Variable v } &&
bin.Right is Expr.Literal { Value: string typeStr })
{
return AnalyzeTypeofGuard(v.Name.Lexeme, typeStr, negated: false);
}
// Pattern 1b: "string" === typeof x (reversed operands)
if (condition is Expr.Binary bin1b &&
bin1b.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin1b.Right is Expr.Unary { Operator.Type: TokenType.TYPEOF, Right: Expr.Variable v1b } &&
bin1b.Left is Expr.Literal { Value: string typeStr1b })
{
return AnalyzeTypeofGuard(v1b.Name.Lexeme, typeStr1b, negated: false);
}
// Pattern 2: typeof x !== "string" or typeof x != "string"
if (condition is Expr.Binary bin2 &&
bin2.Operator.Type is TokenType.BANG_EQUAL or TokenType.BANG_EQUAL_EQUAL &&
bin2.Left is Expr.Unary { Operator.Type: TokenType.TYPEOF, Right: Expr.Variable v2 } &&
bin2.Right is Expr.Literal { Value: string typeStr2 })
{
return AnalyzeTypeofGuard(v2.Name.Lexeme, typeStr2, negated: true);
}
// Pattern 3: x instanceof ClassName
if (condition is Expr.Binary bin3 &&
bin3.Operator.Type == TokenType.INSTANCEOF &&
bin3.Left is Expr.Variable v3 &&
bin3.Right is Expr.Variable classVar)
{
return AnalyzeInstanceofGuard(v3.Name.Lexeme, classVar.Name);
}
// Pattern 4: x === null or x == null. A LOOSE `==` against EITHER null or undefined narrows
// away BOTH (JS: `null == undefined`); only STRICT `===` narrows away just the one compared.
if (condition is Expr.Binary bin4 &&
bin4.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin4.Left is Expr.Variable v4 &&
bin4.Right is Expr.Literal { Value: null })
{
return AnalyzeNullCheck(v4.Name.Lexeme, checkingForNull: true, negated: false, checkBoth: bin4.Operator.Type == TokenType.EQUAL_EQUAL);
}
// Pattern 4b: null === x (reversed)
if (condition is Expr.Binary bin4b &&
bin4b.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin4b.Right is Expr.Variable v4b &&
bin4b.Left is Expr.Literal { Value: null })
{
return AnalyzeNullCheck(v4b.Name.Lexeme, checkingForNull: true, negated: false, checkBoth: bin4b.Operator.Type == TokenType.EQUAL_EQUAL);
}
// Pattern 5: x !== null or x != null
if (condition is Expr.Binary bin5 &&
bin5.Operator.Type is TokenType.BANG_EQUAL or TokenType.BANG_EQUAL_EQUAL &&
bin5.Left is Expr.Variable v5 &&
bin5.Right is Expr.Literal { Value: null })
{
return AnalyzeNullCheck(v5.Name.Lexeme, checkingForNull: true, negated: true, checkBoth: bin5.Operator.Type == TokenType.BANG_EQUAL);
}
// Note: Property null checks (x.prop !== null) are handled by the
// NarrowingPath-based AnalyzePathTypeGuard below
// Pattern 6: x === undefined (undefined is a literal, not a variable)
if (condition is Expr.Binary bin6 &&
bin6.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin6.Left is Expr.Variable v6 &&
bin6.Right is Expr.Literal { Value: Runtime.Types.SharpTSUndefined })
{
return AnalyzeNullCheck(v6.Name.Lexeme, checkingForNull: false, negated: false, checkBoth: bin6.Operator.Type == TokenType.EQUAL_EQUAL);
}
// Pattern 6b: undefined === x (reversed)
if (condition is Expr.Binary bin6b &&
bin6b.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin6b.Right is Expr.Variable v6b &&
bin6b.Left is Expr.Literal { Value: Runtime.Types.SharpTSUndefined })
{
return AnalyzeNullCheck(v6b.Name.Lexeme, checkingForNull: false, negated: false, checkBoth: bin6b.Operator.Type == TokenType.EQUAL_EQUAL);
}
// Pattern 7: x !== undefined
if (condition is Expr.Binary bin7 &&
bin7.Operator.Type is TokenType.BANG_EQUAL or TokenType.BANG_EQUAL_EQUAL &&
bin7.Left is Expr.Variable v7 &&
bin7.Right is Expr.Literal { Value: Runtime.Types.SharpTSUndefined })
{
return AnalyzeNullCheck(v7.Name.Lexeme, checkingForNull: false, negated: true, checkBoth: bin7.Operator.Type == TokenType.BANG_EQUAL);
}
// Pattern 7b: x === <literal> / x !== <literal> (string/number/boolean literal equality).
// null/undefined literals are handled by Patterns 4-7 above; LiteralTypeFor
// returns null for them so this pattern never double-handles.
if (condition is Expr.Binary binLit &&
binLit.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL
or TokenType.BANG_EQUAL or TokenType.BANG_EQUAL_EQUAL)
{
bool negatedLit = binLit.Operator.Type is TokenType.BANG_EQUAL or TokenType.BANG_EQUAL_EQUAL;
if (binLit.Left is Expr.Variable vLit &&
binLit.Right is Expr.Literal { Value: var litVal } &&
LiteralTypeFor(litVal) is TypeInfo litType)
{
return AnalyzeLiteralEqualityGuard(vLit.Name.Lexeme, litType, negatedLit);
}
if (binLit.Right is Expr.Variable vLitR &&
binLit.Left is Expr.Literal { Value: var litValR } &&
LiteralTypeFor(litValR) is TypeInfo litTypeR)
{
return AnalyzeLiteralEqualityGuard(vLitR.Name.Lexeme, litTypeR, negatedLit);
}
}
// Pattern 8: "prop" in x (in operator narrowing)
if (condition is Expr.Binary bin8 &&
bin8.Operator.Type == TokenType.IN &&
bin8.Left is Expr.Literal { Value: string propName } &&
bin8.Right is Expr.Variable v8)
{
return AnalyzeInOperatorGuard(v8.Name.Lexeme, propName);
}
// Pattern 9: x.kind === "literal" (discriminated union narrowing)
if (condition is Expr.Binary bin9 &&
bin9.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin9.Left is Expr.Get { Object: Expr.Variable v9, Name: var propToken } &&
bin9.Right is Expr.Literal { Value: string literalValue })
{
return AnalyzeDiscriminatedUnionGuard(v9.Name.Lexeme, propToken.Lexeme, literalValue);
}
// Pattern 9b: "literal" === x.kind (reversed)
if (condition is Expr.Binary bin9b &&
bin9b.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin9b.Right is Expr.Get { Object: Expr.Variable v9b, Name: var propToken9b } &&
bin9b.Left is Expr.Literal { Value: string literalValue9b })
{
return AnalyzeDiscriminatedUnionGuard(v9b.Name.Lexeme, propToken9b.Lexeme, literalValue9b);
}
// Pattern 10: Array.isArray(x)
if (condition is Expr.Call call &&
call.Callee is Expr.Get { Object: Expr.Variable { Name.Lexeme: "Array" }, Name.Lexeme: "isArray" } &&
call.Arguments.Count == 1 &&
call.Arguments[0] is Expr.Variable v10)
{
return AnalyzeArrayIsArrayGuard(v10.Name.Lexeme);
}
// Pattern 11: User-defined type predicate function call: isString(x)
if (condition is Expr.Call predicateCall)
{
return AnalyzeTypePredicateCall(predicateCall);
}
return (null, null, null);
}
/// <summary>
/// Finds the parameter index for a given parameter name in a function type.
/// Returns -1 if parameter names are not available or the name is not found.
/// </summary>
private static int FindParameterIndex(TypeInfo funcType, string paramName)
{
List<string>? paramNames = funcType switch
{
TypeInfo.Function f => f.ParamNames,
TypeInfo.GenericFunction gf => gf.ParamNames,
_ => null
};
if (paramNames == null) return 0; // Fallback to first param for backwards compatibility
int index = paramNames.IndexOf(paramName);
return index >= 0 ? index : 0; // Fallback to first param if not found
}
/// <summary>
/// Analyzes a call to a user-defined type predicate function like isString(x).
/// Returns narrowing info if the callee has a type predicate return type.
/// </summary>
private (string? VarName, TypeInfo? NarrowedType, TypeInfo? ExcludedType) AnalyzeTypePredicateCall(Expr.Call call)
{
// Get the callee's type
TypeInfo? calleeType = null;
// Handle direct function calls: isString(x)
if (call.Callee is Expr.Variable funcVar)
{
calleeType = _environment.Get(funcVar.Name.Lexeme);
}
// Handle method calls: obj.isString(x)
else if (call.Callee is Expr.Get getExpr)
{
var objType = CheckExpr(getExpr.Object);
calleeType = GetMemberType(objType, getExpr.Name.Lexeme);
}
if (calleeType == null) return (null, null, null);
// Handle GenericFunction by checking if it has a predicate return type
TypeInfo? returnType = calleeType switch
{
TypeInfo.Function func => func.ReturnType,
TypeInfo.GenericFunction gf => gf.ReturnType,
_ => null
};
// Check for type predicate return type (not assertion - those are handled differently)
if (returnType is TypeInfo.TypePredicate pred && !pred.IsAssertion)
{
// Look up the parameter index by name from the function type
int paramIndex = FindParameterIndex(calleeType, pred.ParameterName);
if (paramIndex >= 0 && paramIndex < call.Arguments.Count &&
call.Arguments[paramIndex] is Expr.Variable argVar)
{
string varName = argVar.Name.Lexeme;
TypeInfo? currentType = _environment.Get(varName);
TypeInfo narrowedType = pred.PredicateType;
// A generic predicate (`isFunction<T>(value: T): value is Extract<T, Function>`)
// narrows by the predicate type INSTANTIATED at this call site — the declaration
// form references the callee's own type parameters, which mean nothing here.
// `isFunction(x)` with `x: string | (() => string)` must narrow to `() => string`
// (the conditional distributes over the inferred union), not to a dangling
// `T extends Function ? T : never`.
if (calleeType is TypeInfo.GenericFunction predGf && predGf.TypeParams.Count > 0)
{
var argTypes = call.Arguments
.Select(a => a is Expr.Variable v
? _environment.Get(v.Name.Lexeme) ?? TypeInfo.Any.Shared
: CheckExpr(a))
.ToList();
var typeArgs = InferTypeArguments(predGf, argTypes);
var subs = new Dictionary<string, TypeInfo>();
for (int i = 0; i < predGf.TypeParams.Count && i < typeArgs.Count; i++)
subs[predGf.TypeParams[i].Name] = typeArgs[i];
narrowedType = Substitute(narrowedType, subs);
}
TypeInfo? excludedType = currentType != null
? ExcludeTypeFromUnion(currentType, narrowedType)
: null;
return (varName, narrowedType, excludedType);
}
}
return (null, null, null);
}
/// <summary>
/// Excludes a type from a union, returning the remaining types.
/// </summary>
private static TypeInfo? ExcludeTypeFromUnion(TypeInfo source, TypeInfo toExclude)
{
if (source is TypeInfo.Union union)
{
var remaining = union.FlattenedTypes
.Where(t => t.ToString() != toExclude.ToString())
.ToList();
if (remaining.Count == 0) return TypeInfo.Never.Shared;
if (remaining.Count == 1) return remaining[0];
return new TypeInfo.Union(remaining);
}
// If source equals toExclude, nothing remains
if (source.ToString() == toExclude.ToString())
return TypeInfo.Never.Shared;
// Otherwise, the type doesn't change in else branch
return source;
}
/// <summary>
/// Analyzes typeof type guards like `typeof x === "string"`.
/// </summary>
private (string? VarName, TypeInfo? NarrowedType, TypeInfo? ExcludedType) AnalyzeTypeofGuard(
string varName, string typeStr, bool negated)
{
var currentType = _environment.Get(varName);
if (currentType == null) return (null, null, null);
// Handle unknown type narrowing - typeof checks narrow unknown to specific types
if (currentType is TypeInfo.Unknown)
{
TypeInfo? narrowedType = TypeofStringToType(typeStr);
// Excluded type remains unknown (we don't know what else it could be)
if (negated)
return (varName, TypeInfo.Unknown.Shared, narrowedType);
return (varName, narrowedType, TypeInfo.Unknown.Shared);
}
// Handle any type narrowing
if (currentType is TypeInfo.Any)
{
TypeInfo? narrowedType = TypeofStringToType(typeStr);
if (negated)
return (varName, TypeInfo.Any.Shared, narrowedType);
return (varName, narrowedType, TypeInfo.Any.Shared);
}
if (currentType is TypeInfo.Union union)
{
var flattenedTypes = union.FlattenedTypes;
var matching = flattenedTypes.Where(t => TypeMatchesTypeof(t, typeStr)).ToList();
var nonMatching = flattenedTypes.Where(t => !TypeMatchesTypeof(t, typeStr)).ToList();
TypeInfo? narrowedType = matching.Count == 0 ? null :
matching.Count == 1 ? matching[0] : new TypeInfo.Union(matching);
TypeInfo? excludedType = nonMatching.Count == 0 ? null :
nonMatching.Count == 1 ? nonMatching[0] : new TypeInfo.Union(nonMatching);
if (negated)
return (varName, excludedType, narrowedType);
return (varName, narrowedType, excludedType);
}
// Non-union type: check if current type matches typeof
if (TypeMatchesTypeof(currentType, typeStr))
{
// Current type matches - narrowed stays same, excluded is never
if (negated)
return (varName, TypeInfo.Never.Shared, currentType);
return (varName, currentType, TypeInfo.Never.Shared);
}
else
{
// Current type doesn't match - narrowed is never, excluded stays same
if (negated)
return (varName, currentType, TypeInfo.Never.Shared);
return (varName, TypeInfo.Never.Shared, currentType);
}
}
/// <summary>
/// Converts a typeof result string to the corresponding TypeInfo.
/// </summary>
private static TypeInfo? TypeofStringToType(string typeStr) => typeStr switch
{
"string" => TypeInfo.String.Shared,
"number" => TypeInfo.Primitive.Number,
"boolean" => TypeInfo.Primitive.Boolean,
"bigint" => TypeInfo.BigInt.Shared,
// Narrow to a permissive function type: accepts any args, returns any.
// Without a rest param the type would constrain callers to 0 args,
// breaking common patterns like `if (typeof cb === 'function') cb(err, val)`.
"function" => new TypeInfo.Function(
[new TypeInfo.Array(TypeInfo.Any.Shared)],
TypeInfo.Any.Shared,
RequiredParams: 0,
HasRestParam: true),
"object" => TypeInfo.Object.Shared,
"symbol" => TypeInfo.Symbol.Shared,
"undefined" => TypeInfo.Undefined.Shared,
_ => null
};
/// <summary>
/// Analyzes instanceof type guards like `x instanceof Dog`.
/// </summary>
private (string? VarName, TypeInfo? NarrowedType, TypeInfo? ExcludedType) AnalyzeInstanceofGuard(
string varName, Token classToken)
{
var currentType = _environment.Get(varName);
if (currentType == null) return (null, null, null);
// Look up the class
var classType = _environment.GetTypeBinding(classToken.Lexeme) ?? _environment.Get(classToken.Lexeme);
if (classType == null) return (null, null, null);
// Determine the instance type we're narrowing to
TypeInfo instanceType;
if (classType is TypeInfo.Class cls)
{
instanceType = new TypeInfo.Instance(cls);
}
else if (classType is TypeInfo.MutableClass mc)
{
instanceType = new TypeInfo.Instance(mc.Frozen ?? (TypeInfo)mc);
}
else if (classType is TypeInfo.Instance inst)
{
instanceType = inst;
}
else
{
// Not a class - can't narrow
return (null, null, null);
}
// If current type is a union containing the class or its subclasses, narrow
if (currentType is TypeInfo.Union union)
{
var flattenedTypes = union.FlattenedTypes;
var matching = new List<TypeInfo>();
var nonMatching = new List<TypeInfo>();
foreach (var t in flattenedTypes)
{
if (IsInstanceOf(t, classType))
matching.Add(t);
else
nonMatching.Add(t);
}
TypeInfo? narrowedType = matching.Count == 0 ? instanceType : // Narrow to the class if no match
matching.Count == 1 ? matching[0] : new TypeInfo.Union(matching);
TypeInfo? excludedType = nonMatching.Count == 0 ? null :
nonMatching.Count == 1 ? nonMatching[0] : new TypeInfo.Union(nonMatching);
return (varName, narrowedType, excludedType);
}
// For non-union types (e.g., base class), narrow to the specific class
if (currentType is TypeInfo.Instance currentInst)
{
// If checking instanceof a subclass, narrow to that subclass
return (varName, instanceType, currentType);
}
// For any/unknown types, narrow to the instance type
if (currentType is TypeInfo.Any or TypeInfo.Unknown)
{
return (varName, instanceType, currentType);
}
return (varName, instanceType, currentType);
}
/// <summary>
/// Checks if a type is an instance of a class or its subclass.
/// </summary>
private bool IsInstanceOf(TypeInfo type, TypeInfo classType)
{
if (type is not TypeInfo.Instance inst) return false;
TypeInfo.Class? targetClass = classType switch
{
TypeInfo.Class c => c,
TypeInfo.MutableClass mc => mc.Frozen,
TypeInfo.Instance i when i.ClassType is TypeInfo.Class ic => ic,
_ => null
};
if (targetClass == null) return false;
TypeInfo? current = inst.ClassType switch
{
TypeInfo.Class c => c,
TypeInfo.MutableClass mc => mc.Frozen,
_ => null
};
while (current != null)
{
if (GetClassName(current) == targetClass.Name) return true;
current = GetSuperclass(current);
}
return false;
}
/// <summary>
/// Analyzes null/undefined equality checks like `x === null` or `x !== undefined`.
/// </summary>
private (string? VarName, TypeInfo? NarrowedType, TypeInfo? ExcludedType) AnalyzeNullCheck(
string varName, bool checkingForNull, bool negated, bool checkBoth = false)
{
var currentType = _environment.Get(varName);
if (currentType == null) return (null, null, null);
// checkBoth: a LOOSE comparison (`== null`/`!= null`/`== undefined`/`!= undefined`) against
// EITHER literal narrows away BOTH null and undefined — JS's abstract equality treats them
// as mutually `==`-equal (`null == undefined` is true) — unlike a STRICT `===`/`!==`
// comparison, which narrows away only the specific one compared.
bool IsNullish(TypeInfo t) => checkBoth
? t is TypeInfo.Null or TypeInfo.Undefined
: checkingForNull ? t is TypeInfo.Null : t is TypeInfo.Undefined;
TypeInfo nullishType = checkBoth
? new TypeInfo.Union([TypeInfo.Null.Shared, TypeInfo.Undefined.Shared])
: checkingForNull ? TypeInfo.Null.Shared : TypeInfo.Undefined.Shared;
if (currentType is TypeInfo.Union union)
{
var flattenedTypes = union.FlattenedTypes;
var nonNullish = flattenedTypes.Where(t => !IsNullish(t)).ToList();
var nullish = flattenedTypes.Where(IsNullish).ToList();
TypeInfo? nonNullishType = nonNullish.Count == 0 ? TypeInfo.Never.Shared :
nonNullish.Count == 1 ? nonNullish[0] : new TypeInfo.Union(nonNullish);
TypeInfo? nullishResultType = nullish.Count == 0 ? nullishType :
nullish.Count == 1 ? nullish[0] : new TypeInfo.Union(nullish);
// if (x === null) -> then: null, else: non-null
// if (x !== null) -> then: non-null, else: null
if (negated)
return (varName, nonNullishType, nullishResultType);
return (varName, nullishResultType, nonNullishType);
}
// Non-union type: if it's nullable, we can still narrow
if (currentType is TypeInfo.Null && (checkingForNull || checkBoth))
{
if (negated)
return (varName, TypeInfo.Never.Shared, currentType);
return (varName, currentType, TypeInfo.Never.Shared);
}
if (currentType is TypeInfo.Undefined && (!checkingForNull || checkBoth))
{
if (negated)
return (varName, TypeInfo.Never.Shared, currentType);
return (varName, currentType, TypeInfo.Never.Shared);
}
// Type is not nullable - narrowing has no effect
return (null, null, null);
}
/// <summary>
/// Maps a literal AST value to its literal type for equality narrowing.
/// Returns null for values that don't participate (null/undefined have their
/// own guard patterns).
/// </summary>
private static TypeInfo? LiteralTypeFor(object? value) => value switch
{
string s => new TypeInfo.StringLiteral(s),
int i => new TypeInfo.NumberLiteral(i),
double d => new TypeInfo.NumberLiteral(d),
bool b => new TypeInfo.BooleanLiteral(b),
System.Numerics.BigInteger bi => new TypeInfo.BigIntLiteral(bi),
_ => null
};
/// <summary>
/// Analyzes literal equality guards like `x === "a"` (tsc's narrowTypeByLiteralExpression).
/// For a union, the true branch keeps the matching literal constituent (a general
/// primitive constituent narrows to the literal itself); the false branch drops only
/// the exactly-equal literal constituent — a general `string` stays, since other
/// string values still inhabit it.
/// </summary>
private (string? VarName, TypeInfo? NarrowedType, TypeInfo? ExcludedType) AnalyzeLiteralEqualityGuard(
string varName, TypeInfo literalType, bool negated)
{
var currentType = _environment.Get(varName);
if (currentType == null) return (null, null, null);
static bool LiteralInhabits(TypeInfo general, TypeInfo literal) => general switch
{
TypeInfo.String => literal is TypeInfo.StringLiteral,
TypeInfo.Primitive { Type: TokenType.TYPE_NUMBER } => literal is TypeInfo.NumberLiteral,
TypeInfo.Primitive { Type: TokenType.TYPE_BOOLEAN } => literal is TypeInfo.BooleanLiteral,
TypeInfo.BigInt => literal is TypeInfo.BigIntLiteral,
_ => false
};
if (currentType is TypeInfo.Union union)
{
var matching = new List<TypeInfo>();
var rest = new List<TypeInfo>();
foreach (var t in union.FlattenedTypes)
{
if (TypeInfoEqualityComparer.Instance.Equals(t, literalType))
{
matching.Add(t);
}
else
{
if (LiteralInhabits(t, literalType) &&
!matching.Any(m => TypeInfoEqualityComparer.Instance.Equals(m, literalType)))
{
matching.Add(literalType);
}
rest.Add(t);
}
}
TypeInfo narrowed = matching.Count == 0 ? TypeInfo.Never.Shared :
matching.Count == 1 ? matching[0] : new TypeInfo.Union(matching);
TypeInfo excluded = rest.Count == 0 ? TypeInfo.Never.Shared :
rest.Count == 1 ? rest[0] : new TypeInfo.Union(rest);
if (negated)
return (varName, excluded, narrowed);
return (varName, narrowed, excluded);
}
// Exactly the literal already.
if (TypeInfoEqualityComparer.Instance.Equals(currentType, literalType))
{
if (negated)
return (varName, TypeInfo.Never.Shared, currentType);
return (varName, currentType, TypeInfo.Never.Shared);
}
// A different literal of the same kind can never equal it.
if (currentType is TypeInfo.StringLiteral && literalType is TypeInfo.StringLiteral ||
currentType is TypeInfo.NumberLiteral && literalType is TypeInfo.NumberLiteral ||
currentType is TypeInfo.BooleanLiteral && literalType is TypeInfo.BooleanLiteral ||
currentType is TypeInfo.BigIntLiteral && literalType is TypeInfo.BigIntLiteral)
{
if (negated)
return (varName, currentType, TypeInfo.Never.Shared);
return (varName, TypeInfo.Never.Shared, currentType);
}
// General primitive narrows to the literal in the true branch; the false
// branch can't shrink a general primitive.
if (LiteralInhabits(currentType, literalType))
{
if (negated)
return (varName, currentType, literalType);
return (varName, literalType, currentType);
}
return (null, null, null);
}
/// <summary>
/// Analyzes type guards for any narrowable path (variables, property access chains, etc.).
/// Returns a NarrowingPath-based result that supports nested property access.
/// </summary>
/// <remarks>
/// This is the new unified type guard analysis that supports:
/// - Simple variables: if (x !== null)
/// - Property access: if (obj.prop !== null)
/// - Nested paths: if (obj.a.b.c !== null)
/// </remarks>
private (Narrowing.NarrowingPath? Path, TypeInfo? NarrowedType, TypeInfo? ExcludedType) AnalyzePathTypeGuard(Expr condition)
{
// Pattern: path !== null or path != null. A LOOSE `!=` against EITHER null or undefined
// narrows away BOTH (JS: `null == undefined`); STRICT `!==` narrows away just the one compared.
if (condition is Expr.Binary bin &&
bin.Operator.Type is TokenType.BANG_EQUAL or TokenType.BANG_EQUAL_EQUAL &&
bin.Right is Expr.Literal { Value: null })
{
var path = Narrowing.NarrowingPathExtractor.TryExtract(bin.Left);
if (path != null && Narrowing.NarrowingPathExtractor.IsWithinDepthLimit(path))
{
return AnalyzePathNullCheck(path, bin.Left, negated: true, checkingForNull: true, checkBoth: bin.Operator.Type == TokenType.BANG_EQUAL);
}
}
// Pattern: null !== path (reversed)
if (condition is Expr.Binary bin2 &&
bin2.Operator.Type is TokenType.BANG_EQUAL or TokenType.BANG_EQUAL_EQUAL &&
bin2.Left is Expr.Literal { Value: null })
{
var path = Narrowing.NarrowingPathExtractor.TryExtract(bin2.Right);
if (path != null && Narrowing.NarrowingPathExtractor.IsWithinDepthLimit(path))
{
return AnalyzePathNullCheck(path, bin2.Right, negated: true, checkingForNull: true, checkBoth: bin2.Operator.Type == TokenType.BANG_EQUAL);
}
}
// Pattern: path === null
if (condition is Expr.Binary bin3 &&
bin3.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin3.Right is Expr.Literal { Value: null })
{
var path = Narrowing.NarrowingPathExtractor.TryExtract(bin3.Left);
if (path != null && Narrowing.NarrowingPathExtractor.IsWithinDepthLimit(path))
{
return AnalyzePathNullCheck(path, bin3.Left, negated: false, checkingForNull: true, checkBoth: bin3.Operator.Type == TokenType.EQUAL_EQUAL);
}
}
// Pattern: null === path (reversed)
if (condition is Expr.Binary bin4 &&
bin4.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin4.Left is Expr.Literal { Value: null })
{
var path = Narrowing.NarrowingPathExtractor.TryExtract(bin4.Right);
if (path != null && Narrowing.NarrowingPathExtractor.IsWithinDepthLimit(path))
{
return AnalyzePathNullCheck(path, bin4.Right, negated: false, checkingForNull: true, checkBoth: bin4.Operator.Type == TokenType.EQUAL_EQUAL);
}
}
// Pattern: path !== undefined or path != undefined
if (condition is Expr.Binary bin5 &&
bin5.Operator.Type is TokenType.BANG_EQUAL or TokenType.BANG_EQUAL_EQUAL &&
bin5.Right is Expr.Literal { Value: Runtime.Types.SharpTSUndefined })
{
var path = Narrowing.NarrowingPathExtractor.TryExtract(bin5.Left);
if (path != null && Narrowing.NarrowingPathExtractor.IsWithinDepthLimit(path))
{
return AnalyzePathNullCheck(path, bin5.Left, negated: true, checkingForNull: false, checkBoth: bin5.Operator.Type == TokenType.BANG_EQUAL);
}
}
// Pattern: undefined !== path (reversed)
if (condition is Expr.Binary bin6 &&
bin6.Operator.Type is TokenType.BANG_EQUAL or TokenType.BANG_EQUAL_EQUAL &&
bin6.Left is Expr.Literal { Value: Runtime.Types.SharpTSUndefined })
{
var path = Narrowing.NarrowingPathExtractor.TryExtract(bin6.Right);
if (path != null && Narrowing.NarrowingPathExtractor.IsWithinDepthLimit(path))
{
return AnalyzePathNullCheck(path, bin6.Right, negated: true, checkingForNull: false, checkBoth: bin6.Operator.Type == TokenType.BANG_EQUAL);
}
}
// Pattern: path === undefined
if (condition is Expr.Binary bin7 &&
bin7.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin7.Right is Expr.Literal { Value: Runtime.Types.SharpTSUndefined })
{
var path = Narrowing.NarrowingPathExtractor.TryExtract(bin7.Left);
if (path != null && Narrowing.NarrowingPathExtractor.IsWithinDepthLimit(path))
{
return AnalyzePathNullCheck(path, bin7.Left, negated: false, checkingForNull: false, checkBoth: bin7.Operator.Type == TokenType.EQUAL_EQUAL);
}
}
// Pattern: undefined === path (reversed)
if (condition is Expr.Binary bin8 &&
bin8.Operator.Type is TokenType.EQUAL_EQUAL or TokenType.EQUAL_EQUAL_EQUAL &&
bin8.Left is Expr.Literal { Value: Runtime.Types.SharpTSUndefined })
{
var path = Narrowing.NarrowingPathExtractor.TryExtract(bin8.Right);
if (path != null && Narrowing.NarrowingPathExtractor.IsWithinDepthLimit(path))
{
return AnalyzePathNullCheck(path, bin8.Right, negated: false, checkingForNull: false, checkBoth: bin8.Operator.Type == TokenType.EQUAL_EQUAL);
}
}
// Pattern: truthiness guard — `if (path)`, `if (obj.prop)`, `if (!path)`, etc.
// Placed last so the explicit `=== null` / `!== null` forms above keep their precise
// (literal-preserving) handling; truthiness is the general fallback.
var truthiness = AnalyzePathTruthinessGuard(condition);
if (truthiness.Path != null)
{
return truthiness;
}
return (null, null, null);
}
/// <summary>
/// Analyzes a truthiness guard on a narrowable path: <c>if (x)</c>, <c>if (obj.prop)</c>,
/// <c>if (!x)</c>, and groupings/double-negations thereof. The truthy branch keeps the
/// constituents that can be truthy (always-falsy ones removed); the falsy branch keeps the
/// constituents that can be falsy (always-truthy ones removed). A leading <c>!</c> swaps the
/// two branches. Returns no narrowing when the expression is not a stable path or when
/// neither branch refines the type.
/// </summary>
private (Narrowing.NarrowingPath? Path, TypeInfo? NarrowedType, TypeInfo? ExcludedType) AnalyzePathTruthinessGuard(Expr condition)
{
bool negated = false;
Expr expr = condition;
// Peel parentheses and logical-not so `!x`, `!!x`, and `(x)` resolve to the underlying
// path with the correct branch polarity.
while (true)
{
if (expr is Expr.Grouping g) { expr = g.Expression; continue; }
if (expr is Expr.Unary { Operator.Type: TokenType.BANG } notExpr)
{
negated = !negated;
expr = notExpr.Right;
continue;
}
break;
}
var path = Narrowing.NarrowingPathExtractor.TryExtract(expr);
if (path == null || !Narrowing.NarrowingPathExtractor.IsWithinDepthLimit(path))
{
return (null, null, null);
}
var (truthy, falsy) = ComputeTruthinessNarrowing(CheckExpr(expr));
if (truthy == null || falsy == null)
{
return (null, null, null);
}
return negated ? (path, falsy, truthy) : (path, truthy, falsy);
}
/// <summary>
/// Analyzes compound conditions (&&, ||) and collects all type guard narrowings.
/// For &&, all narrowings apply (intersection). For ||, narrowings only apply if in both branches.
/// </summary>
/// <returns>
/// A list of narrowings to apply. For the then branch, use NarrowedType.
/// For the else branch, use ExcludedType.
/// </returns>
private List<(Narrowing.NarrowingPath Path, TypeInfo NarrowedType, TypeInfo ExcludedType)> AnalyzeCompoundTypeGuards(Expr condition)
{
var narrowings = new List<(Narrowing.NarrowingPath Path, TypeInfo NarrowedType, TypeInfo ExcludedType)>();
void CollectNarrowings(Expr expr)
{
// For &&, collect narrowings from both sides. Apply LHS narrowings while
// analyzing the RHS so CheckExpr calls inside the RHS (e.g. inside
// AnalyzePathNullCheck) see the narrowed receiver. Without this,
// `a != null && a.b != null` errors when reading `a.b`, because `a` is
// still typed `T | null` at the point we compute `a.b`'s current type.
if (expr is Expr.Logical logical && logical.Operator.Type == TokenType.AND_AND)
{
int leftStart = narrowings.Count;
CollectNarrowings(logical.Left);
int leftCount = narrowings.Count - leftStart;
if (leftCount == 0)
{
CollectNarrowings(logical.Right);
return;
}
var narrowedEnv = new TypeEnvironment(_environment);
var narrowedCtx = Narrowing.NarrowingContext.Empty;
for (int i = leftStart; i < leftStart + leftCount; i++)
{
var (path, narrowedType, _) = narrowings[i];
if (path is Narrowing.NarrowingPath.Variable v)
narrowedEnv.Define(v.Name, narrowedType);
else
narrowedCtx = narrowedCtx.WithNarrowing(path, narrowedType);
}
using (new EnvironmentScope(this, narrowedEnv))
{
bool pushed = !narrowedCtx.IsEmpty;
if (pushed) PushNarrowingContext(narrowedCtx);
try
{
CollectNarrowings(logical.Right);
}
finally
{
if (pushed) PopNarrowingContext();
}
}
return;
}
// For ||, a path narrows in the TRUE branch only when BOTH disjuncts
// narrow it — to the union of the two narrowings (`x === "a" || x === "b"`
// gives x: "a" | "b"). The right disjunct is analyzed under the left's
// excluded types (it's only evaluated when the left was false), so its
// excluded type already subtracts both disjuncts — use it as the merged
// exclusion for the else branch (De Morgan). Paths guarded on only one
// side contribute nothing: the other disjunct may have been the true one.
if (expr is Expr.Logical orLogical && orLogical.Operator.Type == TokenType.OR_OR)
{
var leftGuards = AnalyzeCompoundTypeGuards(orLogical.Left);
List<(Narrowing.NarrowingPath Path, TypeInfo NarrowedType, TypeInfo ExcludedType)> rightGuards;
if (leftGuards.Count > 0)
{
var narrowedEnv = new TypeEnvironment(_environment);
var narrowedCtx = Narrowing.NarrowingContext.Empty;
foreach (var (path, _, excludedType) in leftGuards)
{
if (path is Narrowing.NarrowingPath.Variable v)
narrowedEnv.Define(v.Name, excludedType);
else
narrowedCtx = narrowedCtx.WithNarrowing(path, excludedType);
}
using (new EnvironmentScope(this, narrowedEnv))
{
bool pushed = !narrowedCtx.IsEmpty;
if (pushed) PushNarrowingContext(narrowedCtx);
try
{
rightGuards = AnalyzeCompoundTypeGuards(orLogical.Right);
}
finally
{
if (pushed) PopNarrowingContext();
}
}
}
else
{
rightGuards = AnalyzeCompoundTypeGuards(orLogical.Right);
}
foreach (var left in leftGuards)
{
var right = rightGuards.FirstOrDefault(r => r.Path.Equals(left.Path));
if (right.Path is null) continue;
narrowings.Add((left.Path,
UnionOfNarrowedTypes(left.NarrowedType, right.NarrowedType),
right.ExcludedType));
}
return;
}
// `if (x OP= y)` — a compound logical assignment used as a condition. CheckExpr already
// ran (VisitIf checks the condition before narrowing analysis), so LookupVariable(x)
// reflects the post-assignment type CheckLogicalAssign installed; split THAT for the
// guard, same as a bare `if (x)`.
if (expr is Expr.LogicalAssign logicalAssign)
{
narrowings.AddRange(AnalyzeLogicalAssignGuard(logicalAssign));
return;
}
// Try to get a single type guard from this expression
var guard = AnalyzePathTypeGuard(expr);
if (guard.Path != null && guard.NarrowedType != null && guard.ExcludedType != null)
{
narrowings.Add((guard.Path, guard.NarrowedType, guard.ExcludedType));
}
else
{
// Fall back to legacy type guard analysis for variables
var legacyGuard = AnalyzeTypeGuard(expr);
if (legacyGuard.VarName != null && legacyGuard.NarrowedType != null && legacyGuard.ExcludedType != null)
{
var varPath = new Narrowing.NarrowingPath.Variable(legacyGuard.VarName);
narrowings.Add((varPath, legacyGuard.NarrowedType, legacyGuard.ExcludedType));
}
}
}
CollectNarrowings(condition);
return narrowings;
}
/// <summary>
/// Type-guard analysis for `if (x OP= y)`. The LHS narrows the same way a bare `if (x)` would,
/// using its post-assignment type (already installed in the environment by CheckLogicalAssign
/// by the time this runs). For `&&=` specifically, a truthy overall result additionally
/// guarantees the RHS was evaluated (the LHS was truthy) AND the RHS's own value was truthy —
/// `&&=` only assigns/returns the RHS when the LHS is truthy, so a truthy overall result can't
/// come from a falsy RHS. If the RHS is a bare identifier, narrow it too (`||=`/`??=` don't get
/// this: their truthy branch is also reachable via an already-truthy/non-nullish LHS that never
/// touched the RHS at all, so nothing can be said about the RHS's truthiness there).
/// </summary>
private List<(Narrowing.NarrowingPath Path, TypeInfo NarrowedType, TypeInfo ExcludedType)> AnalyzeLogicalAssignGuard(
Expr.LogicalAssign logical)
{
var result = new List<(Narrowing.NarrowingPath, TypeInfo, TypeInfo)>();
var lhsType = LookupVariable(logical.Name);
var lhsPath = new Narrowing.NarrowingPath.Variable(logical.Name.Lexeme);
result.Add((lhsPath, NarrowLogicalTruthy(lhsType), NarrowLogicalFalsy(lhsType)));
if (logical.Operator.Type == TokenType.AND_AND_EQUAL && logical.Value is Expr.Variable rhsVar)
{
var rhsType = LookupVariable(rhsVar.Name);
result.Add((new Narrowing.NarrowingPath.Variable(rhsVar.Name.Lexeme),
NarrowLogicalTruthy(rhsType), NarrowLogicalFalsy(rhsType)));
}
return result;
}
/// <summary>
/// Unions two narrowed types, flattening nested unions and deduplicating.
/// Never contributes nothing (it's the empty union).
/// </summary>
private static TypeInfo UnionOfNarrowedTypes(TypeInfo a, TypeInfo b)
{
var parts = new List<TypeInfo>();
void Add(TypeInfo t)
{
if (t is TypeInfo.Never) return;
IEnumerable<TypeInfo> flat = t is TypeInfo.Union u ? u.FlattenedTypes : [t];
foreach (var f in flat)
{
if (!parts.Any(p => TypeInfoEqualityComparer.Instance.Equals(p, f)))
parts.Add(f);
}
}
Add(a);
Add(b);
return parts.Count == 0 ? TypeInfo.Never.Shared :
parts.Count == 1 ? parts[0] : new TypeInfo.Union(parts);
}
/// <summary>
/// Flattens an <c>||</c> chain and collects the type guard of each
/// disjunct (#216). Callers use the EXCLUDED types: in contexts where the
/// whole disjunction is known false — the right operand of <c>||</c>, or
/// code following a terminating <c>if (a == null || b == null) return;</c>
/// — De Morgan gives the negation of every disjunct. Disjuncts that are
Morty Proxy This is a proxified and sanitized view of the page, visit original site.