diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-22 16:37:49 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-22 16:37:49 +0900 |
| commit | b696eb7608d921ae0e14a4296e412c33340ceee8 (patch) | |
| tree | 9a6937bed42ee550553fdb118b9281d00cdce4d9 /crates/mozart-autoload/src | |
| parent | 6490fe43676919bc1dcc8659ec4e52da225f92e6 (diff) | |
| download | php-mozart-b696eb7608d921ae0e14a4296e412c33340ceee8.tar.gz php-mozart-b696eb7608d921ae0e14a4296e412c33340ceee8.tar.zst php-mozart-b696eb7608d921ae0e14a4296e412c33340ceee8.zip | |
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 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-autoload/src')
| -rw-r--r-- | crates/mozart-autoload/src/autoload.rs | 233 | ||||
| -rw-r--r-- | crates/mozart-autoload/src/lib.rs | 1 | ||||
| -rw-r--r-- | crates/mozart-autoload/src/php_scanner.rs | 629 |
3 files changed, 1 insertions, 862 deletions
diff --git a/crates/mozart-autoload/src/autoload.rs b/crates/mozart-autoload/src/autoload.rs index 2e4158c..27f75ff 100644 --- a/crates/mozart-autoload/src/autoload.rs +++ b/crates/mozart-autoload/src/autoload.rs @@ -1,3 +1,4 @@ +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, HashSet}; @@ -453,238 +454,6 @@ fn generate_autoload_static(static_data: &AutoloadData, suffix: &str) -> String out } -/// Recursively collect PHP files from a directory, skipping excluded paths. -fn collect_php_files( - dir: &Path, - excluded: &[String], - vendor_dir: &Path, - project_dir: &Path, -) -> Vec<PathBuf> { - 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<PathBuf>, -) { - 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. -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<class_name, file_path_expression>` where the path expression -/// uses `$vendorDir` or `$baseDir` as appropriate. -fn scan_classmap_dirs( - dirs: &[PathBuf], - vendor_dir: &Path, - project_dir: &Path, - excluded: &[String], -) -> BTreeMap<String, String> { - 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`. -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. -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)`. -fn scan_psr_for_classmap( - psr4: &BTreeMap<String, Vec<String>>, - psr0: &BTreeMap<String, Vec<String>>, - vendor_dir: &Path, - project_dir: &Path, - excluded: &[String], -) -> ( - BTreeMap<String, String>, - BTreeMap<String, String>, - Vec<String>, -) { - let mut dyn_map: BTreeMap<String, String> = BTreeMap::new(); - let mut static_map: BTreeMap<String, String> = BTreeMap::new(); - let mut violations: Vec<String> = Vec::new(); - - // Helper: resolve a PHP path expression to an absolute path. - let resolve = |expr: &str| -> Option<PathBuf> { - // 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) -} - /// Generate `vendor/composer/platform_check.php`. /// /// Returns `None` if mode is `Disabled` or there are no relevant requirements. diff --git a/crates/mozart-autoload/src/lib.rs b/crates/mozart-autoload/src/lib.rs index 9d798c6..fc80aed 100644 --- a/crates/mozart-autoload/src/lib.rs +++ b/crates/mozart-autoload/src/lib.rs @@ -1,2 +1 @@ pub mod autoload; -pub mod php_scanner; diff --git a/crates/mozart-autoload/src/php_scanner.rs b/crates/mozart-autoload/src/php_scanner.rs deleted file mode 100644 index 3d0d51d..0000000 --- a/crates/mozart-autoload/src/php_scanner.rs +++ /dev/null @@ -1,629 +0,0 @@ -use anyhow::Result; -use regex::Regex; -use std::path::Path; - -/// File extensions considered PHP source files for class scanning. -const PHP_EXTENSIONS: &[&str] = &["php", "inc", "hh"]; - -/// Check if a file path has a PHP-like extension. -fn is_php_file(path: &Path) -> bool { - is_php_ext(path) -} - -/// Public version of the PHP extension check, used by the autoload scanner. -pub fn is_php_ext(path: &Path) -> bool { - path.extension() - .and_then(|e| e.to_str()) - .map(|ext| PHP_EXTENSIONS.iter().any(|&e| ext.eq_ignore_ascii_case(e))) - .unwrap_or(false) -} - -/// Scan a PHP file and return the list of fully-qualified class names declared in it. -/// -/// Returns an empty vec if the file has no relevant extension or no class declarations. -pub fn find_classes(path: &Path) -> Result<Vec<String>> { - if !is_php_file(path) { - return Ok(vec![]); - } - - let contents = std::fs::read_to_string(path)?; - - // Quick check: does the file even contain a class-like keyword? - let quick_re = Regex::new(r"(?i)\b(?:class|interface|trait|enum)\s").unwrap(); - if !quick_re.is_match(&contents) { - return Ok(vec![]); - } - - let cleaned = clean_php_content(&contents); - Ok(extract_declarations(&cleaned)) -} - -/// State machine that strips strings, comments, and heredocs/nowdocs from PHP code. -/// -/// Returns a string of equal byte length where non-PHP content is replaced with spaces -/// so that regex offsets are preserved. Only PHP mode content is kept; everything else -/// is blanked out. -fn clean_php_content(contents: &str) -> String { - let bytes = contents.as_bytes(); - let len = bytes.len(); - let mut out = vec![b' '; len]; - let mut i = 0; - let mut in_php = false; - - while i < len { - if !in_php { - // Look for `<?` - if i + 1 < len && bytes[i] == b'<' && bytes[i + 1] == b'?' { - in_php = true; - out[i] = b' '; - out[i + 1] = b' '; - i += 2; - // Skip optional "php" or "=" - if i + 3 <= len && bytes[i..i + 3].eq_ignore_ascii_case(b"php") { - i += 3; - } else if i < len && bytes[i] == b'=' { - i += 1; - } - continue; - } - i += 1; - continue; - } - - // In PHP mode - // Check for `?>` - if i + 1 < len && bytes[i] == b'?' && bytes[i + 1] == b'>' { - in_php = false; - i += 2; - continue; - } - - // Line comment: // or # - if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'/' { - // Skip to end of line - while i < len && bytes[i] != b'\n' { - i += 1; - } - continue; - } - if bytes[i] == b'#' { - while i < len && bytes[i] != b'\n' { - i += 1; - } - continue; - } - - // Block comment: /* ... */ - if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' { - i += 2; - while i + 1 < len { - if bytes[i] == b'*' && bytes[i + 1] == b'/' { - i += 2; - break; - } - i += 1; - } - continue; - } - - // Single-quoted string - if bytes[i] == b'\'' { - out[i] = b'\''; - i += 1; - while i < len { - if bytes[i] == b'\\' && i + 1 < len { - // escaped character — blank both - i += 2; - } else if bytes[i] == b'\'' { - out[i] = b'\''; - i += 1; - break; - } else { - i += 1; - } - } - continue; - } - - // Double-quoted string - if bytes[i] == b'"' { - out[i] = b'"'; - i += 1; - while i < len { - if bytes[i] == b'\\' && i + 1 < len { - i += 2; - } else if bytes[i] == b'"' { - out[i] = b'"'; - i += 1; - break; - } else { - i += 1; - } - } - continue; - } - - // Heredoc / Nowdoc: <<< - if i + 2 < len && bytes[i] == b'<' && bytes[i + 1] == b'<' && bytes[i + 2] == b'<' { - i += 3; - // Skip whitespace - while i < len && (bytes[i] == b' ' || bytes[i] == b'\t') { - i += 1; - } - - // Nowdoc uses single quotes around label; heredoc may use double quotes. - let is_nowdoc = i < len && bytes[i] == b'\''; - // Skip optional opening quote (single for nowdoc, double for heredoc) - if i < len && (bytes[i] == b'\'' || bytes[i] == b'"') { - i += 1; - } - - // Read label - let label_start = i; - while i < len && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { - i += 1; - } - let label = std::str::from_utf8(&bytes[label_start..i]) - .unwrap_or("") - .to_string(); - - // Skip closing quote of label (must match the opening quote) - let expected_close = if is_nowdoc { b'\'' } else { b'"' }; - if i < len && bytes[i] == expected_close { - i += 1; - } - - // Skip to end of line - while i < len && bytes[i] != b'\n' { - i += 1; - } - if i < len { - i += 1; // consume newline - } - - // Scan for the terminator label on its own line - if !label.is_empty() { - loop { - if i >= len { - break; - } - // Check if current line starts with the label - let line_start = i; - // Skip optional whitespace for indented heredoc (PHP 7.3+) - while i < len && (bytes[i] == b' ' || bytes[i] == b'\t') { - i += 1; - } - let remaining = &bytes[i..]; - let label_bytes = label.as_bytes(); - if remaining.len() >= label_bytes.len() - && &remaining[..label_bytes.len()] == label_bytes - { - let after = i + label_bytes.len(); - // Terminator must be followed by ; or newline or EOF - if after >= len - || bytes[after] == b';' - || bytes[after] == b'\n' - || bytes[after] == b'\r' - { - // Skip to end of this line - i = after; - while i < len && bytes[i] != b'\n' { - i += 1; - } - if i < len { - i += 1; - } - break; - } - } - // Not a terminator line — skip to end of line - i = line_start; - while i < len && bytes[i] != b'\n' { - i += 1; - } - if i < len { - i += 1; - } - } - } - continue; - } - - // Backtick strings (shell exec) - if bytes[i] == b'`' { - out[i] = b'`'; - i += 1; - while i < len { - if bytes[i] == b'\\' && i + 1 < len { - i += 2; - } else if bytes[i] == b'`' { - out[i] = b'`'; - i += 1; - break; - } else { - i += 1; - } - } - continue; - } - - // Keep normal PHP content - out[i] = bytes[i]; - i += 1; - } - - String::from_utf8_lossy(&out).into_owned() -} - -/// Extract fully-qualified class names from cleaned PHP content. -/// -/// Tracks the current namespace and finds class/interface/trait/enum declarations. -fn extract_declarations(cleaned: &str) -> Vec<String> { - let mut results = Vec::new(); - - // Regex for namespace declarations: - // namespace Foo\Bar; — simple - // namespace Foo\Bar { — block - // namespace { — global block - let ns_re = Regex::new( - r"(?x) - \bnamespace\s+ - ((?:[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*\\)*[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*) - \s*[;{] - | - \bnamespace\s*\{ - ", - ) - .unwrap(); - - // Regex for class/interface/trait/enum declarations. - // We need to capture the name; anonymous classes (new class ...) are excluded. - let decl_re = Regex::new( - r"(?x) - \b(?:abstract\s+|final\s+|readonly\s+)* - (?P<kind>class|interface|trait|enum)\s+ - (?P<name>[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*) - ", - ) - .unwrap(); - - let mut current_ns = String::new(); - - // We process namespace changes as we walk through the file. - // Build a list of all namespace and declaration positions. - #[derive(Debug)] - enum Event { - Namespace(usize, String), // position, namespace - Declaration(usize, String), // position, simple name - } - - let mut events: Vec<Event> = Vec::new(); - - // Find namespace declarations - for cap in ns_re.captures_iter(cleaned) { - let pos = cap.get(0).unwrap().start(); - let ns_name = cap - .get(1) - .map(|m| m.as_str().to_string()) - .unwrap_or_default(); - events.push(Event::Namespace(pos, ns_name)); - } - - // Find class/interface/trait/enum declarations - for cap in decl_re.captures_iter(cleaned) { - let pos = cap.get(0).unwrap().start(); - let name = cap.name("name").unwrap().as_str().to_string(); - - // Skip anonymous classes: check if "new" precedes "class" on the same "expression". - // A reliable check: look back for "new " before this match. - let before = &cleaned[..pos]; - let kind = cap.name("kind").unwrap().as_str(); - if kind == "class" { - // Check if "new" appears right before (with possible whitespace/modifiers). - // Simple heuristic: scan backwards for non-whitespace token. - let trimmed = before.trim_end(); - if trimmed.ends_with("new") { - continue; - } - } - - events.push(Event::Declaration(pos, name)); - } - - // Sort all events by position - events.sort_by_key(|e| match e { - Event::Namespace(pos, _) => *pos, - Event::Declaration(pos, _) => *pos, - }); - - // Process events in order - for event in events { - match event { - Event::Namespace(_, ns) => { - current_ns = ns; - } - Event::Declaration(_, name) => { - let fqn = if current_ns.is_empty() { - name - } else { - format!("{}\\{}", current_ns, name) - }; - results.push(fqn); - } - } - } - - results -} - -/// Validate that a class file is correctly placed according to PSR-4. -/// -/// - `class`: fully-qualified class name (e.g. `Foo\Bar\Baz`) -/// - `base_namespace`: the PSR-4 namespace prefix (e.g. `Foo\Bar\`) -/// - `file_path`: absolute path to the PHP file -/// - `base_path`: the directory mapped to `base_namespace` (absolute) -/// -/// Returns `true` if the file path matches the PSR-4 mapping. -pub fn validate_psr4_class( - class: &str, - base_namespace: &str, - file_path: &str, - base_path: &str, -) -> bool { - // Normalize the base namespace: ensure it ends with `\` - let base_ns = if base_namespace.is_empty() || base_namespace.ends_with('\\') { - base_namespace.to_string() - } else { - format!("{base_namespace}\\") - }; - - // Class must start with the base namespace - if !class.starts_with(&*base_ns) { - return false; - } - - // The relative class name after the base namespace - let relative_class = &class[base_ns.len()..]; - - // Convert relative class to a relative file path: replace `\` with `/` - let expected_relative = relative_class.replace('\\', "/"); - let expected_file = format!( - "{}/{}.php", - base_path.trim_end_matches('/'), - expected_relative - ); - - // Normalize both paths for comparison (simplistic: just compare strings) - Path::new(file_path) == Path::new(&expected_file) -} - -/// Validate that a class file is correctly placed according to PSR-0. -/// -/// - `class`: fully-qualified class name (e.g. `Foo_Bar_Baz` or `Foo\Bar`) -/// - `file_path`: absolute path to the PHP file -/// - `base_path`: the base directory for PSR-0 lookup -/// -/// Returns `true` if the file path matches the PSR-0 mapping. -pub fn validate_psr0_class(class: &str, file_path: &str, base_path: &str) -> bool { - // PSR-0: namespace separators AND underscores (in class part) map to directory separators. - // Split on `\` first; the last segment may contain underscores that also become `/`. - let parts: Vec<&str> = class.split('\\').collect(); - let relative = if parts.len() == 1 { - // No namespace: underscores in class name become dir separators - parts[0].replace('_', "/") - } else { - let ns_part = parts[..parts.len() - 1].join("/"); - let class_part = parts[parts.len() - 1].replace('_', "/"); - format!("{}/{}", ns_part, class_part) - }; - - let expected_file = format!("{}/{}.php", base_path.trim_end_matches('/'), relative); - Path::new(file_path) == Path::new(&expected_file) -} - -// ───────────────────────────────────────────────────────────────────────────── -// Tests -// ───────────────────────────────────────────────────────────────────────────── - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Write; - use tempfile::NamedTempFile; - - fn write_php(content: &str) -> NamedTempFile { - let mut f = NamedTempFile::with_suffix(".php").unwrap(); - f.write_all(content.as_bytes()).unwrap(); - f - } - - // ------------------------------------------------------------------------- - // find_classes tests - // ------------------------------------------------------------------------- - - #[test] - fn test_find_classes_simple_class() { - let f = write_php("<?php\nclass Foo {}\n"); - let classes = find_classes(f.path()).unwrap(); - assert_eq!(classes, vec!["Foo"]); - } - - #[test] - fn test_find_classes_with_namespace() { - let f = write_php("<?php\nnamespace Foo\\Bar;\nclass Baz {}\n"); - let classes = find_classes(f.path()).unwrap(); - assert_eq!(classes, vec!["Foo\\Bar\\Baz"]); - } - - #[test] - fn test_find_classes_multiple_classes() { - let f = write_php("<?php\nnamespace App;\nclass Foo {}\nclass Bar {}\ninterface Baz {}\n"); - let classes = find_classes(f.path()).unwrap(); - assert_eq!(classes, vec!["App\\Foo", "App\\Bar", "App\\Baz"]); - } - - #[test] - fn test_find_classes_interface() { - let f = write_php("<?php\ninterface MyInterface {}\n"); - let classes = find_classes(f.path()).unwrap(); - assert_eq!(classes, vec!["MyInterface"]); - } - - #[test] - fn test_find_classes_trait() { - let f = write_php("<?php\ntrait MyTrait {}\n"); - let classes = find_classes(f.path()).unwrap(); - assert_eq!(classes, vec!["MyTrait"]); - } - - #[test] - fn test_find_classes_enum() { - let f = write_php("<?php\nenum Status {}\n"); - let classes = find_classes(f.path()).unwrap(); - assert_eq!(classes, vec!["Status"]); - } - - #[test] - fn test_find_classes_enum_with_backing_type() { - let f = write_php("<?php\nenum Color: string { case Red = 'red'; }\n"); - let classes = find_classes(f.path()).unwrap(); - assert_eq!(classes, vec!["Color"]); - } - - #[test] - fn test_find_classes_anonymous_class_skipped() { - let f = write_php("<?php\n$obj = new class {};\n"); - let classes = find_classes(f.path()).unwrap(); - assert!(classes.is_empty(), "anonymous class should not be scanned"); - } - - #[test] - fn test_find_classes_comments_ignored() { - let f = write_php( - "<?php\n// class FakeClass {}\n/* interface FakeInterface {} */\nclass RealClass {}\n", - ); - let classes = find_classes(f.path()).unwrap(); - assert_eq!(classes, vec!["RealClass"]); - } - - #[test] - fn test_find_classes_strings_ignored() { - let f = write_php( - "<?php\n$s = 'class NotAClass {}';\n$t = \"interface NotAnInterface {}\";\nclass RealClass {}\n", - ); - let classes = find_classes(f.path()).unwrap(); - assert_eq!(classes, vec!["RealClass"]); - } - - #[test] - fn test_find_classes_heredoc_ignored() { - let f = write_php("<?php\n$s = <<<EOT\nclass FakeClass {}\nEOT;\nclass RealClass {}\n"); - let classes = find_classes(f.path()).unwrap(); - assert_eq!(classes, vec!["RealClass"]); - } - - #[test] - fn test_find_classes_empty_file() { - let f = write_php("<?php\n// nothing here\n"); - let classes = find_classes(f.path()).unwrap(); - assert!(classes.is_empty()); - } - - #[test] - fn test_find_classes_no_classes() { - let f = write_php("<?php\necho 'hello';\n"); - let classes = find_classes(f.path()).unwrap(); - assert!(classes.is_empty()); - } - - #[test] - fn test_find_classes_abstract_final() { - let f = write_php("<?php\nabstract class AbstractFoo {}\nfinal class FinalBar {}\n"); - let classes = find_classes(f.path()).unwrap(); - assert!(classes.contains(&"AbstractFoo".to_string())); - assert!(classes.contains(&"FinalBar".to_string())); - } - - #[test] - fn test_find_classes_non_php_extension() { - let mut f = NamedTempFile::with_suffix(".txt").unwrap(); - f.write_all(b"<?php\nclass Foo {}\n").unwrap(); - let classes = find_classes(f.path()).unwrap(); - assert!(classes.is_empty(), "non-PHP extension should be skipped"); - } - - // ------------------------------------------------------------------------- - // PSR-4 validation tests - // ------------------------------------------------------------------------- - - #[test] - fn test_validate_psr4_correct() { - assert!(validate_psr4_class( - "Foo\\Bar\\Baz", - "Foo\\Bar\\", - "/srv/project/src/Baz.php", - "/srv/project/src" - )); - } - - #[test] - fn test_validate_psr4_wrong_path() { - assert!(!validate_psr4_class( - "Foo\\Bar\\Baz", - "Foo\\Bar\\", - "/srv/project/src/Wrong.php", - "/srv/project/src" - )); - } - - #[test] - fn test_validate_psr4_namespace_mismatch() { - assert!(!validate_psr4_class( - "Other\\Baz", - "Foo\\Bar\\", - "/srv/project/src/Baz.php", - "/srv/project/src" - )); - } - - #[test] - fn test_validate_psr4_nested() { - assert!(validate_psr4_class( - "App\\Http\\Controllers\\HomeController", - "App\\", - "/project/src/Http/Controllers/HomeController.php", - "/project/src" - )); - } - - // ------------------------------------------------------------------------- - // PSR-0 validation tests - // ------------------------------------------------------------------------- - - #[test] - fn test_validate_psr0_simple() { - assert!(validate_psr0_class( - "Foo_Bar_Baz", - "/srv/project/src/Foo/Bar/Baz.php", - "/srv/project/src" - )); - } - - #[test] - fn test_validate_psr0_with_namespace() { - assert!(validate_psr0_class( - "Foo\\Bar", - "/srv/project/src/Foo/Bar.php", - "/srv/project/src" - )); - } - - #[test] - fn test_validate_psr0_wrong_path() { - assert!(!validate_psr0_class( - "Foo_Bar", - "/srv/project/src/Foo/Baz.php", - "/srv/project/src" - )); - } -} |
