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(e) => {
235 eprintln!(
236 "mir: warning: failed to parse {}: {e} (vendor PSR-4 map will be empty)",
237 installed_path.display()
238 );
239 return;
240 }
241 };
242
243 let packages = if let Some(arr) = value.get("packages").and_then(|v| v.as_array()) {
244 arr.clone()
245 } else if let Some(arr) = value.as_array() {
246 arr.clone()
247 } else {
248 return;
249 };
250
251 let vendor_dir = root.join("vendor");
252
253 for pkg in &packages {
254 let pkg_name = pkg.get("name").and_then(|v| v.as_str()).unwrap_or("");
255 let pkg_dir = vendor_dir.join(pkg_name);
256 if let Some(autoload) = pkg.get("autoload") {
257 parse_autoload_section(autoload, &pkg_dir, entries, extras);
258 }
259 }
260}
261
262fn read_classmap(vendor_dir: &Path, base_dir: &Path) -> FxHashMap<String, PathBuf> {
271 let path = vendor_dir.join("composer/autoload_classmap.php");
272 let Ok(bytes) = std::fs::read(&path) else {
273 return FxHashMap::default();
274 };
275 let content = String::from_utf8_lossy(&bytes);
276 parse_composer_autoload_array(&content, vendor_dir, base_dir)
277 .into_iter()
278 .collect()
279}
280
281fn read_files_autoload(vendor_dir: &Path, base_dir: &Path) -> Vec<PathBuf> {
285 let path = vendor_dir.join("composer/autoload_files.php");
286 if let Ok(bytes) = std::fs::read(&path) {
287 let content = String::from_utf8_lossy(&bytes);
288 return parse_composer_autoload_array(&content, vendor_dir, base_dir)
289 .into_iter()
290 .map(|(_, p)| p)
291 .filter(|p| p.is_file())
292 .collect();
293 }
294 let installed_path = vendor_dir.join("composer/installed.json");
296 let content = match std::fs::read_to_string(&installed_path) {
297 Ok(c) => c,
298 Err(_) => return Vec::new(),
299 };
300 let value: serde_json::Value = match serde_json::from_str(&content) {
301 Ok(v) => v,
302 Err(e) => {
303 eprintln!(
304 "mir: warning: failed to parse {}: {e} (autoload.files from vendor will be empty)",
305 installed_path.display()
306 );
307 return Vec::new();
308 }
309 };
310 let packages = if let Some(arr) = value.get("packages").and_then(|v| v.as_array()) {
311 arr.clone()
312 } else if let Some(arr) = value.as_array() {
313 arr.clone()
314 } else {
315 return Vec::new();
316 };
317 let mut out = Vec::new();
318 for pkg in &packages {
319 let pkg_name = pkg.get("name").and_then(|v| v.as_str()).unwrap_or("");
320 let pkg_dir = vendor_dir.join(pkg_name);
321 if let Some(files) = pkg.get("autoload").and_then(|a| a.get("files")) {
322 collect_path_array(files, &pkg_dir, &mut out);
323 }
324 }
325 let _ = base_dir; out.into_iter().filter(|p| p.is_file()).collect()
327}
328
329impl Psr4Map {
330 pub fn from_composer(root: &Path) -> Result<Self, ComposerError> {
331 let composer_path = root.join("composer.json");
332 let content = std::fs::read_to_string(&composer_path)?;
333 let value: serde_json::Value = serde_json::from_str(&content)?;
334
335 let has_autoload = value.get("autoload").is_some() || value.get("autoload-dev").is_some();
336 if !has_autoload {
337 return Err(ComposerError::MissingAutoload);
338 }
339
340 let mut project_entries: Vec<(String, PathBuf)> = Vec::new();
341 let mut project_extra_paths: Vec<PathBuf> = Vec::new();
342
343 if let Some(autoload) = value.get("autoload") {
344 parse_autoload_section(
345 autoload,
346 root,
347 &mut project_entries,
348 &mut project_extra_paths,
349 );
350 }
351 if let Some(autoload) = value.get("autoload-dev") {
352 parse_autoload_section(
353 autoload,
354 root,
355 &mut project_entries,
356 &mut project_extra_paths,
357 );
358 }
359
360 project_entries.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
361
362 let mut vendor_entries: Vec<(String, PathBuf)> = Vec::new();
363 let mut vendor_extra_paths: Vec<PathBuf> = Vec::new();
364 parse_vendor(root, &mut vendor_entries, &mut vendor_extra_paths);
365 vendor_entries.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
366
367 let vendor_dir = root.join("vendor");
371 let classmap = read_classmap(&vendor_dir, root);
372
373 let vendor_eager_files = read_files_autoload(&vendor_dir, root);
377
378 Ok(Psr4Map {
379 project_entries,
380 vendor_entries,
381 project_extra_paths,
382 vendor_extra_paths,
383 classmap,
384 vendor_eager_files,
385 root: root.to_path_buf(),
386 })
387 }
388
389 pub fn project_files(&self) -> Vec<PathBuf> {
390 let mut out = Vec::new();
391 for (_, dir) in &self.project_entries {
392 crate::batch::collect_php_files(dir, &mut out);
393 }
394 for path in &self.project_extra_paths {
395 collect_php_path(path, &mut out);
396 }
397 out
398 }
399
400 pub fn vendor_files(&self) -> Vec<PathBuf> {
401 let mut out = Vec::new();
402 for (_, dir) in &self.vendor_entries {
403 crate::batch::collect_php_files(dir, &mut out);
404 }
405 for path in &self.vendor_extra_paths {
406 collect_php_path(path, &mut out);
407 }
408 out
409 }
410
411 pub fn resolve(&self, fqcn: &str) -> Option<PathBuf> {
422 let key = fqcn.trim_start_matches('\\');
423 for (prefix, dir) in self
424 .project_entries
425 .iter()
426 .chain(self.vendor_entries.iter())
427 {
428 if key.starts_with(prefix.as_str()) {
429 let relative = &key[prefix.len()..];
430 let file_path = dir.join(relative.replace('\\', "/")).with_extension("php");
431 if file_path.exists() {
432 return Some(file_path);
433 }
434 }
435 }
436 if let Some(path) = self.classmap.get(key) {
437 if path.exists() {
438 return Some(path.clone());
439 }
440 }
441 None
442 }
443
444 pub fn vendor_eager_files(&self) -> Vec<PathBuf> {
452 self.vendor_eager_files.clone()
453 }
454
455 pub fn all_vendor_files(&self) -> Vec<PathBuf> {
467 let mut seen: rustc_hash::FxHashSet<PathBuf> = rustc_hash::FxHashSet::default();
468 let mut out = Vec::new();
469 let mut push = |p: PathBuf, out: &mut Vec<PathBuf>| {
470 if seen.insert(p.clone()) {
471 out.push(p);
472 }
473 };
474 for p in self.vendor_files() {
475 push(p, &mut out);
476 }
477 for p in self.classmap.values() {
480 if p.is_file() {
481 push(p.clone(), &mut out);
482 }
483 }
484 for p in self.vendor_eager_files() {
485 push(p, &mut out);
486 }
487 out
488 }
489
490 pub fn classmap_len(&self) -> usize {
493 self.classmap.len()
494 }
495}
496
497fn collect_php_path(path: &Path, out: &mut Vec<PathBuf>) {
500 let Ok(meta) = std::fs::metadata(path) else {
501 return;
502 };
503 if meta.is_file() {
504 if path.extension().and_then(|e| e.to_str()) == Some("php") {
505 out.push(path.to_path_buf());
506 }
507 } else if meta.is_dir() {
508 crate::batch::collect_php_files(path, out);
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use std::fs;
516
517 fn make_temp_project(name: &str) -> PathBuf {
518 let dir = std::env::temp_dir().join(format!("mir_psr4_{name}"));
519 let _ = fs::remove_dir_all(&dir);
520 fs::create_dir_all(&dir).unwrap();
521 dir
522 }
523
524 #[test]
525 fn parse_project_entries() {
526 let root = make_temp_project("parse_project_entries");
527 fs::write(
528 root.join("composer.json"),
529 r#"{
530 "autoload": {
531 "psr-4": { "App\\": "src/", "App\\Models\\": "src/models/" }
532 },
533 "autoload-dev": {
534 "psr-4": { "Tests\\": "tests/" }
535 }
536 }"#,
537 )
538 .unwrap();
539
540 let map = Psr4Map::from_composer(&root).unwrap();
541
542 let prefixes: Vec<&str> = map
543 .project_entries
544 .iter()
545 .map(|(p, _)| p.as_str())
546 .collect();
547 assert!(prefixes.contains(&"App\\Models\\"), "missing App\\Models\\");
548 assert!(prefixes.contains(&"App\\"), "missing App\\");
549 assert!(prefixes.contains(&"Tests\\"), "missing Tests\\");
550 }
551
552 #[test]
553 fn longest_prefix_first() {
554 let root = make_temp_project("longest_prefix_first");
555 fs::write(
556 root.join("composer.json"),
557 r#"{
558 "autoload": {
559 "psr-4": { "App\\": "src/", "App\\Models\\": "src/models/" }
560 }
561 }"#,
562 )
563 .unwrap();
564
565 let map = Psr4Map::from_composer(&root).unwrap();
566
567 assert_eq!(map.project_entries[0].0, "App\\Models\\");
568 }
569
570 #[test]
571 fn missing_autoload_section_is_error() {
572 let root = make_temp_project("missing_autoload");
573 fs::write(root.join("composer.json"), r#"{ "name": "my/pkg" }"#).unwrap();
574
575 let result = Psr4Map::from_composer(&root);
576 assert!(
577 matches!(result, Err(ComposerError::MissingAutoload)),
578 "expected MissingAutoload error"
579 );
580 }
581
582 #[test]
583 fn composer_v2_installed() {
584 let root = make_temp_project("composer_v2");
585 fs::write(
586 root.join("composer.json"),
587 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
588 )
589 .unwrap();
590
591 let vendor_dir = root.join("vendor/composer");
592 fs::create_dir_all(&vendor_dir).unwrap();
593 fs::write(
594 vendor_dir.join("installed.json"),
595 r#"{
596 "packages": [
597 {
598 "name": "vendor/pkg",
599 "autoload": { "psr-4": { "Vendor\\Pkg\\": "src/" } }
600 }
601 ]
602 }"#,
603 )
604 .unwrap();
605 fs::create_dir_all(root.join("vendor/vendor/pkg/src")).unwrap();
606
607 let map = Psr4Map::from_composer(&root).unwrap();
608 let prefixes: Vec<&str> = map.vendor_entries.iter().map(|(p, _)| p.as_str()).collect();
609 assert!(prefixes.contains(&"Vendor\\Pkg\\"), "missing Vendor\\Pkg\\");
610 }
611
612 #[test]
613 fn composer_v1_installed() {
614 let root = make_temp_project("composer_v1");
615 fs::write(
616 root.join("composer.json"),
617 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
618 )
619 .unwrap();
620
621 let vendor_dir = root.join("vendor/composer");
622 fs::create_dir_all(&vendor_dir).unwrap();
623 fs::write(
624 vendor_dir.join("installed.json"),
625 r#"[
626 {
627 "name": "vendor/pkg",
628 "autoload": { "psr-4": { "Vendor\\Pkg\\": "src/" } }
629 }
630 ]"#,
631 )
632 .unwrap();
633 fs::create_dir_all(root.join("vendor/vendor/pkg/src")).unwrap();
634
635 let map = Psr4Map::from_composer(&root).unwrap();
636 let prefixes: Vec<&str> = map.vendor_entries.iter().map(|(p, _)| p.as_str()).collect();
637 assert!(prefixes.contains(&"Vendor\\Pkg\\"), "missing Vendor\\Pkg\\");
638 }
639
640 #[test]
641 fn missing_installed_json() {
642 let root = make_temp_project("missing_installed");
643 fs::write(
644 root.join("composer.json"),
645 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
646 )
647 .unwrap();
648 let map = Psr4Map::from_composer(&root).unwrap();
649 assert!(map.vendor_entries.is_empty());
650 }
651
652 #[test]
653 fn project_files_returns_php_files() {
654 let root = make_temp_project("project_files");
655 let src = root.join("src");
656 fs::create_dir_all(&src).unwrap();
657 fs::write(src.join("Foo.php"), "<?php class Foo {}").unwrap();
658 fs::write(src.join("README.md"), "not php").unwrap();
659 fs::write(
660 root.join("composer.json"),
661 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
662 )
663 .unwrap();
664
665 let map = Psr4Map::from_composer(&root).unwrap();
666 let files = map.project_files();
667 assert_eq!(files.len(), 1);
668 assert!(files[0].ends_with("Foo.php"));
669 }
670
671 #[test]
672 fn resolve_existing_file() {
673 let root = make_temp_project("resolve_existing");
674 let models = root.join("src/models");
675 fs::create_dir_all(&models).unwrap();
676 fs::write(models.join("User.php"), "<?php class User {}").unwrap();
677 fs::write(
678 root.join("composer.json"),
679 r#"{"autoload":{"psr-4":{"App\\Models\\":"src/models/","App\\":"src/"}}}"#,
680 )
681 .unwrap();
682
683 let map = Psr4Map::from_composer(&root).unwrap();
684 let result = map.resolve("App\\Models\\User");
685 assert!(result.is_some(), "expected a resolved path");
686 assert!(result.unwrap().ends_with("User.php"));
687 }
688
689 #[test]
690 fn resolve_missing_file() {
691 let root = make_temp_project("resolve_missing");
692 fs::create_dir_all(root.join("src")).unwrap();
693 fs::write(
694 root.join("composer.json"),
695 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
696 )
697 .unwrap();
698
699 let map = Psr4Map::from_composer(&root).unwrap();
700 let result = map.resolve("App\\Models\\User");
701 assert!(result.is_none());
702 }
703
704 #[test]
705 fn boundary_check() {
706 let root = make_temp_project("boundary_check");
707 fs::create_dir_all(root.join("src")).unwrap();
708 fs::write(
709 root.join("composer.json"),
710 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
711 )
712 .unwrap();
713
714 let map = Psr4Map::from_composer(&root).unwrap();
715 let result = map.resolve("Application\\Foo");
717 assert!(
718 result.is_none(),
719 "App\\ prefix must not match Application\\Foo"
720 );
721 }
722
723 #[test]
724 fn array_valued_psr4_dirs() {
725 let root = make_temp_project("array_dirs");
726 let src = root.join("src");
727 let lib = root.join("lib");
728 fs::create_dir_all(&src).unwrap();
729 fs::create_dir_all(&lib).unwrap();
730 fs::write(src.join("Foo.php"), "<?php class Foo {}").unwrap();
731 fs::write(lib.join("Bar.php"), "<?php class Bar {}").unwrap();
732 fs::write(
733 root.join("composer.json"),
734 r#"{"autoload":{"psr-4":{"App\\":["src/","lib/"]}}}"#,
735 )
736 .unwrap();
737
738 let map = Psr4Map::from_composer(&root).unwrap();
739 assert_eq!(
741 map.project_entries.len(),
742 2,
743 "expected 2 entries for array-valued dir"
744 );
745 let files = map.project_files();
746 assert_eq!(files.len(), 2, "expected Foo.php and Bar.php");
747 }
748
749 #[test]
754 fn project_classmap_dir_is_collected() {
755 let root = make_temp_project("project_classmap");
756 let lib = root.join("lib");
757 fs::create_dir_all(&lib).unwrap();
758 fs::write(lib.join("Legacy.php"), "<?php class Legacy {}").unwrap();
759 fs::write(
760 root.join("composer.json"),
761 r#"{"autoload":{"classmap":["lib/"]}}"#,
762 )
763 .unwrap();
764
765 let map = Psr4Map::from_composer(&root).unwrap();
766 let files = map.project_files();
767 assert_eq!(files.len(), 1);
768 assert!(files[0].ends_with("Legacy.php"));
769 }
770
771 #[test]
772 fn project_files_autoload_is_collected() {
773 let root = make_temp_project("project_files_autoload");
774 fs::write(root.join("helpers.php"), "<?php function my_helper() {}").unwrap();
775 fs::write(
776 root.join("composer.json"),
777 r#"{"autoload":{"files":["helpers.php"]}}"#,
778 )
779 .unwrap();
780
781 let map = Psr4Map::from_composer(&root).unwrap();
782 let files = map.project_files();
783 assert_eq!(files.len(), 1);
784 assert!(files[0].ends_with("helpers.php"));
785 }
786
787 #[test]
788 fn project_psr0_dir_is_collected() {
789 let root = make_temp_project("project_psr0");
790 let lib = root.join("legacy");
791 fs::create_dir_all(&lib).unwrap();
792 fs::write(lib.join("Old.php"), "<?php class Old {}").unwrap();
793 fs::write(
794 root.join("composer.json"),
795 r#"{"autoload":{"psr-0":{"":"legacy/"}}}"#,
796 )
797 .unwrap();
798
799 let map = Psr4Map::from_composer(&root).unwrap();
800 let files = map.project_files();
801 assert_eq!(files.len(), 1);
802 assert!(files[0].ends_with("Old.php"));
803 }
804
805 #[test]
806 fn vendor_classmap_is_collected() {
807 let root = make_temp_project("vendor_classmap");
808 fs::write(
809 root.join("composer.json"),
810 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
811 )
812 .unwrap();
813 let vendor_dir = root.join("vendor/composer");
814 fs::create_dir_all(&vendor_dir).unwrap();
815 fs::write(
816 vendor_dir.join("installed.json"),
817 r#"{
818 "packages": [{
819 "name": "vendor/pkg",
820 "autoload": { "classmap": ["src/"] }
821 }]
822 }"#,
823 )
824 .unwrap();
825 let pkg_src = root.join("vendor/vendor/pkg/src");
826 fs::create_dir_all(&pkg_src).unwrap();
827 fs::write(pkg_src.join("Legacy.php"), "<?php class Legacy {}").unwrap();
828
829 let map = Psr4Map::from_composer(&root).unwrap();
830 let files = map.vendor_files();
831 assert_eq!(files.len(), 1);
832 assert!(files[0].ends_with("Legacy.php"));
833 }
834
835 #[test]
836 fn vendor_files_autoload_is_collected() {
837 let root = make_temp_project("vendor_files_autoload");
838 fs::write(
839 root.join("composer.json"),
840 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
841 )
842 .unwrap();
843 let vendor_dir = root.join("vendor/composer");
844 fs::create_dir_all(&vendor_dir).unwrap();
845 fs::write(
846 vendor_dir.join("installed.json"),
847 r#"{
848 "packages": [{
849 "name": "vendor/pkg",
850 "autoload": { "files": ["bootstrap.php"] }
851 }]
852 }"#,
853 )
854 .unwrap();
855 let pkg_dir = root.join("vendor/vendor/pkg");
856 fs::create_dir_all(&pkg_dir).unwrap();
857 fs::write(
858 pkg_dir.join("bootstrap.php"),
859 "<?php function pkg_bootstrap() {}",
860 )
861 .unwrap();
862
863 let map = Psr4Map::from_composer(&root).unwrap();
864 let files = map.vendor_files();
865 assert_eq!(files.len(), 1);
866 assert!(files[0].ends_with("bootstrap.php"));
867 }
868
869 #[test]
870 fn vendor_psr0_is_collected() {
871 let root = make_temp_project("vendor_psr0");
872 fs::write(
873 root.join("composer.json"),
874 r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
875 )
876 .unwrap();
877 let vendor_dir = root.join("vendor/composer");
878 fs::create_dir_all(&vendor_dir).unwrap();
879 fs::write(
880 vendor_dir.join("installed.json"),
881 r#"{
882 "packages": [{
883 "name": "vendor/pkg",
884 "autoload": { "psr-0": { "Old_": "src/" } }
885 }]
886 }"#,
887 )
888 .unwrap();
889 let pkg_src = root.join("vendor/vendor/pkg/src/Old");
890 fs::create_dir_all(&pkg_src).unwrap();
891 fs::write(pkg_src.join("Thing.php"), "<?php class Old_Thing {}").unwrap();
892
893 let map = Psr4Map::from_composer(&root).unwrap();
894 let files = map.vendor_files();
895 assert_eq!(files.len(), 1);
896 assert!(files[0].ends_with("Thing.php"));
897 }
898}