|
18 | 18 | /// - [`super::conditional_resolution`]: PHPStan conditional return type |
19 | 19 | /// resolution at call sites. |
20 | 20 | use crate::Backend; |
| 21 | +use crate::docblock; |
21 | 22 | use crate::types::*; |
22 | 23 |
|
23 | 24 | use super::conditional_resolution::{ |
@@ -191,6 +192,26 @@ impl Backend { |
191 | 192 | return vec![]; |
192 | 193 | } |
193 | 194 |
|
| 195 | + // ── Array-element access: `$var[]` ── |
| 196 | + // When the subject ends with `[]`, the user wrote `$var[0]->` or |
| 197 | + // `$var[$key]->`. Resolve the base variable's generic/iterable |
| 198 | + // type and extract the element type. |
| 199 | + if let Some(base_var) = subject.strip_suffix("[]") |
| 200 | + && base_var.starts_with('$') |
| 201 | + { |
| 202 | + let resolved = Self::resolve_array_element_type( |
| 203 | + base_var, |
| 204 | + content, |
| 205 | + cursor_offset, |
| 206 | + current_class, |
| 207 | + all_classes, |
| 208 | + class_loader, |
| 209 | + ); |
| 210 | + if !resolved.is_empty() { |
| 211 | + return resolved; |
| 212 | + } |
| 213 | + } |
| 214 | + |
194 | 215 | // ── Variable like `$var` — resolve via assignments / parameter hints ── |
195 | 216 | if subject.starts_with('$') { |
196 | 217 | // When the cursor is inside a class, use the enclosing class |
@@ -243,6 +264,43 @@ impl Backend { |
243 | 264 | /// |
244 | 265 | /// Returns all candidate classes when the return type is a union |
245 | 266 | /// (e.g. `A|B`). |
| 267 | + /// Resolve the element type of an array/list variable accessed with `[]`. |
| 268 | + /// |
| 269 | + /// Given a base variable name like `$admins`, searches backward from |
| 270 | + /// `cursor_offset` for a `@var` / `@param` docblock annotation that |
| 271 | + /// declares a generic iterable type (e.g. `array<int, AdminUser>`, |
| 272 | + /// `list<User>`, `User[]`). Extracts the element type and resolves |
| 273 | + /// it to `ClassInfo`. |
| 274 | + fn resolve_array_element_type( |
| 275 | + base_var: &str, |
| 276 | + content: &str, |
| 277 | + cursor_offset: u32, |
| 278 | + current_class: Option<&ClassInfo>, |
| 279 | + all_classes: &[ClassInfo], |
| 280 | + class_loader: &dyn Fn(&str) -> Option<ClassInfo>, |
| 281 | + ) -> Vec<ClassInfo> { |
| 282 | + let current_class_name = current_class.map(|c| c.name.as_str()).unwrap_or(""); |
| 283 | + |
| 284 | + // Search backward from the cursor for a @var/@param annotation on |
| 285 | + // this variable that includes a generic type. |
| 286 | + let raw_type = match docblock::find_iterable_raw_type_in_source( |
| 287 | + content, |
| 288 | + cursor_offset as usize, |
| 289 | + base_var, |
| 290 | + ) { |
| 291 | + Some(t) => t, |
| 292 | + None => return vec![], |
| 293 | + }; |
| 294 | + |
| 295 | + // Extract the generic element type (e.g. `list<User>` → `User`). |
| 296 | + let element_type = match docblock::types::extract_generic_value_type(&raw_type) { |
| 297 | + Some(t) => t, |
| 298 | + None => return vec![], |
| 299 | + }; |
| 300 | + |
| 301 | + Self::type_hint_to_classes(&element_type, current_class_name, all_classes, class_loader) |
| 302 | + } |
| 303 | + |
246 | 304 | pub(super) fn resolve_call_return_types( |
247 | 305 | call_body: &str, |
248 | 306 | text_args: &str, |
|
0 commit comments