Extract the call expression that precedes the opening paren at `open`. Handles: - `foo(` → `"foo"` - `$this->method(` → `"$this->method"` - `$var->method(` → `"$var->method"` - `ClassName::method(` → `"ClassName::method"` - `self::method(` / `static::method(` / `parent::method(` → as-is - `new ClassName(` → `"new ClassName"` - `(new Foo())->method(` → `"$this->method"` etc. — simplified
(chars: &[char], open: usize)
| 234 | /// - `new ClassName(` → `"new ClassName"` |
| 235 | /// - `(new Foo())->method(` → `"$this->method"` etc. — simplified |
| 236 | pub fn extract_call_expression(chars: &[char], open: usize) -> Option<String> { |
| 237 | if open == 0 { |
| 238 | return None; |
| 239 | } |
| 240 | |
| 241 | let mut i = open; |
| 242 | |
| 243 | // Skip whitespace before `(` |
| 244 | while i > 0 && chars[i - 1] == ' ' { |
| 245 | i -= 1; |
| 246 | } |
| 247 | |
| 248 | if i == 0 { |
| 249 | return None; |
| 250 | } |
| 251 | |
| 252 | // ── If preceded by `)`, this is a chained call like `foo()->bar(`. |
| 253 | // We won't try to resolve through call chains for named args — the |
| 254 | // complexity is high and the user can rely on member completion. |
| 255 | // But we DO need to handle `(new Foo)(` — skip for now. |
| 256 | if chars[i - 1] == ')' { |
| 257 | return None; |
| 258 | } |
| 259 | |
| 260 | // ── Read the identifier (function/method name) ────────────────── |
| 261 | let ident_end = i; |
| 262 | while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_' || chars[i - 1] == '\\') { |
| 263 | i -= 1; |
| 264 | } |
| 265 | if i == ident_end { |
| 266 | return None; |
| 267 | } |
| 268 | let ident: String = chars[i..ident_end].iter().collect(); |
| 269 | |
| 270 | // ── Check what precedes the identifier ────────────────────────── |
| 271 | |
| 272 | // Instance method: `->method(` |
| 273 | if i >= 2 && chars[i - 2] == '-' && chars[i - 1] == '>' { |
| 274 | let subject = extract_subject_before_arrow(chars, i - 2); |
| 275 | if !subject.is_empty() { |
| 276 | return Some(format!("{}->{}", subject, ident)); |
| 277 | } |
| 278 | return None; |
| 279 | } |
| 280 | |
| 281 | // Null-safe method: `?->method(` |
| 282 | if i >= 3 && chars[i - 3] == '?' && chars[i - 2] == '-' && chars[i - 1] == '>' { |
| 283 | let subject = extract_subject_before_arrow(chars, i - 3); |
| 284 | if !subject.is_empty() { |
| 285 | return Some(format!("{}->{}", subject, ident)); |
| 286 | } |
| 287 | return None; |
| 288 | } |
| 289 | |
| 290 | // Static method: `::method(` |
| 291 | if i >= 2 && chars[i - 2] == ':' && chars[i - 1] == ':' { |
| 292 | let class_name = extract_class_name_backward(chars, i - 2); |
| 293 | if !class_name.is_empty() { |
no test coverage detected