From b696eb7608d921ae0e14a4296e412c33340ceee8 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 22 Feb 2026 16:37:49 +0900 Subject: refactor: reorganize crates to match Composer subpackage structure Rename mozart-constraint to mozart-semver (mirrors composer/semver) and extract mozart-class-map-generator from mozart-autoload (mirrors composer/class-map-generator). No logic changes. Co-Authored-By: Claude Opus 4.6 --- crates/mozart-class-map-generator/src/classmap.rs | 239 ++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 crates/mozart-class-map-generator/src/classmap.rs (limited to 'crates/mozart-class-map-generator/src/classmap.rs') diff --git a/crates/mozart-class-map-generator/src/classmap.rs b/crates/mozart-class-map-generator/src/classmap.rs new file mode 100644 index 0000000..e1631f4 --- /dev/null +++ b/crates/mozart-class-map-generator/src/classmap.rs @@ -0,0 +1,239 @@ +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +/// Recursively collect PHP files from a directory, skipping excluded paths. +pub fn collect_php_files( + dir: &Path, + excluded: &[String], + vendor_dir: &Path, + project_dir: &Path, +) -> Vec { + let mut result = Vec::new(); + if !dir.is_dir() { + return result; + } + collect_php_files_inner(dir, excluded, vendor_dir, project_dir, &mut result); + result +} + +fn collect_php_files_inner( + dir: &Path, + excluded: &[String], + vendor_dir: &Path, + project_dir: &Path, + result: &mut Vec, +) { + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return, + }; + for entry in entries.flatten() { + let path = entry.path(); + + // Check if path matches any excluded pattern + if is_excluded(&path, excluded, vendor_dir, project_dir) { + continue; + } + + if path.is_dir() { + collect_php_files_inner(&path, excluded, vendor_dir, project_dir, result); + } else if crate::php_scanner::is_php_ext(&path) { + result.push(path); + } + } +} + +/// Check whether a path matches any of the excluded patterns. +pub fn is_excluded( + path: &Path, + excluded: &[String], + vendor_dir: &Path, + project_dir: &Path, +) -> bool { + for exc in excluded { + // Excluded patterns can be relative to project_dir or absolute + let exc_path = if Path::new(exc).is_absolute() { + PathBuf::from(exc) + } else { + project_dir.join(exc) + }; + if path.starts_with(&exc_path) || path == exc_path { + return true; + } + // Also check relative to vendor_dir + let exc_vendor = vendor_dir.join(exc); + if path.starts_with(&exc_vendor) || path == exc_vendor { + return true; + } + } + false +} + +/// Scan directories for PHP class declarations and return a classmap. +/// +/// `dirs` is a list of absolute directory paths to scan. +/// Returns a `BTreeMap` where the path expression +/// uses `$vendorDir` or `$baseDir` as appropriate. +pub fn scan_classmap_dirs( + dirs: &[PathBuf], + vendor_dir: &Path, + project_dir: &Path, + excluded: &[String], +) -> BTreeMap { + let mut classmap = BTreeMap::new(); + + for dir in dirs { + let files = collect_php_files(dir, excluded, vendor_dir, project_dir); + for file in files { + match crate::php_scanner::find_classes(&file) { + Ok(classes) => { + for class in classes { + let path_expr = path_to_php_expr(&file, vendor_dir, project_dir); + classmap.entry(class).or_insert(path_expr); + } + } + Err(_) => continue, + } + } + } + + classmap +} + +/// Convert an absolute file path to a PHP path expression using `$vendorDir` or `$baseDir`. +pub fn path_to_php_expr(file: &Path, vendor_dir: &Path, project_dir: &Path) -> String { + if let Ok(rel) = file.strip_prefix(vendor_dir) { + let rel_str = rel.to_string_lossy().replace('\\', "/"); + format!("$vendorDir . '/{rel_str}'") + } else if let Ok(rel) = file.strip_prefix(project_dir) { + let rel_str = rel.to_string_lossy().replace('\\', "/"); + format!("$baseDir . '/{rel_str}'") + } else { + // Fall back to absolute path + let abs = file.to_string_lossy().replace('\\', "/"); + format!("'{abs}'") + } +} + +/// Convert an absolute file path to a static PHP path expression using `__DIR__ . '/..` form. +pub fn path_to_static_expr(file: &Path, vendor_dir: &Path, project_dir: &Path) -> String { + if let Ok(rel) = file.strip_prefix(vendor_dir) { + let rel_str = rel.to_string_lossy().replace('\\', "/"); + format!("__DIR__ . '/..' . '/{rel_str}'") + } else if let Ok(rel) = file.strip_prefix(project_dir) { + let rel_str = rel.to_string_lossy().replace('\\', "/"); + format!("__DIR__ . '/../..' . '/{rel_str}'") + } else { + let abs = file.to_string_lossy().replace('\\', "/"); + format!("'{abs}'") + } +} + +/// Scan PSR-4 and PSR-0 directories for class declarations (used in optimize mode). +/// +/// Returns `(dynamic_classmap, static_classmap, psr_violations)`. +pub fn scan_psr_for_classmap( + psr4: &BTreeMap>, + psr0: &BTreeMap>, + vendor_dir: &Path, + project_dir: &Path, + excluded: &[String], +) -> ( + BTreeMap, + BTreeMap, + Vec, +) { + let mut dyn_map: BTreeMap = BTreeMap::new(); + let mut static_map: BTreeMap = BTreeMap::new(); + let mut violations: Vec = Vec::new(); + + // Helper: resolve a PHP path expression to an absolute path. + let resolve = |expr: &str| -> Option { + // Expressions look like: + // $vendorDir . '/psr/log/src' + // $baseDir . '/src' + // __DIR__ . '/..' . '/psr/log/src' + // __DIR__ . '/../..' . '/src' + if let Some(rest) = expr.strip_prefix("$vendorDir . '") { + let rel = rest.trim_end_matches('\''); + Some(vendor_dir.join(rel.trim_start_matches('/'))) + } else if let Some(rest) = expr.strip_prefix("$baseDir . '") { + let rel = rest.trim_end_matches('\''); + Some(project_dir.join(rel.trim_start_matches('/'))) + } else if expr == "$vendorDir" { + Some(vendor_dir.to_path_buf()) + } else if expr == "$baseDir" { + Some(project_dir.to_path_buf()) + } else { + None + } + }; + + // Scan PSR-4 dirs + for (ns, paths) in psr4 { + for path_expr in paths { + if let Some(abs_dir) = resolve(path_expr) { + let files = collect_php_files(&abs_dir, excluded, vendor_dir, project_dir); + for file in files { + match crate::php_scanner::find_classes(&file) { + Ok(classes) => { + for class in classes { + // PSR-4 validation + let file_str = file.to_string_lossy(); + let dir_str = abs_dir.to_string_lossy(); + let base_ns = ns.as_str(); + if !crate::php_scanner::validate_psr4_class( + &class, base_ns, &file_str, &dir_str, + ) { + violations.push(format!( + "Class {class} in {file_str} does not comply with PSR-4 (namespace prefix: {ns})" + )); + } + let dyn_expr = path_to_php_expr(&file, vendor_dir, project_dir); + let static_expr = + path_to_static_expr(&file, vendor_dir, project_dir); + dyn_map.entry(class.clone()).or_insert(dyn_expr); + static_map.entry(class).or_insert(static_expr); + } + } + Err(_) => continue, + } + } + } + } + } + + // Scan PSR-0 dirs + for (ns, paths) in psr0 { + for path_expr in paths { + if let Some(abs_dir) = resolve(path_expr) { + let files = collect_php_files(&abs_dir, excluded, vendor_dir, project_dir); + for file in files { + match crate::php_scanner::find_classes(&file) { + Ok(classes) => { + for class in classes { + let file_str = file.to_string_lossy(); + let dir_str = abs_dir.to_string_lossy(); + if !crate::php_scanner::validate_psr0_class( + &class, &file_str, &dir_str, + ) { + violations.push(format!( + "Class {class} in {file_str} does not comply with PSR-0 (namespace prefix: {ns})" + )); + } + let dyn_expr = path_to_php_expr(&file, vendor_dir, project_dir); + let static_expr = + path_to_static_expr(&file, vendor_dir, project_dir); + dyn_map.entry(class.clone()).or_insert(dyn_expr); + static_map.entry(class).or_insert(static_expr); + } + } + Err(_) => continue, + } + } + } + } + } + + (dyn_map, static_map, violations) +} -- cgit v1.3.1