1use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16use bynk_project::{PackageGraph, PackageId};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct DependencyTarget {
21 pub alias: String,
23 pub package: PackageId,
25 pub unit: String,
27 pub canonical: String,
29 pub exported: bool,
31}
32
33#[derive(Debug, Clone)]
35pub struct DependencyView {
36 graph: PackageGraph,
37}
38
39impl DependencyView {
40 pub fn for_root(root: &Path, files: &HashMap<PathBuf, String>) -> Option<Self> {
43 let (graph, _problems) = bynk_project::resolve_graph(root, files);
44 graph.map(|graph| DependencyView { graph })
45 }
46
47 pub fn from_files(files: &HashMap<PathBuf, String>) -> Option<Self> {
56 let mut manifests: Vec<&PathBuf> = files
57 .keys()
58 .filter(|p| p.file_name().is_some_and(|n| n == "bynk.toml"))
59 .filter(|p| {
60 files[*p].lines().any(|l| {
61 let l = l.trim_start();
62 l.starts_with("[dependencies") || l.starts_with("dependencies.")
63 })
64 })
65 .collect();
66 manifests.sort();
67 let views: Vec<(&Path, Self)> = manifests
68 .into_iter()
69 .filter_map(|m| {
70 let dir = m.parent()?;
71 Some((dir, Self::for_root(dir, files)?))
72 })
73 .collect();
74 let is_dependency = |dir: &Path| {
75 views
76 .iter()
77 .any(|(_, v)| v.graph.packages.iter().any(|p| same_dir(&p.dir, dir)))
78 };
79 let held = |dir: &Path| {
80 files
81 .keys()
82 .filter(|p| p.extension().is_some_and(|e| e == "bynk") && p.starts_with(dir))
83 .count()
84 };
85 let mut best: Option<(usize, Self)> = None;
86 for (dir, view) in views.iter().filter(|(d, _)| !is_dependency(d)) {
87 let n = held(dir);
88 if best.as_ref().is_none_or(|(b, _)| n > *b) {
89 best = Some((n, view.clone()));
90 }
91 }
92 best.map(|(_, v)| v)
93 }
94
95 pub fn aliases(&self) -> impl Iterator<Item = (&str, &PackageId)> {
97 self.graph
98 .root_aliases
99 .iter()
100 .map(|(alias, &i)| (alias.as_str(), &self.graph.packages[i].id))
101 }
102
103 pub fn resolve(&self, written: &str) -> Option<DependencyTarget> {
106 let (alias, unit) = written.split_once('.')?;
107 let &i = self.graph.root_aliases.get(alias)?;
108 let package = &self.graph.packages[i];
109 let exported = package
110 .exports
111 .as_ref()
112 .is_none_or(|list| list.iter().any(|e| e == unit));
113 Some(DependencyTarget {
114 alias: alias.to_owned(),
115 package: package.id.clone(),
116 unit: unit.to_owned(),
117 canonical: package.id.canonical(unit),
118 exported,
119 })
120 }
121
122 pub fn package_of(&self, path: &Path) -> Option<&PackageId> {
126 self.graph
127 .package_of(path)
128 .map(|i| &self.graph.packages[i].id)
129 }
130
131 pub fn is_dependency_file(&self, path: &Path) -> bool {
133 self.package_of(path).is_some()
134 }
135
136 pub fn aliases_of(&self, id: &PackageId) -> Vec<&str> {
138 self.aliases()
139 .filter(|(_, p)| *p == id)
140 .map(|(a, _)| a)
141 .collect()
142 }
143
144 pub fn exports(&self, id: &PackageId, unit: &str) -> bool {
146 self.graph
147 .by_id(id)
148 .map(|i| &self.graph.packages[i])
149 .is_some_and(|p| {
150 p.exports
151 .as_ref()
152 .is_none_or(|l| l.iter().any(|e| e == unit))
153 })
154 }
155}
156
157pub fn unit_sources_for<'a>(
162 unit_sources: &'a HashMap<String, Vec<PathBuf>>,
163 view: Option<&DependencyView>,
164 written: &str,
165) -> Option<&'a Vec<PathBuf>> {
166 unit_sources
167 .get(written)
168 .or_else(|| unit_sources.get(&view?.resolve(written)?.canonical))
169}
170
171fn same_dir(a: &Path, b: &Path) -> bool {
174 let norm = |p: &Path| {
175 let abs = std::path::absolute(p).unwrap_or_else(|_| p.to_path_buf());
176 bynk_project::paths::normalize_rel(&abs)
177 };
178 norm(a) == norm(b)
179}
180
181pub fn hover_note(target: &DependencyTarget) -> String {
183 let mut note = format!(
184 "From dependency `{}` (alias `{}`).",
185 target.package.name(),
186 target.alias
187 );
188 if !target.exported {
189 note.push_str(" Not exported: `bynk check` refuses this reference.");
190 }
191 note
192}