tsz_solver/narrowing/mod.rs
1//! Type narrowing for discriminated unions and type guards.
2//!
3//! Discriminated unions are unions where each member has a common "discriminant"
4//! property with a literal type that uniquely identifies that member.
5//!
6//! Example:
7//! ```typescript
8//! type Action =
9//! | { type: "add", value: number }
10//! | { type: "remove", id: string }
11//! | { type: "clear" };
12//!
13//! function handle(action: Action) {
14//! if (action.type === "add") {
15//! // action is narrowed to { type: "add", value: number }
16//! }
17//! }
18//! ```
19//!
20//! ## `TypeGuard` Abstraction
21//!
22//! The `TypeGuard` enum provides an AST-agnostic representation of narrowing
23//! conditions. This allows the Solver to perform pure type algebra without
24//! depending on AST nodes.
25//!
26//! Architecture:
27//! - **Checker**: Extracts `TypeGuard` from AST nodes (WHERE)
28//! - **Solver**: Applies `TypeGuard` to types (WHAT)
29
30mod compound;
31mod discriminants;
32mod instanceof;
33mod property;
34pub(crate) mod utils;
35
36// Re-export utility functions from the utils submodule
37pub use utils::{
38 find_discriminants, is_definitely_nullish, is_nullish_type, narrow_by_discriminant,
39 narrow_by_typeof, remove_definitely_falsy_types, remove_nullish, split_nullish_type,
40 type_contains_undefined,
41};
42
43use crate::relations::subtype::TypeResolver;
44use crate::type_queries::{UnionMembersKind, classify_for_union_members};
45#[cfg(test)]
46use crate::types::*;
47use crate::types::{FunctionShape, LiteralValue, ParamInfo, TypeData, TypeId};
48use crate::utils::{TypeIdExt, union_or_single};
49use crate::visitor::{
50 index_access_parts, intersection_list_id, is_function_type_db, is_object_like_type_db,
51 lazy_def_id, literal_value, object_shape_id, object_with_index_shape_id, template_literal_id,
52 type_param_info, union_list_id,
53};
54use crate::{QueryDatabase, TypeDatabase};
55use rustc_hash::FxHashMap;
56use std::cell::RefCell;
57use tracing::{Level, span, trace};
58use tsz_common::interner::Atom;
59
60/// The result of a `typeof` expression, restricted to the 8 standard JavaScript types.
61///
62/// Using an enum instead of `String` eliminates heap allocation per typeof guard.
63/// TypeScript's `typeof` operator only returns these 8 values.
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65pub enum TypeofKind {
66 String,
67 Number,
68 Boolean,
69 BigInt,
70 Symbol,
71 Undefined,
72 Object,
73 Function,
74}
75
76impl TypeofKind {
77 /// Parse a typeof result string into a `TypeofKind`.
78 /// Returns None for non-standard typeof strings (which don't narrow).
79 pub fn parse(s: &str) -> Option<Self> {
80 match s {
81 "string" => Some(Self::String),
82 "number" => Some(Self::Number),
83 "boolean" => Some(Self::Boolean),
84 "bigint" => Some(Self::BigInt),
85 "symbol" => Some(Self::Symbol),
86 "undefined" => Some(Self::Undefined),
87 "object" => Some(Self::Object),
88 "function" => Some(Self::Function),
89 _ => None,
90 }
91 }
92
93 /// Get the string representation of this typeof kind.
94 pub const fn as_str(&self) -> &'static str {
95 match self {
96 Self::String => "string",
97 Self::Number => "number",
98 Self::Boolean => "boolean",
99 Self::BigInt => "bigint",
100 Self::Symbol => "symbol",
101 Self::Undefined => "undefined",
102 Self::Object => "object",
103 Self::Function => "function",
104 }
105 }
106}
107
108/// AST-agnostic representation of a type narrowing condition.
109///
110/// This enum represents various guards that can narrow a type, without
111/// depending on AST nodes like `NodeIndex` or `SyntaxKind`.
112///
113/// # Examples
114/// ```typescript
115/// typeof x === "string" -> TypeGuard::Typeof(TypeofKind::String)
116/// x instanceof MyClass -> TypeGuard::Instanceof(MyClass_type)
117/// x === null -> TypeGuard::NullishEquality
118/// x -> TypeGuard::Truthy
119/// x.kind === "circle" -> TypeGuard::Discriminant { property: "kind", value: "circle" }
120/// ```
121#[derive(Clone, Debug, PartialEq)]
122pub enum TypeGuard {
123 /// `typeof x === "typename"`
124 ///
125 /// Narrows a union to only members matching the typeof result.
126 /// For example, narrowing `string | number` with `Typeof(TypeofKind::String)` yields `string`.
127 Typeof(TypeofKind),
128
129 /// `x instanceof Class`
130 ///
131 /// Narrows to the class type or its subtypes.
132 Instanceof(TypeId),
133
134 /// `x === literal` or `x !== literal`
135 ///
136 /// Narrows to exactly that literal type (for equality) or excludes it (for inequality).
137 LiteralEquality(TypeId),
138
139 /// `x == null` or `x != null` (checks both null and undefined)
140 ///
141 /// JavaScript/TypeScript treats `== null` as matching both `null` and `undefined`.
142 NullishEquality,
143
144 /// `x` (truthiness check in a conditional)
145 ///
146 /// Removes falsy types from a union: `null`, `undefined`, `false`, `0`, `""`, `NaN`.
147 Truthy,
148
149 /// `x.prop === literal` or `x.payload.type === "value"` (Discriminated Union narrowing)
150 ///
151 /// Narrows a union of object types based on a discriminant property.
152 ///
153 /// # Examples
154 /// - Top-level: `{ kind: "A" } | { kind: "B" }` with `path: ["kind"]` yields `{ kind: "A" }`
155 /// - Nested: `{ payload: { type: "user" } } | { payload: { type: "product" } }`
156 /// with `path: ["payload", "type"]` yields `{ payload: { type: "user" } }`
157 Discriminant {
158 /// Property path from base to discriminant (e.g., ["payload", "type"])
159 property_path: Vec<Atom>,
160 /// The literal value to match against
161 value_type: TypeId,
162 },
163
164 /// `prop in x`
165 ///
166 /// Narrows to types that have the specified property.
167 InProperty(Atom),
168
169 /// `x is T` or `asserts x is T` (User-Defined Type Guard)
170 ///
171 /// Narrows a type based on a user-defined type predicate function.
172 ///
173 /// # Examples
174 /// ```typescript
175 /// function isString(x: any): x is string { ... }
176 /// function assertDefined(x: any): asserts x is Date { ... }
177 ///
178 /// if (isString(x)) { x; // string }
179 /// assertDefined(x); x; // Date
180 /// ```
181 ///
182 /// - `type_id: Some(T)`: The type to narrow to (e.g., `string` or `Date`)
183 /// - `type_id: None`: Truthiness assertion (`asserts x`), behaves like `Truthy`
184 /// - `asserts: true`: This is an assertion (throws if false), affects control flow
185 Predicate {
186 type_id: Option<TypeId>,
187 asserts: bool,
188 },
189
190 /// `Array.isArray(x)`
191 ///
192 /// Narrows a type to only array-like types (arrays, tuples, readonly arrays).
193 ///
194 /// # Examples
195 /// ```typescript
196 /// function process(x: string[] | number | { length: number }) {
197 /// if (Array.isArray(x)) {
198 /// x; // string[] (not number or the object)
199 /// }
200 /// }
201 /// ```
202 ///
203 /// This preserves element types - `string[] | number[]` stays as `string[] | number[]`,
204 /// it doesn't collapse to `any[]`.
205 Array,
206
207 /// `array.every(predicate)` where predicate has type predicate
208 ///
209 /// Narrows an array's element type based on a type predicate.
210 ///
211 /// # Examples
212 /// ```typescript
213 /// const arr: (number | string)[] = ['aaa'];
214 /// const isString = (x: unknown): x is string => typeof x === 'string';
215 /// if (arr.every(isString)) {
216 /// arr; // string[] (element type narrowed from number | string to string)
217 /// }
218 /// ```
219 ///
220 /// This only applies to arrays. For non-array types, the type is unchanged.
221 ArrayElementPredicate {
222 /// The type to narrow array elements to
223 element_type: TypeId,
224 },
225}
226
227#[inline]
228pub(crate) fn union_or_single_preserve(db: &dyn TypeDatabase, types: Vec<TypeId>) -> TypeId {
229 match types.len() {
230 0 => TypeId::NEVER,
231 1 => types[0],
232 _ => db.union_from_sorted_vec(types),
233 }
234}
235
236/// Result of a narrowing operation.
237///
238/// Represents the types in both branches of a condition.
239#[derive(Clone, Debug)]
240pub struct NarrowingResult {
241 /// The type in the "true" branch of the condition
242 pub true_type: TypeId,
243 /// The type in the "false" branch of the condition
244 pub false_type: TypeId,
245}
246
247/// Result of finding discriminant properties in a union.
248#[derive(Clone, Debug)]
249pub struct DiscriminantInfo {
250 /// The name of the discriminant property
251 pub property_name: Atom,
252 /// Map from literal value to the union member type
253 pub variants: Vec<(TypeId, TypeId)>, // (literal_type, member_type)
254}
255
256/// Narrowing context for type guards and control flow analysis.
257/// Shared across multiple narrowing contexts to persist resolution results.
258#[derive(Default, Clone, Debug)]
259pub struct NarrowingCache {
260 /// Cache for type resolution (Lazy/App/Template -> Structural)
261 pub resolve_cache: RefCell<FxHashMap<TypeId, TypeId>>,
262 /// Cache for top-level property type lookups (TypeId, `PropName`) -> `PropType`
263 pub property_cache: RefCell<FxHashMap<(TypeId, Atom), Option<TypeId>>>,
264}
265
266impl NarrowingCache {
267 pub fn new() -> Self {
268 Self::default()
269 }
270}
271
272/// Narrowing context for type guards and control flow analysis.
273pub struct NarrowingContext<'a> {
274 pub(crate) db: &'a dyn QueryDatabase,
275 /// Optional `TypeResolver` for resolving Lazy types (e.g., type aliases).
276 /// When present, this enables proper narrowing of type aliases like `type Shape = Circle | Square`.
277 pub(crate) resolver: Option<&'a dyn TypeResolver>,
278 /// Cache for narrowing operations.
279 /// If provided, uses the shared cache; otherwise uses a local ephemeral cache.
280 pub(crate) cache: std::borrow::Cow<'a, NarrowingCache>,
281}
282
283impl<'a> NarrowingContext<'a> {
284 pub fn new(db: &'a dyn QueryDatabase) -> Self {
285 NarrowingContext {
286 db,
287 resolver: None,
288 cache: std::borrow::Cow::Owned(NarrowingCache::new()),
289 }
290 }
291
292 /// Create a new context with a shared cache.
293 pub fn with_cache(db: &'a dyn QueryDatabase, cache: &'a NarrowingCache) -> Self {
294 NarrowingContext {
295 db,
296 resolver: None,
297 cache: std::borrow::Cow::Borrowed(cache),
298 }
299 }
300
301 /// Set the `TypeResolver` for this context.
302 ///
303 /// This enables proper resolution of Lazy types (type aliases) during narrowing.
304 /// The resolver should be borrowed from the Checker's `TypeEnvironment`.
305 pub fn with_resolver(mut self, resolver: &'a dyn TypeResolver) -> Self {
306 self.resolver = Some(resolver);
307 self
308 }
309
310 /// Resolve a type to its structural representation.
311 ///
312 /// Unwraps:
313 /// - Lazy types (evaluates them using resolver if available, otherwise falls back to db)
314 /// - Application types (evaluates the generic instantiation)
315 ///
316 /// This ensures that type aliases, interfaces, and generics are resolved
317 /// to their actual structural types before performing narrowing operations.
318 pub(crate) fn resolve_type(&self, type_id: TypeId) -> TypeId {
319 if let Some(&cached) = self.cache.resolve_cache.borrow().get(&type_id) {
320 return cached;
321 }
322
323 let result = self.resolve_type_uncached(type_id);
324 self.cache
325 .resolve_cache
326 .borrow_mut()
327 .insert(type_id, result);
328 result
329 }
330
331 fn resolve_type_uncached(&self, mut type_id: TypeId) -> TypeId {
332 // Prevent infinite loops with a fuel counter
333 let mut fuel = 100;
334
335 while fuel > 0 {
336 fuel -= 1;
337
338 // 1. Handle Lazy types (DefId-based, not SymbolRef)
339 // If we have a TypeResolver, try to resolve Lazy types through it first
340 if let Some(def_id) = lazy_def_id(self.db, type_id) {
341 if let Some(resolver) = self.resolver
342 && let Some(resolved) =
343 resolver.resolve_lazy(def_id, self.db.as_type_database())
344 {
345 type_id = resolved;
346 continue;
347 }
348 // Fallback to database evaluation if no resolver or resolution failed
349 type_id = self.db.evaluate_type(type_id);
350 continue;
351 }
352
353 // 2. Handle Application types (Generics)
354 // CRITICAL: When a resolver is available (from the checker's TypeEnvironment),
355 // use it to resolve the Application's base type and instantiate with args.
356 // Without the resolver, generic type aliases like `Box<number>` can't resolve
357 // their DefId-based base types, causing narrowing to fail on discriminated
358 // unions wrapped in generics.
359 if let Some(TypeData::Application(app_id)) = self.db.lookup(type_id) {
360 if let Some(resolver) = self.resolver {
361 let app = self.db.type_application(app_id);
362 // Try to resolve the base type's DefId and instantiate manually
363 if let Some(def_id) = lazy_def_id(self.db, app.base) {
364 let resolved_body =
365 resolver.resolve_lazy(def_id, self.db.as_type_database());
366 let type_params = resolver.get_lazy_type_params(def_id);
367 if let (Some(body), Some(params)) = (resolved_body, type_params) {
368 let instantiated =
369 crate::instantiation::instantiate::instantiate_generic(
370 self.db.as_type_database(),
371 body,
372 ¶ms,
373 &app.args,
374 );
375 type_id = instantiated;
376 continue;
377 }
378 }
379 }
380 // Fallback: use db.evaluate_type (works when resolver isn't needed)
381 type_id = self.db.evaluate_type(type_id);
382 continue;
383 }
384
385 // 3. Handle TemplateLiteral types that can be fully evaluated to string literals.
386 // Template literal spans may contain Lazy(DefId) types (e.g., `${EnumType.Member}`)
387 // that must be resolved before evaluation. We resolve all lazy spans first,
388 // rebuild the template literal, then let the evaluator handle it.
389 if let Some(TypeData::TemplateLiteral(spans_id)) = self.db.lookup(type_id) {
390 use crate::types::TemplateSpan;
391 let spans = self.db.template_list(spans_id);
392 let mut new_spans = Vec::with_capacity(spans.len());
393 let mut changed = false;
394 for span in spans.iter() {
395 match span {
396 TemplateSpan::Type(inner_id) => {
397 let resolved = self.resolve_type(*inner_id);
398 if resolved != *inner_id {
399 changed = true;
400 }
401 new_spans.push(TemplateSpan::Type(resolved));
402 }
403 other => new_spans.push(other.clone()),
404 }
405 }
406 let eval_input = if changed {
407 self.db.template_literal(new_spans)
408 } else {
409 type_id
410 };
411 let evaluated = self.db.evaluate_type(eval_input);
412 if evaluated != type_id {
413 type_id = evaluated;
414 continue;
415 }
416 }
417
418 // It's a structural type (Object, Union, Intersection, Primitive)
419 break;
420 }
421
422 type_id
423 }
424
425 /// Narrow a type based on a typeof check.
426 ///
427 /// Example: `typeof x === "string"` narrows `string | number` to `string`
428 pub fn narrow_by_typeof(&self, source_type: TypeId, typeof_result: &str) -> TypeId {
429 let _span =
430 span!(Level::TRACE, "narrow_by_typeof", source_type = source_type.0, %typeof_result)
431 .entered();
432
433 // CRITICAL FIX: Narrow `any` for typeof checks
434 // TypeScript narrows `any` for typeof/instanceof/Array.isArray/user-defined guards
435 // But NOT for equality/truthiness/in operator
436 if source_type == TypeId::UNKNOWN || source_type == TypeId::ANY {
437 return match typeof_result {
438 "string" => TypeId::STRING,
439 "number" => TypeId::NUMBER,
440 "boolean" => TypeId::BOOLEAN,
441 "bigint" => TypeId::BIGINT,
442 "symbol" => TypeId::SYMBOL,
443 "undefined" => TypeId::UNDEFINED,
444 "object" => self.db.union2(TypeId::OBJECT, TypeId::NULL),
445 "function" => self.function_type(),
446 _ => source_type,
447 };
448 }
449
450 let target_type = match typeof_result {
451 "string" => TypeId::STRING,
452 "number" => TypeId::NUMBER,
453 "boolean" => TypeId::BOOLEAN,
454 "bigint" => TypeId::BIGINT,
455 "symbol" => TypeId::SYMBOL,
456 "undefined" => TypeId::UNDEFINED,
457 "object" => TypeId::OBJECT, // includes null
458 "function" => return self.narrow_to_function(source_type),
459 _ => return source_type,
460 };
461
462 self.narrow_to_type(source_type, target_type)
463 }
464
465 /// Narrow a type to include only members assignable to target.
466 pub fn narrow_to_type(&self, source_type: TypeId, target_type: TypeId) -> TypeId {
467 let _span = span!(
468 Level::TRACE,
469 "narrow_to_type",
470 source_type = source_type.0,
471 target_type = target_type.0
472 )
473 .entered();
474
475 // CRITICAL FIX: Resolve Lazy/Ref types to inspect their structure.
476 // This fixes the "Missing type resolution" bug where type aliases and
477 // generics weren't being narrowed correctly.
478 let resolved_source = self.resolve_type(source_type);
479
480 // Gracefully handle resolution failures: if evaluation fails but the input
481 // wasn't ERROR, we can't narrow structurally. Return original source to
482 // avoid cascading ERRORs through the type system.
483 if resolved_source == TypeId::ERROR && source_type != TypeId::ERROR {
484 trace!("Source type resolution failed, returning original source");
485 return source_type;
486 }
487
488 // Resolve target for consistency
489 let resolved_target = self.resolve_type(target_type);
490 if resolved_target == TypeId::ERROR && target_type != TypeId::ERROR {
491 trace!("Target type resolution failed, returning original source");
492 return source_type;
493 }
494
495 // If source is the target, return it
496 if resolved_source == resolved_target {
497 trace!("Source type equals target type, returning unchanged");
498 return source_type;
499 }
500
501 // Special case: unknown can be narrowed to any type through type guards
502 // This handles cases like: if (typeof x === "string") where x: unknown
503 if resolved_source == TypeId::UNKNOWN {
504 trace!("Narrowing unknown to specific type via type guard");
505 return target_type;
506 }
507
508 // Special case: any can be narrowed to any type through type guards
509 // This handles cases like: if (x === null) where x: any
510 // CRITICAL: Unlike unknown, any MUST be narrowed to match target type
511 if resolved_source == TypeId::ANY {
512 trace!("Narrowing any to specific type via type guard");
513 return target_type;
514 }
515
516 // If source is a union, filter members
517 // Use resolved_source for structural inspection
518 if let Some(members) = union_list_id(self.db, resolved_source) {
519 let members = self.db.type_list(members);
520 trace!(
521 "Narrowing union with {} members to type {}",
522 members.len(),
523 target_type.0
524 );
525 let matching: Vec<TypeId> = members
526 .iter()
527 .filter_map(|&member| {
528 if let Some(narrowed) = self.narrow_type_param(member, target_type) {
529 return Some(narrowed);
530 }
531 if self.is_assignable_to(member, target_type) {
532 return Some(member);
533 }
534 // CRITICAL FIX: Check if target_type is a subtype of member
535 // This handles cases like narrowing string | number by "hello"
536 // where "hello" is a subtype of string, so we should narrow to "hello"
537 if crate::relations::subtype::is_subtype_of_with_db(self.db, target_type, member) {
538 return Some(target_type);
539 }
540 // CRITICAL FIX: instanceof Array matching
541 // When narrowing by `instanceof Array`, if the member is array-like and target
542 // is a Lazy/Application type (which includes Array<T> interface references),
543 // assume it's the global Array and match the member.
544 // This handles: `x: Message | Message[]` with `instanceof Array` should keep `Message[]`.
545 // At runtime, instanceof only checks prototype chain, not generic type arguments.
546 if self.is_array_like(member) {
547 use crate::type_queries;
548 // Check if target is a type reference or generic application (Array<T>)
549 let is_target_lazy_or_app = type_queries::is_type_reference(self.db, resolved_target)
550 || type_queries::is_generic_type(self.db, resolved_target);
551
552 trace!("Member is array-like: member={}, target={}, is_target_lazy_or_app={}",
553 member.0, resolved_target.0, is_target_lazy_or_app);
554
555 if is_target_lazy_or_app {
556 trace!("Array member with lazy/app target (likely Array interface), keeping member");
557 return Some(member);
558 }
559 }
560 None
561 })
562 .collect();
563
564 if matching.is_empty() {
565 trace!("No matching members found, returning NEVER");
566 return TypeId::NEVER;
567 } else if matching.len() == 1 {
568 trace!("Found single matching member, returning {}", matching[0].0);
569 return matching[0];
570 }
571 trace!(
572 "Found {} matching members, creating new union",
573 matching.len()
574 );
575 return self.db.union(matching);
576 }
577
578 // Check if this is a type parameter that needs narrowing
579 // Use resolved_source to handle type parameters behind aliases
580 if let Some(narrowed) = self.narrow_type_param(resolved_source, target_type) {
581 trace!("Narrowed type parameter to {}", narrowed.0);
582 return narrowed;
583 }
584
585 // Task 13: Handle boolean -> literal narrowing
586 // When narrowing boolean to true or false, return the corresponding literal
587 if resolved_source == TypeId::BOOLEAN {
588 let is_target_true = if let Some(lit) = literal_value(self.db, resolved_target) {
589 matches!(lit, LiteralValue::Boolean(true))
590 } else {
591 resolved_target == TypeId::BOOLEAN_TRUE
592 };
593
594 if is_target_true {
595 trace!("Narrowing boolean to true");
596 return TypeId::BOOLEAN_TRUE;
597 }
598
599 let is_target_false = if let Some(lit) = literal_value(self.db, resolved_target) {
600 matches!(lit, LiteralValue::Boolean(false))
601 } else {
602 resolved_target == TypeId::BOOLEAN_FALSE
603 };
604
605 if is_target_false {
606 trace!("Narrowing boolean to false");
607 return TypeId::BOOLEAN_FALSE;
608 }
609 }
610
611 // Check if source is assignable to target using resolved types for comparison
612 if self.is_assignable_to(resolved_source, resolved_target) {
613 trace!("Source type is assignable to target, returning source");
614 source_type
615 } else if crate::relations::subtype::is_subtype_of_with_db(
616 self.db,
617 resolved_target,
618 resolved_source,
619 ) {
620 // CRITICAL FIX: Check if target is a subtype of source (reverse narrowing)
621 // This handles cases like narrowing string to "hello" where "hello" is a subtype of string
622 // The inference engine uses this to narrow upper bounds by lower bounds
623 trace!("Target is subtype of source, returning target");
624 target_type
625 } else {
626 trace!("Source type is not assignable to target, returning NEVER");
627 TypeId::NEVER
628 }
629 }
630
631 /// Check if a literal type is assignable to a target for narrowing purposes.
632 ///
633 /// Handles union decomposition: if the target is a union, checks each member.
634 /// Falls back to `narrow_to_type` to determine if the literal can narrow to the target.
635 pub fn literal_assignable_to(&self, literal: TypeId, target: TypeId) -> bool {
636 if literal == target || target == TypeId::ANY || target == TypeId::UNKNOWN {
637 return true;
638 }
639
640 if let UnionMembersKind::Union(members) = classify_for_union_members(self.db, target) {
641 return members
642 .iter()
643 .any(|&member| self.literal_assignable_to(literal, member));
644 }
645
646 self.narrow_to_type(literal, target) != TypeId::NEVER
647 }
648
649 /// Narrow a type to exclude members assignable to target.
650 pub fn narrow_excluding_type(&self, source_type: TypeId, excluded_type: TypeId) -> TypeId {
651 if let Some(members) = intersection_list_id(self.db, source_type) {
652 let members = self.db.type_list(members);
653 let mut narrowed_members = Vec::with_capacity(members.len());
654 let mut changed = false;
655 for &member in members.iter() {
656 let narrowed = self.narrow_excluding_type(member, excluded_type);
657 if narrowed == TypeId::NEVER {
658 return TypeId::NEVER;
659 }
660 if narrowed != member {
661 changed = true;
662 }
663 narrowed_members.push(narrowed);
664 }
665 if !changed {
666 return source_type;
667 }
668 return self.db.intersection(narrowed_members);
669 }
670
671 // If source is a union, filter out matching members
672 if let Some(members) = union_list_id(self.db, source_type) {
673 let members = self.db.type_list(members);
674 let remaining: Vec<TypeId> = members
675 .iter()
676 .filter_map(|&member| {
677 if intersection_list_id(self.db, member).is_some() {
678 return self
679 .narrow_excluding_type(member, excluded_type)
680 .non_never();
681 }
682 if let Some(narrowed) = self.narrow_type_param_excluding(member, excluded_type)
683 {
684 return narrowed.non_never();
685 }
686 if self.is_assignable_to(member, excluded_type) {
687 None
688 } else {
689 Some(member)
690 }
691 })
692 .collect();
693
694 tracing::trace!(
695 remaining_count = remaining.len(),
696 remaining = ?remaining.iter().map(|t| t.0).collect::<Vec<_>>(),
697 "narrow_excluding_type: union filter result"
698 );
699 if remaining.is_empty() {
700 return TypeId::NEVER;
701 } else if remaining.len() == 1 {
702 return remaining[0];
703 }
704 return self.db.union(remaining);
705 }
706
707 if let Some(narrowed) = self.narrow_type_param_excluding(source_type, excluded_type) {
708 return narrowed;
709 }
710
711 // Special case: boolean type (treat as true | false union)
712 // Task 13: Fix Boolean Narrowing Logic
713 // When excluding true or false from boolean, return the other literal
714 // When excluding both true and false from boolean, return never
715 if source_type == TypeId::BOOLEAN
716 || source_type == TypeId::BOOLEAN_TRUE
717 || source_type == TypeId::BOOLEAN_FALSE
718 {
719 // Check if excluded_type is a boolean literal
720 let is_excluding_true = if let Some(lit) = literal_value(self.db, excluded_type) {
721 matches!(lit, LiteralValue::Boolean(true))
722 } else {
723 excluded_type == TypeId::BOOLEAN_TRUE
724 };
725
726 let is_excluding_false = if let Some(lit) = literal_value(self.db, excluded_type) {
727 matches!(lit, LiteralValue::Boolean(false))
728 } else {
729 excluded_type == TypeId::BOOLEAN_FALSE
730 };
731
732 // Handle exclusion from boolean, true, or false
733 if source_type == TypeId::BOOLEAN {
734 if is_excluding_true {
735 // Excluding true from boolean -> return false
736 return TypeId::BOOLEAN_FALSE;
737 } else if is_excluding_false {
738 // Excluding false from boolean -> return true
739 return TypeId::BOOLEAN_TRUE;
740 }
741 // If excluding BOOLEAN, let the final is_assignable_to check handle it below
742 } else if source_type == TypeId::BOOLEAN_TRUE {
743 if is_excluding_true {
744 // Excluding true from true -> return never
745 return TypeId::NEVER;
746 }
747 // For other cases (e.g., excluding BOOLEAN from TRUE),
748 // let the final is_assignable_to check handle it below
749 } else if source_type == TypeId::BOOLEAN_FALSE && is_excluding_false {
750 // Excluding false from false -> return never
751 return TypeId::NEVER;
752 }
753 // For other cases, let the final is_assignable_to check handle it below
754 // CRITICAL: Do NOT return source_type here.
755 // Fall through to the standard is_assignable_to check below.
756 // This handles edge cases like narrow_excluding_type(TRUE, BOOLEAN) -> NEVER
757 }
758
759 // If source is assignable to excluded, return never
760 if self.is_assignable_to(source_type, excluded_type) {
761 TypeId::NEVER
762 } else {
763 source_type
764 }
765 }
766
767 /// Narrow a type by excluding multiple types at once (batched version).
768 ///
769 /// This is an optimized version of `narrow_excluding_type` for cases like
770 /// switch default clauses where we need to exclude many types at once.
771 /// It avoids creating intermediate union types and reduces complexity from O(N²) to O(N).
772 ///
773 /// # Arguments
774 /// * `source_type` - The type to narrow (typically a union)
775 /// * `excluded_types` - Types to exclude from the source
776 ///
777 /// # Returns
778 /// The narrowed type with all excluded types removed
779 pub fn narrow_excluding_types(&self, source_type: TypeId, excluded_types: &[TypeId]) -> TypeId {
780 if excluded_types.is_empty() {
781 return source_type;
782 }
783
784 // For small lists, use sequential narrowing (avoids HashSet overhead)
785 if excluded_types.len() <= 4 {
786 let mut result = source_type;
787 for &excluded in excluded_types {
788 result = self.narrow_excluding_type(result, excluded);
789 if result == TypeId::NEVER {
790 return TypeId::NEVER;
791 }
792 }
793 return result;
794 }
795
796 // For larger lists, use HashSet for O(1) lookup
797 let excluded_set: rustc_hash::FxHashSet<TypeId> = excluded_types.iter().copied().collect();
798
799 // Handle union source type
800 if let Some(members) = union_list_id(self.db, source_type) {
801 let members = self.db.type_list(members);
802 let remaining: Vec<TypeId> = members
803 .iter()
804 .filter_map(|&member| {
805 // Fast path: direct identity check against the set
806 if excluded_set.contains(&member) {
807 return None;
808 }
809
810 // Handle intersection members
811 if intersection_list_id(self.db, member).is_some() {
812 return self
813 .narrow_excluding_types(member, excluded_types)
814 .non_never();
815 }
816
817 // Handle type parameters
818 if let Some(narrowed) =
819 self.narrow_type_param_excluding_set(member, &excluded_set)
820 {
821 return narrowed.non_never();
822 }
823
824 // Slow path: check assignability for complex cases
825 // This handles cases where the member isn't identical to an excluded type
826 // but might still be assignable to one (e.g., literal subtypes)
827 for &excluded in &excluded_set {
828 if self.is_assignable_to(member, excluded) {
829 return None;
830 }
831 }
832 Some(member)
833 })
834 .collect();
835
836 if remaining.is_empty() {
837 return TypeId::NEVER;
838 } else if remaining.len() == 1 {
839 return remaining[0];
840 }
841 return self.db.union(remaining);
842 }
843
844 // Handle single type (not a union)
845 if excluded_set.contains(&source_type) {
846 return TypeId::NEVER;
847 }
848
849 // Check assignability for single type
850 for &excluded in &excluded_set {
851 if self.is_assignable_to(source_type, excluded) {
852 return TypeId::NEVER;
853 }
854 }
855
856 source_type
857 }
858
859 /// Helper for `narrow_excluding_types` with type parameters
860 fn narrow_type_param_excluding_set(
861 &self,
862 source: TypeId,
863 excluded_set: &rustc_hash::FxHashSet<TypeId>,
864 ) -> Option<TypeId> {
865 let info = type_param_info(self.db, source)?;
866
867 let constraint = info.constraint?;
868 if constraint == source || constraint == TypeId::UNKNOWN {
869 return None;
870 }
871
872 // Narrow the constraint by excluding all types in the set
873 let excluded_vec: Vec<TypeId> = excluded_set.iter().copied().collect();
874 let narrowed_constraint = self.narrow_excluding_types(constraint, &excluded_vec);
875
876 if narrowed_constraint == constraint {
877 return None;
878 }
879 if narrowed_constraint == TypeId::NEVER {
880 return Some(TypeId::NEVER);
881 }
882
883 Some(self.db.intersection2(source, narrowed_constraint))
884 }
885
886 /// Narrow to function types only.
887 fn narrow_to_function(&self, source_type: TypeId) -> TypeId {
888 if let Some(members) = union_list_id(self.db, source_type) {
889 let members = self.db.type_list(members);
890 let functions: Vec<TypeId> = members
891 .iter()
892 .filter_map(|&member| {
893 if let Some(narrowed) = self.narrow_type_param_to_function(member) {
894 return narrowed.non_never();
895 }
896 self.is_function_type(member).then_some(member)
897 })
898 .collect();
899
900 return union_or_single(self.db, functions);
901 }
902
903 if let Some(narrowed) = self.narrow_type_param_to_function(source_type) {
904 return narrowed;
905 }
906
907 if self.is_function_type(source_type) {
908 source_type
909 } else if source_type == TypeId::OBJECT {
910 self.function_type()
911 } else if let Some(shape_id) = object_shape_id(self.db, source_type) {
912 let shape = self.db.object_shape(shape_id);
913 if shape.properties.is_empty() {
914 self.function_type()
915 } else {
916 TypeId::NEVER
917 }
918 } else if let Some(shape_id) = object_with_index_shape_id(self.db, source_type) {
919 let shape = self.db.object_shape(shape_id);
920 if shape.properties.is_empty()
921 && shape.string_index.is_none()
922 && shape.number_index.is_none()
923 {
924 self.function_type()
925 } else {
926 TypeId::NEVER
927 }
928 } else if index_access_parts(self.db, source_type).is_some() {
929 // For indexed access types like T[K], narrow to T[K] & Function
930 // This handles cases like: typeof obj[key] === 'function'
931 let function_type = self.function_type();
932 self.db.intersection2(source_type, function_type)
933 } else {
934 TypeId::NEVER
935 }
936 }
937
938 /// Check if a type is a function type.
939 /// Uses the visitor pattern from `solver::visitor`.
940 fn is_function_type(&self, type_id: TypeId) -> bool {
941 is_function_type_db(self.db, type_id)
942 }
943
944 /// Narrow a type to exclude function-like members (typeof !== "function").
945 pub fn narrow_excluding_function(&self, source_type: TypeId) -> TypeId {
946 if let Some(members) = union_list_id(self.db, source_type) {
947 let members = self.db.type_list(members);
948 let remaining: Vec<TypeId> = members
949 .iter()
950 .filter_map(|&member| {
951 if let Some(narrowed) = self.narrow_type_param_excluding_function(member) {
952 return narrowed.non_never();
953 }
954 if self.is_function_type(member) {
955 None
956 } else {
957 Some(member)
958 }
959 })
960 .collect();
961
962 return union_or_single(self.db, remaining);
963 }
964
965 if let Some(narrowed) = self.narrow_type_param_excluding_function(source_type) {
966 return narrowed;
967 }
968
969 if self.is_function_type(source_type) {
970 TypeId::NEVER
971 } else {
972 source_type
973 }
974 }
975
976 /// Check if a type has typeof "object".
977 /// Uses the visitor pattern from `solver::visitor`.
978 fn is_object_typeof(&self, type_id: TypeId) -> bool {
979 is_object_like_type_db(self.db, type_id)
980 }
981
982 fn narrow_type_param(&self, source: TypeId, target: TypeId) -> Option<TypeId> {
983 let info = type_param_info(self.db, source)?;
984
985 let constraint = info.constraint.unwrap_or(TypeId::UNKNOWN);
986 if constraint == source {
987 return None;
988 }
989
990 let narrowed_constraint = if constraint == TypeId::UNKNOWN {
991 target
992 } else {
993 self.narrow_to_type(constraint, target)
994 };
995
996 if narrowed_constraint == TypeId::NEVER {
997 return None;
998 }
999
1000 Some(self.db.intersection2(source, narrowed_constraint))
1001 }
1002
1003 fn narrow_type_param_to_function(&self, source: TypeId) -> Option<TypeId> {
1004 let info = type_param_info(self.db, source)?;
1005
1006 let constraint = info.constraint.unwrap_or(TypeId::UNKNOWN);
1007 if constraint == source || constraint == TypeId::UNKNOWN {
1008 let function_type = self.function_type();
1009 return Some(self.db.intersection2(source, function_type));
1010 }
1011
1012 let narrowed_constraint = self.narrow_to_function(constraint);
1013 if narrowed_constraint == TypeId::NEVER {
1014 return None;
1015 }
1016
1017 Some(self.db.intersection2(source, narrowed_constraint))
1018 }
1019
1020 fn narrow_type_param_excluding(&self, source: TypeId, excluded: TypeId) -> Option<TypeId> {
1021 let info = type_param_info(self.db, source)?;
1022
1023 let constraint = info.constraint?;
1024 if constraint == source || constraint == TypeId::UNKNOWN {
1025 return None;
1026 }
1027
1028 let narrowed_constraint = self.narrow_excluding_type(constraint, excluded);
1029 if narrowed_constraint == constraint {
1030 return None;
1031 }
1032 if narrowed_constraint == TypeId::NEVER {
1033 return Some(TypeId::NEVER);
1034 }
1035
1036 Some(self.db.intersection2(source, narrowed_constraint))
1037 }
1038
1039 fn narrow_type_param_excluding_function(&self, source: TypeId) -> Option<TypeId> {
1040 let info = type_param_info(self.db, source)?;
1041
1042 let constraint = info.constraint.unwrap_or(TypeId::UNKNOWN);
1043 if constraint == source || constraint == TypeId::UNKNOWN {
1044 return Some(source);
1045 }
1046
1047 let narrowed_constraint = self.narrow_excluding_function(constraint);
1048 if narrowed_constraint == constraint {
1049 return Some(source);
1050 }
1051 if narrowed_constraint == TypeId::NEVER {
1052 return Some(TypeId::NEVER);
1053 }
1054
1055 Some(self.db.intersection2(source, narrowed_constraint))
1056 }
1057
1058 pub(crate) fn function_type(&self) -> TypeId {
1059 let rest_array = self.db.array(TypeId::ANY);
1060 let rest_param = ParamInfo {
1061 name: None,
1062 type_id: rest_array,
1063 optional: false,
1064 rest: true,
1065 };
1066 self.db.function(FunctionShape {
1067 params: vec![rest_param],
1068 this_type: None,
1069 return_type: TypeId::ANY,
1070 type_params: Vec::new(),
1071 type_predicate: None,
1072 is_constructor: false,
1073 is_method: false,
1074 })
1075 }
1076
1077 /// Check if a type is a JS primitive that can never pass `instanceof`.
1078 /// Includes string, number, boolean, bigint, symbol, undefined, null,
1079 /// void, never, and their literal forms.
1080 fn is_js_primitive(&self, type_id: TypeId) -> bool {
1081 matches!(
1082 type_id,
1083 TypeId::STRING
1084 | TypeId::NUMBER
1085 | TypeId::BOOLEAN
1086 | TypeId::BIGINT
1087 | TypeId::SYMBOL
1088 | TypeId::UNDEFINED
1089 | TypeId::NULL
1090 | TypeId::VOID
1091 | TypeId::NEVER
1092 | TypeId::BOOLEAN_TRUE
1093 | TypeId::BOOLEAN_FALSE
1094 ) || matches!(self.db.lookup(type_id), Some(TypeData::Literal(_)))
1095 }
1096
1097 /// Simple assignability check for narrowing purposes.
1098 fn is_assignable_to(&self, source: TypeId, target: TypeId) -> bool {
1099 if source == target {
1100 return true;
1101 }
1102
1103 // never is assignable to everything
1104 if source == TypeId::NEVER {
1105 return true;
1106 }
1107
1108 // everything is assignable to any/unknown
1109 if target.is_any_or_unknown() {
1110 return true;
1111 }
1112
1113 // Literal to base type
1114 if let Some(lit) = literal_value(self.db, source) {
1115 match (lit, target) {
1116 (LiteralValue::String(_), t) if t == TypeId::STRING => return true,
1117 (LiteralValue::Number(_), t) if t == TypeId::NUMBER => return true,
1118 (LiteralValue::Boolean(_), t) if t == TypeId::BOOLEAN => return true,
1119 (LiteralValue::BigInt(_), t) if t == TypeId::BIGINT => return true,
1120 _ => {}
1121 }
1122 }
1123
1124 // object/null for typeof "object"
1125 if target == TypeId::OBJECT {
1126 if source == TypeId::NULL {
1127 return true;
1128 }
1129 if self.is_object_typeof(source) {
1130 return true;
1131 }
1132 return false;
1133 }
1134
1135 if let Some(members) = intersection_list_id(self.db, source) {
1136 let members = self.db.type_list(members);
1137 if members
1138 .iter()
1139 .any(|member| self.is_assignable_to(*member, target))
1140 {
1141 return true;
1142 }
1143 }
1144
1145 if target == TypeId::STRING && template_literal_id(self.db, source).is_some() {
1146 return true;
1147 }
1148
1149 // Check if source is assignable to any member of a union target
1150 if let Some(members) = union_list_id(self.db, target) {
1151 let members = self.db.type_list(members);
1152 if members
1153 .iter()
1154 .any(|&member| self.is_assignable_to(source, member))
1155 {
1156 return true;
1157 }
1158 }
1159
1160 // Fallback: use full structural/nominal subtype check.
1161 // This handles class inheritance (Derived extends Base), interface
1162 // implementations, and other structural relationships that the
1163 // fast-path checks above don't cover.
1164 // CRITICAL: Resolve Lazy(DefId) types before the subtype check.
1165 // Without resolution, two unrelated interfaces (e.g., Cat and Dog)
1166 // remain as opaque Lazy types and the SubtypeChecker can't distinguish them.
1167 let source = self.resolve_type(source);
1168 let target = self.resolve_type(target);
1169 if source == target {
1170 return true;
1171 }
1172 crate::relations::subtype::is_subtype_of_with_db(self.db, source, target)
1173 }
1174
1175 /// Applies a type guard to narrow a type.
1176 ///
1177 /// This is the main entry point for AST-agnostic type narrowing.
1178 /// The Checker extracts a `TypeGuard` from AST nodes, and the Solver
1179 /// applies it to compute the narrowed type.
1180 ///
1181 /// # Arguments
1182 /// * `source_type` - The type to narrow
1183 /// * `guard` - The guard condition (extracted from AST by Checker)
1184 /// * `sense` - If true, narrow for the "true" branch; if false, narrow for the "false" branch
1185 ///
1186 /// # Returns
1187 /// The narrowed type after applying the guard.
1188 ///
1189 /// # Examples
1190 /// ```ignore
1191 /// // typeof x === "string"
1192 /// let guard = TypeGuard::Typeof(TypeofKind::String);
1193 /// let narrowed = narrowing.narrow_type(string_or_number, &guard, true);
1194 /// assert_eq!(narrowed, TypeId::STRING);
1195 ///
1196 /// // x !== null (negated sense)
1197 /// let guard = TypeGuard::NullishEquality;
1198 /// let narrowed = narrowing.narrow_type(string_or_null, &guard, false);
1199 /// // Result should exclude null and undefined
1200 /// ```
1201 pub fn narrow_type(&self, source_type: TypeId, guard: &TypeGuard, sense: bool) -> TypeId {
1202 match guard {
1203 TypeGuard::Typeof(typeof_kind) => {
1204 let type_name = typeof_kind.as_str();
1205 if sense {
1206 self.narrow_by_typeof(source_type, type_name)
1207 } else {
1208 // Negation: exclude typeof type
1209 self.narrow_by_typeof_negation(source_type, type_name)
1210 }
1211 }
1212
1213 TypeGuard::Instanceof(instance_type) => {
1214 if sense {
1215 // Positive: x instanceof Class
1216 // Special case: `unknown` instanceof X narrows to X (or object if X unknown)
1217 // This must be handled here in the solver, not in the checker.
1218 if source_type == TypeId::UNKNOWN {
1219 return *instance_type;
1220 }
1221
1222 // CRITICAL: The payload is already the Instance Type (extracted by Checker)
1223 // Use narrow_by_instance_type for instanceof-specific semantics:
1224 // type parameters with matching constraints are kept, but anonymous
1225 // object types that happen to be structurally compatible are excluded.
1226 // Primitive types are filtered out since they can never pass instanceof.
1227 let narrowed = self.narrow_by_instance_type(source_type, *instance_type);
1228
1229 if narrowed != TypeId::NEVER || source_type == TypeId::NEVER {
1230 return narrowed;
1231 }
1232
1233 // Fallback 1: If standard narrowing returns NEVER but source wasn't NEVER,
1234 // it might be an interface vs class check (which is allowed in TS).
1235 // Use intersection in that case.
1236 let intersection = self.db.intersection2(source_type, *instance_type);
1237 if intersection != TypeId::NEVER {
1238 return intersection;
1239 }
1240
1241 // Fallback 2: If even intersection fails, narrow to object-like types.
1242 // On the true branch of instanceof, we know the value must be some
1243 // kind of object (primitives can never pass instanceof).
1244 self.narrow_to_objectish(source_type)
1245 } else {
1246 // Negative: !(x instanceof Class)
1247 // Keep primitives (they can never pass instanceof) and exclude
1248 // non-primitive types assignable to the instance type.
1249 if *instance_type == TypeId::OBJECT {
1250 source_type
1251 } else {
1252 self.narrow_by_instanceof_false(source_type, *instance_type)
1253 }
1254 }
1255 }
1256
1257 TypeGuard::LiteralEquality(literal_type) => {
1258 if sense {
1259 // Equality: narrow to the literal type
1260 self.narrow_to_type(source_type, *literal_type)
1261 } else {
1262 // Inequality: exclude the literal type
1263 self.narrow_excluding_type(source_type, *literal_type)
1264 }
1265 }
1266
1267 TypeGuard::NullishEquality => {
1268 if sense {
1269 // Equality with null: narrow to null | undefined
1270 self.db.union(vec![TypeId::NULL, TypeId::UNDEFINED])
1271 } else {
1272 // Inequality: exclude null and undefined
1273 let without_null = self.narrow_excluding_type(source_type, TypeId::NULL);
1274 self.narrow_excluding_type(without_null, TypeId::UNDEFINED)
1275 }
1276 }
1277
1278 TypeGuard::Truthy => {
1279 if sense {
1280 // Truthy: remove null and undefined (TypeScript doesn't narrow other falsy values)
1281 self.narrow_by_truthiness(source_type)
1282 } else {
1283 // Falsy: narrow to the falsy component(s)
1284 // This handles cases like: if (!x) where x: string → "" in false branch
1285 self.narrow_to_falsy(source_type)
1286 }
1287 }
1288
1289 TypeGuard::Discriminant {
1290 property_path,
1291 value_type,
1292 } => {
1293 // Use narrow_by_discriminant_for_type which handles type parameters
1294 // by narrowing the constraint and returning T & NarrowedConstraint
1295 self.narrow_by_discriminant_for_type(source_type, property_path, *value_type, sense)
1296 }
1297
1298 TypeGuard::InProperty(property_name) => {
1299 if sense {
1300 // Positive: "prop" in x - narrow to types that have the property
1301 self.narrow_by_property_presence(source_type, *property_name, true)
1302 } else {
1303 // Negative: !("prop" in x) - narrow to types that don't have the property
1304 self.narrow_by_property_presence(source_type, *property_name, false)
1305 }
1306 }
1307
1308 TypeGuard::Predicate { type_id, asserts } => {
1309 match type_id {
1310 Some(target_type) => {
1311 // Type guard with specific type: is T or asserts T
1312 if sense {
1313 // True branch: narrow source to the predicate type.
1314 // Following TSC's narrowType logic:
1315 // 1. For unions: filter members using narrow_to_type
1316 // 2. For non-unions:
1317 // a. source <: target → return source
1318 // b. target <: source → return target
1319 // c. otherwise → return source & target
1320 //
1321 // Following TSC's narrowType logic which uses
1322 // isTypeSubtypeOf (not isTypeAssignableTo) to decide
1323 // whether source is already specific enough.
1324 //
1325 // If source is a strict subtype of the target, return
1326 // source (it's already more specific). If target is a
1327 // strict subtype of source, return target (narrowing
1328 // down). Otherwise, return the intersection.
1329 //
1330 // narrow_to_type uses assignability internally, which is
1331 // too loose for type predicates (e.g. {} is assignable to
1332 // Record<string,unknown> but not a subtype).
1333 let resolved_source = self.resolve_type(source_type);
1334
1335 if resolved_source == self.resolve_type(*target_type) {
1336 source_type
1337 } else if resolved_source == TypeId::UNKNOWN
1338 || resolved_source == TypeId::ANY
1339 {
1340 *target_type
1341 } else if union_list_id(self.db, resolved_source).is_some() {
1342 // For unions: filter members, fall back to
1343 // intersection if nothing matches.
1344 let narrowed = self.narrow_to_type(source_type, *target_type);
1345 if narrowed == TypeId::NEVER && source_type != TypeId::NEVER {
1346 self.db.intersection2(source_type, *target_type)
1347 } else {
1348 narrowed
1349 }
1350 } else {
1351 // Non-union source: use narrow_to_type first.
1352 // If it returns source unchanged (assignable but
1353 // possibly losing structural info) or NEVER (no
1354 // overlap), fall back to intersection.
1355 let narrowed = self.narrow_to_type(source_type, *target_type);
1356 if narrowed == source_type && narrowed != *target_type {
1357 // Source was unchanged — intersect to preserve
1358 // target's structural info (index sigs, etc.)
1359 self.db.intersection2(source_type, *target_type)
1360 } else if narrowed == TypeId::NEVER && source_type != TypeId::NEVER
1361 {
1362 self.db.intersection2(source_type, *target_type)
1363 } else {
1364 narrowed
1365 }
1366 }
1367 } else if *asserts {
1368 // CRITICAL: For assertion functions, the false branch is unreachable
1369 // (the function throws if the assertion fails), so we don't narrow
1370 source_type
1371 } else {
1372 // False branch for regular type guards: exclude the target type
1373 self.narrow_excluding_type(source_type, *target_type)
1374 }
1375 }
1376 None => {
1377 // Truthiness assertion: asserts x
1378 // Behaves like TypeGuard::Truthy (narrows to truthy in true branch)
1379 if *asserts {
1380 self.narrow_by_truthiness(source_type)
1381 } else {
1382 source_type
1383 }
1384 }
1385 }
1386 }
1387
1388 TypeGuard::Array => {
1389 if sense {
1390 // Positive: Array.isArray(x) - narrow to array-like types
1391 self.narrow_to_array(source_type)
1392 } else {
1393 // Negative: !Array.isArray(x) - exclude array-like types
1394 self.narrow_excluding_array(source_type)
1395 }
1396 }
1397
1398 TypeGuard::ArrayElementPredicate { element_type } => {
1399 trace!(
1400 ?element_type,
1401 ?sense,
1402 "Applying ArrayElementPredicate guard"
1403 );
1404 if sense {
1405 // True branch: narrow array element type
1406 let result = self.narrow_array_element_type(source_type, *element_type);
1407 trace!(?result, "ArrayElementPredicate narrowing result");
1408 result
1409 } else {
1410 // False branch: we don't narrow (arr.every could be false for various reasons)
1411 trace!("ArrayElementPredicate false branch, no narrowing");
1412 source_type
1413 }
1414 }
1415 }
1416 }
1417}
1418
1419#[cfg(test)]
1420#[path = "../../tests/narrowing_tests.rs"]
1421mod tests;