Extract the subject expression before an arrow operator (`->`). `chars` is the line as a char slice. `arrow_pos` is the index of the `-` character (i.e. `chars[arrow_pos] == '-'` and `chars[arrow_pos + 1] == '>'`). Handles: - `$this->`, `$var->` (simple variable) - `$this->prop->` (property chain) - `$this?->prop->` (nullsafe property chain) - `app()->` (function call) - `$this->getService()->`
(chars: &[char], arrow_pos: usize)
| 289 | /// - `Status::Active->` (enum case access) |
| 290 | /// - `tryFrom($int)?->` (nullsafe after call) |
| 291 | fn extract_arrow_subject(chars: &[char], arrow_pos: usize) -> String { |
| 292 | // Position just before the `->` |
| 293 | let mut end = arrow_pos; |
| 294 | |
| 295 | // Skip whitespace |
| 296 | let mut i = end; |
| 297 | while i > 0 && chars[i - 1] == ' ' { |
| 298 | i -= 1; |
| 299 | } |
| 300 | |
| 301 | // Skip the `?` of the nullsafe `?->` operator so that the rest |
| 302 | // of the extraction logic sees the expression before the `?` |
| 303 | // (e.g. the `)` of a call expression like `tryFrom($int)?->`, |
| 304 | // or a simple variable like `$var?->`). |
| 305 | if i > 0 && chars[i - 1] == '?' { |
| 306 | i -= 1; |
| 307 | } |
| 308 | |
| 309 | // Update `end` so the fallback `extract_simple_variable` at the |
| 310 | // bottom of this function also starts from the correct position |
| 311 | // (past any `?` and whitespace). |
| 312 | end = i; |
| 313 | |
| 314 | // ── Array access: detect `]` ── |
| 315 | // e.g. `$admins[0]->`, `$admins[$key]->`, `$config['key']->` |
| 316 | // Also handles chained access: `$response['items'][0]->` |
| 317 | // |
| 318 | // Walk backward through one or more balanced `[…]` pairs, collecting |
| 319 | // each bracket segment. The segments are stored innermost-first and |
| 320 | // reversed at the end so the final subject reads left-to-right. |
| 321 | if i > 0 && chars[i - 1] == ']' { |
| 322 | let mut segments: Vec<String> = Vec::new(); |
| 323 | // Track the raw bracket ranges so we can reconstruct the |
| 324 | // array literal base when there is no `$var` prefix. |
| 325 | let mut bracket_ranges: Vec<(usize, usize)> = Vec::new(); |
| 326 | let mut pos = i; |
| 327 | |
| 328 | while pos > 0 |
| 329 | && chars[pos - 1] == ']' |
| 330 | && let Some(bracket_open) = skip_balanced_brackets_back(chars, pos) |
| 331 | { |
| 332 | let inner: String = chars[bracket_open + 1..pos - 1].iter().collect(); |
| 333 | let inner_trimmed = inner.trim(); |
| 334 | // Quoted string key → preserve it so the resolver can look |
| 335 | // up the specific key in an array shape type annotation. |
| 336 | if (inner_trimmed.starts_with('\'') && inner_trimmed.ends_with('\'')) |
| 337 | || (inner_trimmed.starts_with('"') && inner_trimmed.ends_with('"')) |
| 338 | { |
| 339 | segments.push(format!("[{}]", inner_trimmed)); |
| 340 | } else { |
| 341 | // Generic / numeric index → strip to `[]`. |
| 342 | segments.push("[]".to_string()); |
| 343 | } |
| 344 | bracket_ranges.push((bracket_open, pos)); |
| 345 | pos = bracket_open; |
| 346 | } |
| 347 | |
| 348 | if !segments.is_empty() { |