Skip to main content

bynk_ide/
packages.rs

1//! #2223 follow-up: what the editor needs to know about a project's path
2//! dependencies.
3//!
4//! The compile folds a dependency's units in under canonical names
5//! (`bynk_check::packages::apply_graph`), but completion, hover and the
6//! unit-name rungs of go-to-definition read source text, where a consumer
7//! writes `money.currency` and the dependency declares plain `currency`. A
8//! [`DependencyView`] bridges the two: it resolves a written `<alias>.<unit>`
9//! to the dependency and the unit it declares, says whether the unit is
10//! exported, and tells which files belong to a dependency, so the editor
11//! offers exactly what the compiler accepts.
12
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16use bynk_project::{PackageGraph, PackageId};
17
18/// A written dependency reference, resolved.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct DependencyTarget {
21    /// The alias the reference was written with.
22    pub alias: String,
23    /// The dependency it names.
24    pub package: PackageId,
25    /// The unit name the dependency itself declares.
26    pub unit: String,
27    /// The compiler's canonical name for the unit.
28    pub canonical: String,
29    /// Whether the dependency's `exports` reach it.
30    pub exported: bool,
31}
32
33/// The project's dependency graph, as the editor reads it.
34#[derive(Debug, Clone)]
35pub struct DependencyView {
36    graph: PackageGraph,
37}
38
39impl DependencyView {
40    /// The view for the project at `root`, reading manifests from `files`
41    /// first, as the build does. `None` when it declares no dependencies.
42    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    /// The view for the project whose manifest is in `files`, when the caller
48    /// doesn't know the project root (the language server does, and calls
49    /// [`Self::for_root`]). `files` may hold other manifests too — an open
50    /// dependency's, a sibling project's. Of the manifests declaring
51    /// dependencies (`[dependencies]`, `[dependencies.<alias>]`, or a dotted
52    /// `dependencies.<alias> = …` key), a dependency of another is never the
53    /// project; of the rest, the one whose own directory holds the most of
54    /// `files`' `.bynk` sources is, ties broken by path.
55    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    /// The project's aliases, in order.
96    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    /// Resolve `written` (`money.currency`) when its first segment is one of
104    /// the project's aliases.
105    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    /// The dependency whose source trees hold `path`, if any. `path` may be
123    /// relative; it is compared as `std::path::absolute` gives it, as the
124    /// compile tags its files.
125    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    /// Whether `path` is one of a dependency's files.
132    pub fn is_dependency_file(&self, path: &Path) -> bool {
133        self.package_of(path).is_some()
134    }
135
136    /// The aliases the project reaches `id` by.
137    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    /// Whether `id` shares `unit` (its `exports`, or every unit when absent).
145    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
157/// The project-relative source paths of the unit a `uses`/`consumes` target
158/// names, from an analysed round's `unit_sources` — what go-to-definition
159/// opens. A dependency unit is keyed by its canonical name, so `money.currency`
160/// is looked up through `view` when it isn't a key itself.
161pub 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
171/// Whether two directories are the same, once each is made absolute and
172/// resolved lexically.
173fn 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
181/// The line hover appends for a dependency unit.
182pub 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}