tsz_checker/checkers/iterable_checker.rs
1//! Iterable/iterator protocol checking and for-of element type computation.
2
3use crate::diagnostics::{diagnostic_codes, diagnostic_messages, format_message};
4use crate::query_boundaries::iterable_checker::{
5 AsyncIterableTypeKind, ForOfElementKind, FullIterableTypeKind, call_signatures_for_type,
6 classify_async_iterable_type, classify_for_of_element_type, classify_full_iterable_type,
7 function_shape_for_type, is_array_type, is_string_literal_type, is_string_type, is_tuple_type,
8 union_members_for_type,
9};
10use crate::state::CheckerState;
11use tsz_parser::parser::NodeIndex;
12use tsz_solver::TypeId;
13
14// =============================================================================
15// Iterable Type Checking Methods
16// =============================================================================
17
18impl<'a> CheckerState<'a> {
19 // =========================================================================
20 // Iterable Protocol Checking
21 // =========================================================================
22
23 /// Check if a type is iterable (has Symbol.iterator protocol).
24 ///
25 /// A type is iterable if it is:
26 /// - String type
27 /// - Array type
28 /// - Tuple type
29 /// - Has a [Symbol.iterator] method
30 /// - A union where all members are iterable
31 /// - An intersection where at least one member is iterable
32 pub fn is_iterable_type(&mut self, type_id: TypeId) -> bool {
33 // Intrinsic types that are always iterable or not iterable
34 if type_id == TypeId::ANY || type_id == TypeId::UNKNOWN || type_id == TypeId::ERROR {
35 return true; // Don't report errors on any/unknown/error
36 }
37 if type_id == TypeId::STRING {
38 return true;
39 }
40 if type_id == TypeId::NUMBER
41 || type_id == TypeId::BOOLEAN
42 || type_id == TypeId::VOID
43 || type_id == TypeId::NULL
44 || type_id == TypeId::UNDEFINED
45 || type_id == TypeId::NEVER
46 || type_id == TypeId::SYMBOL
47 || type_id == TypeId::BIGINT
48 {
49 return false;
50 }
51
52 self.is_iterable_type_classified(type_id)
53 }
54
55 /// Internal helper that uses the solver's classification enum to determine iterability.
56 fn is_iterable_type_classified(&mut self, type_id: TypeId) -> bool {
57 let kind = classify_full_iterable_type(self.ctx.types, type_id);
58 match kind {
59 FullIterableTypeKind::Array(_)
60 | FullIterableTypeKind::Tuple(_)
61 | FullIterableTypeKind::StringLiteral(_) => true,
62 FullIterableTypeKind::Union(members) => {
63 members.iter().all(|&m| self.is_iterable_type(m))
64 }
65 FullIterableTypeKind::Intersection(members) => {
66 // Intersection is iterable if at least one member is iterable
67 members.iter().any(|&m| self.is_iterable_type(m))
68 }
69 FullIterableTypeKind::Object(shape_id) => {
70 // Check if object has a [Symbol.iterator] method
71 // Fall back to property access resolution for computed properties
72 // (e.g., `[Symbol.iterator]: any` may not be stored as a method in the shape)
73 self.object_has_iterator_method(shape_id)
74 || self.type_has_symbol_iterator_via_property_access(type_id)
75 }
76 FullIterableTypeKind::Application { .. } => {
77 // Application types (Set<T>, Map<K,V>, Iterable<T>, etc.) may have
78 // Lazy(DefId) bases that can't be resolved through the type classification.
79 // Use the full property access resolution which handles all the complex
80 // resolution paths including Application types with Lazy bases from lib files.
81 self.type_has_symbol_iterator_via_property_access(type_id)
82 }
83 FullIterableTypeKind::TypeParameter { constraint } => {
84 if let Some(c) = constraint {
85 self.is_iterable_type(c)
86 } else {
87 // Unconstrained type parameters (extends unknown/any) should not error
88 // TypeScript does NOT emit TS2488 for unconstrained type parameters
89 false
90 }
91 }
92 FullIterableTypeKind::Readonly(inner) => {
93 // Unwrap readonly wrapper and check inner type
94 self.is_iterable_type(inner)
95 }
96 // Index access, Conditional, Mapped - not directly iterable
97 FullIterableTypeKind::ComplexType => false,
98 // Functions, classes without Symbol.iterator are not iterable
99 FullIterableTypeKind::FunctionOrCallable => {
100 // Callable types can have properties (including [Symbol.iterator])
101 self.type_has_symbol_iterator_via_property_access(type_id)
102 }
103 // Lazy(DefId) from lib files - use property access to resolve
104 FullIterableTypeKind::NotIterable => {
105 self.type_has_symbol_iterator_via_property_access(type_id)
106 }
107 }
108 }
109
110 /// Check if an object shape has a Symbol.iterator method.
111 ///
112 /// An object is iterable if it has a [Symbol.iterator]() method that returns an iterator.
113 /// An iterator (with just a `next()` method) is NOT automatically iterable.
114 fn object_has_iterator_method(&self, shape_id: tsz_solver::ObjectShapeId) -> bool {
115 let shape = self.ctx.types.object_shape(shape_id);
116
117 // Check for [Symbol.iterator] method (iterable protocol)
118 for prop in &shape.properties {
119 let prop_name = self.ctx.types.resolve_atom_ref(prop.name);
120 if prop_name.as_ref() == "[Symbol.iterator]" {
121 if prop.is_method {
122 return true;
123 }
124 // Non-method properties typed as `any` are callable, so treat them as valid.
125 // e.g., `class Foo { [Symbol.iterator]: any; }`
126 if prop.type_id == TypeId::ANY {
127 return true;
128 }
129 }
130 }
131
132 false
133 }
134
135 /// Check if a type has [Symbol.iterator] using the full property access resolution.
136 /// This handles Application types (Set<T>, Map<K,V>) with Lazy(DefId) bases from lib
137 /// files, Callable types with iterator properties, and other complex cases where simple
138 /// shape inspection fails but the full checker resolution machinery can find the property.
139 fn type_has_symbol_iterator_via_property_access(&mut self, type_id: TypeId) -> bool {
140 use tsz_solver::operations::property::PropertyAccessResult;
141 let result = self.resolve_property_access_with_env(type_id, "[Symbol.iterator]");
142 matches!(result, PropertyAccessResult::Success { .. })
143 }
144
145 /// Check if a type has a numeric index signature, making it "array-like".
146 /// TypeScript allows array destructuring of array-like types without [Symbol.iterator]().
147 pub(crate) fn has_numeric_index_signature(&mut self, type_id: TypeId) -> bool {
148 // Resolve lazy types first
149 let type_id = self.resolve_lazy_type(type_id);
150 match classify_full_iterable_type(self.ctx.types, type_id) {
151 FullIterableTypeKind::Object(shape_id) => {
152 let shape = self.ctx.types.object_shape(shape_id);
153 shape.number_index.is_some()
154 }
155 FullIterableTypeKind::Application { base } => self.has_numeric_index_signature(base),
156 FullIterableTypeKind::Readonly(inner) => self.has_numeric_index_signature(inner),
157 FullIterableTypeKind::Union(members) => members
158 .iter()
159 .all(|&m| self.is_iterable_type(m) || self.has_numeric_index_signature(m)),
160 _ => false,
161 }
162 }
163
164 /// Check if a type is async iterable (has Symbol.asyncIterator protocol).
165 pub fn is_async_iterable_type(&mut self, type_id: TypeId) -> bool {
166 // Intrinsic types that are always iterable or not iterable
167 if type_id == TypeId::ANY || type_id == TypeId::UNKNOWN || type_id == TypeId::ERROR {
168 return true; // Don't report errors on any/unknown/error
169 }
170
171 // Resolve lazy types before checking
172 let type_id = self.resolve_lazy_type(type_id);
173
174 self.is_async_iterable_type_classified(type_id)
175 }
176
177 /// Internal helper that uses the solver's classification enum to determine async iterability.
178 fn is_async_iterable_type_classified(&mut self, type_id: TypeId) -> bool {
179 match classify_async_iterable_type(self.ctx.types, type_id) {
180 AsyncIterableTypeKind::Union(members) => {
181 members.iter().all(|&m| self.is_async_iterable_type(m))
182 }
183 AsyncIterableTypeKind::Object(shape_id) => {
184 // Check if object has a [Symbol.asyncIterator] method
185 let shape = self.ctx.types.object_shape(shape_id);
186 for prop in &shape.properties {
187 let prop_name = self.ctx.types.resolve_atom_ref(prop.name);
188 if prop_name.as_ref() == "[Symbol.asyncIterator]"
189 && prop.is_method
190 && self.is_callable_with_no_required_args(prop.type_id)
191 {
192 return true;
193 }
194 }
195 false
196 }
197 AsyncIterableTypeKind::Readonly(inner) => {
198 // Unwrap readonly wrapper and check inner type
199 self.is_async_iterable_type(inner)
200 }
201 AsyncIterableTypeKind::NotAsyncIterable => {
202 // Use property access to check for [Symbol.asyncIterator] on types
203 // that couldn't be classified (e.g., Application types with Lazy bases).
204 use tsz_solver::operations::property::PropertyAccessResult;
205 let result =
206 self.resolve_property_access_with_env(type_id, "[Symbol.asyncIterator]");
207 match result {
208 PropertyAccessResult::Success { type_id, .. } => {
209 self.is_callable_with_no_required_args(type_id)
210 }
211 _ => false,
212 }
213 }
214 }
215 }
216
217 /// Returns true when a callable type can be invoked with zero arguments.
218 ///
219 /// The async iterable protocol requires `[Symbol.asyncIterator]()` to be callable
220 /// without arguments. A required parameter (e.g. `(x: number) => ...`) is invalid.
221 fn is_callable_with_no_required_args(&self, callable_type: TypeId) -> bool {
222 if callable_type == TypeId::ANY
223 || callable_type == TypeId::UNKNOWN
224 || callable_type == TypeId::ERROR
225 {
226 return true;
227 }
228
229 if let Some(sig) = function_shape_for_type(self.ctx.types, callable_type) {
230 return sig.params.iter().all(|p| p.optional || p.rest);
231 }
232
233 if let Some(call_signatures) = call_signatures_for_type(self.ctx.types, callable_type) {
234 return call_signatures
235 .iter()
236 .any(|sig| sig.params.iter().all(|p| p.optional || p.rest));
237 }
238
239 false
240 }
241
242 // =========================================================================
243 // For-Of Element Type Computation
244 // =========================================================================
245
246 /// Compute the element type produced by a `for (... of expr)` loop.
247 ///
248 /// Handles arrays, tuples, unions, strings, and custom iterators via
249 /// the `[Symbol.iterator]().next().value` protocol.
250 pub fn for_of_element_type(&mut self, iterable_type: TypeId) -> TypeId {
251 if iterable_type == TypeId::ANY
252 || iterable_type == TypeId::UNKNOWN
253 || iterable_type == TypeId::ERROR
254 {
255 return iterable_type;
256 }
257
258 // String iteration yields string
259 if iterable_type == TypeId::STRING {
260 return TypeId::STRING;
261 }
262
263 // Resolve lazy types (type aliases) before computing element type
264 let iterable_type = self.resolve_lazy_type(iterable_type);
265
266 self.for_of_element_type_classified(iterable_type, 0)
267 }
268
269 /// Internal helper that uses the solver's classification enum to compute element type.
270 /// The depth parameter prevents infinite loops from circular readonly types.
271 fn for_of_element_type_classified(&mut self, type_id: TypeId, depth: usize) -> TypeId {
272 let factory = self.ctx.types.factory();
273 if depth > 100 {
274 return TypeId::ANY;
275 }
276
277 // Handle string types (including string literals)
278 if type_id == TypeId::STRING {
279 return TypeId::STRING;
280 }
281
282 match classify_for_of_element_type(self.ctx.types, type_id) {
283 ForOfElementKind::Array(elem) => elem,
284 ForOfElementKind::Tuple(elements) => {
285 let member_types: Vec<TypeId> = elements.iter().map(|e| e.type_id).collect();
286 tsz_solver::utils::union_or_single(self.ctx.types, member_types)
287 }
288 ForOfElementKind::Union(members) => {
289 let mut element_types = Vec::with_capacity(members.len());
290 for member in members {
291 element_types.push(self.for_of_element_type_classified(member, depth + 1));
292 }
293 factory.union(element_types)
294 }
295 ForOfElementKind::Readonly(inner) => {
296 // Unwrap readonly wrapper and compute element type for inner
297 self.for_of_element_type_classified(inner, depth + 1)
298 }
299 ForOfElementKind::String => TypeId::STRING,
300 ForOfElementKind::Other => {
301 // For custom iterators, Application types (Map, Set), etc.,
302 // try to resolve the element type via the iterator protocol:
303 // type_id[Symbol.iterator]().next().value
304 self.resolve_iterator_element_type(type_id)
305 }
306 }
307 }
308
309 /// Resolve the element type of an iterable via the iterator protocol.
310 ///
311 /// Follows the chain: type[Symbol.iterator] → call result → .`next()` → .value
312 /// Returns ANY as fallback if the protocol cannot be resolved.
313 fn resolve_iterator_element_type(&mut self, type_id: TypeId) -> TypeId {
314 use tsz_solver::operations::property::PropertyAccessResult;
315
316 // Step 1: Get [Symbol.iterator] property
317 let iterator_fn = self.resolve_property_access_with_env(type_id, "[Symbol.iterator]");
318 let iterator_fn_type = match &iterator_fn {
319 PropertyAccessResult::Success { type_id, .. } => *type_id,
320 _ => return TypeId::ANY,
321 };
322
323 // Step 2: Get the return type of the iterator function (call it)
324 let iterator_type = self.get_call_return_type(iterator_fn_type);
325
326 // If the iterator function returns `any` (e.g., `[Symbol.iterator]() { return this; }`
327 // where `this` type inference fails), fall back to using the original object type.
328 // This is the common pattern where the object IS the iterator.
329 let iterator_type = if iterator_type == TypeId::ANY {
330 type_id
331 } else {
332 iterator_type
333 };
334
335 // Step 3: Get .next() on the iterator
336 let next_result = self.resolve_property_access_with_env(iterator_type, "next");
337 let next_fn_type = match &next_result {
338 PropertyAccessResult::Success { type_id, .. } => *type_id,
339 _ => return TypeId::ANY,
340 };
341
342 // Step 4: Get the return type of next()
343 let next_return = self.get_call_return_type(next_fn_type);
344
345 // Step 5: Get .value from the IteratorResult
346 let value_result = self.resolve_property_access_with_env(next_return, "value");
347 match &value_result {
348 PropertyAccessResult::Success { type_id, .. } => *type_id,
349 _ => TypeId::ANY,
350 }
351 }
352
353 /// Get the return type of calling a function type.
354 /// Returns ANY if the type is not callable.
355 fn get_call_return_type(&self, fn_type: TypeId) -> TypeId {
356 if fn_type == TypeId::ANY {
357 return TypeId::ANY;
358 }
359 if let Some(sig) = function_shape_for_type(self.ctx.types, fn_type) {
360 return sig.return_type;
361 }
362 if let Some(call_signatures) = call_signatures_for_type(self.ctx.types, fn_type) {
363 return call_signatures
364 .first()
365 .map_or(TypeId::ANY, |sig| sig.return_type);
366 }
367 TypeId::ANY
368 }
369
370 // =========================================================================
371 // For-Of Iterability Checking with Error Reporting
372 // =========================================================================
373
374 /// Check iterability of a for-of expression and emit TS2488/TS2495/TS2504 if not iterable.
375 ///
376 /// Returns `true` if the type is iterable (or async iterable for for-await-of).
377 pub fn check_for_of_iterability(
378 &mut self,
379 expr_type: TypeId,
380 expr_idx: NodeIndex,
381 is_async: bool,
382 ) -> bool {
383 // Skip error/any/unknown types to prevent false positives
384 if expr_type == TypeId::ANY || expr_type == TypeId::UNKNOWN || expr_type == TypeId::ERROR {
385 return true;
386 }
387
388 // Resolve lazy types (type aliases) before checking iterability
389 let expr_type = self.resolve_lazy_type(expr_type);
390
391 // Check if the expression is nullish (undefined/null)
392 // Emit TS18050 "The value 'undefined'/'null' cannot be used here"
393 // when trying to iterate over undefined/null
394 if expr_type == TypeId::NULL || expr_type == TypeId::UNDEFINED {
395 self.report_nullish_object(expr_idx, expr_type, true);
396 return false;
397 }
398
399 // For async for-of, first check async iterable, then fall back to sync iterable
400 if is_async {
401 if self.is_async_iterable_type(expr_type) || self.is_iterable_type(expr_type) {
402 return true;
403 }
404 // Not async iterable - emit TS2504
405 if let Some((start, end)) = self.get_node_span(expr_idx) {
406 let type_str = self.format_type(expr_type);
407 let message = format_message(
408 diagnostic_messages::TYPE_MUST_HAVE_A_SYMBOL_ASYNCITERATOR_METHOD_THAT_RETURNS_AN_ASYNC_ITERATOR,
409 &[&type_str],
410 );
411 self.error(
412 start,
413 end.saturating_sub(start),
414 message,
415 diagnostic_codes::TYPE_MUST_HAVE_A_SYMBOL_ASYNCITERATOR_METHOD_THAT_RETURNS_AN_ASYNC_ITERATOR,
416 );
417 }
418 return false;
419 }
420
421 // In ES5 mode (without downlevelIteration), for-of only works with arrays and strings.
422 // - Emit TS2802 if the type has Symbol.iterator (iterable but requires ES2015/downlevelIteration).
423 // - Emit TS2461 if the type contains a string constituent but the remaining non-string
424 // type is not array-like (TSC strips strings from union before checking array-likeness).
425 // - Emit TS2495 if the type is neither an array nor a string (not iterable at all).
426 if self.ctx.compiler_options.target.is_es5() {
427 if self.is_array_or_tuple_or_string(expr_type) {
428 return true;
429 }
430 // Mirror TSC's logic: strip string-like members from union types.
431 // If there were string members, the "remaining" non-string type still needs to be
432 // array-like, and the error message changes from TS2495 → TS2461 (no "or string type"
433 // suffix because the string part is already accounted for).
434 let has_string_constituent = self.has_string_constituent(expr_type);
435 let allows_strings = !has_string_constituent;
436 if let Some((start, end)) = self.get_node_span(expr_idx) {
437 let type_str = self.format_type(expr_type);
438 // Check if the type has Symbol.iterator (iterable but not usable in ES5 for-of
439 // without downlevelIteration). These emit TS2802 instead of TS2495/TS2461.
440 if self.is_iterable_type(expr_type) {
441 let message = format_message(
442 diagnostic_messages::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
443 &[&type_str],
444 );
445 self.error(
446 start,
447 end.saturating_sub(start),
448 message,
449 diagnostic_codes::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
450 );
451 } else if allows_strings {
452 // No string in union: "Type is not an array type or a string type" (TS2495)
453 let message = format_message(
454 diagnostic_messages::TYPE_IS_NOT_AN_ARRAY_TYPE_OR_A_STRING_TYPE,
455 &[&type_str],
456 );
457 self.error(
458 start,
459 end.saturating_sub(start),
460 message,
461 diagnostic_codes::TYPE_IS_NOT_AN_ARRAY_TYPE_OR_A_STRING_TYPE,
462 );
463 } else {
464 // Has string constituent but non-string part is not array-like: TS2461
465 let message = format_message(
466 diagnostic_messages::TYPE_IS_NOT_AN_ARRAY_TYPE,
467 &[&type_str],
468 );
469 self.error(
470 start,
471 end.saturating_sub(start),
472 message,
473 diagnostic_codes::TYPE_IS_NOT_AN_ARRAY_TYPE,
474 );
475 }
476 }
477 return false;
478 }
479
480 // Regular for-of (ES2015+) - check sync iterability
481 if self.is_iterable_type(expr_type) {
482 return true;
483 }
484
485 // Not iterable - emit TS2488
486
487 if let Some((start, end)) = self.get_node_span(expr_idx) {
488 let type_str = self.format_type(expr_type);
489 let message = format_message(
490 diagnostic_messages::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
491 &[&type_str],
492 );
493 self.error(
494 start,
495 end.saturating_sub(start),
496 message,
497 diagnostic_codes::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
498 );
499 }
500 false
501 }
502
503 /// Check iterability of a spread argument and emit TS2488 if not iterable.
504 ///
505 /// Used for spread in array literals and function call arguments.
506 /// Returns `true` if the type is iterable.
507 pub fn check_spread_iterability(&mut self, spread_type: TypeId, expr_idx: NodeIndex) -> bool {
508 // In ES5 without downlevel iteration, spread requires an array/tuple source.
509 // Match tsc by emitting TS2461 for non-array spread arguments.
510 if self.ctx.compiler_options.target.is_es5() {
511 if spread_type == TypeId::ANY || spread_type == TypeId::UNKNOWN {
512 return true;
513 }
514
515 let resolved = self.resolve_lazy_type(spread_type);
516 if self.is_array_or_tuple_type(resolved) || self.has_numeric_index_signature(resolved) {
517 return true;
518 }
519
520 if let Some((start, end)) = self.get_node_span(expr_idx) {
521 let type_str = self.format_type(resolved);
522 if self.is_iterable_type(resolved) {
523 let message = format_message(
524 diagnostic_messages::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
525 &[&type_str],
526 );
527 self.error(
528 start,
529 end.saturating_sub(start),
530 message,
531 diagnostic_codes::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
532 );
533 } else {
534 let message = format_message(
535 diagnostic_messages::TYPE_IS_NOT_AN_ARRAY_TYPE,
536 &[&type_str],
537 );
538 self.error(
539 start,
540 end.saturating_sub(start),
541 message,
542 diagnostic_codes::TYPE_IS_NOT_AN_ARRAY_TYPE,
543 );
544 }
545 }
546 return false;
547 }
548
549 // Skip error types and any/unknown
550 if spread_type == TypeId::ANY
551 || spread_type == TypeId::UNKNOWN
552 || spread_type == TypeId::ERROR
553 {
554 return true;
555 }
556
557 // Resolve lazy types (type aliases) before checking iterability
558 let spread_type = self.resolve_lazy_type(spread_type);
559
560 if self.is_iterable_type(spread_type) {
561 return true;
562 }
563
564 // Not iterable - emit TS2488
565
566 if let Some((start, end)) = self.get_node_span(expr_idx) {
567 let type_str = self.format_type(spread_type);
568 let message = format_message(
569 diagnostic_messages::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
570 &[&type_str],
571 );
572 self.error(
573 start,
574 end.saturating_sub(start),
575 message,
576 diagnostic_codes::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
577 );
578 }
579 false
580 }
581
582 /// Check iterability for array destructuring patterns and emit TS2488 if not iterable.
583 ///
584 /// This function is called before assigning types to binding elements in array
585 /// destructuring to ensure that the source type is iterable.
586 ///
587 /// ## Parameters:
588 /// - `pattern_idx`: The array binding pattern node index
589 /// - `pattern_type`: The type being destructured
590 /// - `init_expr`: The initializer expression (used for error location)
591 ///
592 /// ## Validation:
593 /// - Checks if `pattern_type` is iterable
594 /// - Emits TS2488 if the type is not iterable
595 /// - Skips check for ANY, UNKNOWN, ERROR types (defer to other checks)
596 pub fn check_destructuring_iterability(
597 &mut self,
598 pattern_idx: NodeIndex,
599 pattern_type: TypeId,
600 init_expr: NodeIndex,
601 ) -> bool {
602 // Skip check for types that defer to other validation
603 if pattern_type == TypeId::ANY
604 || pattern_type == TypeId::UNKNOWN
605 || pattern_type == TypeId::ERROR
606 {
607 return true;
608 }
609
610 // TypeScript allows empty array destructuring patterns on any type (including null/undefined)
611 // Example: let [] = null; // No error
612 // Skip iterability check if the pattern is empty.
613 //
614 // Track whether this is an assignment target (`[a] = value`) vs a binding pattern
615 // (`let [a] = value`) so ES5-specific TS2461 can stay scoped to declarations.
616 let mut is_assignment_array_target = false;
617 if let Some(pattern_node) = self.ctx.arena.get(pattern_idx) {
618 is_assignment_array_target =
619 pattern_node.kind == tsz_parser::parser::syntax_kind_ext::ARRAY_LITERAL_EXPRESSION;
620 if let Some(binding_pattern) = self.ctx.arena.get_binding_pattern(pattern_node)
621 && binding_pattern.elements.nodes.is_empty()
622 {
623 return true;
624 }
625 }
626
627 // Resolve lazy types (type aliases) before checking iterability
628 let resolved_type = self.resolve_lazy_type(pattern_type);
629
630 // In array destructuring, TypeScript still reports TS2488 for `never`.
631 if resolved_type == TypeId::NEVER {
632 let error_idx = pattern_idx;
633 if let Some((start, end)) = self.get_node_span(error_idx) {
634 let type_str = self.format_type(pattern_type);
635 let message = format_message(
636 diagnostic_messages::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
637 &[&type_str],
638 );
639 self.error(
640 start,
641 end.saturating_sub(start),
642 message,
643 diagnostic_codes::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
644 );
645 }
646 return false;
647 }
648
649 // In ES5 mode (without downlevelIteration), array destructuring requires actual arrays.
650 // - Emit TS2802 if the type has Symbol.iterator (iterable but requires ES2015/downlevelIteration).
651 // - Emit TS2461 if the type is not an array type.
652 if self.ctx.compiler_options.target.is_es5() && !is_assignment_array_target {
653 // Nested binding patterns can be fed an over-widened union from positional
654 // destructuring inference (e.g. `[a, [b]] = [1, ["x"]]`). tsc does not report
655 // TS2461 for these cases.
656 if init_expr.is_none()
657 && tsz_solver::type_queries::get_union_members(self.ctx.types, resolved_type)
658 .is_some_and(|members| {
659 members
660 .iter()
661 .any(|&member| self.is_array_or_tuple_type(member))
662 })
663 {
664 return true;
665 }
666 if self.is_array_or_tuple_type(resolved_type) {
667 return true;
668 }
669 // For destructuring diagnostics, anchor to the binding pattern.
670 let error_idx = pattern_idx;
671 if let Some((start, end)) = self.get_node_span(error_idx) {
672 let type_str = self.format_type(pattern_type);
673 // Check if the type has Symbol.iterator (iterable but not usable in ES5
674 // without downlevelIteration). These emit TS2802 instead of TS2461.
675 if self.is_iterable_type(resolved_type) {
676 let message = format_message(
677 diagnostic_messages::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
678 &[&type_str],
679 );
680 self.error(
681 start,
682 end.saturating_sub(start),
683 message,
684 diagnostic_codes::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
685 );
686 } else {
687 let message = format_message(
688 diagnostic_messages::TYPE_IS_NOT_AN_ARRAY_TYPE,
689 &[&type_str],
690 );
691 self.error(
692 start,
693 end.saturating_sub(start),
694 message,
695 diagnostic_codes::TYPE_IS_NOT_AN_ARRAY_TYPE,
696 );
697 }
698 }
699 return false;
700 }
701
702 // Check if the type is iterable (ES2015+)
703 if self.is_iterable_type(resolved_type) {
704 return true;
705 }
706
707 // TypeScript also allows array destructuring for "array-like" types
708 // (types with numeric index signatures) even without [Symbol.iterator]()
709 if self.has_numeric_index_signature(resolved_type) {
710 return true;
711 }
712
713 // Not iterable - emit TS2488
714
715 // For destructuring diagnostics, anchor to the binding pattern.
716 let error_idx = pattern_idx;
717
718 if let Some((start, end)) = self.get_node_span(error_idx) {
719 let type_str = self.format_type(pattern_type);
720 let message = format_message(
721 diagnostic_messages::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
722 &[&type_str],
723 );
724 self.error(
725 start,
726 end.saturating_sub(start),
727 message,
728 diagnostic_codes::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
729 );
730 }
731 false
732 }
733
734 // =========================================================================
735 // ES5 Type Classification Helpers
736 // =========================================================================
737
738 /// Check if a type is an array or tuple type (for ES5 destructuring).
739 fn is_array_or_tuple_type(&self, type_id: TypeId) -> bool {
740 if is_array_type(self.ctx.types, type_id) || is_tuple_type(self.ctx.types, type_id) {
741 return true;
742 }
743 // Check unions: all members must be array/tuple
744 if let Some(members) = union_members_for_type(self.ctx.types, type_id) {
745 return members
746 .iter()
747 .all(|&member| self.is_array_or_tuple_type(member));
748 }
749 false
750 }
751
752 /// Check if a type contains a string-like constituent (for ES5 for-of error discrimination).
753 ///
754 /// This mirrors TSC's `hasStringConstituent` check: when a union type contains a string
755 /// member alongside non-array types, the error changes from TS2495 to TS2461.
756 fn has_string_constituent(&self, type_id: TypeId) -> bool {
757 if type_id == TypeId::STRING || is_string_type(self.ctx.types, type_id) {
758 return true;
759 }
760 if is_string_literal_type(self.ctx.types, type_id) {
761 return true;
762 }
763 if let Some(members) = union_members_for_type(self.ctx.types, type_id) {
764 return members.iter().any(|&m| self.has_string_constituent(m));
765 }
766 false
767 }
768
769 /// Check if a type is an array, tuple, or string type (for ES5 for-of).
770 fn is_array_or_tuple_or_string(&self, type_id: TypeId) -> bool {
771 if type_id == TypeId::STRING || is_string_type(self.ctx.types, type_id) {
772 return true;
773 }
774 if is_array_type(self.ctx.types, type_id) || is_tuple_type(self.ctx.types, type_id) {
775 return true;
776 }
777 // String literals count as string types
778 if is_string_literal_type(self.ctx.types, type_id) {
779 return true;
780 }
781 // Check unions: all members must be array/tuple/string
782 if let Some(members) = union_members_for_type(self.ctx.types, type_id) {
783 return members
784 .iter()
785 .all(|&member| self.is_array_or_tuple_or_string(member));
786 }
787 false
788 }
789}