1use std::collections::{BTreeMap, HashMap};
14use std::path::{Path, PathBuf};
15
16use bynk_project::UnitKind;
17use bynk_syntax::CompileError;
18use bynk_syntax::span::Span;
19
20pub const EXPORT_CONTEXT: &str = "bynk.project.export_context";
22pub const EXPORT_UNKNOWN: &str = "bynk.project.export_unknown";
24
25pub fn manifest_diagnostics(
29 project_root: Option<&Path>,
30 overlay: &HashMap<PathBuf, String>,
31 kinds: &BTreeMap<String, UnitKind>,
32) -> Vec<CompileError> {
33 let exports = project_root
34 .and_then(|root| bynk_project::read_package_config(root, overlay))
35 .and_then(|package| package.exports)
36 .unwrap_or_default();
37 check_exports(&exports, kinds)
38}
39
40pub fn check_exports(exports: &[String], kinds: &BTreeMap<String, UnitKind>) -> Vec<CompileError> {
43 let mut out = Vec::new();
44 let mut seen = std::collections::HashSet::new();
45 for entry in exports.iter().filter(|e| seen.insert(e.as_str())) {
47 let kind = kinds
48 .get(entry)
49 .filter(|_| crate::firstparty::unit(entry).is_none());
50 match kind {
51 Some(UnitKind::Commons | UnitKind::Adapter) => {}
52 Some(UnitKind::Context) => out.push(
53 CompileError::new(
54 EXPORT_CONTEXT,
55 Span::default(),
56 format!(
57 "`bynk.toml`'s `[package] exports` lists `{entry}`, which is a context — a package shares commons and adapters, never a context"
58 ),
59 )
60 .with_note(
61 "a context is a deployment unit, owned by the project that deploys it; remove it from `exports`",
62 ),
63 ),
64 _ => out.push(
65 CompileError::new(
66 EXPORT_UNKNOWN,
67 Span::default(),
68 format!(
69 "`bynk.toml`'s `[package] exports` lists `{entry}`, which isn't a commons or adapter this project declares"
70 ),
71 )
72 .with_note(match did_you_mean(entry, kinds) {
73 Some(name) => format!("did you mean `{name}`?"),
74 None => "an entry names a commons or adapter declared in this project's sources"
75 .to_string(),
76 }),
77 ),
78 }
79 }
80 out
81}
82
83fn did_you_mean<'a>(entry: &str, kinds: &'a BTreeMap<String, UnitKind>) -> Option<&'a str> {
85 kinds
86 .iter()
87 .filter(|(name, kind)| {
88 matches!(kind, UnitKind::Commons | UnitKind::Adapter)
89 && crate::firstparty::unit(name).is_none()
90 })
91 .map(|(name, _)| (strsim(entry, name), name.as_str()))
92 .filter(|(d, _)| *d <= 2)
93 .min_by_key(|(d, _)| *d)
94 .map(|(_, name)| name)
95}
96
97fn strsim(a: &str, b: &str) -> usize {
99 let b: Vec<char> = b.chars().collect();
100 let mut row: Vec<usize> = (0..=b.len()).collect();
101 for (i, ca) in a.chars().enumerate() {
102 let mut prev = row[0];
103 row[0] = i + 1;
104 for (j, cb) in b.iter().enumerate() {
105 let cur = row[j + 1];
106 row[j + 1] = (prev + usize::from(ca != *cb)).min(row[j] + 1).min(cur + 1);
107 prev = cur;
108 }
109 }
110 row[b.len()]
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 fn kinds() -> BTreeMap<String, UnitKind> {
118 BTreeMap::from([
119 ("money".to_string(), UnitKind::Commons),
120 ("payments".to_string(), UnitKind::Adapter),
121 ("shop.checkout".to_string(), UnitKind::Context),
122 ("bynk".to_string(), UnitKind::Commons),
123 ])
124 }
125
126 fn codes(exports: &[&str]) -> Vec<&'static str> {
127 let exports: Vec<String> = exports.iter().map(|s| s.to_string()).collect();
128 check_exports(&exports, &kinds())
129 .iter()
130 .map(|e| {
131 if e.category == EXPORT_CONTEXT {
132 EXPORT_CONTEXT
133 } else {
134 EXPORT_UNKNOWN
135 }
136 })
137 .collect()
138 }
139
140 #[test]
141 fn commons_and_adapters_are_shareable() {
142 assert!(codes(&["money", "payments"]).is_empty());
143 }
144
145 #[test]
146 fn a_repeated_entry_is_reported_once() {
147 assert_eq!(codes(&["shop.checkout", "shop.checkout"]), [EXPORT_CONTEXT]);
148 }
149
150 #[test]
151 fn a_context_is_never_shareable() {
152 assert_eq!(codes(&["shop.checkout"]), [EXPORT_CONTEXT]);
153 }
154
155 #[test]
156 fn an_undeclared_or_first_party_unit_is_unknown() {
157 assert_eq!(codes(&["mony", "bynk"]), [EXPORT_UNKNOWN, EXPORT_UNKNOWN]);
158 let notes = &check_exports(&["mony".to_string()], &kinds())[0].notes;
159 assert_eq!(notes, &["did you mean `money`?".to_string()]);
160 }
161}