|
94 | 94 | //! `@implements ArrayAccess<TKey, TValue>`. |
95 | 95 | //! PHPStan ref: `stubs/WeakMap.stub` |
96 | 96 | //! |
| 97 | +//! 10. **`IteratorIterator`** — phpstorm-stubs lack `@template` and `@mixin`. |
| 98 | +//! PHPStan adds `@template TKey`, `@template TValue`, |
| 99 | +//! `@template TIterator of Traversable<TKey, TValue>`, |
| 100 | +//! `@implements OuterIterator<TKey, TValue>`, |
| 101 | +//! `@mixin TIterator`. The `@mixin` makes methods from the wrapped |
| 102 | +//! iterator available on the wrapper. |
| 103 | +//! PHPStan ref: `stubs/iterable.stub` |
| 104 | +//! |
97 | 105 | //! ## Removing patches |
98 | 106 | //! |
99 | 107 | //! When phpstorm-stubs gains proper annotations for a patched symbol, |
@@ -188,6 +196,7 @@ pub fn apply_class_stub_patches(class: &mut ClassInfo) { |
188 | 196 | "SplFixedArray" => patch_spl_fixed_array(class), |
189 | 197 | "SplObjectStorage" => patch_spl_object_storage(class), |
190 | 198 | "WeakMap" => patch_weak_map(class), |
| 199 | + "IteratorIterator" => patch_iterator_iterator(class), |
191 | 200 | _ => {} |
192 | 201 | } |
193 | 202 | } |
@@ -317,6 +326,62 @@ fn patch_weak_map(class: &mut ClassInfo) { |
317 | 326 | add_implements_generics(class, "ArrayAccess", &["TKey", "TValue"]); |
318 | 327 | } |
319 | 328 |
|
| 329 | +/// Add `@template TKey`, `@template TValue`, |
| 330 | +/// `@template TIterator of Traversable<TKey, TValue>`, |
| 331 | +/// `@implements OuterIterator<TKey, TValue>`, |
| 332 | +/// `@mixin TIterator`. |
| 333 | +/// |
| 334 | +/// PHPStan ref: `stubs/iterable.stub` |
| 335 | +fn patch_iterator_iterator(class: &mut ClassInfo) { |
| 336 | + if !class.template_params.is_empty() { |
| 337 | + return; |
| 338 | + } |
| 339 | + add_templates(class, &[("TKey", None), ("TValue", None)]); |
| 340 | + // TIterator has a complex bound `Traversable<TKey, TValue>` — add it |
| 341 | + // manually since `add_templates` only handles simple string bounds. |
| 342 | + let t_iter = atom("TIterator"); |
| 343 | + if !class.template_params.contains(&t_iter) { |
| 344 | + class.template_params.push(t_iter); |
| 345 | + } |
| 346 | + class |
| 347 | + .template_param_bounds |
| 348 | + .entry(atom("TIterator")) |
| 349 | + .or_insert_with(|| { |
| 350 | + PhpType::Generic( |
| 351 | + "Traversable".to_string(), |
| 352 | + vec![ |
| 353 | + PhpType::Named("TKey".to_string()), |
| 354 | + PhpType::Named("TValue".to_string()), |
| 355 | + ], |
| 356 | + ) |
| 357 | + }); |
| 358 | + add_implements_generics(class, "OuterIterator", &["TKey", "TValue"]); |
| 359 | + // Add @mixin TIterator so that methods from the wrapped iterator |
| 360 | + // are available on the wrapper. |
| 361 | + if !class.mixins.contains(&t_iter) { |
| 362 | + class.mixins.push(t_iter); |
| 363 | + } |
| 364 | + // Patch the constructor: add template binding TIterator → $iterator |
| 365 | + // so that `new IteratorIterator(new Subject())` infers TIterator = Subject. |
| 366 | + if let Some(ctor_idx) = class |
| 367 | + .methods |
| 368 | + .iter() |
| 369 | + .position(|m| m.name.as_str() == "__construct") |
| 370 | + { |
| 371 | + let mut ctor = (*class.methods[ctor_idx]).clone(); |
| 372 | + let binding = (atom("TIterator"), atom("$iterator")); |
| 373 | + if !ctor.template_bindings.iter().any(|(t, _)| t == &binding.0) { |
| 374 | + ctor.template_bindings.push(binding); |
| 375 | + } |
| 376 | + // Update the parameter type hint from Traversable to TIterator |
| 377 | + // so that classify_template_binding recognises a Direct binding. |
| 378 | + if let Some(param) = ctor.parameters.iter_mut().find(|p| p.name == "$iterator") { |
| 379 | + param.type_hint = Some(PhpType::Named("TIterator".to_string())); |
| 380 | + } |
| 381 | + class.methods.make_mut()[ctor_idx] = std::sync::Arc::new(ctor); |
| 382 | + } |
| 383 | +} |
| 384 | + |
320 | 385 | // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
321 | 386 | // Helpers |
322 | 387 | // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
@@ -651,4 +716,27 @@ mod tests { |
651 | 716 | assert_eq!(class.template_params, original_params); |
652 | 717 | assert!(class.implements_generics.is_empty()); |
653 | 718 | } |
| 719 | + |
| 720 | + #[test] |
| 721 | + fn iterator_iterator_gets_templates_and_mixin() { |
| 722 | + let mut class = empty_class("IteratorIterator"); |
| 723 | + apply_class_stub_patches(&mut class); |
| 724 | + |
| 725 | + assert_eq!( |
| 726 | + class.template_params, |
| 727 | + vec![atom("TKey"), atom("TValue"), atom("TIterator")] |
| 728 | + ); |
| 729 | + assert!( |
| 730 | + class |
| 731 | + .implements_generics |
| 732 | + .iter() |
| 733 | + .any(|(n, args)| n.as_str() == "OuterIterator" && args.len() == 2), |
| 734 | + "Should have @implements OuterIterator<TKey, TValue>" |
| 735 | + ); |
| 736 | + assert_eq!(class.mixins, vec![atom("TIterator")]); |
| 737 | + assert!( |
| 738 | + class.template_param_bounds.contains_key(&atom("TIterator")), |
| 739 | + "TIterator should have a bound" |
| 740 | + ); |
| 741 | + } |
654 | 742 | } |
0 commit comments