Skip to main content

tsz_solver/
narrowing_discriminants.rs

1//! Discriminant-based narrowing for discriminated unions.
2//!
3//! Handles narrowing of types based on discriminant property checks.
4//! For example, `action.type === "add"` narrows `Action` to
5//! `{ type: "add", value: number }`.
6//!
7//! Key functions:
8//! - `find_discriminants`: Identifies discriminant properties in unions
9//! - `narrow_by_discriminant`: Narrows to matching union members
10//! - `narrow_by_excluding_discriminant`: Excludes matching union members
11
12use 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    /// Find discriminant properties in a union type.
29    ///
30    /// A discriminant property is one where:
31    /// 1. All union members have the property
32    /// 2. Each member has a unique literal type for that property
33    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        // Collect all property names from all members
52        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                // Track all property names
65                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                // Non-object member - can't have discriminants
73                return vec![];
74            }
75        }
76
77        // Check each property to see if it's a valid discriminant
78        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                // Find this property in the member
87                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                        // Must be a literal type
95                        if is_literal_type_db(self.db, ty) {
96                            // Must be unique among members
97                            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                        // Property doesn't exist in this member
110                        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    /// Get the type of a property at a nested path within a type.
128    ///
129    /// # Examples
130    /// - `get_type_at_path(type, ["payload"])` -> type of `payload` property
131    /// - `get_type_at_path(type, ["payload", "type"])` -> type of `payload.type`
132    ///
133    /// Returns `None` if:
134    /// - The type doesn't have the property at any level in the path
135    /// - An intermediate type in the path is not an object type
136    ///
137    /// **NOTE**: Uses `resolve_property_access` which correctly handles optional properties.
138    /// For optional properties that don't exist on a specific union member, returns
139    /// `TypeId::UNDEFINED` to indicate the property could be undefined (not a definitive mismatch).
140    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            // Handle ANY - any property access on any returns any
148            if type_id == TypeId::ANY {
149                return Some(TypeId::ANY);
150            }
151
152            // Resolve Lazy types
153            type_id = self.resolve_type(type_id);
154
155            // Handle Union - return union of property types from all members
156            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            // Use resolve_property_access for proper optional property handling
173            // This correctly handles properties that are optional (prop?: type)
174            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                    // Property found - use its type
182                    // For optional properties, this already includes `undefined` in the union
183                    type_id = prop_type_id;
184                }
185                PropertyAccessResult::PropertyNotFound { .. } => {
186                    // Property truly doesn't exist on this type
187                    // This union member doesn't have the discriminant property, so filter it out
188                    return None;
189                }
190                PropertyAccessResult::PossiblyNullOrUndefined { property_type, .. } => {
191                    // CRITICAL FIX: For optional properties (prop?: type), we need to preserve
192                    // both the property type AND undefined in the union.
193                    // This ensures that is_subtype_of(circle, "circle" | undefined) works correctly.
194                    if let Some(prop_ty) = property_type {
195                        // Create union: property_type | undefined
196                        type_id = self.db.union2(prop_ty, TypeId::UNDEFINED);
197                    } else {
198                        // No property type, just undefined
199                        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    /// Fast path for top-level property lookup on object members.
212    ///
213    /// This avoids `PropertyAccessEvaluator` for the common discriminant pattern
214    /// `x.kind === "..."` where we only need a direct property read from object-like
215    /// union members. Falls back to the general path for complex structures.
216    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        // Cache the resolved property type so hot paths avoid an extra resolve pass.
223        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        // Keep this fast path conservative: intersections and complex wrappers
238        // should use the full evaluator-based path for correctness.
239        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                // Properties are sorted by Atom id.
252                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    /// Fast literal-only subtype check used by discriminant hot paths.
268    ///
269    /// Returns `None` when either side is non-literal (or not a string/number
270    /// literal) so callers can fall back to the full subtype relation.
271    #[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    /// Fast narrowing for `x.<prop> === literal` / `!== literal` over union members.
290    ///
291    /// Returns `None` to request fallback to the general evaluator-based implementation
292    /// when the structure is too complex for this direct path.
293    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                // true branch: keep members where literal <: property_type
314                self.literal_subtype_fast(literal_value, prop_type)
315                    .unwrap_or_else(|| is_subtype_of(self.db, literal_value, prop_type))
316            } else {
317                // false branch: exclude members where property_type <: excluded_literal
318                !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    /// Narrow a union type based on a discriminant property check.
339    ///
340    /// Example: `action.type === "add"` narrows `Action` to `{ type: "add", value: number }`
341    ///
342    /// Uses a filtering approach: checks each union member individually to see if
343    /// the property could match the literal value. This is more flexible than the
344    /// old `find_discriminants` approach which required ALL members to have the
345    /// property with unique literal values.
346    ///
347    /// # Arguments
348    /// Narrow a type by discriminant, handling type parameter constraints.
349    ///
350    /// If the type is a type parameter with a constraint, narrows the constraint
351    /// and intersects with the type parameter when the constraint is affected.
352    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    /// - `union_type`: The union type to narrow
386    /// - `property_path`: Path to the discriminant property (e.g., ["payload", "type"])
387    /// - `literal_value`: The literal value to match
388    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        // CRITICAL: Resolve Lazy types before checking for union members
404        // This ensures type aliases are resolved to their actual union types
405        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        // CRITICAL FIX: Use classify_for_union_members instead of union_list_id
413        // This correctly handles intersections containing unions, nested unions, etc.
414        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                // Convert Vec to slice for iteration
418                single_member_storage = members_list.into_iter().collect::<Vec<_>>();
419                &single_member_storage
420            }
421            UnionMembersKind::NotUnion => {
422                // Not a union at all - treat as single member
423                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            // Special case: any and unknown always match
460            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            // CRITICAL: Resolve Lazy types before checking for object shape
467            // This ensures type aliases are resolved to their actual types
468            let resolved_member = self.resolve_type(member);
469
470            // Handle Intersection types: check all intersection members for the property
471            let intersection_members = intersection_list_id(self.db, resolved_member)
472                .map(|members_id| self.db.type_list(members_id).to_vec());
473
474            // Helper function to check if a type has a matching property at the path
475            let check_member_for_property = |check_type_id: TypeId| -> bool {
476                // Get the type at the property path
477                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                        // Property doesn't exist on this member
485                        trace!(
486                            "Member {} does not have property path {:?}",
487                            check_type_id.0, property_path
488                        );
489                        return false;
490                    }
491                };
492
493                // CRITICAL: Resolve Lazy types in property type before comparison.
494                // Property types like `E.A` may be stored as Lazy(DefId) references
495                // that need to be resolved to their actual enum literal types.
496                let resolved_prop_type = self.resolve_type(prop_type);
497
498                // CRITICAL: Use is_subtype_of(literal_value, property_type)
499                // NOT the reverse! This was the bug in the reverted commit.
500                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            // Check for property match
518            let has_property_match = if let Some(ref intersection) = intersection_members {
519                // For Intersection: at least one member must have the property
520                intersection.iter().any(|&m| check_member_for_property(m))
521            } else {
522                // For non-Intersection: check the single member
523                check_member_for_property(resolved_member)
524            };
525
526            if has_property_match {
527                matching.push(member);
528            }
529        }
530
531        // Return result based on matches
532
533        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    /// Narrow a union type by excluding variants with a specific discriminant value.
553    ///
554    /// Example: `action.type !== "add"` narrows to `{ type: "remove", ... } | { type: "clear" }`
555    ///
556    /// Uses the inverse logic of `narrow_by_discriminant`: we exclude a member
557    /// ONLY if its property is definitely and only the excluded value.
558    ///
559    /// For example:
560    /// - prop is "a", exclude "a" -> exclude (property is always "a")
561    /// - prop is "a" | "b", exclude "a" -> keep (could be "b")
562    /// - prop doesn't exist -> keep (property doesn't match excluded value)
563    ///
564    /// # Arguments
565    /// - `union_type`: The union type to narrow
566    /// - `property_path`: Path to the discriminant property (e.g., ["payload", "type"])
567    /// - `excluded_value`: The literal value to exclude
568    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        // CRITICAL: Resolve Lazy types before checking for union members
584        // This ensures type aliases are resolved to their actual union types
585        let resolved_type = self.resolve_type(union_type);
586
587        // CRITICAL FIX: Use classify_for_union_members instead of union_list_id
588        // This correctly handles intersections containing unions, nested unions, etc.
589        // Consistent with narrow_by_discriminant.
590        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            // Special case: any and unknown always kept (could have any property value)
628            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            // CRITICAL: Resolve Lazy types before checking for object shape
638            let resolved_member = self.resolve_type(member);
639
640            // Handle Intersection types: check all intersection members for the property
641            let intersection_members = intersection_list_id(self.db, resolved_member)
642                .map(|members_id| self.db.type_list(members_id).to_vec());
643
644            // Helper function to check if a member should be excluded
645            // Returns true if member should be KEPT (not excluded)
646            let should_keep_member = |check_type_id: TypeId| -> bool {
647                // Get the type at the property path
648                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                        // Property doesn't exist - keep the member
656                        trace!(
657                            "Member {} does not have property path, keeping",
658                            check_type_id.0
659                        );
660                        return true;
661                    }
662                };
663
664                // CRITICAL: Resolve Lazy types in property type before comparison.
665                let resolved_prop_type = self.resolve_type(prop_type);
666
667                // Exclude member ONLY if property type is subtype of excluded value
668                // This means the property is ALWAYS the excluded value
669                // REVERSE of narrow_by_discriminant logic
670                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 // Member should be excluded
678                } 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 // Member should be kept
684                }
685            };
686
687            // Check if member should be kept
688            let keep_member = if let Some(ref intersection) = intersection_members {
689                // CRITICAL: For Intersection exclusion, use ALL not ANY
690                // If ANY intersection member has the excluded property value,
691                // the ENTIRE intersection must be excluded.
692                // Example: { kind: "A" } & { data: string } with x.kind !== "A"
693                //   -> { kind: "A" } has "A" (excluded) -> exclude entire intersection
694                intersection.iter().all(|&m| should_keep_member(m))
695            } else {
696                // For non-Intersection: check the single member
697                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    /// Narrow a union type by excluding variants with any of the specified discriminant values.
709    ///
710    /// This is an optimized batch version of `narrow_by_excluding_discriminant` for switch statements.
711    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        // Put excluded values into a HashSet for O(1) lookup
745        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            // Helper to check if member should be kept
764            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, // Keep if property missing
772                };
773
774                let resolved_prop_type = self.resolve_type(prop_type);
775
776                // Optimization: if property type is directly in excluded set (literal match)
777                if excluded_set.contains(&resolved_prop_type) {
778                    return false; // Exclude
779                }
780
781                // Subtype check for each excluded value
782                for &excluded in excluded_values {
783                    if is_subtype_of(self.db, resolved_prop_type, excluded) {
784                        return false; // Exclude
785                    }
786                }
787                true // Keep
788            };
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}