Try to find a standalone function by name, checking user-defined functions first, then falling back to embedded PHP stubs. The lookup order is: 1. `global_functions` — functions from Composer autoload files and opened/changed files. 2. `stub_function_index` — built-in PHP functions embedded from phpstorm-stubs. Parsed lazily on first access and cached in `global_functions` under a `phpantom-stub
(&self, candidates: &[&str])
| 534 | /// FQN via use-map, the namespace-qualified name). The first match |
| 535 | /// wins. |
| 536 | pub fn find_or_load_function(&self, candidates: &[&str]) -> Option<FunctionInfo> { |
| 537 | // ── Phase 1: Check global_functions (user code + already-cached stubs) ── |
| 538 | { |
| 539 | let fmap = self.global_functions.read(); |
| 540 | for &name in candidates { |
| 541 | if let Some((_, info)) = fmap.get(name) { |
| 542 | return Some(info.clone()); |
| 543 | } |
| 544 | } |
| 545 | } |
| 546 | |
| 547 | // ── Phase 1.5: Check autoload_function_index (byte-level scan) ── |
| 548 | // The lightweight `find_symbols` byte-level scan discovers |
| 549 | // function names at startup without a full AST parse, for both |
| 550 | // non-Composer projects (workspace scan) and Composer projects |
| 551 | // (autoload_files.php scan). When a candidate matches here, we |
| 552 | // lazily call `update_ast` on the file to get a complete |
| 553 | // `FunctionInfo` and cache it in global_functions so subsequent |
| 554 | // lookups hit Phase 1. |
| 555 | // |
| 556 | // Note: the lazy parse is a full AST parse (`update_ast`), which |
| 557 | // is the same cost as opening the file. This is acceptable |
| 558 | // because it only happens once per function, on first access. |
| 559 | { |
| 560 | let idx = self.autoload_function_index.read(); |
| 561 | for &name in candidates { |
| 562 | if let Some(path) = idx.get(name) { |
| 563 | let path = path.clone(); |
| 564 | drop(idx); // release read lock before parsing |
| 565 | |
| 566 | if let Ok(content) = std::fs::read_to_string(&path) { |
| 567 | let uri = crate::util::path_to_uri(&path); |
| 568 | self.update_ast(&uri, &content); |
| 569 | |
| 570 | // Re-check global_functions after parsing. |
| 571 | let fmap = self.global_functions.read(); |
| 572 | for &retry_name in candidates { |
| 573 | if let Some((_, info)) = fmap.get(retry_name) { |
| 574 | return Some(info.clone()); |
| 575 | } |
| 576 | } |
| 577 | } |
| 578 | break; // Only try one file per lookup |
| 579 | } |
| 580 | } |
| 581 | } |
| 582 | |
| 583 | // ── Phase 1.75: Last-resort lazy parse of known autoload files ── |
| 584 | // The byte-level scanner misses functions wrapped in |
| 585 | // `if (! function_exists(...))` guards (brace depth > 0). |
| 586 | // These are common in Laravel helpers and similar packages. |
| 587 | // As a safety net, lazily parse each known autoload file via |
| 588 | // `update_ast` until the function is found. Each file is |
| 589 | // parsed at most once: subsequent lookups hit Phase 1 |
| 590 | // (`global_functions`). |
| 591 | { |
| 592 | let paths = self.autoload_file_paths.read().clone(); |
| 593 | for path in &paths { |