aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-autoload
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-autoload')
-rw-r--r--crates/mozart-autoload/Cargo.toml17
-rw-r--r--crates/mozart-autoload/src/autoload.rs1597
-rw-r--r--crates/mozart-autoload/src/dump.rs340
-rw-r--r--crates/mozart-autoload/src/lib.rs4
4 files changed, 0 insertions, 1958 deletions
diff --git a/crates/mozart-autoload/Cargo.toml b/crates/mozart-autoload/Cargo.toml
deleted file mode 100644
index 571d70f..0000000
--- a/crates/mozart-autoload/Cargo.toml
+++ /dev/null
@@ -1,17 +0,0 @@
-[package]
-name = "mozart-autoload"
-version.workspace = true
-edition.workspace = true
-
-[dependencies]
-mozart-class-map-generator.workspace = true
-mozart-core.workspace = true
-mozart-registry.workspace = true
-anyhow.workspace = true
-indexmap.workspace = true
-md5.workspace = true
-regex.workspace = true
-serde_json.workspace = true
-
-[dev-dependencies]
-tempfile.workspace = true
diff --git a/crates/mozart-autoload/src/autoload.rs b/crates/mozart-autoload/src/autoload.rs
deleted file mode 100644
index 21a1de2..0000000
--- a/crates/mozart-autoload/src/autoload.rs
+++ /dev/null
@@ -1,1597 +0,0 @@
-use indexmap::IndexSet;
-use mozart_class_map_generator::{scan_classmap_dirs, scan_psr_for_classmap};
-use mozart_registry::installed::InstalledPackages;
-use mozart_registry::lockfile::LockedPackage;
-use std::collections::BTreeMap;
-use std::path::{Path, PathBuf};
-
-// Embed Composer PHP files from the submodule at compile time.
-const CLASSLOADER_PHP: &str =
- include_str!("../../../composer/src/Composer/Autoload/ClassLoader.php");
-const INSTALLED_VERSIONS_PHP: &str =
- include_str!("../../../composer/src/Composer/InstalledVersions.php");
-const COMPOSER_LICENSE: &str = include_str!("../../../composer/LICENSE");
-
-/// How platform requirements are checked during autoloader generation.
-#[derive(Debug, Clone, PartialEq, Eq, Default)]
-pub enum PlatformCheckMode {
- /// Check all platform requirements (php, ext-*, lib-*).
- #[default]
- Full,
- /// Only check the PHP version requirement.
- PhpOnly,
- /// Disable platform requirement checks entirely.
- Disabled,
-}
-
-/// Result of autoload generation, reporting statistics and warnings.
-pub struct GenerateResult {
- pub class_count: usize,
- pub has_psr_violations: bool,
- pub has_ambiguous_classes: bool,
-}
-
-/// Configuration for autoload generation.
-pub struct AutoloadConfig {
- /// Absolute path to the project root (where composer.json lives).
- pub project_dir: PathBuf,
- /// Absolute path to the vendor directory.
- pub vendor_dir: PathBuf,
- /// Whether dev-mode autoloading is active (include autoload-dev rules).
- pub dev_mode: bool,
- /// Unique suffix for the autoloader class names (typically the lock file content-hash).
- /// Used to generate `ComposerAutoloaderInit{suffix}` and `ComposerStaticInit{suffix}`.
- pub suffix: String,
- /// When true, emit `$loader->setClassMapAuthoritative(true)` in the generated autoloader.
- pub classmap_authoritative: bool,
- /// When true, scan PSR-4/PSR-0 directories and generate a full classmap (optimize mode).
- pub optimize: bool,
- /// When true, generate APCu-based class caching in the autoloader.
- pub apcu: bool,
- /// Optional prefix for APCu cache keys (implies `apcu`).
- pub apcu_prefix: Option<String>,
- /// When true, return an error on PSR mapping violations detected during classmap scan.
- pub strict_psr: bool,
- /// When true, return exit code 2 if ambiguous class mappings are detected.
- pub strict_ambiguous: bool,
- /// How to handle platform requirement checks.
- pub platform_check: PlatformCheckMode,
- /// When true, skip all platform requirement checks.
- pub ignore_platform_reqs: bool,
-}
-
-/// Collected autoload mappings from all packages.
-pub struct AutoloadData {
- /// PSR-4: namespace prefix -> list of directory path expressions.
- /// Each path is a PHP expression string like `$vendorDir . '/psr/log/src'`.
- pub psr4: BTreeMap<String, Vec<String>>,
- /// PSR-0: namespace prefix -> list of directory path expressions.
- /// (Empty in Phase 2.2, populated in 5.6.)
- pub psr0: BTreeMap<String, Vec<String>>,
- /// Classmap entries: class name -> file path expression.
- /// (Empty in Phase 2.2, populated in 5.6.)
- pub classmap: BTreeMap<String, String>,
- /// Files to include on every request: file_identifier -> path expression.
- pub files: BTreeMap<String, String>,
-}
-
-/// Escape a string for use in a PHP single-quoted string literal.
-pub fn php_escape(s: &str) -> String {
- s.replace('\\', "\\\\").replace('\'', "\\'")
-}
-
-/// Compute the file identifier matching Composer's `getFileIdentifier()`.
-/// This is the MD5 hex digest of `"package_name:path"`.
-pub fn file_identifier(package_name: &str, path: &str) -> String {
- let input = format!("{package_name}:{path}");
- format!("{:x}", md5::compute(input.as_bytes()))
-}
-
-/// Extract a path or array of paths from a JSON value.
-/// Handles both string and array-of-strings (Composer allows both).
-fn json_to_paths(value: &serde_json::Value) -> Vec<String> {
- match value {
- serde_json::Value::String(s) => vec![s.clone()],
- serde_json::Value::Array(arr) => arr
- .iter()
- .filter_map(|v| v.as_str().map(|s| s.to_string()))
- .collect(),
- _ => vec![],
- }
-}
-
-/// Strip trailing slash from a path component.
-fn strip_trailing_slash(s: &str) -> &str {
- s.trim_end_matches('/')
-}
-
-/// Normalize a PSR-4 namespace: ensure it ends with `\`.
-/// (The empty string "" is valid and is left as-is.)
-fn normalize_namespace(ns: &str) -> String {
- if ns.is_empty() || ns.ends_with('\\') {
- ns.to_string()
- } else {
- format!("{ns}\\")
- }
-}
-
-/// Build a PHP path expression from a base expression and a relative path component.
-///
-/// For vendor packages: `base_expr` = `"$vendorDir"`, `pkg_path` = `"psr/log"`,
-/// `sub_path` = `"src/"` → result: `"$vendorDir . '/psr/log/src'"`.
-///
-/// For root packages: `base_expr` = `"$baseDir"`, `pkg_path` = `""`,
-/// `sub_path` = `"src/"` → result: `"$baseDir . '/src'"`.
-fn build_path_expr(base_expr: &str, pkg_path: &str, sub_path: &str) -> String {
- let sub = strip_trailing_slash(sub_path);
- let combined = if pkg_path.is_empty() {
- sub.to_string()
- } else if sub.is_empty() {
- pkg_path.to_string()
- } else {
- format!("{pkg_path}/{sub}")
- };
-
- if combined.is_empty() {
- base_expr.to_string()
- } else {
- format!("{base_expr} . '/{combined}'")
- }
-}
-
-/// Process an autoload JSON value and merge its rules into `data`.
-///
-/// `pkg_path` is the package-relative path segment within vendor.
-/// For vendor packages it is `"vendor/name"` (e.g. `"psr/log"`).
-/// For the root package it is `""`.
-///
-/// `dyn_base` is the dynamic PHP variable: `"$vendorDir"` or `"$baseDir"`.
-/// `static_base` is the static PHP expression: `"__DIR__ . '/..'"` or `"__DIR__ . '/../.'"`.
-fn process_autoload_value(
- autoload_val: &serde_json::Value,
- package_name: &str,
- pkg_path: &str,
- dyn_base: &str,
- static_base: &str,
- data: &mut AutoloadData,
- static_data: &mut AutoloadData,
-) {
- // PSR-4
- if let Some(psr4_obj) = autoload_val.get("psr-4").and_then(|v| v.as_object()) {
- for (ns_raw, paths_val) in psr4_obj {
- let ns = normalize_namespace(ns_raw);
- let paths = json_to_paths(paths_val);
- let entry = data.psr4.entry(ns.clone()).or_default();
- let static_entry = static_data.psr4.entry(ns).or_default();
- for path in paths {
- entry.push(build_path_expr(dyn_base, pkg_path, &path));
- static_entry.push(build_path_expr(static_base, pkg_path, &path));
- }
- }
- }
-
- // PSR-0
- if let Some(psr0_obj) = autoload_val.get("psr-0").and_then(|v| v.as_object()) {
- for (ns_raw, paths_val) in psr0_obj {
- let ns = ns_raw.clone();
- let paths = json_to_paths(paths_val);
- let entry = data.psr0.entry(ns.clone()).or_default();
- let static_entry = static_data.psr0.entry(ns).or_default();
- for path in paths {
- entry.push(build_path_expr(dyn_base, pkg_path, &path));
- static_entry.push(build_path_expr(static_base, pkg_path, &path));
- }
- }
- }
-
- // Files
- if let Some(files_arr) = autoload_val.get("files").and_then(|v| v.as_array()) {
- for file_val in files_arr {
- if let Some(file_path) = file_val.as_str() {
- let id = file_identifier(package_name, file_path);
- let expr = build_path_expr(dyn_base, pkg_path, file_path);
- let static_expr = build_path_expr(static_base, pkg_path, file_path);
- data.files.insert(id.clone(), expr);
- static_data.files.insert(id, static_expr);
- }
- }
- }
-}
-
-/// Collect autoload rules from all installed packages and the root package.
-///
-/// Returns a tuple of `(dynamic_data, static_data)` where:
-/// - `dynamic_data` uses `$vendorDir` / `$baseDir` path expressions (for autoload_psr4.php, etc.)
-/// - `static_data` uses `__DIR__ . '/..'` path expressions (for autoload_static.php)
-fn collect_autoloads(
- installed: &InstalledPackages,
- root_autoload: Option<&serde_json::Value>,
- root_autoload_dev: Option<&serde_json::Value>,
- root_package_name: &str,
- dev_mode: bool,
-) -> (AutoloadData, AutoloadData) {
- let mut data = AutoloadData {
- psr4: BTreeMap::new(),
- psr0: BTreeMap::new(),
- classmap: BTreeMap::new(),
- files: BTreeMap::new(),
- };
- let mut static_data = AutoloadData {
- psr4: BTreeMap::new(),
- psr0: BTreeMap::new(),
- classmap: BTreeMap::new(),
- files: BTreeMap::new(),
- };
-
- // Process each installed package
- for pkg in &installed.packages {
- if let Some(autoload_val) = &pkg.autoload {
- process_autoload_value(
- autoload_val,
- &pkg.name,
- &pkg.name, // pkg_path within vendor
- "$vendorDir",
- "__DIR__ . '/..'",
- &mut data,
- &mut static_data,
- );
- }
- }
-
- // Process root package autoload
- if let Some(autoload_val) = root_autoload {
- process_autoload_value(
- autoload_val,
- root_package_name,
- "", // no pkg_path for root
- "$baseDir",
- "__DIR__ . '/../..'",
- &mut data,
- &mut static_data,
- );
- }
-
- // Process root package autoload-dev (only in dev mode)
- if dev_mode && let Some(autoload_dev_val) = root_autoload_dev {
- process_autoload_value(
- autoload_dev_val,
- root_package_name,
- "",
- "$baseDir",
- "__DIR__ . '/../..'",
- &mut data,
- &mut static_data,
- );
- }
-
- (data, static_data)
-}
-
-/// Generate `vendor/composer/autoload_psr4.php`.
-fn generate_autoload_psr4(data: &AutoloadData) -> String {
- let mut out = String::new();
- out.push_str("<?php\n\n// autoload_psr4.php @generated by Composer\n\n");
- out.push_str("$vendorDir = dirname(__DIR__);\n");
- out.push_str("$baseDir = dirname($vendorDir);\n\n");
- out.push_str("return array(\n");
-
- // krsort: reverse alphabetical (longer/more specific namespaces first)
- let mut sorted: Vec<(&String, &Vec<String>)> = data.psr4.iter().collect();
- sorted.sort_by(|(a, _), (b, _)| b.cmp(a));
-
- for (ns, paths) in &sorted {
- let escaped_ns = php_escape(ns);
- if paths.len() == 1 {
- out.push_str(&format!(" '{}' => array({}),\n", escaped_ns, paths[0]));
- } else {
- out.push_str(&format!(" '{}' => array(\n", escaped_ns));
- for path in paths.iter() {
- out.push_str(&format!(" {},\n", path));
- }
- out.push_str(" ),\n");
- }
- }
-
- out.push_str(");\n");
- out
-}
-
-/// Generate `vendor/composer/autoload_namespaces.php` (PSR-0, empty for Phase 2.2).
-fn generate_autoload_namespaces(data: &AutoloadData) -> String {
- let mut out = String::new();
- out.push_str("<?php\n\n// autoload_namespaces.php @generated by Composer\n\n");
- out.push_str("$vendorDir = dirname(__DIR__);\n");
- out.push_str("$baseDir = dirname($vendorDir);\n\n");
- out.push_str("return array(\n");
-
- let mut sorted: Vec<(&String, &Vec<String>)> = data.psr0.iter().collect();
- sorted.sort_by(|(a, _), (b, _)| b.cmp(a));
-
- for (ns, paths) in &sorted {
- let escaped_ns = php_escape(ns);
- if paths.len() == 1 {
- out.push_str(&format!(" '{}' => array({}),\n", escaped_ns, paths[0]));
- } else {
- out.push_str(&format!(" '{}' => array(\n", escaped_ns));
- for path in paths.iter() {
- out.push_str(&format!(" {},\n", path));
- }
- out.push_str(" ),\n");
- }
- }
-
- out.push_str(");\n");
- out
-}
-
-/// Generate `vendor/composer/autoload_classmap.php`.
-/// Always contains `Composer\InstalledVersions`; classmap scanning deferred to Phase 5.6.
-fn generate_autoload_classmap(data: &AutoloadData) -> String {
- let mut out = String::new();
- out.push_str("<?php\n\n// autoload_classmap.php @generated by Composer\n\n");
- out.push_str("$vendorDir = dirname(__DIR__);\n");
- out.push_str("$baseDir = dirname($vendorDir);\n\n");
- out.push_str("return array(\n");
- out.push_str(
- " 'Composer\\\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',\n",
- );
-
- // Include any additional classmap entries from data
- for (class, path) in &data.classmap {
- let escaped_class = php_escape(class);
- out.push_str(&format!(" '{}' => {},\n", escaped_class, path));
- }
-
- out.push_str(");\n");
- out
-}
-
-/// Generate `vendor/composer/autoload_files.php`.
-/// Returns `None` if there are no files to autoload.
-fn generate_autoload_files(data: &AutoloadData) -> Option<String> {
- if data.files.is_empty() {
- return None;
- }
-
- let mut out = String::new();
- out.push_str("<?php\n\n// autoload_files.php @generated by Composer\n\n");
- out.push_str("$vendorDir = dirname(__DIR__);\n");
- out.push_str("$baseDir = dirname($vendorDir);\n\n");
- out.push_str("return array(\n");
-
- for (id, path) in &data.files {
- out.push_str(&format!(" '{}' => {},\n", id, path));
- }
-
- out.push_str(");\n");
- Some(out)
-}
-
-/// Generate `vendor/composer/autoload_static.php`.
-///
-/// `static_data` must have been collected with `__DIR__ . '/..'` path prefixes.
-fn generate_autoload_static(static_data: &AutoloadData, suffix: &str) -> String {
- let mut out = String::new();
- out.push_str("<?php\n\n// autoload_static.php @generated by Composer\n\n");
- out.push_str("namespace Composer\\Autoload;\n\n");
- out.push_str(&format!("class ComposerStaticInit{suffix}\n{{\n"));
-
- // $files
- if !static_data.files.is_empty() {
- out.push_str(" public static $files = array (\n");
- for (id, path) in &static_data.files {
- out.push_str(&format!(" '{id}' => {path},\n"));
- }
- out.push_str(" );\n\n");
- }
-
- // $prefixLengthsPsr4 — group by first character of namespace
- if !static_data.psr4.is_empty() {
- // Group namespaces by first character, sorted reverse
- let mut by_char: BTreeMap<char, Vec<(&String, usize)>> = BTreeMap::new();
-
- let mut sorted_ns: Vec<&String> = static_data.psr4.keys().collect();
- sorted_ns.sort_by(|a, b| b.cmp(a));
-
- for ns in sorted_ns {
- if let Some(first_char) = ns.chars().next() {
- // The byte length in PHP (single-quoted string with single backslashes)
- // ns in our data uses single backslash (stored as-is from JSON).
- let byte_len = ns.len();
- by_char.entry(first_char).or_default().push((ns, byte_len));
- }
- }
-
- out.push_str(" public static $prefixLengthsPsr4 = array (\n");
- // Sort characters in reverse order too
- let mut chars: Vec<char> = by_char.keys().copied().collect();
- chars.sort_by(|a, b| b.cmp(a));
- for ch in &chars {
- out.push_str(&format!(" '{ch}' =>\n array (\n"));
- if let Some(entries) = by_char.get(ch) {
- for (ns, len) in entries {
- let escaped_ns = php_escape(ns);
- out.push_str(&format!(" '{escaped_ns}' => {len},\n"));
- }
- }
- out.push_str(" ),\n");
- }
- out.push_str(" );\n\n");
-
- // $prefixDirsPsr4
- out.push_str(" public static $prefixDirsPsr4 = array (\n");
- let mut sorted_ns2: Vec<(&String, &Vec<String>)> = static_data.psr4.iter().collect();
- sorted_ns2.sort_by(|(a, _), (b, _)| b.cmp(a));
- for (ns, paths) in sorted_ns2 {
- let escaped_ns = php_escape(ns);
- out.push_str(&format!(" '{escaped_ns}' =>\n array (\n"));
- for (i, path) in paths.iter().enumerate() {
- out.push_str(&format!(" {i} => {path},\n"));
- }
- out.push_str(" ),\n");
- }
- out.push_str(" );\n\n");
- }
-
- // $classMap — always contains Composer\InstalledVersions
- out.push_str(" public static $classMap = array (\n");
- out.push_str(
- " 'Composer\\\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',\n",
- );
- for (class, path) in &static_data.classmap {
- let escaped_class = php_escape(class);
- out.push_str(&format!(" '{}' => {},\n", escaped_class, path));
- }
- out.push_str(" );\n\n");
-
- // getInitializer
- out.push_str(" public static function getInitializer(ClassLoader $loader)\n {\n");
- out.push_str(" return \\Closure::bind(function () use ($loader) {\n");
-
- if !static_data.psr4.is_empty() {
- out.push_str(&format!(
- " $loader->prefixLengthsPsr4 = ComposerStaticInit{suffix}::$prefixLengthsPsr4;\n"
- ));
- out.push_str(&format!(
- " $loader->prefixDirsPsr4 = ComposerStaticInit{suffix}::$prefixDirsPsr4;\n"
- ));
- }
- out.push_str(&format!(
- " $loader->classMap = ComposerStaticInit{suffix}::$classMap;\n"
- ));
- out.push_str("\n }, null, ClassLoader::class);\n }\n}\n");
-
- out
-}
-
-/// Generate `vendor/composer/platform_check.php`.
-///
-/// Returns `None` if mode is `Disabled` or there are no relevant requirements.
-fn generate_platform_check(
- packages: &[LockedPackage],
- root_require: Option<&serde_json::Value>,
- mode: &PlatformCheckMode,
- dev_package_names: &IndexSet<String>,
-) -> Option<String> {
- if matches!(mode, PlatformCheckMode::Disabled) {
- return None;
- }
-
- // Collect PHP version constraint from root require
- let mut php_constraint: Option<String> = None;
- if let Some(req_obj) = root_require.and_then(|v| v.as_object())
- && let Some(v) = req_obj.get("php").and_then(|v| v.as_str())
- {
- php_constraint = Some(v.to_string());
- }
-
- // Collect extension requirements from packages (prod only)
- let mut ext_reqs: Vec<(String, String)> = Vec::new();
- if matches!(mode, PlatformCheckMode::Full) {
- for pkg in packages {
- let is_dev = dev_package_names.contains(&pkg.name.to_lowercase());
- if is_dev {
- continue;
- }
- for (req_name, req_constraint) in &pkg.require {
- let lower = req_name.to_lowercase();
- if lower.starts_with("ext-") {
- ext_reqs.push((req_name.clone(), req_constraint.clone()));
- }
- }
- }
- ext_reqs.sort();
- ext_reqs.dedup();
- }
-
- if php_constraint.is_none() && ext_reqs.is_empty() {
- return None;
- }
-
- let mut out = String::new();
- out.push_str("<?php\n\n");
- out.push_str("// platform_check.php @generated by Composer\n\n");
- out.push_str("$issues = array();\n\n");
-
- if let Some(ref constraint) = php_constraint {
- // Emit a simple PHP version check
- let escaped = php_escape(constraint);
- out.push_str(&format!("// PHP version check: {constraint}\n"));
- out.push_str("if (!(PHP_VERSION_ID >= 50600)) {\n");
- out.push_str(&format!(
- " $issues[] = 'Your Composer dependencies require a PHP version \"{escaped}\". You are running ' . PHP_VERSION . '.';\n"
- ));
- out.push_str("}\n\n");
- }
-
- for (ext_name, _constraint) in &ext_reqs {
- let ext_short = ext_name.trim_start_matches("ext-");
- let escaped_ext = php_escape(ext_short);
- out.push_str(&format!("if (!extension_loaded('{escaped_ext}')) {{\n"));
- out.push_str(&format!(
- " $issues[] = 'Your Composer dependencies require the \"{escaped_ext}\" PHP extension to be installed.';\n"
- ));
- out.push_str("}\n\n");
- }
-
- out.push_str("if ($issues) {\n");
- out.push_str(" if (!headers_sent()) {\n");
- out.push_str(" header('HTTP/1.1 500 Internal Server Error');\n");
- out.push_str(" }\n");
- out.push_str(" if (!ini_get('display_errors')) {\n");
- out.push_str(" if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {\n");
- out.push_str(" fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL);\n");
- out.push_str(" } elseif (!headers_sent()) {\n");
- out.push_str(" echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL;\n");
- out.push_str(" }\n");
- out.push_str(" }\n");
- out.push_str(" trigger_error(\n");
- out.push_str(
- " 'Composer detected issues in your platform: ' . implode(' ', $issues),\n",
- );
- out.push_str(" E_USER_ERROR\n");
- out.push_str(" );\n");
- out.push_str("}\n");
-
- Some(out)
-}
-
-/// Generate `vendor/composer/autoload_real.php`.
-fn generate_autoload_real(
- suffix: &str,
- has_files: bool,
- classmap_authoritative: bool,
- apcu: bool,
- apcu_prefix: Option<&str>,
- has_platform_check: bool,
-) -> String {
- let mut out = String::new();
- out.push_str("<?php\n\n");
- out.push_str("// autoload_real.php @generated by Composer\n\n");
- out.push_str(&format!("class ComposerAutoloaderInit{suffix}\n"));
- out.push_str("{\n");
- out.push_str(" private static $loader;\n\n");
- out.push_str(" public static function loadClassLoader($class)\n");
- out.push_str(" {\n");
- out.push_str(" if ('Composer\\Autoload\\ClassLoader' === $class) {\n");
- out.push_str(" require __DIR__ . '/ClassLoader.php';\n");
- out.push_str(" }\n");
- out.push_str(" }\n\n");
- out.push_str(" /**\n");
- out.push_str(" * @return \\Composer\\Autoload\\ClassLoader\n");
- out.push_str(" */\n");
- out.push_str(" public static function getLoader()\n");
- out.push_str(" {\n");
- out.push_str(" if (null !== self::$loader) {\n");
- out.push_str(" return self::$loader;\n");
- out.push_str(" }\n\n");
- out.push_str(&format!(
- " spl_autoload_register(array('ComposerAutoloaderInit{suffix}', 'loadClassLoader'), true, true);\n"
- ));
- out.push_str(
- " self::$loader = $loader = new \\Composer\\Autoload\\ClassLoader(\\dirname(__DIR__));\n",
- );
- out.push_str(&format!(
- " spl_autoload_unregister(array('ComposerAutoloaderInit{suffix}', 'loadClassLoader'));\n\n"
- ));
- if has_platform_check {
- out.push_str(" require __DIR__ . '/platform_check.php';\n");
- }
- out.push_str(" require __DIR__ . '/autoload_static.php';\n");
- out.push_str(&format!(
- " call_user_func(\\Composer\\Autoload\\ComposerStaticInit{suffix}::getInitializer($loader));\n\n"
- ));
- out.push_str(" $loader->register(true);\n");
-
- if classmap_authoritative {
- out.push_str(" $loader->setClassMapAuthoritative(true);\n");
- }
-
- if apcu {
- let prefix = apcu_prefix.unwrap_or(suffix);
- let escaped = php_escape(prefix);
- out.push_str(&format!(" $loader->setApcuPrefix('{escaped}');\n"));
- }
-
- if has_files {
- out.push('\n');
- out.push_str(&format!(
- " $filesToLoad = \\Composer\\Autoload\\ComposerStaticInit{suffix}::$files;\n"
- ));
- out.push_str(
- " $requireFile = \\Closure::bind(static function ($fileIdentifier, $file) {\n",
- );
- out.push_str(
- " if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {\n",
- );
- out.push_str(
- " $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;\n",
- );
- out.push('\n');
- out.push_str(" require $file;\n");
- out.push_str(" }\n");
- out.push_str(" }, null, null);\n");
- out.push_str(" foreach ($filesToLoad as $fileIdentifier => $file) {\n");
- out.push_str(" $requireFile($fileIdentifier, $file);\n");
- out.push_str(" }\n");
- }
-
- out.push('\n');
- out.push_str(" return $loader;\n");
- out.push_str(" }\n");
- out.push_str("}\n");
- out
-}
-
-/// Generate `vendor/autoload.php` (the entry point).
-fn generate_autoload_php(suffix: &str) -> String {
- let mut out = String::new();
- out.push_str("<?php\n\n");
- out.push_str("// autoload.php @generated by Composer\n\n");
- out.push_str("if (PHP_VERSION_ID < 50600) {\n");
- out.push_str(" if (!headers_sent()) {\n");
- out.push_str(" header('HTTP/1.1 500 Internal Server Error');\n");
- out.push_str(" }\n");
- out.push_str(" $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via \"composer self-update --2.2\". Aborting.'.PHP_EOL;\n");
- out.push_str(" if (!ini_get('display_errors')) {\n");
- out.push_str(" if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {\n");
- out.push_str(" fwrite(STDERR, $err);\n");
- out.push_str(" } elseif (!headers_sent()) {\n");
- out.push_str(" echo $err;\n");
- out.push_str(" }\n");
- out.push_str(" }\n");
- out.push_str(" throw new RuntimeException($err);\n");
- out.push_str("}\n\n");
- out.push_str("require_once __DIR__ . '/composer/autoload_real.php';\n\n");
- out.push_str(&format!(
- "return ComposerAutoloaderInit{suffix}::getLoader();\n"
- ));
- out
-}
-
-/// Generate `vendor/composer/installed.php`.
-fn generate_installed_php(
- root_name: &str,
- root_type: &str,
- installed: &InstalledPackages,
- dev_mode: bool,
-) -> String {
- let dev_str = if dev_mode { "true" } else { "false" };
-
- let mut out = String::new();
- out.push_str("<?php return array(\n");
- out.push_str(" 'root' => array(\n");
- out.push_str(&format!(" 'name' => '{}',\n", php_escape(root_name)));
- out.push_str(" 'pretty_version' => 'dev-main',\n");
- out.push_str(" 'version' => 'dev-main',\n");
- out.push_str(" 'reference' => null,\n");
- out.push_str(&format!(" 'type' => '{}',\n", php_escape(root_type)));
- out.push_str(" 'install_path' => __DIR__ . '/../../',\n");
- out.push_str(" 'aliases' => array(),\n");
- out.push_str(&format!(" 'dev' => {dev_str},\n"));
- out.push_str(" ),\n");
- out.push_str(" 'versions' => array(\n");
-
- for pkg in &installed.packages {
- let version = &pkg.version;
- let version_normalized = pkg.version_normalized.as_deref().unwrap_or(version);
- let pkg_type = pkg.package_type.as_deref().unwrap_or("library");
- let is_dev = installed
- .dev_package_names
- .iter()
- .any(|n| n.eq_ignore_ascii_case(&pkg.name));
- let is_dev_str = if is_dev { "true" } else { "false" };
-
- out.push_str(&format!(" '{}' => array(\n", php_escape(&pkg.name)));
- out.push_str(&format!(
- " 'pretty_version' => '{}',\n",
- php_escape(version)
- ));
- out.push_str(&format!(
- " 'version' => '{}',\n",
- php_escape(version_normalized)
- ));
- out.push_str(" 'reference' => null,\n");
- out.push_str(&format!(
- " 'type' => '{}',\n",
- php_escape(pkg_type)
- ));
- // Install path relative to vendor/composer/installed.php: __DIR__ . '/./' . relative_name
- // The install_path stored is like '../psr/log', relative to vendor/composer/
- // So from vendor/composer/, the package is at __DIR__ . '/../psr/log/'
- out.push_str(&format!(
- " 'install_path' => __DIR__ . '/../{}/',\n",
- pkg.name
- ));
- out.push_str(" 'aliases' => array(),\n");
- out.push_str(&format!(" 'dev_requirement' => {is_dev_str},\n"));
- out.push_str(" ),\n");
- }
-
- out.push_str(" ),\n");
- out.push_str(");\n");
- out
-}
-
-/// Determine the autoloader suffix.
-///
-/// Priority:
-/// 1. Existing `vendor/autoload.php` suffix (carry over to avoid breaking existing references).
-/// 2. Lock file `content-hash` (if locked).
-/// 3. Fall back to a timestamp-based hex string.
-pub fn determine_suffix(working_dir: &Path, vendor_dir: &Path) -> anyhow::Result<String> {
- // Try existing autoload.php
- let autoload_path = vendor_dir.join("autoload.php");
- if autoload_path.exists() {
- let content = std::fs::read_to_string(&autoload_path)?;
- if let Some(start) = content.find("ComposerAutoloaderInit") {
- let rest = &content[start + "ComposerAutoloaderInit".len()..];
- if let Some(end) = rest.find("::") {
- let suffix = &rest[..end];
- if !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_hexdigit()) {
- return Ok(suffix.to_string());
- }
- }
- }
- }
-
- // Try composer.lock content-hash
- let lock_path = working_dir.join("composer.lock");
- if lock_path.exists() {
- let lock = mozart_registry::lockfile::LockFile::read_from_file(&lock_path)?;
- return Ok(lock.content_hash);
- }
-
- // Fall back to MD5 of current timestamp
- let ts = format!("{:?}", std::time::SystemTime::now());
- Ok(format!("{:x}", md5::compute(ts.as_bytes())))
-}
-
-/// Generate all autoloader files for the given project.
-///
-/// This is the main entry point called by `install` and `dump-autoload`.
-pub fn generate(config: &AutoloadConfig) -> anyhow::Result<GenerateResult> {
- // 1. Read installed.json
- let installed = InstalledPackages::read(&config.vendor_dir)?;
-
- // 2. Read root package autoload from composer.json
- let composer_json_path = config.project_dir.join("composer.json");
- let (root_autoload, root_autoload_dev, root_name, root_type) = if composer_json_path.exists() {
- let content = std::fs::read_to_string(&composer_json_path)?;
- let value: serde_json::Value = serde_json::from_str(&content)?;
- (
- value.get("autoload").cloned(),
- value.get("autoload-dev").cloned(),
- value
- .get("name")
- .and_then(|n| n.as_str())
- .unwrap_or("__root__")
- .to_string(),
- value
- .get("type")
- .and_then(|t| t.as_str())
- .unwrap_or("project")
- .to_string(),
- )
- } else {
- (None, None, "__root__".to_string(), "project".to_string())
- };
-
- // 3. Collect autoload data
- let (mut data, mut static_data) = collect_autoloads(
- &installed,
- root_autoload.as_ref(),
- root_autoload_dev.as_ref(),
- &root_name,
- config.dev_mode,
- );
-
- // 3a. Read classmap dirs declared in composer.json
- let excluded: Vec<String> = root_autoload
- .as_ref()
- .and_then(|v| v.get("exclude-from-classmap"))
- .and_then(|v| v.as_array())
- .map(|arr| {
- arr.iter()
- .filter_map(|v| v.as_str().map(|s| s.to_string()))
- .collect()
- })
- .unwrap_or_default();
-
- // Scan explicit classmap dirs from all packages
- let mut classmap_dirs: Vec<PathBuf> = Vec::new();
-
- // Collect classmap dirs from installed packages
- for pkg in &installed.packages {
- if let Some(autoload_val) = &pkg.autoload
- && let Some(cm_arr) = autoload_val.get("classmap").and_then(|v| v.as_array())
- {
- for cm_val in cm_arr {
- if let Some(cm_path) = cm_val.as_str() {
- let abs = config.vendor_dir.join(&pkg.name).join(cm_path);
- classmap_dirs.push(abs);
- }
- }
- }
- }
-
- // Collect classmap dirs from root autoload
- if let Some(autoload_val) = root_autoload.as_ref()
- && let Some(cm_arr) = autoload_val.get("classmap").and_then(|v| v.as_array())
- {
- for cm_val in cm_arr {
- if let Some(cm_path) = cm_val.as_str() {
- let abs = config.project_dir.join(cm_path);
- classmap_dirs.push(abs);
- }
- }
- }
-
- // Scan classmap dirs
- let mut ambiguous_found = false;
- if !classmap_dirs.is_empty() {
- let scanned = scan_classmap_dirs(
- &classmap_dirs,
- &config.vendor_dir,
- &config.project_dir,
- &excluded,
- );
- for (class, path_expr) in scanned {
- if let Some(existing) = data.classmap.get(&class)
- && existing != &path_expr
- {
- ambiguous_found = true;
- }
- // Also generate the static expression
- // We store the dynamic expression in data.classmap; static_data.classmap
- // will be populated similarly. For now we insert into both.
- data.classmap.entry(class.clone()).or_insert(path_expr);
- // Generate corresponding static expr by replacing dynamic prefixes
- // (static_data classmap is populated in the static pass below)
- }
- }
-
- // 3b. Optimize mode: scan PSR-4/PSR-0 dirs for classmap
- let do_optimize = config.optimize || config.classmap_authoritative;
- let mut psr_violations: Vec<String> = Vec::new();
-
- if do_optimize {
- let (opt_dyn, opt_static, violations) = scan_psr_for_classmap(
- &data.psr4,
- &data.psr0,
- &config.vendor_dir,
- &config.project_dir,
- &excluded,
- );
- psr_violations = violations;
- for (class, path_expr) in opt_dyn {
- if let Some(existing) = data.classmap.get(&class)
- && existing != &path_expr
- {
- ambiguous_found = true;
- }
- data.classmap.entry(class).or_insert(path_expr);
- }
- for (class, path_expr) in opt_static {
- static_data.classmap.entry(class).or_insert(path_expr);
- }
- }
-
- // 3c. Handle strict-psr violations
- if config.strict_psr && !psr_violations.is_empty() {
- for violation in &psr_violations {
- eprintln!("PSR violation: {violation}");
- }
- return Err(anyhow::anyhow!(
- "PSR mapping violations detected (--strict-psr). Run without --strict-psr to ignore."
- ));
- }
-
- // 4. Generate and write files
- let composer_dir = config.vendor_dir.join("composer");
- std::fs::create_dir_all(&composer_dir)?;
-
- std::fs::write(
- composer_dir.join("autoload_psr4.php"),
- generate_autoload_psr4(&data),
- )?;
- std::fs::write(
- composer_dir.join("autoload_namespaces.php"),
- generate_autoload_namespaces(&data),
- )?;
- std::fs::write(
- composer_dir.join("autoload_classmap.php"),
- generate_autoload_classmap(&data),
- )?;
-
- if let Some(files_content) = generate_autoload_files(&data) {
- std::fs::write(composer_dir.join("autoload_files.php"), files_content)?;
- } else {
- // Remove stale file if it exists
- let files_path = composer_dir.join("autoload_files.php");
- if files_path.exists() {
- std::fs::remove_file(files_path)?;
- }
- }
-
- // 4a. Generate platform_check.php if needed
- let dev_package_names_set: IndexSet<String> = installed
- .dev_package_names
- .iter()
- .map(|n| n.to_lowercase())
- .collect();
-
- // Re-read composer.json for root require (not from autoload, but from root "require" key)
- let root_require_val: Option<serde_json::Value> = if composer_json_path.exists() {
- let content = std::fs::read_to_string(&composer_json_path)?;
- let value: serde_json::Value = serde_json::from_str(&content)?;
- value.get("require").cloned()
- } else {
- None
- };
-
- let all_locked: Vec<LockedPackage> = {
- // Collect locked packages from installed for platform check
- // (installed.packages are LockedPackage-compatible via InstalledPackageEntry)
- // We'll build minimal LockedPackage-like data from installed entries
- installed
- .packages
- .iter()
- .map(|p| mozart_registry::lockfile::LockedPackage {
- name: p.name.clone(),
- version: p.version.clone(),
- version_normalized: p.version_normalized.clone(),
- source: None,
- dist: None,
- require: std::collections::BTreeMap::new(),
- require_dev: std::collections::BTreeMap::new(),
- conflict: std::collections::BTreeMap::new(),
- provide: std::collections::BTreeMap::new(),
- replace: std::collections::BTreeMap::new(),
- suggest: None,
- package_type: p.package_type.clone(),
- autoload: p.autoload.clone(),
- autoload_dev: None,
- license: None,
- description: None,
- homepage: None,
- keywords: None,
- authors: None,
- support: None,
- funding: None,
- time: None,
- extra_fields: std::collections::BTreeMap::new(),
- })
- .collect()
- };
-
- let effective_mode = if config.ignore_platform_reqs {
- PlatformCheckMode::Disabled
- } else {
- config.platform_check.clone()
- };
-
- let platform_check_content = generate_platform_check(
- &all_locked,
- root_require_val.as_ref(),
- &effective_mode,
- &dev_package_names_set,
- );
- let has_platform_check = platform_check_content.is_some();
-
- if let Some(content) = platform_check_content {
- std::fs::write(composer_dir.join("platform_check.php"), content)?;
- } else {
- let pc_path = composer_dir.join("platform_check.php");
- if pc_path.exists() {
- std::fs::remove_file(pc_path)?;
- }
- }
-
- let has_files = !data.files.is_empty();
- let use_apcu = config.apcu || config.apcu_prefix.is_some();
- std::fs::write(
- composer_dir.join("autoload_static.php"),
- generate_autoload_static(&static_data, &config.suffix),
- )?;
- std::fs::write(
- composer_dir.join("autoload_real.php"),
- generate_autoload_real(
- &config.suffix,
- has_files,
- config.classmap_authoritative,
- use_apcu,
- config.apcu_prefix.as_deref(),
- has_platform_check,
- ),
- )?;
- std::fs::write(
- config.vendor_dir.join("autoload.php"),
- generate_autoload_php(&config.suffix),
- )?;
-
- // 5. Copy ClassLoader.php, InstalledVersions.php, LICENSE
- std::fs::write(composer_dir.join("ClassLoader.php"), CLASSLOADER_PHP)?;
- std::fs::write(
- composer_dir.join("InstalledVersions.php"),
- INSTALLED_VERSIONS_PHP,
- )?;
- std::fs::write(composer_dir.join("LICENSE"), COMPOSER_LICENSE)?;
-
- // 6. Generate installed.php
- std::fs::write(
- composer_dir.join("installed.php"),
- generate_installed_php(&root_name, &root_type, &installed, config.dev_mode),
- )?;
-
- Ok(GenerateResult {
- class_count: data.classmap.len(),
- has_psr_violations: !psr_violations.is_empty(),
- has_ambiguous_classes: ambiguous_found,
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use mozart_registry::installed::{InstalledPackageEntry, InstalledPackages};
- use std::collections::BTreeMap;
- use tempfile::tempdir;
-
- fn make_installed_pkg(name: &str, version: &str) -> InstalledPackageEntry {
- InstalledPackageEntry {
- name: name.to_string(),
- version: version.to_string(),
- version_normalized: None,
- source: None,
- dist: None,
- package_type: Some("library".to_string()),
- install_path: Some(format!("../{name}")),
- autoload: None,
- aliases: vec![],
- homepage: None,
- support: None,
- extra_fields: BTreeMap::new(),
- }
- }
-
- fn make_installed_pkg_with_autoload(
- name: &str,
- version: &str,
- autoload: serde_json::Value,
- ) -> InstalledPackageEntry {
- let mut entry = make_installed_pkg(name, version);
- entry.autoload = Some(autoload);
- entry
- }
-
- // -------------------------------------------------------------------------
- // Helper function tests
- // -------------------------------------------------------------------------
-
- #[test]
- fn test_php_escape_backslash() {
- assert_eq!(php_escape("Psr\\Log\\"), "Psr\\\\Log\\\\");
- }
-
- #[test]
- fn test_php_escape_quote() {
- assert_eq!(php_escape("don't"), "don\\'t");
- }
-
- #[test]
- fn test_php_escape_mixed() {
- assert_eq!(php_escape("A\\B'C"), "A\\\\B\\'C");
- }
-
- #[test]
- fn test_file_identifier_known_vector() {
- // Known test vector from Composer docs:
- // md5("symfony/polyfill-php80:bootstrap.php") = "a4a119a56e50fbb293281d9a48007e0e"
- let id = file_identifier("symfony/polyfill-php80", "bootstrap.php");
- assert_eq!(id, "a4a119a56e50fbb293281d9a48007e0e");
- }
-
- #[test]
- fn test_file_identifier_format() {
- let id = file_identifier("psr/log", "src/functions.php");
- // Should be 32 hex chars (MD5)
- assert_eq!(id.len(), 32);
- assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
- }
-
- #[test]
- fn test_json_to_paths_string() {
- let v = serde_json::json!("src/");
- assert_eq!(json_to_paths(&v), vec!["src/"]);
- }
-
- #[test]
- fn test_json_to_paths_array() {
- let v = serde_json::json!(["src/", "lib/"]);
- assert_eq!(json_to_paths(&v), vec!["src/", "lib/"]);
- }
-
- #[test]
- fn test_json_to_paths_invalid() {
- let v = serde_json::json!(42);
- assert!(json_to_paths(&v).is_empty());
- }
-
- // -------------------------------------------------------------------------
- // collect_autoloads tests
- // -------------------------------------------------------------------------
-
- #[test]
- fn test_collect_autoloads_psr4_basic() {
- let mut installed = InstalledPackages::new();
- installed.upsert(make_installed_pkg_with_autoload(
- "psr/log",
- "3.0.2",
- serde_json::json!({"psr-4": {"Psr\\Log\\": "src/"}}),
- ));
-
- let (data, _static_data) = collect_autoloads(&installed, None, None, "__root__", false);
-
- assert!(data.psr4.contains_key("Psr\\Log\\"));
- let paths = &data.psr4["Psr\\Log\\"];
- assert_eq!(paths.len(), 1);
- assert_eq!(paths[0], "$vendorDir . '/psr/log/src'");
- }
-
- #[test]
- fn test_collect_autoloads_psr4_multiple_dirs() {
- let mut installed = InstalledPackages::new();
- installed.upsert(make_installed_pkg_with_autoload(
- "monolog/monolog",
- "3.8.0",
- serde_json::json!({"psr-4": {"Monolog\\": ["src/Monolog", "lib/"]}}),
- ));
-
- let (data, _static_data) = collect_autoloads(&installed, None, None, "__root__", false);
-
- let paths = &data.psr4["Monolog\\"];
- assert_eq!(paths.len(), 2);
- assert_eq!(paths[0], "$vendorDir . '/monolog/monolog/src/Monolog'");
- assert_eq!(paths[1], "$vendorDir . '/monolog/monolog/lib'");
- }
-
- #[test]
- fn test_collect_autoloads_files() {
- let mut installed = InstalledPackages::new();
- installed.upsert(make_installed_pkg_with_autoload(
- "symfony/polyfill-php80",
- "1.32.0",
- serde_json::json!({"files": ["bootstrap.php"]}),
- ));
-
- let (data, _static_data) = collect_autoloads(&installed, None, None, "__root__", false);
-
- // The identifier should match Composer's MD5 computation
- let expected_id = "a4a119a56e50fbb293281d9a48007e0e";
- assert!(data.files.contains_key(expected_id));
- assert_eq!(
- data.files[expected_id],
- "$vendorDir . '/symfony/polyfill-php80/bootstrap.php'"
- );
- }
-
- #[test]
- fn test_collect_autoloads_root_package() {
- let installed = InstalledPackages::new();
- let root_autoload = serde_json::json!({"psr-4": {"App\\": "src/"}});
-
- let (data, _static_data) = collect_autoloads(
- &installed,
- Some(&root_autoload),
- None,
- "myproject/app",
- false,
- );
-
- assert!(data.psr4.contains_key("App\\"));
- let paths = &data.psr4["App\\"];
- assert_eq!(paths[0], "$baseDir . '/src'");
- }
-
- #[test]
- fn test_collect_autoloads_root_autoload_dev_included_when_dev() {
- let installed = InstalledPackages::new();
- let root_autoload_dev = serde_json::json!({"psr-4": {"Tests\\": "tests/"}});
-
- let (data, _) = collect_autoloads(
- &installed,
- None,
- Some(&root_autoload_dev),
- "myproject/app",
- true, // dev_mode = true
- );
-
- assert!(data.psr4.contains_key("Tests\\"));
- }
-
- #[test]
- fn test_collect_autoloads_root_autoload_dev_excluded_when_no_dev() {
- let installed = InstalledPackages::new();
- let root_autoload_dev = serde_json::json!({"psr-4": {"Tests\\": "tests/"}});
-
- let (data, _) = collect_autoloads(
- &installed,
- None,
- Some(&root_autoload_dev),
- "myproject/app",
- false, // dev_mode = false
- );
-
- assert!(!data.psr4.contains_key("Tests\\"));
- }
-
- // -------------------------------------------------------------------------
- // generate_autoload_psr4 tests
- // -------------------------------------------------------------------------
-
- #[test]
- fn test_generate_autoload_psr4_output() {
- let mut installed = InstalledPackages::new();
- installed.upsert(make_installed_pkg_with_autoload(
- "psr/log",
- "3.0.2",
- serde_json::json!({"psr-4": {"Psr\\Log\\": "src/"}}),
- ));
-
- let (data, _) = collect_autoloads(&installed, None, None, "__root__", false);
- let output = generate_autoload_psr4(&data);
-
- assert!(output.contains("<?php"));
- assert!(output.contains("autoload_psr4.php @generated by Composer"));
- assert!(output.contains("$vendorDir = dirname(__DIR__);"));
- assert!(output.contains("$baseDir = dirname($vendorDir);"));
- assert!(output.contains("'Psr\\\\Log\\\\'"));
- assert!(output.contains("$vendorDir . '/psr/log/src'"));
- assert!(output.starts_with("<?php\n"));
- }
-
- #[test]
- fn test_generate_autoload_psr4_empty() {
- let data = AutoloadData {
- psr4: BTreeMap::new(),
- psr0: BTreeMap::new(),
- classmap: BTreeMap::new(),
- files: BTreeMap::new(),
- };
- let output = generate_autoload_psr4(&data);
- assert!(output.contains("return array(\n);"));
- }
-
- #[test]
- fn test_generate_autoload_psr4_sorted_reverse() {
- let mut installed = InstalledPackages::new();
- installed.upsert(make_installed_pkg_with_autoload(
- "aaa/pkg",
- "1.0.0",
- serde_json::json!({"psr-4": {"Aaa\\": "src/"}}),
- ));
- installed.upsert(make_installed_pkg_with_autoload(
- "zzz/pkg",
- "1.0.0",
- serde_json::json!({"psr-4": {"Zzz\\": "src/"}}),
- ));
-
- let (data, _) = collect_autoloads(&installed, None, None, "__root__", false);
- let output = generate_autoload_psr4(&data);
-
- // Zzz should appear before Aaa (reverse sort)
- let zzz_pos = output.find("Zzz").unwrap();
- let aaa_pos = output.find("Aaa").unwrap();
- assert!(
- zzz_pos < aaa_pos,
- "Zzz should appear before Aaa (reverse sort)"
- );
- }
-
- // -------------------------------------------------------------------------
- // generate_autoload_static tests
- // -------------------------------------------------------------------------
-
- #[test]
- fn test_generate_autoload_static_output() {
- let mut installed = InstalledPackages::new();
- installed.upsert(make_installed_pkg_with_autoload(
- "psr/log",
- "3.0.2",
- serde_json::json!({"psr-4": {"Psr\\Log\\": "src/"}}),
- ));
-
- let (_, static_data) = collect_autoloads(&installed, None, None, "__root__", false);
- let output = generate_autoload_static(&static_data, "abc123");
-
- assert!(output.contains("class ComposerStaticInitabc123"));
- assert!(output.contains("$prefixLengthsPsr4"));
- assert!(output.contains("$prefixDirsPsr4"));
- assert!(output.contains("$classMap"));
- assert!(output.contains("Composer\\\\InstalledVersions"));
- assert!(output.contains("getInitializer"));
- assert!(output.contains("__DIR__ . '/..' . '/psr/log/src'"));
- }
-
- #[test]
- fn test_generate_autoload_static_prefix_lengths() {
- let mut installed = InstalledPackages::new();
- // "Psr\Log\" = 8 bytes (with single backslashes)
- installed.upsert(make_installed_pkg_with_autoload(
- "psr/log",
- "3.0.2",
- serde_json::json!({"psr-4": {"Psr\\Log\\": "src/"}}),
- ));
-
- let (_, static_data) = collect_autoloads(&installed, None, None, "__root__", false);
- let output = generate_autoload_static(&static_data, "test");
-
- // The namespace "Psr\Log\" is 8 bytes
- assert!(output.contains("'Psr\\\\Log\\\\' => 8"));
- }
-
- // -------------------------------------------------------------------------
- // generate_autoload_real tests
- // -------------------------------------------------------------------------
-
- #[test]
- fn test_generate_autoload_real_with_files() {
- let output = generate_autoload_real("abc123", true, false, false, None, false);
- assert!(output.contains("class ComposerAutoloaderInitabc123"));
- assert!(output.contains("ComposerStaticInitabc123::$files"));
- assert!(output.contains("$requireFile"));
- assert!(output.contains("__composer_autoload_files"));
- }
-
- #[test]
- fn test_generate_autoload_real_without_files() {
- let output = generate_autoload_real("abc123", false, false, false, None, false);
- assert!(output.contains("class ComposerAutoloaderInitabc123"));
- assert!(!output.contains("$filesToLoad"));
- assert!(!output.contains("__composer_autoload_files"));
- }
-
- #[test]
- fn test_generate_autoload_real_apcu() {
- let output = generate_autoload_real("abc123", false, false, true, None, false);
- assert!(output.contains("setApcuPrefix('abc123')"));
- }
-
- #[test]
- fn test_generate_autoload_real_apcu_custom_prefix() {
- let output = generate_autoload_real("abc123", false, false, true, Some("myprefix"), false);
- assert!(output.contains("setApcuPrefix('myprefix')"));
- }
-
- #[test]
- fn test_generate_autoload_real_platform_check() {
- let output = generate_autoload_real("abc123", false, false, false, None, true);
- assert!(output.contains("require __DIR__ . '/platform_check.php'"));
- }
-
- #[test]
- fn test_generate_autoload_real_no_platform_check() {
- let output = generate_autoload_real("abc123", false, false, false, None, false);
- assert!(!output.contains("platform_check.php"));
- }
-
- // -------------------------------------------------------------------------
- // generate_installed_php tests
- // -------------------------------------------------------------------------
-
- #[test]
- fn test_generate_installed_php() {
- let mut installed = InstalledPackages::new();
- let mut pkg = make_installed_pkg("psr/log", "3.0.2");
- pkg.version_normalized = Some("3.0.2.0".to_string());
- installed.upsert(pkg);
-
- let output = generate_installed_php("myproject/app", "project", &installed, true);
-
- assert!(output.contains("'name' => 'myproject/app'"));
- assert!(output.contains("'type' => 'project'"));
- assert!(output.contains("'dev' => true"));
- assert!(output.contains("'psr/log'"));
- assert!(output.contains("'pretty_version' => '3.0.2'"));
- assert!(output.contains("'version' => '3.0.2.0'"));
- assert!(output.contains("__DIR__ . '/../psr/log/'"));
- assert!(output.contains("'dev_requirement' => false"));
- }
-
- #[test]
- fn test_generate_installed_php_dev_package() {
- let mut installed = InstalledPackages::new();
- installed.upsert(make_installed_pkg("phpunit/phpunit", "11.0.0"));
- installed
- .dev_package_names
- .push("phpunit/phpunit".to_string());
-
- let output = generate_installed_php("test/project", "project", &installed, true);
-
- assert!(output.contains("'dev_requirement' => true"));
- }
-
- // -------------------------------------------------------------------------
- // generate() integration test
- // -------------------------------------------------------------------------
-
- #[test]
- fn test_generate_full_roundtrip() {
- let dir = tempdir().unwrap();
- let project_dir = dir.path().to_path_buf();
- let vendor_dir = project_dir.join("vendor");
-
- // Write a minimal composer.json
- std::fs::write(
- project_dir.join("composer.json"),
- r#"{"name": "test/project", "type": "project", "autoload": {"psr-4": {"App\\": "src/"}}}"#,
- )
- .unwrap();
-
- // Write a minimal installed.json
- let mut installed = InstalledPackages::new();
- installed.upsert(make_installed_pkg_with_autoload(
- "psr/log",
- "3.0.2",
- serde_json::json!({"psr-4": {"Psr\\Log\\": "src/"}}),
- ));
- installed.write(&vendor_dir).unwrap();
-
- let config = AutoloadConfig {
- project_dir: project_dir.clone(),
- vendor_dir: vendor_dir.clone(),
- dev_mode: false,
- suffix: "abc123def456".to_string(),
- classmap_authoritative: false,
- optimize: false,
- apcu: false,
- apcu_prefix: None,
- strict_psr: false,
- strict_ambiguous: false,
- platform_check: PlatformCheckMode::Disabled,
- ignore_platform_reqs: false,
- };
-
- generate(&config).unwrap();
-
- // Verify all expected files exist
- assert!(
- vendor_dir.join("autoload.php").exists(),
- "autoload.php should exist"
- );
- assert!(
- vendor_dir.join("composer/autoload_psr4.php").exists(),
- "autoload_psr4.php should exist"
- );
- assert!(
- vendor_dir.join("composer/autoload_namespaces.php").exists(),
- "autoload_namespaces.php should exist"
- );
- assert!(
- vendor_dir.join("composer/autoload_classmap.php").exists(),
- "autoload_classmap.php should exist"
- );
- assert!(
- vendor_dir.join("composer/autoload_static.php").exists(),
- "autoload_static.php should exist"
- );
- assert!(
- vendor_dir.join("composer/autoload_real.php").exists(),
- "autoload_real.php should exist"
- );
- assert!(
- vendor_dir.join("composer/ClassLoader.php").exists(),
- "ClassLoader.php should exist"
- );
- assert!(
- vendor_dir.join("composer/InstalledVersions.php").exists(),
- "InstalledVersions.php should exist"
- );
- assert!(
- vendor_dir.join("composer/installed.php").exists(),
- "installed.php should exist"
- );
- assert!(
- vendor_dir.join("composer/LICENSE").exists(),
- "LICENSE should exist"
- );
- // autoload_files.php should NOT exist (no files autoloading)
- assert!(
- !vendor_dir.join("composer/autoload_files.php").exists(),
- "autoload_files.php should not exist when no files"
- );
-
- // Check autoload.php content
- let autoload_php = std::fs::read_to_string(vendor_dir.join("autoload.php")).unwrap();
- assert!(autoload_php.contains("ComposerAutoloaderInitabc123def456"));
-
- // Check autoload_psr4.php
- let psr4_php =
- std::fs::read_to_string(vendor_dir.join("composer/autoload_psr4.php")).unwrap();
- assert!(psr4_php.contains("Psr\\\\Log\\\\"));
- assert!(psr4_php.contains("App\\\\"));
- assert!(psr4_php.contains("$vendorDir . '/psr/log/src'"));
- assert!(psr4_php.contains("$baseDir . '/src'"));
-
- // Check installed.php
- let installed_php =
- std::fs::read_to_string(vendor_dir.join("composer/installed.php")).unwrap();
- assert!(installed_php.contains("'name' => 'test/project'"));
- assert!(installed_php.contains("'psr/log'"));
- }
-
- #[test]
- fn test_generate_with_files_autoload() {
- let dir = tempdir().unwrap();
- let project_dir = dir.path().to_path_buf();
- let vendor_dir = project_dir.join("vendor");
-
- std::fs::write(
- project_dir.join("composer.json"),
- r#"{"name": "test/project", "type": "project"}"#,
- )
- .unwrap();
-
- let mut installed = InstalledPackages::new();
- installed.upsert(make_installed_pkg_with_autoload(
- "symfony/polyfill-php80",
- "1.32.0",
- serde_json::json!({"files": ["bootstrap.php"]}),
- ));
- installed.write(&vendor_dir).unwrap();
-
- let config = AutoloadConfig {
- project_dir: project_dir.clone(),
- vendor_dir: vendor_dir.clone(),
- dev_mode: false,
- suffix: "test".to_string(),
- classmap_authoritative: false,
- optimize: false,
- apcu: false,
- apcu_prefix: None,
- strict_psr: false,
- strict_ambiguous: false,
- platform_check: PlatformCheckMode::Disabled,
- ignore_platform_reqs: false,
- };
-
- generate(&config).unwrap();
-
- // autoload_files.php SHOULD exist
- assert!(
- vendor_dir.join("composer/autoload_files.php").exists(),
- "autoload_files.php should exist when files are present"
- );
-
- let files_php =
- std::fs::read_to_string(vendor_dir.join("composer/autoload_files.php")).unwrap();
- assert!(files_php.contains("a4a119a56e50fbb293281d9a48007e0e"));
- assert!(files_php.contains("$vendorDir . '/symfony/polyfill-php80/bootstrap.php'"));
-
- // autoload_real.php should contain the files loading block
- let real_php =
- std::fs::read_to_string(vendor_dir.join("composer/autoload_real.php")).unwrap();
- assert!(real_php.contains("$filesToLoad"));
- }
-}
diff --git a/crates/mozart-autoload/src/dump.rs b/crates/mozart-autoload/src/dump.rs
deleted file mode 100644
index 103c683..0000000
--- a/crates/mozart-autoload/src/dump.rs
+++ /dev/null
@@ -1,340 +0,0 @@
-//! `Composer\Autoload\AutoloadGenerator::dump` extension.
-//!
-//! [`mozart_core::composer::AutoloadGenerator`] is a state container in
-//! `mozart-core`; the dumping algorithm itself sits here in
-//! `mozart-autoload` because it pulls in the classmap scanner,
-//! installed.json reader, and PHP-emission helpers. This module hangs
-//! `dump()` off the generator via [`AutoloadGeneratorExt`] so callers
-//! can still write `composer.autoload_generator().dump(...)`, matching
-//! `$composer->getAutoloadGenerator()->dump(...)` in PHP.
-//!
-//! Bring [`AutoloadGeneratorExt`] into scope at the call site:
-//!
-//! ```ignore
-//! use mozart_autoload::AutoloadGeneratorExt;
-//! ```
-//!
-//! See `Composer\Autoload\AutoloadGenerator::dump()` (the ~500-line
-//! implementation in `composer/src/Composer/Autoload/AutoloadGenerator.php`)
-//! for the upstream semantics.
-
-use std::collections::BTreeMap;
-use std::path::PathBuf;
-
-use mozart_core::composer::{
- AutoloadDumpOptions, AutoloadGenerator, InstallationManager, LocalRepository, Locker,
- PlatformRequirementFilter,
-};
-use mozart_core::config::Config;
-use mozart_core::package::RawPackageData;
-
-use crate::autoload::{AutoloadConfig, PlatformCheckMode, generate};
-
-/// Mirror of `Composer\ClassMapGenerator\ClassMap` — the return value
-/// of `AutoloadGenerator::dump`. PHP's class is a `Countable` carrying
-/// the discovered class map plus PSR-violation and ambiguous-class
-/// records; Mozart only models the slice that command handlers need to
-/// branch on today (`count`, `has_psr_violations`, `has_ambiguous_classes`).
-///
-/// The `map` / `psr_violations` / `ambiguous_classes` fields are
-/// currently populated from the existing [`generate`]'s coarse
-/// summary — once `generate` is refactored to expose the full classmap
-/// these fields will hold the real entries.
-pub struct ClassMap {
- map: BTreeMap<String, String>,
- psr_violations: Vec<String>,
- ambiguous_classes: BTreeMap<String, Vec<String>>,
-}
-
-impl ClassMap {
- /// Mirror of `ClassMap::count`.
- pub fn count(&self) -> usize {
- self.map.len()
- }
-
- /// Mirror of `count($classMap->getPsrViolations()) > 0`. PHP returns
- /// the violation strings; commands typically only need the boolean.
- pub fn has_psr_violations(&self) -> bool {
- !self.psr_violations.is_empty()
- }
-
- /// Mirror of `count($classMap->getAmbiguousClasses($filter)) > 0`.
- /// `with_filter = true` applies PHP's default test/fixture/example
- /// path filter; `false` skips it (the `$duplicatesFilter = false`
- /// branch upstream).
- pub fn has_ambiguous_classes(&self, with_filter: bool) -> bool {
- if !with_filter {
- return !self.ambiguous_classes.is_empty();
- }
- let pattern = regex_filter_default();
- self.ambiguous_classes.values().any(|paths| {
- paths
- .iter()
- .any(|p| !pattern.is_match(&p.replace('\\', "/")))
- })
- }
-
- /// Read access to the underlying map (`getMap()` upstream).
- pub fn map(&self) -> &BTreeMap<String, String> {
- &self.map
- }
-
- /// Read access to the PSR-violation warnings.
- pub fn psr_violations(&self) -> &[String] {
- &self.psr_violations
- }
-
- /// Read access to the ambiguous-class records.
- pub fn ambiguous_classes(&self) -> &BTreeMap<String, Vec<String>> {
- &self.ambiguous_classes
- }
-}
-
-fn regex_filter_default() -> regex::Regex {
- use std::sync::OnceLock;
- static RE: OnceLock<regex::Regex> = OnceLock::new();
- RE.get_or_init(|| {
- // `{/(test|fixture|example|stub)s?/}i` from PHP's
- // ClassMap::getAmbiguousClasses default.
- regex::Regex::new(r"(?i)/(test|fixture|example|stub)s?/")
- .expect("default ambiguous filter compiles")
- })
- .clone()
-}
-
-/// Extension trait hanging `dump()` off
-/// [`mozart_core::composer::AutoloadGenerator`]. Mirrors
-/// `Composer\Autoload\AutoloadGenerator::dump()`.
-///
-/// Bring this trait into scope (`use mozart_autoload::AutoloadGeneratorExt;`)
-/// to make the method visible.
-///
-/// Diverges from PHP in one place: the per-call toggles PHP fixes via
-/// `setDryRun` / `setDevMode` / … on the generator are passed in here
-/// as an [`AutoloadDumpOptions`] argument, because Mozart's
-/// [`AutoloadGenerator`] is stateless.
-pub trait AutoloadGeneratorExt {
- /// Mirror of `AutoloadGenerator::dump(Config $config,
- /// InstalledRepositoryInterface $localRepo, RootPackageInterface
- /// $rootPackage, InstallationManager $installationManager, string
- /// $targetDir, bool $scanPsrPackages = false, ?string $suffix = null,
- /// ?Locker $locker = null, bool $strictAmbiguous = false)`.
- ///
- /// Mozart-specific notes:
- /// - `options` carries the toggles PHP fixes via setters on the
- /// generator (`setDryRun`, `setDevMode`, `setApcu`, …).
- /// - `target_dir` is currently unused (the underlying [`generate`]
- /// always writes into `vendor_dir/composer`); the parameter is
- /// kept on the signature so the call site mirrors PHP and we can
- /// honour it once the writer is parameterised.
- /// - `local_repo` and `root_package` are accepted to mirror the
- /// PHP signature, but [`generate`] currently re-reads them from
- /// `installed.json` / `composer.json`. Refactoring to consume the
- /// passed-in values lives in a follow-up.
- #[allow(clippy::too_many_arguments)]
- fn dump(
- &self,
- options: &AutoloadDumpOptions,
- config: &Config,
- local_repo: &LocalRepository,
- root_package: &RawPackageData,
- installation_manager: &InstallationManager,
- target_dir: &str,
- scan_psr_packages: bool,
- suffix: Option<&str>,
- locker: &Locker,
- strict_ambiguous: bool,
- ) -> anyhow::Result<ClassMap>;
-}
-
-impl AutoloadGeneratorExt for AutoloadGenerator {
- fn dump(
- &self,
- options: &AutoloadDumpOptions,
- config: &Config,
- _local_repo: &LocalRepository,
- _root_package: &RawPackageData,
- installation_manager: &InstallationManager,
- _target_dir: &str,
- scan_psr_packages: bool,
- suffix: Option<&str>,
- locker: &Locker,
- strict_ambiguous: bool,
- ) -> anyhow::Result<ClassMap> {
- // Mirrors PHP: classmap-authoritative implies PSR scanning so
- // every class gets a fixed map entry.
- let scan = scan_psr_packages || options.class_map_authoritative;
-
- // Mirrors PHP's `if (null === $this->devMode)` branch: read the
- // `dev` flag from `vendor/composer/installed.json` when no
- // explicit dev-mode has been set on the options.
- let dev_mode = match options.dev_mode {
- Some(m) => m,
- None => read_installed_dev_flag(installation_manager.vendor_dir()),
- };
-
- // Mirrors PHP's suffix resolution chain in `dump()`:
- // 1. explicit argument
- // 2. `Config::get('autoloader-suffix')`
- // 3. existing `vendor/autoload.php`'s `ComposerAutoloaderInit{X}`
- // 4. `composer.lock`'s `content-hash` (when locked)
- // 5. random hex
- let resolved_suffix = resolve_suffix(suffix, config, installation_manager, locker)?;
-
- // Mirrors PHP: `$basePath = realpath(getcwd())`. We don't have
- // an explicit project_dir on the generator, but `vendor_dir`'s
- // parent matches the project root for the common
- // `vendor-dir = "vendor"` layout. When the user points
- // `vendor-dir` outside the project we fall back to `.`.
- let project_dir = installation_manager
- .vendor_dir()
- .parent()
- .map(|p| p.to_path_buf())
- .unwrap_or_else(|| PathBuf::from("."));
-
- // Mirrors PHP's `$checkPlatform = $config->get('platform-check') !==
- // false && !($filter instanceof IgnoreAllPlatformRequirementFilter)`.
- let platform_check = if matches!(
- options.platform_requirement_filter,
- PlatformRequirementFilter::IgnoreAll
- ) {
- PlatformCheckMode::Disabled
- } else {
- platform_check_mode_from_config(&config.platform_check)
- };
-
- let cfg = AutoloadConfig {
- project_dir,
- vendor_dir: installation_manager.vendor_dir().to_path_buf(),
- dev_mode,
- suffix: resolved_suffix,
- classmap_authoritative: options.class_map_authoritative,
- optimize: scan,
- apcu: options.apcu,
- apcu_prefix: options.apcu_prefix.clone(),
- // `dump()` does not surface a `--strict-psr` option (that's
- // a separate command-line flag on `dump-autoload`); the
- // generator only reports violations via `ClassMap`.
- strict_psr: false,
- strict_ambiguous,
- platform_check,
- ignore_platform_reqs: matches!(
- options.platform_requirement_filter,
- PlatformRequirementFilter::IgnoreAll
- ),
- };
-
- if options.dry_run {
- // PHP's dry-run still scans and returns the classmap but
- // skips file writes. The current [`generate`] does not
- // expose a dry-run hook, so we return an empty ClassMap
- // for now and surface the limitation here rather than
- // silently writing files.
- return Ok(ClassMap {
- map: BTreeMap::new(),
- psr_violations: Vec::new(),
- ambiguous_classes: BTreeMap::new(),
- });
- }
-
- let result = generate(&cfg)?;
-
- // Mozart's `GenerateResult` only carries summary flags
- // (`class_count`, `has_psr_violations`, `has_ambiguous_classes`),
- // not the actual class-name / path entries that PHP's `ClassMap`
- // exposes. We project the summary onto a `ClassMap` shape so
- // command code that only branches on `count()` / `has_*()` works
- // today; refactoring `generate` to surface the full map is
- // tracked as follow-up work.
- let mut map = BTreeMap::new();
- for i in 0..result.class_count {
- map.insert(format!("__mozart_placeholder_{i}"), String::new());
- }
- let psr_violations = if result.has_psr_violations {
- vec![String::from(
- "PSR-0/4 violation detected (details not yet surfaced)",
- )]
- } else {
- Vec::new()
- };
- let mut ambiguous_classes = BTreeMap::new();
- if result.has_ambiguous_classes {
- ambiguous_classes.insert("__mozart_placeholder".to_string(), Vec::new());
- }
-
- Ok(ClassMap {
- map,
- psr_violations,
- ambiguous_classes,
- })
- }
-}
-
-fn read_installed_dev_flag(vendor_dir: &std::path::Path) -> bool {
- let path = vendor_dir.join("composer/installed.json");
- if !path.exists() {
- return false;
- }
- let Ok(content) = std::fs::read_to_string(&path) else {
- return false;
- };
- let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) else {
- return false;
- };
- value.get("dev").and_then(|v| v.as_bool()).unwrap_or(false)
-}
-
-fn resolve_suffix(
- explicit: Option<&str>,
- config: &Config,
- installation_manager: &InstallationManager,
- locker: &Locker,
-) -> anyhow::Result<String> {
- if let Some(s) = explicit
- && !s.is_empty()
- {
- return Ok(s.to_string());
- }
- if let Some(s) = config.autoloader_suffix.as_ref()
- && !s.is_empty()
- {
- return Ok(s.clone());
- }
- let vendor_path = installation_manager.vendor_dir();
- let autoload_path = vendor_path.join("autoload.php");
- if autoload_path.exists()
- && let Ok(content) = std::fs::read_to_string(&autoload_path)
- && let Some(start) = content.find("ComposerAutoloaderInit")
- {
- let rest = &content[start + "ComposerAutoloaderInit".len()..];
- if let Some(end) = rest.find("::") {
- let candidate = &rest[..end];
- if !candidate.is_empty() && candidate.chars().all(|c| c.is_ascii_hexdigit()) {
- return Ok(candidate.to_string());
- }
- }
- }
- if locker.is_locked()
- && let Some(data) = locker.lock_data()?
- && !data.content_hash.is_empty()
- {
- return Ok(data.content_hash);
- }
- // Fall back to MD5 of the current timestamp (mirrors PHP's
- // `bin2hex(random_bytes(16))` — both produce a 32-char hex token
- // that participates only in classloader naming).
- let ts = format!("{:?}", std::time::SystemTime::now());
- Ok(format!("{:x}", md5::compute(ts.as_bytes())))
-}
-
-fn platform_check_mode_from_config(platform_check: &serde_json::Value) -> PlatformCheckMode {
- match platform_check {
- serde_json::Value::Bool(false) => PlatformCheckMode::Disabled,
- serde_json::Value::Bool(true) => PlatformCheckMode::Full,
- serde_json::Value::String(s) if s == "php-only" => PlatformCheckMode::PhpOnly,
- // Anything else (including JSON null / unknown strings) falls
- // through to `Full` — the safe default that PHP also picks
- // when the value is truthy-but-not-`"php-only"`.
- _ => PlatformCheckMode::Full,
- }
-}
diff --git a/crates/mozart-autoload/src/lib.rs b/crates/mozart-autoload/src/lib.rs
deleted file mode 100644
index 0ee48fe..0000000
--- a/crates/mozart-autoload/src/lib.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-pub mod autoload;
-pub mod dump;
-
-pub use dump::{AutoloadGeneratorExt, ClassMap};