From 34f26b7887fef7ae0fa2a79cde503327da1214ad Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 27 Jun 2026 17:01:59 +0200 Subject: [PATCH] Index framework global helpers loaded outside Composer autoload Some frameworks ship their global function aliases in a `*_global.php` file that sits beside an autoloaded `functions.php` but is pulled in by the application bootstrap rather than Composer's `files` autoload, so it never appears in `autoload_files.php`. CakePHP is the canonical case: `src/Core/functions_global.php` defines `__`, `h`, `env`, `pr`, ... and is loaded via `require CAKE . 'functions.php'` in `config/bootstrap.php`. The autoload-file scan only followed `autoload_files.php` and their require_once chains, so these globals were invisible and every `__()` call reported "unknown function". On a real CakePHP 5 app this was about 1000 of 1330 analyze findings (968 of them just `__`). Seed the scan with any `*_global.php` sibling of an autoload entry. The lookup is anchored to existing autoload entries (one read_dir per unique directory) instead of a blind walk of the vendor tree, and the existing full-parse pass picks up the function_exists-guarded definitions. --- src/composer.rs | 103 +++++++++++++++++++++++++++++++++++++++++++++++- src/server.rs | 9 +++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/composer.rs b/src/composer.rs index 2155fa1c..25e41984 100644 --- a/src/composer.rs +++ b/src/composer.rs @@ -21,7 +21,7 @@ //! Composer JSON parsing is delegated to the [`mago_composer`] crate, //! which provides typed Rust structs for the full `composer.json` schema. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; @@ -342,6 +342,54 @@ pub fn parse_autoload_files(workspace_root: &Path, vendor_dir: &str) -> Vec Vec { + let mut out = Vec::new(); + let mut scanned_dirs = HashSet::new(); + let mut seen_files = HashSet::new(); + + for file in autoload_files { + let Some(dir) = file.parent() else { + continue; + }; + if !scanned_dirs.insert(dir.to_path_buf()) { + continue; + } + let Ok(entries) = fs::read_dir(dir) else { + continue; + }; + for entry in entries.flatten() { + let path = entry.path(); + let Some(name) = path.file_name().and_then(|n| n.to_str()) else { + continue; + }; + if name.ends_with("_global.php") && path.is_file() && seen_files.insert(path.clone()) { + out.push(path); + } + } + } + + out +} + // ── PSR-4 path abstraction ───────────────────────────────────────── // // `mago-composer` emits two structurally identical but nominally @@ -972,6 +1020,59 @@ mod tests { // ── detect_drupal_web_root ────────────────────────────────────── + #[test] + fn discovers_global_sibling_beside_autoload_file() { + // Mirrors CakePHP: an autoloaded `functions.php` sits next to a + // `functions_global.php` sibling that Composer never lists. + let dir = tempfile::tempdir().unwrap(); + let core = dir.path().join("Core"); + std::fs::create_dir_all(&core).unwrap(); + let autoloaded = core.join("functions.php"); + let sibling = core.join("functions_global.php"); + std::fs::write( + &autoloaded, + " = autoload_files; + file_queue.extend(sibling_globals); let mut visited: HashSet = HashSet::new(); while let Some(file_path) = file_queue.pop() {