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