Skip to main content

fallow_graph/graph/
package_entry.rs

1//! Find the graph module of a package entry path.
2//!
3//! The public API entry sets of `fallow-core` and `fallow-engine` both use
4//! this lookup, so both crates match a package entry to the same module.
5
6use std::path::Path;
7
8use fallow_types::discover::FileId;
9
10use super::ModuleGraph;
11
12impl ModuleGraph {
13    /// Return the module of the package entry `entry_path`.
14    ///
15    /// `lookup` maps a module path to its `FileId`. The lookup tries the path
16    /// as given, then its canonical form. Then it compares the canonical
17    /// form with the canonical path of each module under `package_root`. That
18    /// last step finds a module that the walk reached through a symlink, and
19    /// the package scope keeps a miss bounded by the package file count.
20    #[must_use]
21    pub fn package_entry_file_id(
22        &self,
23        package_root: &Path,
24        entry_path: &Path,
25        lookup: impl Fn(&Path) -> Option<FileId>,
26    ) -> Option<FileId> {
27        lookup(entry_path).or_else(|| {
28            let canonical = dunce::canonicalize(entry_path).ok()?;
29            lookup(&canonical).or_else(|| {
30                match_canonical_entry_under_package(
31                    self.modules
32                        .iter()
33                        .map(|module| (module.path.as_path(), module.file_id)),
34                    package_root,
35                    &canonical,
36                )
37            })
38        })
39    }
40}
41
42/// Return the `FileId` of the first candidate under `package_root` whose
43/// canonical form equals `canonical_entry`.
44fn match_canonical_entry_under_package<'a>(
45    candidates: impl Iterator<Item = (&'a Path, FileId)>,
46    package_root: &Path,
47    canonical_entry: &Path,
48) -> Option<FileId> {
49    candidates
50        .filter(|(path, _)| path.starts_with(package_root))
51        .find_map(|(path, file_id)| {
52            (dunce::canonicalize(path).ok().as_deref() == Some(canonical_entry)).then_some(file_id)
53        })
54}
55
56#[cfg(all(test, unix))]
57mod tests {
58    use super::*;
59
60    // A module whose discovered (raw) path goes through a symlinked directory
61    // has a raw path that differs from the canonical entry path. The raw-map
62    // lookup cannot reach it, so the package-scoped canonical match must.
63    #[cfg_attr(miri, ignore)]
64    #[test]
65    fn scoped_canonical_matches_module_reached_through_symlink() {
66        let dir = tempfile::tempdir().unwrap();
67        let real_dir = dir.path().join("real");
68        std::fs::create_dir(&real_dir).unwrap();
69        let real_file = real_dir.join("mod.ts");
70        std::fs::write(&real_file, "export const x = 1;\n").unwrap();
71        let link_dir = dir.path().join("link");
72        std::os::unix::fs::symlink(&real_dir, &link_dir).unwrap();
73
74        let module_raw_path = link_dir.join("mod.ts");
75        let canonical_entry = dunce::canonicalize(&real_file).unwrap();
76        let package_root = dir.path();
77
78        let candidates = [(module_raw_path.as_path(), FileId(7))];
79        assert_eq!(
80            match_canonical_entry_under_package(
81                candidates.iter().copied(),
82                package_root,
83                &canonical_entry,
84            ),
85            Some(FileId(7)),
86        );
87
88        let outside_root = dir.path().join("other-package");
89        assert_eq!(
90            match_canonical_entry_under_package(
91                candidates.iter().copied(),
92                &outside_root,
93                &canonical_entry,
94            ),
95            None,
96        );
97
98        let unrelated = dunce::canonicalize(dir.path()).unwrap().join("nope.ts");
99        assert_eq!(
100            match_canonical_entry_under_package(
101                candidates.iter().copied(),
102                package_root,
103                &unrelated,
104            ),
105            None,
106        );
107    }
108}