1use crate::narrowing::{DiscriminantInfo, NarrowingContext, union_or_single_preserve};
13use crate::operations_property::{PropertyAccessEvaluator, PropertyAccessResult};
14use crate::subtype::is_subtype_of;
15use crate::type_queries::{
16 LiteralValueKind, UnionMembersKind, classify_for_literal_value, classify_for_union_members,
17};
18use crate::types::{PropertyLookup, TypeId};
19use crate::visitor::{
20 intersection_list_id, is_literal_type_db, object_shape_id, object_with_index_shape_id,
21 union_list_id,
22};
23use rustc_hash::FxHashSet;
24use tracing::{Level, span, trace};
25use tsz_common::interner::Atom;
26
27impl<'a> NarrowingContext<'a> {
28 pub fn find_discriminants(&self, union_type: TypeId) -> Vec<DiscriminantInfo> {
34 let _span = span!(
35 Level::TRACE,
36 "find_discriminants",
37 union_type = union_type.0
38 )
39 .entered();
40
41 let members = match union_list_id(self.db, union_type) {
42 Some(members_id) => self.db.type_list(members_id),
43 None => return vec![],
44 };
45
46 if members.len() < 2 {
47 trace!("Union has fewer than 2 members, skipping discriminant search");
48 return vec![];
49 }
50
51 let mut all_properties: Vec<Atom> = Vec::new();
53 let mut member_props: Vec<Vec<(Atom, TypeId)>> = Vec::new();
54
55 for &member in members.iter() {
56 if let Some(shape_id) = object_shape_id(self.db, member) {
57 let shape = self.db.object_shape(shape_id);
58 let props_vec: Vec<(Atom, TypeId)> = shape
59 .properties
60 .iter()
61 .map(|p| (p.name, p.type_id))
62 .collect();
63
64 for (name, _) in &props_vec {
66 if !all_properties.contains(name) {
67 all_properties.push(*name);
68 }
69 }
70 member_props.push(props_vec);
71 } else {
72 return vec![];
74 }
75 }
76
77 let mut discriminants = Vec::new();
79
80 for prop_name in &all_properties {
81 let mut is_discriminant = true;
82 let mut variants: Vec<(TypeId, TypeId)> = Vec::new();
83 let mut seen_literals: Vec<TypeId> = Vec::new();
84
85 for (i, props) in member_props.iter().enumerate() {
86 let prop_type = props
88 .iter()
89 .find(|(name, _)| name == prop_name)
90 .map(|(_, ty)| *ty);
91
92 match prop_type {
93 Some(ty) => {
94 if is_literal_type_db(self.db, ty) {
96 if seen_literals.contains(&ty) {
98 is_discriminant = false;
99 break;
100 }
101 seen_literals.push(ty);
102 variants.push((ty, members[i]));
103 } else {
104 is_discriminant = false;
105 break;
106 }
107 }
108 None => {
109 is_discriminant = false;
111 break;
112 }
113 }
114 }
115
116 if is_discriminant && !variants.is_empty() {
117 discriminants.push(DiscriminantInfo {
118 property_name: *prop_name,
119 variants,
120 });
121 }
122 }
123
124 discriminants
125 }
126
127 fn get_type_at_path(
141 &self,
142 mut type_id: TypeId,
143 path: &[Atom],
144 evaluator: &PropertyAccessEvaluator<'_>,
145 ) -> Option<TypeId> {
146 for (i, &prop_name) in path.iter().enumerate() {
147 if type_id == TypeId::ANY {
149 return Some(TypeId::ANY);
150 }
151
152 type_id = self.resolve_type(type_id);
154
155 if let Some(members_id) = union_list_id(self.db, type_id) {
157 let members = self.db.type_list(members_id);
158 let remaining_path = &path[i..];
159 let prop_types: Vec<TypeId> = members
160 .iter()
161 .filter_map(|&member| self.get_type_at_path(member, remaining_path, evaluator))
162 .collect();
163
164 if prop_types.is_empty() {
165 return None;
166 } else if prop_types.len() == 1 {
167 return Some(prop_types[0]);
168 }
169 return Some(self.db.union(prop_types));
170 }
171
172 let prop_name_arc = self.db.resolve_atom_ref(prop_name);
175 let prop_name_str = prop_name_arc.as_ref();
176 match evaluator.resolve_property_access(type_id, prop_name_str) {
177 PropertyAccessResult::Success {
178 type_id: prop_type_id,
179 ..
180 } => {
181 type_id = prop_type_id;
184 }
185 PropertyAccessResult::PropertyNotFound { .. } => {
186 return None;
189 }
190 PropertyAccessResult::PossiblyNullOrUndefined { property_type, .. } => {
191 if let Some(prop_ty) = property_type {
195 type_id = self.db.union2(prop_ty, TypeId::UNDEFINED);
197 } else {
198 type_id = TypeId::UNDEFINED;
200 }
201 }
202 PropertyAccessResult::IsUnknown => {
203 return Some(TypeId::ANY);
204 }
205 }
206 }
207
208 Some(type_id)
209 }
210
211 fn get_top_level_property_type_fast(&self, type_id: TypeId, property: Atom) -> Option<TypeId> {
217 let key = (type_id, property);
218 if let Some(&cached) = self.cache.property_cache.borrow().get(&key) {
219 return cached;
220 }
221
222 let result = self
224 .get_top_level_property_type_fast_uncached(type_id, property)
225 .map(|prop_type| self.resolve_type(prop_type));
226 self.cache.property_cache.borrow_mut().insert(key, result);
227 result
228 }
229
230 fn get_top_level_property_type_fast_uncached(
231 &self,
232 mut type_id: TypeId,
233 property: Atom,
234 ) -> Option<TypeId> {
235 type_id = self.resolve_type(type_id);
236
237 if intersection_list_id(self.db, type_id).is_some() {
240 return None;
241 }
242
243 let shape_id = object_shape_id(self.db, type_id)
244 .or_else(|| object_with_index_shape_id(self.db, type_id))?;
245 let shape = self.db.object_shape(shape_id);
246
247 let prop = match self.db.object_property_index(shape_id, property) {
248 PropertyLookup::Found(idx) => shape.properties.get(idx),
249 PropertyLookup::NotFound => None,
250 PropertyLookup::Uncached => {
251 shape
253 .properties
254 .binary_search_by_key(&property, |p| p.name)
255 .ok()
256 .and_then(|idx| shape.properties.get(idx))
257 }
258 }?;
259
260 Some(if prop.optional {
261 self.db.union2(prop.type_id, TypeId::UNDEFINED)
262 } else {
263 prop.type_id
264 })
265 }
266
267 #[inline]
272 fn literal_subtype_fast(&self, source: TypeId, target: TypeId) -> Option<bool> {
273 if source == target {
274 return Some(true);
275 }
276
277 match (
278 classify_for_literal_value(self.db, source),
279 classify_for_literal_value(self.db, target),
280 ) {
281 (LiteralValueKind::String(a), LiteralValueKind::String(b)) => Some(a == b),
282 (LiteralValueKind::Number(a), LiteralValueKind::Number(b)) => Some(a == b),
283 (LiteralValueKind::String(_), LiteralValueKind::Number(_))
284 | (LiteralValueKind::Number(_), LiteralValueKind::String(_)) => Some(false),
285 _ => None,
286 }
287 }
288
289 fn fast_narrow_top_level_discriminant(
294 &self,
295 original_union_type: TypeId,
296 members: &[TypeId],
297 property: Atom,
298 literal_value: TypeId,
299 keep_matching: bool,
300 ) -> Option<TypeId> {
301 let mut kept = Vec::with_capacity(members.len());
302
303 for &member in members {
304 if member.is_any_or_unknown() {
305 kept.push(member);
306 continue;
307 }
308
309 let prop_type = self.get_top_level_property_type_fast(member, property)?;
310 let should_keep = if prop_type == literal_value {
311 keep_matching
312 } else if keep_matching {
313 self.literal_subtype_fast(literal_value, prop_type)
315 .unwrap_or_else(|| is_subtype_of(self.db, literal_value, prop_type))
316 } else {
317 !self
319 .literal_subtype_fast(prop_type, literal_value)
320 .unwrap_or_else(|| is_subtype_of(self.db, prop_type, literal_value))
321 };
322
323 if should_keep {
324 kept.push(member);
325 }
326 }
327
328 if keep_matching && kept.is_empty() {
329 return Some(TypeId::NEVER);
330 }
331 if keep_matching && kept.len() == members.len() {
332 return Some(original_union_type);
333 }
334
335 Some(union_or_single_preserve(self.db, kept))
336 }
337
338 pub fn narrow_by_discriminant_for_type(
353 &self,
354 type_id: TypeId,
355 prop_path: &[Atom],
356 literal_type: TypeId,
357 is_true_branch: bool,
358 ) -> TypeId {
359 use crate::type_queries::{
360 TypeParameterConstraintKind, classify_for_type_parameter_constraint,
361 };
362
363 if let TypeParameterConstraintKind::TypeParameter {
364 constraint: Some(constraint),
365 } = classify_for_type_parameter_constraint(self.db, type_id)
366 && constraint != type_id
367 {
368 let narrowed_constraint = if is_true_branch {
369 self.narrow_by_discriminant(constraint, prop_path, literal_type)
370 } else {
371 self.narrow_by_excluding_discriminant(constraint, prop_path, literal_type)
372 };
373 if narrowed_constraint != constraint {
374 return self.db.intersection(vec![type_id, narrowed_constraint]);
375 }
376 }
377
378 if is_true_branch {
379 self.narrow_by_discriminant(type_id, prop_path, literal_type)
380 } else {
381 self.narrow_by_excluding_discriminant(type_id, prop_path, literal_type)
382 }
383 }
384
385 pub fn narrow_by_discriminant(
389 &self,
390 union_type: TypeId,
391 property_path: &[Atom],
392 literal_value: TypeId,
393 ) -> TypeId {
394 let _span = span!(
395 Level::TRACE,
396 "narrow_by_discriminant",
397 union_type = union_type.0,
398 property_path_len = property_path.len(),
399 literal_value = literal_value.0
400 )
401 .entered();
402
403 let resolved_type = self.resolve_type(union_type);
406
407 trace!(
408 "narrow_by_discriminant: union_type={}, resolved_type={}, property_path={:?}, literal_value={}",
409 union_type.0, resolved_type.0, property_path, literal_value.0
410 );
411
412 let single_member_storage: Vec<TypeId>;
415 let members: &[TypeId] = match classify_for_union_members(self.db, resolved_type) {
416 UnionMembersKind::Union(members_list) => {
417 single_member_storage = members_list.into_iter().collect::<Vec<_>>();
419 &single_member_storage
420 }
421 UnionMembersKind::NotUnion => {
422 single_member_storage = vec![resolved_type];
424 &single_member_storage
425 }
426 };
427
428 trace!("narrow_by_discriminant: members={:?}", members);
429
430 trace!(
431 "Checking {} member(s) for discriminant match",
432 members.len()
433 );
434
435 trace!(
436 "Narrowing union with {} members by discriminant property",
437 members.len()
438 );
439
440 if property_path.len() == 1
441 && let Some(fast_result) = self.fast_narrow_top_level_discriminant(
442 union_type,
443 members,
444 property_path[0],
445 literal_value,
446 true,
447 )
448 {
449 return fast_result;
450 }
451
452 let mut matching: Vec<TypeId> = Vec::new();
453 let property_evaluator = match self.resolver {
454 Some(resolver) => PropertyAccessEvaluator::with_resolver(self.db, resolver),
455 None => PropertyAccessEvaluator::new(self.db),
456 };
457
458 for &member in members {
459 if member.is_any_or_unknown() {
461 trace!("Member {} is any/unknown, keeping in true branch", member.0);
462 matching.push(member);
463 continue;
464 }
465
466 let resolved_member = self.resolve_type(member);
469
470 let intersection_members = intersection_list_id(self.db, resolved_member)
472 .map(|members_id| self.db.type_list(members_id).to_vec());
473
474 let check_member_for_property = |check_type_id: TypeId| -> bool {
476 let prop_type = match self.get_type_at_path(
478 check_type_id,
479 property_path,
480 &property_evaluator,
481 ) {
482 Some(t) => t,
483 None => {
484 trace!(
486 "Member {} does not have property path {:?}",
487 check_type_id.0, property_path
488 );
489 return false;
490 }
491 };
492
493 let resolved_prop_type = self.resolve_type(prop_type);
497
498 let matches = is_subtype_of(self.db, literal_value, resolved_prop_type);
501
502 if matches {
503 trace!(
504 "Member {} has property path {:?} with type {}, literal {} matches",
505 check_type_id.0, property_path, prop_type.0, literal_value.0
506 );
507 } else {
508 trace!(
509 "Member {} has property path {:?} with type {}, literal {} does not match",
510 check_type_id.0, property_path, prop_type.0, literal_value.0
511 );
512 }
513
514 matches
515 };
516
517 let has_property_match = if let Some(ref intersection) = intersection_members {
519 intersection.iter().any(|&m| check_member_for_property(m))
521 } else {
522 check_member_for_property(resolved_member)
524 };
525
526 if has_property_match {
527 matching.push(member);
528 }
529 }
530
531 if matching.is_empty() {
534 trace!("No members matched discriminant check, returning never");
535 TypeId::NEVER
536 } else if matching.len() == members.len() {
537 trace!("All members matched, returning original");
538 union_type
539 } else if matching.len() == 1 {
540 trace!("Narrowed to single member");
541 matching[0]
542 } else {
543 trace!(
544 "Narrowed to {} of {} members",
545 matching.len(),
546 members.len()
547 );
548 self.db.union(matching)
549 }
550 }
551
552 pub fn narrow_by_excluding_discriminant(
569 &self,
570 union_type: TypeId,
571 property_path: &[Atom],
572 excluded_value: TypeId,
573 ) -> TypeId {
574 let _span = span!(
575 Level::TRACE,
576 "narrow_by_excluding_discriminant",
577 union_type = union_type.0,
578 property_path_len = property_path.len(),
579 excluded_value = excluded_value.0
580 )
581 .entered();
582
583 let resolved_type = self.resolve_type(union_type);
586
587 let single_member_storage: Vec<TypeId>;
591 let members: &[TypeId] = match classify_for_union_members(self.db, resolved_type) {
592 UnionMembersKind::Union(members_list) => {
593 single_member_storage = members_list.into_iter().collect::<Vec<_>>();
594 &single_member_storage
595 }
596 UnionMembersKind::NotUnion => {
597 single_member_storage = vec![resolved_type];
598 &single_member_storage
599 }
600 };
601
602 trace!(
603 "Excluding discriminant value {} from union with {} members",
604 excluded_value.0,
605 members.len()
606 );
607
608 if property_path.len() == 1
609 && let Some(fast_result) = self.fast_narrow_top_level_discriminant(
610 union_type,
611 members,
612 property_path[0],
613 excluded_value,
614 false,
615 )
616 {
617 return fast_result;
618 }
619
620 let mut remaining: Vec<TypeId> = Vec::new();
621 let property_evaluator = match self.resolver {
622 Some(resolver) => PropertyAccessEvaluator::with_resolver(self.db, resolver),
623 None => PropertyAccessEvaluator::new(self.db),
624 };
625
626 for &member in members {
627 if member.is_any_or_unknown() {
629 trace!(
630 "Member {} is any/unknown, keeping in false branch",
631 member.0
632 );
633 remaining.push(member);
634 continue;
635 }
636
637 let resolved_member = self.resolve_type(member);
639
640 let intersection_members = intersection_list_id(self.db, resolved_member)
642 .map(|members_id| self.db.type_list(members_id).to_vec());
643
644 let should_keep_member = |check_type_id: TypeId| -> bool {
647 let prop_type = match self.get_type_at_path(
649 check_type_id,
650 property_path,
651 &property_evaluator,
652 ) {
653 Some(t) => t,
654 None => {
655 trace!(
657 "Member {} does not have property path, keeping",
658 check_type_id.0
659 );
660 return true;
661 }
662 };
663
664 let resolved_prop_type = self.resolve_type(prop_type);
666
667 let should_exclude = is_subtype_of(self.db, resolved_prop_type, excluded_value);
671
672 if should_exclude {
673 trace!(
674 "Member {} has property path type {} which is subtype of excluded {}, excluding",
675 check_type_id.0, prop_type.0, excluded_value.0
676 );
677 false } else {
679 trace!(
680 "Member {} has property path type {} which is not subtype of excluded {}, keeping",
681 check_type_id.0, prop_type.0, excluded_value.0
682 );
683 true }
685 };
686
687 let keep_member = if let Some(ref intersection) = intersection_members {
689 intersection.iter().all(|&m| should_keep_member(m))
695 } else {
696 should_keep_member(resolved_member)
698 };
699
700 if keep_member {
701 remaining.push(member);
702 }
703 }
704
705 union_or_single_preserve(self.db, remaining)
706 }
707
708 pub fn narrow_by_excluding_discriminant_values(
712 &self,
713 union_type: TypeId,
714 property_path: &[Atom],
715 excluded_values: &[TypeId],
716 ) -> TypeId {
717 if excluded_values.is_empty() {
718 return union_type;
719 }
720
721 let _span = span!(
722 Level::TRACE,
723 "narrow_by_excluding_discriminant_values",
724 union_type = union_type.0,
725 property_path_len = property_path.len(),
726 excluded_count = excluded_values.len()
727 )
728 .entered();
729
730 let resolved_type = self.resolve_type(union_type);
731
732 let single_member_storage: Vec<TypeId>;
733 let members: &[TypeId] = match classify_for_union_members(self.db, resolved_type) {
734 UnionMembersKind::Union(members_list) => {
735 single_member_storage = members_list.into_iter().collect::<Vec<_>>();
736 &single_member_storage
737 }
738 UnionMembersKind::NotUnion => {
739 single_member_storage = vec![resolved_type];
740 &single_member_storage
741 }
742 };
743
744 let excluded_set: FxHashSet<TypeId> = excluded_values.iter().copied().collect();
746
747 let mut remaining: Vec<TypeId> = Vec::new();
748 let property_evaluator = match self.resolver {
749 Some(resolver) => PropertyAccessEvaluator::with_resolver(self.db, resolver),
750 None => PropertyAccessEvaluator::new(self.db),
751 };
752
753 for &member in members {
754 if member.is_any_or_unknown() {
755 remaining.push(member);
756 continue;
757 }
758
759 let resolved_member = self.resolve_type(member);
760 let intersection_members = intersection_list_id(self.db, resolved_member)
761 .map(|members_id| self.db.type_list(members_id).to_vec());
762
763 let should_keep_member = |check_type_id: TypeId| -> bool {
765 let prop_type = match self.get_type_at_path(
766 check_type_id,
767 property_path,
768 &property_evaluator,
769 ) {
770 Some(t) => t,
771 None => return true, };
773
774 let resolved_prop_type = self.resolve_type(prop_type);
775
776 if excluded_set.contains(&resolved_prop_type) {
778 return false; }
780
781 for &excluded in excluded_values {
783 if is_subtype_of(self.db, resolved_prop_type, excluded) {
784 return false; }
786 }
787 true };
789
790 let keep_member = if let Some(ref intersection) = intersection_members {
791 intersection.iter().all(|&m| should_keep_member(m))
792 } else {
793 should_keep_member(resolved_member)
794 };
795
796 if keep_member {
797 remaining.push(member);
798 }
799 }
800
801 union_or_single_preserve(self.db, remaining)
802 }
803}