Skip to main content

affected_core/resolvers/
mod.rs

1use anyhow::Result;
2use std::path::Path;
3
4use crate::types::{Ecosystem, PackageId, ProjectGraph};
5
6pub mod cargo;
7pub mod go;
8pub mod gradle;
9pub mod maven;
10pub mod npm;
11pub mod python;
12pub mod yarn;
13
14/// Trait implemented by each ecosystem resolver.
15pub trait Resolver {
16    /// Which ecosystem this resolver handles.
17    fn ecosystem(&self) -> Ecosystem;
18
19    /// Can this resolver handle the project at the given root path?
20    fn detect(&self, root: &Path) -> bool;
21
22    /// Build the full project graph: packages + dependency edges.
23    fn resolve(&self, root: &Path) -> Result<ProjectGraph>;
24
25    /// Given a file path (relative to project root), return which package owns it.
26    fn package_for_file(&self, graph: &ProjectGraph, file: &Path) -> Option<PackageId>;
27
28    /// Return the shell command to run tests for a given package.
29    fn test_command(&self, package_id: &PackageId) -> Vec<String>;
30}
31
32/// Return all available resolvers.
33pub fn all_resolvers() -> Vec<Box<dyn Resolver>> {
34    vec![
35        Box::new(cargo::CargoResolver),
36        Box::new(yarn::YarnResolver), // Yarn before Npm: .yarnrc.yml takes priority
37        Box::new(npm::NpmResolver),
38        Box::new(go::GoResolver),
39        Box::new(python::PythonResolver),
40        Box::new(maven::MavenResolver),
41        Box::new(gradle::GradleResolver),
42    ]
43}
44
45/// Auto-select the first matching resolver for a project.
46pub fn detect_resolver(root: &Path) -> Result<Box<dyn Resolver>> {
47    for resolver in all_resolvers() {
48        if resolver.detect(root) {
49            return Ok(resolver);
50        }
51    }
52    anyhow::bail!("No supported project type detected at {}", root.display())
53}
54
55/// Map a file to its owning package using longest-prefix directory matching.
56pub fn file_to_package(graph: &ProjectGraph, file: &Path) -> Option<PackageId> {
57    let mut best: Option<(&PackageId, usize)> = None;
58
59    for (id, pkg) in &graph.packages {
60        // Get package path relative to project root
61        let pkg_rel = pkg.path.strip_prefix(&graph.root).unwrap_or(&pkg.path);
62
63        if file.starts_with(pkg_rel) {
64            let depth = pkg_rel.components().count();
65            if best.is_none() || depth > best.unwrap().1 {
66                best = Some((id, depth));
67            }
68        }
69    }
70
71    best.map(|(id, _)| id.clone())
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::types::Package;
78    use std::collections::HashMap;
79    use std::path::PathBuf;
80
81    fn make_project_graph(pkgs: &[(&str, &str)]) -> ProjectGraph {
82        let root = PathBuf::from("/project");
83        let mut packages = HashMap::new();
84        for (name, rel_path) in pkgs {
85            let id = PackageId(name.to_string());
86            packages.insert(
87                id.clone(),
88                Package {
89                    id: id.clone(),
90                    name: name.to_string(),
91                    version: None,
92                    path: root.join(rel_path),
93                    manifest_path: root.join(rel_path).join("Cargo.toml"),
94                },
95            );
96        }
97        ProjectGraph {
98            packages,
99            edges: vec![],
100            root,
101        }
102    }
103
104    #[test]
105    fn test_file_to_package_basic() {
106        let pg = make_project_graph(&[("core", "crates/core"), ("cli", "crates/cli")]);
107        let result = file_to_package(&pg, &PathBuf::from("crates/core/src/lib.rs"));
108        assert_eq!(result, Some(PackageId("core".into())));
109    }
110
111    #[test]
112    fn test_file_to_package_no_match() {
113        let pg = make_project_graph(&[("core", "crates/core")]);
114        let result = file_to_package(&pg, &PathBuf::from("scripts/build.sh"));
115        assert!(result.is_none());
116    }
117
118    #[test]
119    fn test_file_to_package_longest_prefix() {
120        // Nested packages: "crates/foo" and "crates/foo/bar"
121        let pg = make_project_graph(&[("foo", "crates/foo"), ("foo-bar", "crates/foo/bar")]);
122
123        // File in nested package should match the deeper one
124        let result = file_to_package(&pg, &PathBuf::from("crates/foo/bar/src/lib.rs"));
125        assert_eq!(result, Some(PackageId("foo-bar".into())));
126
127        // File in parent package should match the shallower one
128        let result = file_to_package(&pg, &PathBuf::from("crates/foo/src/lib.rs"));
129        assert_eq!(result, Some(PackageId("foo".into())));
130    }
131
132    #[test]
133    fn test_file_to_package_root_level_file() {
134        let pg = make_project_graph(&[("core", "crates/core")]);
135        let result = file_to_package(&pg, &PathBuf::from("README.md"));
136        assert!(result.is_none());
137    }
138
139    #[test]
140    fn test_detect_resolver_cargo() {
141        let dir = tempfile::tempdir().unwrap();
142        std::fs::write(dir.path().join("Cargo.toml"), "[workspace]\nmembers = []\n").unwrap();
143
144        let resolver = detect_resolver(dir.path()).unwrap();
145        assert_eq!(resolver.ecosystem(), Ecosystem::Cargo);
146    }
147
148    #[test]
149    fn test_detect_resolver_none() {
150        let dir = tempfile::tempdir().unwrap();
151        assert!(detect_resolver(dir.path()).is_err());
152    }
153
154    #[test]
155    fn test_all_resolvers_count() {
156        assert_eq!(all_resolvers().len(), 7);
157    }
158}