1use rustc_hash::FxHashMap;
2use std::path::{Path, PathBuf};
3use thiserror::Error;
4
5#[derive(Debug, Error)]
10pub enum ComposerError {
11 #[error("composer I/O error: {0}")]
12 Io(#[from] std::io::Error),
13 #[error("composer JSON error: {0}")]
14 Json(#[from] serde_json::Error),
15 #[error("composer.json has no autoload section")]
16 MissingAutoload,
17}
18
19#[derive(Clone)]
35pub struct Psr4Map {
36 project_entries: Vec<(String, PathBuf)>,
37 vendor_entries: Vec<(String, PathBuf)>,
38 project_extra_paths: Vec<PathBuf>,
39 vendor_extra_paths: Vec<PathBuf>,
40 classmap: FxHashMap<String, PathBuf>,
47 vendor_eager_files: Vec<PathBuf>,
53 #[allow(dead_code)] root: PathBuf,
55}
56
57fn ensure_trailing_backslash(prefix: &str) -> String {
58 if prefix.ends_with('\\') {
59 prefix.to_string()
60 } else {
61 format!("{prefix}\\")
62 }
63}
64
65fn collect_prefix_dirs(
68 value: &serde_json::Value,
69 prefix: &str,
70 base: &Path,
71 entries: &mut Vec<(String, PathBuf)>,
72) {
73 let pfx = ensure_trailing_backslash(prefix);
74 if let Some(d) = value.as_str() {
75 entries.push((pfx, base.join(d)));
76 } else if let Some(arr) = value.as_array() {
77 for item in arr {
78 if let Some(d) = item.as_str() {
79 entries.push((pfx.clone(), base.join(d)));
80 }
81 }
82 }
83}
84
85fn collect_path_array(value: &serde_json::Value, base: &Path, out: &mut Vec<PathBuf>) {
87 if let Some(arr) = value.as_array() {
88 for item in arr {
89 if let Some(s) = item.as_str() {
90 out.push(base.join(s));
91 }
92 }
93 }
94}
95
96fn parse_autoload_section(
97 autoload: &serde_json::Value,
98 base: &Path,
99 entries: &mut Vec<(String, PathBuf)>,
100 extras: &mut Vec<PathBuf>,
101) {
102 if let Some(map) = autoload.get("psr-4").and_then(|v| v.as_object()) {
103 for (prefix, dir) in map {
104 collect_prefix_dirs(dir, prefix, base, entries);
105 }
106 }
107 if let Some(map) = autoload.get("psr-0").and_then(|v| v.as_object()) {
114 for (_, dir) in map {
115 if let Some(d) = dir.as_str() {
116 extras.push(base.join(d));
117 } else if let Some(arr) = dir.as_array() {
118 for item in arr {
119 if let Some(d) = item.as_str() {
120 extras.push(base.join(d));
121 }
122 }
123 }
124 }
125 }
126 if let Some(cm) = autoload.get("classmap") {
127 collect_path_array(cm, base, extras);
128 }
129 if let Some(files) = autoload.get("files") {
130 collect_path_array(files, base, extras);
131 }
132}
133
134fn parse_composer_autoload_array(
153 content: &str,
154 vendor_dir: &Path,
155 base_dir: &Path,
156) -> Vec<(String, PathBuf)> {
157 let mut out = Vec::new();
158 for line in content.lines() {
159 let line = line.trim();
160 let (key, rest) = match extract_quoted(line) {
162 Some(p) => p,
163 None => continue,
164 };
165 let rest = rest.trim_start();
166 let rest = match rest.strip_prefix("=>") {
167 Some(r) => r.trim_start(),
168 None => continue,
169 };
170 let (var, rest) = match rest.strip_prefix('$') {
171 Some(r) => {
172 let end = r
173 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
174 .unwrap_or(r.len());
175 (&r[..end], &r[end..])
176 }
177 None => continue,
178 };
179 let rest = rest.trim_start();
180 let rest = match rest.strip_prefix('.') {
181 Some(r) => r.trim_start(),
182 None => continue,
183 };
184 let (path_frag, _) = match extract_quoted(rest) {
185 Some(p) => p,
186 None => continue,
187 };
188 let base = match var {
189 "vendorDir" => vendor_dir,
190 "baseDir" => base_dir,
191 _ => continue,
192 };
193 let path_rel = path_frag.trim_start_matches('/');
195 out.push((key, base.join(path_rel)));
196 }
197 out
198}
199
200fn extract_quoted(s: &str) -> Option<(String, &str)> {
204 let mut it = s.char_indices();
205 let (_, quote) = it.next()?;
206 if quote != '\'' && quote != '"' {
207 return None;
208 }
209 let mut out = String::new();
210 let mut escape = false;
211 for (i, ch) in it {
212 if escape {
213 out.push(ch);
214 escape = false;
215 } else if ch == '\\' {
216 escape = true;
217 } else if ch == quote {
218 return Some((out, &s[i + ch.len_utf8()..]));
219 } else {
220 out.push(ch);
221 }
222 }
223 None
224}
225
226fn parse_vendor(root: &Path, entries: &mut Vec<(String, PathBuf)>, extras: &mut Vec<PathBuf>) {
227 let installed_path = root.join("vendor/composer/installed.json");
228 let content = match std::fs::read_to_string(&installed_path) {
229 Ok(c) => c,
230 Err(_) => return,
231 };
232 let value: serde_json::Value = match serde_json::from_str(&content) {
233 Ok(v) => v,
234 Err(_) => return,
235 };
236
237 let packages = if let Some(arr) = value.get("packages").and_then(|v| v.as_array()) {
238 arr.clone()
239 } else if let Some(arr) = value.as_array() {
240 arr.clone()
241 } else {
242 return;
243 };
244
245 let vendor_dir = root.join("vendor");
246
247 for pkg in &packages {
248 let pkg_name = pkg.get("name").and_then(|v| v.as_str()).unwrap_or("");
249 let pkg_dir = vendor_dir.join(pkg_name);
250 if let Some(autoload) = pkg.get("autoload") {
251 parse_autoload_section(autoload, &pkg_dir, entries, extras);
252 }
253 }
254}
255
256fn read_classmap(vendor_dir: &Path, base_dir: &Path) -> FxHashMap<String, PathBuf> {
265 let path = vendor_dir.join("composer/autoload_classmap.php");
266 let Ok(bytes) = std::fs::read(&path) else {
267 return FxHashMap::default();
268 };
269 let content = String::from_utf8_lossy(&bytes);
270 parse_composer_autoload_array(&content, vendor_dir, base_dir)
271 .into_iter()
272 .collect()
273}
274
275fn read_files_autoload(vendor_dir: &Path, base_dir: &Path) -> Vec<PathBuf> {
279 let path = vendor_dir.join("composer/autoload_files.php");
280 if let Ok(bytes) = std::fs::read(&path) {
281 let content = String::from_utf8_lossy(&bytes);
282 return parse_composer_autoload_array(&content, vendor_dir, base_dir)
283 .into_iter()
284 .map(|(_, p)| p)
285 .filter(|p| p.is_file())
286 .collect();
287 }
288 let installed_path = vendor_dir.join("composer/installed.json");
290 let content = match std::fs::read_to_string(&installed_path) {
291 Ok(c) => c,
292 Err(_) => return Vec::new(),
293 };
294 let value: serde_json::Value = match serde_json::from_str(&content) {
295 Ok(v) => v,
296 Err(_) => return Vec::new(),
297 };
298 let packages = if let Some(arr) = value.get("packages").and_then(|v| v.as_array()) {
299 arr.clone()
300 } else if let Some(arr) = value.as_array() {
301 arr.clone()
302 } else {
303 return Vec::new();
304 };
305 let mut out = Vec::new();
306 for pkg in &packages {
307 let pkg_name = pkg.get("name").and_then(|v| v.as_str()).unwrap_or("");
308 let pkg_dir = vendor_dir.join(pkg_name);
309 if let Some(files) = pkg.get("autoload").and_then(|a| a.get("files")) {
310 collect_path_array(files, &pkg_dir, &mut out);
311 }
312 }
313 let _ = base_dir; out.into_iter().filter(|p| p.is_file()).collect()
315}
316
317impl Psr4Map {
318 pub fn from_composer(root: &Path) -> Result<Self, ComposerError> {
319 let composer_path = root.join("composer.json");
320 let content = std::fs::read_to_string(&composer_path)?;
321 let value: serde_json::Value = serde_json::from_str(&content)?;
322
323 let has_autoload = value.get("autoload").is_some() || value.get("autoload-dev").is_some();
324 if !has_autoload {
325 return Err(ComposerError::MissingAutoload);
326 }
327
328 let mut project_entries: Vec<(String, PathBuf)> = Vec::new();
329 let mut project_extra_paths: Vec<PathBuf> = Vec::new();
330
331 if let Some(autoload) = value.get("autoload") {
332 parse_autoload_section(
333 autoload,
334 root,
335 &mut project_entries,
336 &mut project_extra_paths,
337 );
338 }
339 if let Some(autoload) = value.get("autoload-dev") {
340 parse_autoload_section(
341 autoload,
342 root,
343 &mut project_entries,
344 &mut project_extra_paths,
345 );
346 }
347
348 project_entries.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
349
350 let mut vendor_entries: Vec<(String, PathBuf)> = Vec::new();
351 let mut vendor_extra_paths: Vec<PathBuf> = Vec::new();
352 parse_vendor(root, &mut vendor_entries, &mut vendor_extra_paths);
353 vendor_entries.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
354
355 let vendor_dir = root.join("vendor");
359 let classmap = read_classmap(&vendor_dir, root);
360
361 let vendor_eager_files = read_files_autoload(&vendor_dir, root);
365
366 Ok(Psr4Map {
367 project_entries,
368 vendor_entries,
369 project_extra_paths,
370 vendor_extra_paths,
371 classmap,
372 vendor_eager_files,
373 root: root.to_path_buf(),
374 })
375 }
376
377 pub fn project_files(&self) -> Vec<PathBuf> {
378 let mut out = Vec::new();
379 for (_, dir) in &self.project_entries {
380 crate::batch::collect_php_files(dir, &mut out);
381 }
382 for path in &self.project_extra_paths {
383 collect_php_path(path, &mut out);
384 }
385 out
386 }
387
388 pub fn vendor_files(&self) -> Vec<PathBuf> {
389 let mut out = Vec::new();
390 for (_, dir) in &self.vendor_entries {
391 crate::batch::collect_php_files(dir, &mut out);
392 }
393 for path in &self.vendor_extra_paths {
394 collect_php_path(path, &mut out);
395 }
396 out
397 }
398
399 pub fn resolve(&self, fqcn: &str) -> Option<PathBuf> {
410 let key = fqcn.trim_start_matches('\\');
411 for (prefix, dir) in self
412 .project_entries
413 .iter()
414 .chain(self.vendor_entries.iter())
415 {
416 if key.starts_with(prefix.as_str()) {
417 let relative = &key[prefix.len()..];
418 let file_path = dir.join(relative.replace('\\', "/")).with_extension("php");
419 if file_path.exists() {
420 return Some(file_path);
421 }
422 }
423 }
424 if let Some(path) = self.classmap.get(key) {
425 if path.exists() {
426 return Some(path.clone());
427 }
428 }
429 None
430 }
431
432 pub fn vendor_eager_files(&self) -> Vec<PathBuf> {
440 self.vendor_eager_files.clone()
441 }
442
443 pub fn all_vendor_files(&self) -> Vec<PathBuf> {
455 let mut seen: rustc_hash::FxHashSet<PathBuf> = rustc_hash::FxHashSet::default();
456 let mut out = Vec::new();
457 let mut push = |p: PathBuf, out: &mut Vec<PathBuf>| {
458 if seen.insert(p.clone()) {
459 out.push(p);
460 }
461 };
462 for p in self.vendor_files() {
463 push(p, &mut out);
464 }
465 for p in self.classmap.values() {
468 if p.is_file() {
469 push(p.clone(), &mut out);
470 }
471 }
472 for p in self.vendor_eager_files() {
473 push(p, &mut out);
474 }
475 out
476 }
477
478 pub fn classmap_len(&self) -> usize {
481 self.classmap.len()
482 }
483}
484
485fn collect_php_path(path: &Path, out: &mut Vec<PathBuf>) {
488 let Ok(meta) = std::fs::metadata(path) else {
489 return;
490 };
491 if meta.is_file() {
492 if path.extension().and_then(|e| e.to_str()) == Some("php") {
493 out.push(path.to_path_buf());
494 }
495 } else if meta.is_dir() {
496 crate::batch::collect_php_files(path, out);
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use std::fs;
504
505 fn make_temp_project(name: &str) -> PathBuf {
506 let dir = std::env::temp_dir().join(format!("mir_psr4_{name}"));
507 let _ = fs::remove_dir_all(&dir);
508 fs::create_dir_all(&dir).unwrap();
509 dir
510 }
511
512 #[test]
513 fn parse_project_entries() {
514 let root = make_temp_project("parse_project_entries");
515 fs::write(
516 root.join("composer.json"),
517 r#"{
518 "autoload": {
519 "psr-4": { "App\\": "src/", "App\\Models\\": "src/models/" }
520 },
521 "autoload-dev": {
522 "psr-4": { "Tests\\": "tests/" }
523 }
524 }"#,
525 )
526 .unwrap();
527
528 let map = Psr4Map::from_composer(&root).unwrap();
529
530 let prefixes: Vec<&str> = map
531 .project_entries
532 .iter()
533 .map(|(p, _)| p.as_str())
534 .collect();
535 assert!(prefixes.contains(&"App\\Models\\"), "missing App\\Models\\");
536 assert!(prefixes.contains(&"App\\"), "missing App\\");
537 assert!(prefixes.contains(&"Tests\\"), "missing Tests\\");
538 }
539
540 #[test]
541 fn longest_prefix_first() {
542 let root = make_temp_project("longest_prefix_first");
543 fs::write(
544 root.join("composer.json"),
545 r#"{
546 "autoload": {
547 "psr-4": { "App\\": "src/", "App\\Models\\": "src/models/" }
548 }
549 }"#,
550 )
551 .unwrap();
552
553 let map = Psr4Map::from_composer(&root).unwrap();
554
555 assert_eq!(map.project_entries[0].0, "App\\Models\\");
556 }
557
558 #[test]
559 fn missing_autoload_section_is_error() {
560 let root = make_temp_project("missing_autoload");
561 fs::write(root.join("composer.json"), r#"{ "name": "my/pkg" }"#).unwrap();
562
563 let result = Psr4Map::from_composer(&root);
564 assert!(
565 matches!(result, Err(ComposerError::MissingAutoload)),
566 "expected MissingAutoload error"
567 );
568 }
569
570 #[test]
571 fn composer_v2_installed() {
572 let root = make_temp_project("composer_v2");
573 fs::write(
574 root.join("composer.json"),
575 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
576 )
577 .unwrap();
578
579 let vendor_dir = root.join("vendor/composer");
580 fs::create_dir_all(&vendor_dir).unwrap();
581 fs::write(
582 vendor_dir.join("installed.json"),
583 r#"{
584 "packages": [
585 {
586 "name": "vendor/pkg",
587 "autoload": { "psr-4": { "Vendor\\Pkg\\": "src/" } }
588 }
589 ]
590 }"#,
591 )
592 .unwrap();
593 fs::create_dir_all(root.join("vendor/vendor/pkg/src")).unwrap();
594
595 let map = Psr4Map::from_composer(&root).unwrap();
596 let prefixes: Vec<&str> = map.vendor_entries.iter().map(|(p, _)| p.as_str()).collect();
597 assert!(prefixes.contains(&"Vendor\\Pkg\\"), "missing Vendor\\Pkg\\");
598 }
599
600 #[test]
601 fn composer_v1_installed() {
602 let root = make_temp_project("composer_v1");
603 fs::write(
604 root.join("composer.json"),
605 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
606 )
607 .unwrap();
608
609 let vendor_dir = root.join("vendor/composer");
610 fs::create_dir_all(&vendor_dir).unwrap();
611 fs::write(
612 vendor_dir.join("installed.json"),
613 r#"[
614 {
615 "name": "vendor/pkg",
616 "autoload": { "psr-4": { "Vendor\\Pkg\\": "src/" } }
617 }
618 ]"#,
619 )
620 .unwrap();
621 fs::create_dir_all(root.join("vendor/vendor/pkg/src")).unwrap();
622
623 let map = Psr4Map::from_composer(&root).unwrap();
624 let prefixes: Vec<&str> = map.vendor_entries.iter().map(|(p, _)| p.as_str()).collect();
625 assert!(prefixes.contains(&"Vendor\\Pkg\\"), "missing Vendor\\Pkg\\");
626 }
627
628 #[test]
629 fn missing_installed_json() {
630 let root = make_temp_project("missing_installed");
631 fs::write(
632 root.join("composer.json"),
633 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
634 )
635 .unwrap();
636 let map = Psr4Map::from_composer(&root).unwrap();
637 assert!(map.vendor_entries.is_empty());
638 }
639
640 #[test]
641 fn project_files_returns_php_files() {
642 let root = make_temp_project("project_files");
643 let src = root.join("src");
644 fs::create_dir_all(&src).unwrap();
645 fs::write(src.join("Foo.php"), "<?php class Foo {}").unwrap();
646 fs::write(src.join("README.md"), "not php").unwrap();
647 fs::write(
648 root.join("composer.json"),
649 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
650 )
651 .unwrap();
652
653 let map = Psr4Map::from_composer(&root).unwrap();
654 let files = map.project_files();
655 assert_eq!(files.len(), 1);
656 assert!(files[0].ends_with("Foo.php"));
657 }
658
659 #[test]
660 fn resolve_existing_file() {
661 let root = make_temp_project("resolve_existing");
662 let models = root.join("src/models");
663 fs::create_dir_all(&models).unwrap();
664 fs::write(models.join("User.php"), "<?php class User {}").unwrap();
665 fs::write(
666 root.join("composer.json"),
667 r#"{"autoload":{"psr-4":{"App\\Models\\":"src/models/","App\\":"src/"}}}"#,
668 )
669 .unwrap();
670
671 let map = Psr4Map::from_composer(&root).unwrap();
672 let result = map.resolve("App\\Models\\User");
673 assert!(result.is_some(), "expected a resolved path");
674 assert!(result.unwrap().ends_with("User.php"));
675 }
676
677 #[test]
678 fn resolve_missing_file() {
679 let root = make_temp_project("resolve_missing");
680 fs::create_dir_all(root.join("src")).unwrap();
681 fs::write(
682 root.join("composer.json"),
683 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
684 )
685 .unwrap();
686
687 let map = Psr4Map::from_composer(&root).unwrap();
688 let result = map.resolve("App\\Models\\User");
689 assert!(result.is_none());
690 }
691
692 #[test]
693 fn boundary_check() {
694 let root = make_temp_project("boundary_check");
695 fs::create_dir_all(root.join("src")).unwrap();
696 fs::write(
697 root.join("composer.json"),
698 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
699 )
700 .unwrap();
701
702 let map = Psr4Map::from_composer(&root).unwrap();
703 let result = map.resolve("Application\\Foo");
705 assert!(
706 result.is_none(),
707 "App\\ prefix must not match Application\\Foo"
708 );
709 }
710
711 #[test]
712 fn array_valued_psr4_dirs() {
713 let root = make_temp_project("array_dirs");
714 let src = root.join("src");
715 let lib = root.join("lib");
716 fs::create_dir_all(&src).unwrap();
717 fs::create_dir_all(&lib).unwrap();
718 fs::write(src.join("Foo.php"), "<?php class Foo {}").unwrap();
719 fs::write(lib.join("Bar.php"), "<?php class Bar {}").unwrap();
720 fs::write(
721 root.join("composer.json"),
722 r#"{"autoload":{"psr-4":{"App\\":["src/","lib/"]}}}"#,
723 )
724 .unwrap();
725
726 let map = Psr4Map::from_composer(&root).unwrap();
727 assert_eq!(
729 map.project_entries.len(),
730 2,
731 "expected 2 entries for array-valued dir"
732 );
733 let files = map.project_files();
734 assert_eq!(files.len(), 2, "expected Foo.php and Bar.php");
735 }
736
737 #[test]
742 fn project_classmap_dir_is_collected() {
743 let root = make_temp_project("project_classmap");
744 let lib = root.join("lib");
745 fs::create_dir_all(&lib).unwrap();
746 fs::write(lib.join("Legacy.php"), "<?php class Legacy {}").unwrap();
747 fs::write(
748 root.join("composer.json"),
749 r#"{"autoload":{"classmap":["lib/"]}}"#,
750 )
751 .unwrap();
752
753 let map = Psr4Map::from_composer(&root).unwrap();
754 let files = map.project_files();
755 assert_eq!(files.len(), 1);
756 assert!(files[0].ends_with("Legacy.php"));
757 }
758
759 #[test]
760 fn project_files_autoload_is_collected() {
761 let root = make_temp_project("project_files_autoload");
762 fs::write(root.join("helpers.php"), "<?php function my_helper() {}").unwrap();
763 fs::write(
764 root.join("composer.json"),
765 r#"{"autoload":{"files":["helpers.php"]}}"#,
766 )
767 .unwrap();
768
769 let map = Psr4Map::from_composer(&root).unwrap();
770 let files = map.project_files();
771 assert_eq!(files.len(), 1);
772 assert!(files[0].ends_with("helpers.php"));
773 }
774
775 #[test]
776 fn project_psr0_dir_is_collected() {
777 let root = make_temp_project("project_psr0");
778 let lib = root.join("legacy");
779 fs::create_dir_all(&lib).unwrap();
780 fs::write(lib.join("Old.php"), "<?php class Old {}").unwrap();
781 fs::write(
782 root.join("composer.json"),
783 r#"{"autoload":{"psr-0":{"":"legacy/"}}}"#,
784 )
785 .unwrap();
786
787 let map = Psr4Map::from_composer(&root).unwrap();
788 let files = map.project_files();
789 assert_eq!(files.len(), 1);
790 assert!(files[0].ends_with("Old.php"));
791 }
792
793 #[test]
794 fn vendor_classmap_is_collected() {
795 let root = make_temp_project("vendor_classmap");
796 fs::write(
797 root.join("composer.json"),
798 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
799 )
800 .unwrap();
801 let vendor_dir = root.join("vendor/composer");
802 fs::create_dir_all(&vendor_dir).unwrap();
803 fs::write(
804 vendor_dir.join("installed.json"),
805 r#"{
806 "packages": [{
807 "name": "vendor/pkg",
808 "autoload": { "classmap": ["src/"] }
809 }]
810 }"#,
811 )
812 .unwrap();
813 let pkg_src = root.join("vendor/vendor/pkg/src");
814 fs::create_dir_all(&pkg_src).unwrap();
815 fs::write(pkg_src.join("Legacy.php"), "<?php class Legacy {}").unwrap();
816
817 let map = Psr4Map::from_composer(&root).unwrap();
818 let files = map.vendor_files();
819 assert_eq!(files.len(), 1);
820 assert!(files[0].ends_with("Legacy.php"));
821 }
822
823 #[test]
824 fn vendor_files_autoload_is_collected() {
825 let root = make_temp_project("vendor_files_autoload");
826 fs::write(
827 root.join("composer.json"),
828 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
829 )
830 .unwrap();
831 let vendor_dir = root.join("vendor/composer");
832 fs::create_dir_all(&vendor_dir).unwrap();
833 fs::write(
834 vendor_dir.join("installed.json"),
835 r#"{
836 "packages": [{
837 "name": "vendor/pkg",
838 "autoload": { "files": ["bootstrap.php"] }
839 }]
840 }"#,
841 )
842 .unwrap();
843 let pkg_dir = root.join("vendor/vendor/pkg");
844 fs::create_dir_all(&pkg_dir).unwrap();
845 fs::write(
846 pkg_dir.join("bootstrap.php"),
847 "<?php function pkg_bootstrap() {}",
848 )
849 .unwrap();
850
851 let map = Psr4Map::from_composer(&root).unwrap();
852 let files = map.vendor_files();
853 assert_eq!(files.len(), 1);
854 assert!(files[0].ends_with("bootstrap.php"));
855 }
856
857 #[test]
858 fn vendor_psr0_is_collected() {
859 let root = make_temp_project("vendor_psr0");
860 fs::write(
861 root.join("composer.json"),
862 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
863 )
864 .unwrap();
865 let vendor_dir = root.join("vendor/composer");
866 fs::create_dir_all(&vendor_dir).unwrap();
867 fs::write(
868 vendor_dir.join("installed.json"),
869 r#"{
870 "packages": [{
871 "name": "vendor/pkg",
872 "autoload": { "psr-0": { "Old_": "src/" } }
873 }]
874 }"#,
875 )
876 .unwrap();
877 let pkg_src = root.join("vendor/vendor/pkg/src/Old");
878 fs::create_dir_all(&pkg_src).unwrap();
879 fs::write(pkg_src.join("Thing.php"), "<?php class Old_Thing {}").unwrap();
880
881 let map = Psr4Map::from_composer(&root).unwrap();
882 let files = map.vendor_files();
883 assert_eq!(files.len(), 1);
884 assert!(files[0].ends_with("Thing.php"));
885 }
886}