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 classmap_len(&self) -> usize {
446 self.classmap.len()
447 }
448}
449
450fn collect_php_path(path: &Path, out: &mut Vec<PathBuf>) {
453 let Ok(meta) = std::fs::metadata(path) else {
454 return;
455 };
456 if meta.is_file() {
457 if path.extension().and_then(|e| e.to_str()) == Some("php") {
458 out.push(path.to_path_buf());
459 }
460 } else if meta.is_dir() {
461 crate::batch::collect_php_files(path, out);
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use std::fs;
469
470 fn make_temp_project(name: &str) -> PathBuf {
471 let dir = std::env::temp_dir().join(format!("mir_psr4_{name}"));
472 let _ = fs::remove_dir_all(&dir);
473 fs::create_dir_all(&dir).unwrap();
474 dir
475 }
476
477 #[test]
478 fn parse_project_entries() {
479 let root = make_temp_project("parse_project_entries");
480 fs::write(
481 root.join("composer.json"),
482 r#"{
483 "autoload": {
484 "psr-4": { "App\\": "src/", "App\\Models\\": "src/models/" }
485 },
486 "autoload-dev": {
487 "psr-4": { "Tests\\": "tests/" }
488 }
489 }"#,
490 )
491 .unwrap();
492
493 let map = Psr4Map::from_composer(&root).unwrap();
494
495 let prefixes: Vec<&str> = map
496 .project_entries
497 .iter()
498 .map(|(p, _)| p.as_str())
499 .collect();
500 assert!(prefixes.contains(&"App\\Models\\"), "missing App\\Models\\");
501 assert!(prefixes.contains(&"App\\"), "missing App\\");
502 assert!(prefixes.contains(&"Tests\\"), "missing Tests\\");
503 }
504
505 #[test]
506 fn longest_prefix_first() {
507 let root = make_temp_project("longest_prefix_first");
508 fs::write(
509 root.join("composer.json"),
510 r#"{
511 "autoload": {
512 "psr-4": { "App\\": "src/", "App\\Models\\": "src/models/" }
513 }
514 }"#,
515 )
516 .unwrap();
517
518 let map = Psr4Map::from_composer(&root).unwrap();
519
520 assert_eq!(map.project_entries[0].0, "App\\Models\\");
521 }
522
523 #[test]
524 fn missing_autoload_section_is_error() {
525 let root = make_temp_project("missing_autoload");
526 fs::write(root.join("composer.json"), r#"{ "name": "my/pkg" }"#).unwrap();
527
528 let result = Psr4Map::from_composer(&root);
529 assert!(
530 matches!(result, Err(ComposerError::MissingAutoload)),
531 "expected MissingAutoload error"
532 );
533 }
534
535 #[test]
536 fn composer_v2_installed() {
537 let root = make_temp_project("composer_v2");
538 fs::write(
539 root.join("composer.json"),
540 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
541 )
542 .unwrap();
543
544 let vendor_dir = root.join("vendor/composer");
545 fs::create_dir_all(&vendor_dir).unwrap();
546 fs::write(
547 vendor_dir.join("installed.json"),
548 r#"{
549 "packages": [
550 {
551 "name": "vendor/pkg",
552 "autoload": { "psr-4": { "Vendor\\Pkg\\": "src/" } }
553 }
554 ]
555 }"#,
556 )
557 .unwrap();
558 fs::create_dir_all(root.join("vendor/vendor/pkg/src")).unwrap();
559
560 let map = Psr4Map::from_composer(&root).unwrap();
561 let prefixes: Vec<&str> = map.vendor_entries.iter().map(|(p, _)| p.as_str()).collect();
562 assert!(prefixes.contains(&"Vendor\\Pkg\\"), "missing Vendor\\Pkg\\");
563 }
564
565 #[test]
566 fn composer_v1_installed() {
567 let root = make_temp_project("composer_v1");
568 fs::write(
569 root.join("composer.json"),
570 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
571 )
572 .unwrap();
573
574 let vendor_dir = root.join("vendor/composer");
575 fs::create_dir_all(&vendor_dir).unwrap();
576 fs::write(
577 vendor_dir.join("installed.json"),
578 r#"[
579 {
580 "name": "vendor/pkg",
581 "autoload": { "psr-4": { "Vendor\\Pkg\\": "src/" } }
582 }
583 ]"#,
584 )
585 .unwrap();
586 fs::create_dir_all(root.join("vendor/vendor/pkg/src")).unwrap();
587
588 let map = Psr4Map::from_composer(&root).unwrap();
589 let prefixes: Vec<&str> = map.vendor_entries.iter().map(|(p, _)| p.as_str()).collect();
590 assert!(prefixes.contains(&"Vendor\\Pkg\\"), "missing Vendor\\Pkg\\");
591 }
592
593 #[test]
594 fn missing_installed_json() {
595 let root = make_temp_project("missing_installed");
596 fs::write(
597 root.join("composer.json"),
598 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
599 )
600 .unwrap();
601 let map = Psr4Map::from_composer(&root).unwrap();
602 assert!(map.vendor_entries.is_empty());
603 }
604
605 #[test]
606 fn project_files_returns_php_files() {
607 let root = make_temp_project("project_files");
608 let src = root.join("src");
609 fs::create_dir_all(&src).unwrap();
610 fs::write(src.join("Foo.php"), "<?php class Foo {}").unwrap();
611 fs::write(src.join("README.md"), "not php").unwrap();
612 fs::write(
613 root.join("composer.json"),
614 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
615 )
616 .unwrap();
617
618 let map = Psr4Map::from_composer(&root).unwrap();
619 let files = map.project_files();
620 assert_eq!(files.len(), 1);
621 assert!(files[0].ends_with("Foo.php"));
622 }
623
624 #[test]
625 fn resolve_existing_file() {
626 let root = make_temp_project("resolve_existing");
627 let models = root.join("src/models");
628 fs::create_dir_all(&models).unwrap();
629 fs::write(models.join("User.php"), "<?php class User {}").unwrap();
630 fs::write(
631 root.join("composer.json"),
632 r#"{"autoload":{"psr-4":{"App\\Models\\":"src/models/","App\\":"src/"}}}"#,
633 )
634 .unwrap();
635
636 let map = Psr4Map::from_composer(&root).unwrap();
637 let result = map.resolve("App\\Models\\User");
638 assert!(result.is_some(), "expected a resolved path");
639 assert!(result.unwrap().ends_with("User.php"));
640 }
641
642 #[test]
643 fn resolve_missing_file() {
644 let root = make_temp_project("resolve_missing");
645 fs::create_dir_all(root.join("src")).unwrap();
646 fs::write(
647 root.join("composer.json"),
648 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
649 )
650 .unwrap();
651
652 let map = Psr4Map::from_composer(&root).unwrap();
653 let result = map.resolve("App\\Models\\User");
654 assert!(result.is_none());
655 }
656
657 #[test]
658 fn boundary_check() {
659 let root = make_temp_project("boundary_check");
660 fs::create_dir_all(root.join("src")).unwrap();
661 fs::write(
662 root.join("composer.json"),
663 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
664 )
665 .unwrap();
666
667 let map = Psr4Map::from_composer(&root).unwrap();
668 let result = map.resolve("Application\\Foo");
670 assert!(
671 result.is_none(),
672 "App\\ prefix must not match Application\\Foo"
673 );
674 }
675
676 #[test]
677 fn array_valued_psr4_dirs() {
678 let root = make_temp_project("array_dirs");
679 let src = root.join("src");
680 let lib = root.join("lib");
681 fs::create_dir_all(&src).unwrap();
682 fs::create_dir_all(&lib).unwrap();
683 fs::write(src.join("Foo.php"), "<?php class Foo {}").unwrap();
684 fs::write(lib.join("Bar.php"), "<?php class Bar {}").unwrap();
685 fs::write(
686 root.join("composer.json"),
687 r#"{"autoload":{"psr-4":{"App\\":["src/","lib/"]}}}"#,
688 )
689 .unwrap();
690
691 let map = Psr4Map::from_composer(&root).unwrap();
692 assert_eq!(
694 map.project_entries.len(),
695 2,
696 "expected 2 entries for array-valued dir"
697 );
698 let files = map.project_files();
699 assert_eq!(files.len(), 2, "expected Foo.php and Bar.php");
700 }
701
702 #[test]
707 fn project_classmap_dir_is_collected() {
708 let root = make_temp_project("project_classmap");
709 let lib = root.join("lib");
710 fs::create_dir_all(&lib).unwrap();
711 fs::write(lib.join("Legacy.php"), "<?php class Legacy {}").unwrap();
712 fs::write(
713 root.join("composer.json"),
714 r#"{"autoload":{"classmap":["lib/"]}}"#,
715 )
716 .unwrap();
717
718 let map = Psr4Map::from_composer(&root).unwrap();
719 let files = map.project_files();
720 assert_eq!(files.len(), 1);
721 assert!(files[0].ends_with("Legacy.php"));
722 }
723
724 #[test]
725 fn project_files_autoload_is_collected() {
726 let root = make_temp_project("project_files_autoload");
727 fs::write(root.join("helpers.php"), "<?php function my_helper() {}").unwrap();
728 fs::write(
729 root.join("composer.json"),
730 r#"{"autoload":{"files":["helpers.php"]}}"#,
731 )
732 .unwrap();
733
734 let map = Psr4Map::from_composer(&root).unwrap();
735 let files = map.project_files();
736 assert_eq!(files.len(), 1);
737 assert!(files[0].ends_with("helpers.php"));
738 }
739
740 #[test]
741 fn project_psr0_dir_is_collected() {
742 let root = make_temp_project("project_psr0");
743 let lib = root.join("legacy");
744 fs::create_dir_all(&lib).unwrap();
745 fs::write(lib.join("Old.php"), "<?php class Old {}").unwrap();
746 fs::write(
747 root.join("composer.json"),
748 r#"{"autoload":{"psr-0":{"":"legacy/"}}}"#,
749 )
750 .unwrap();
751
752 let map = Psr4Map::from_composer(&root).unwrap();
753 let files = map.project_files();
754 assert_eq!(files.len(), 1);
755 assert!(files[0].ends_with("Old.php"));
756 }
757
758 #[test]
759 fn vendor_classmap_is_collected() {
760 let root = make_temp_project("vendor_classmap");
761 fs::write(
762 root.join("composer.json"),
763 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
764 )
765 .unwrap();
766 let vendor_dir = root.join("vendor/composer");
767 fs::create_dir_all(&vendor_dir).unwrap();
768 fs::write(
769 vendor_dir.join("installed.json"),
770 r#"{
771 "packages": [{
772 "name": "vendor/pkg",
773 "autoload": { "classmap": ["src/"] }
774 }]
775 }"#,
776 )
777 .unwrap();
778 let pkg_src = root.join("vendor/vendor/pkg/src");
779 fs::create_dir_all(&pkg_src).unwrap();
780 fs::write(pkg_src.join("Legacy.php"), "<?php class Legacy {}").unwrap();
781
782 let map = Psr4Map::from_composer(&root).unwrap();
783 let files = map.vendor_files();
784 assert_eq!(files.len(), 1);
785 assert!(files[0].ends_with("Legacy.php"));
786 }
787
788 #[test]
789 fn vendor_files_autoload_is_collected() {
790 let root = make_temp_project("vendor_files_autoload");
791 fs::write(
792 root.join("composer.json"),
793 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
794 )
795 .unwrap();
796 let vendor_dir = root.join("vendor/composer");
797 fs::create_dir_all(&vendor_dir).unwrap();
798 fs::write(
799 vendor_dir.join("installed.json"),
800 r#"{
801 "packages": [{
802 "name": "vendor/pkg",
803 "autoload": { "files": ["bootstrap.php"] }
804 }]
805 }"#,
806 )
807 .unwrap();
808 let pkg_dir = root.join("vendor/vendor/pkg");
809 fs::create_dir_all(&pkg_dir).unwrap();
810 fs::write(
811 pkg_dir.join("bootstrap.php"),
812 "<?php function pkg_bootstrap() {}",
813 )
814 .unwrap();
815
816 let map = Psr4Map::from_composer(&root).unwrap();
817 let files = map.vendor_files();
818 assert_eq!(files.len(), 1);
819 assert!(files[0].ends_with("bootstrap.php"));
820 }
821
822 #[test]
823 fn vendor_psr0_is_collected() {
824 let root = make_temp_project("vendor_psr0");
825 fs::write(
826 root.join("composer.json"),
827 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
828 )
829 .unwrap();
830 let vendor_dir = root.join("vendor/composer");
831 fs::create_dir_all(&vendor_dir).unwrap();
832 fs::write(
833 vendor_dir.join("installed.json"),
834 r#"{
835 "packages": [{
836 "name": "vendor/pkg",
837 "autoload": { "psr-0": { "Old_": "src/" } }
838 }]
839 }"#,
840 )
841 .unwrap();
842 let pkg_src = root.join("vendor/vendor/pkg/src/Old");
843 fs::create_dir_all(&pkg_src).unwrap();
844 fs::write(pkg_src.join("Thing.php"), "<?php class Old_Thing {}").unwrap();
845
846 let map = Psr4Map::from_composer(&root).unwrap();
847 let files = map.vendor_files();
848 assert_eq!(files.len(), 1);
849 assert!(files[0].ends_with("Thing.php"));
850 }
851}