affected_core/resolvers/
mod.rs1use 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
24pub trait Resolver: sealed::Sealed {
28 fn ecosystem(&self) -> Ecosystem;
30
31 fn detect(&self, root: &Path) -> bool;
33
34 fn resolve(&self, root: &Path) -> Result<ProjectGraph>;
36
37 fn package_for_file(&self, graph: &ProjectGraph, file: &Path) -> Option<PackageId>;
39
40 fn test_command(&self, package_id: &PackageId) -> Vec<String>;
42}
43
44pub fn all_resolvers() -> Vec<Box<dyn Resolver>> {
46 vec![
47 Box::new(cargo::CargoResolver),
48 Box::new(yarn::YarnResolver), Box::new(bun::BunResolver), 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
63pub 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
73pub 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 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 let pg = make_project_graph(&[("foo", "crates/foo"), ("foo-bar", "crates/foo/bar")]);
140
141 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 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}