1use std::collections::{BTreeMap, BTreeSet, HashMap};
28use std::path::{Path, PathBuf};
29
30use bynk_project::{PackageGraph, PackageId, ParsedFile, UnitKind};
31use bynk_syntax::CompileError;
32use bynk_syntax::ast::{CommonsItem, Ident, QualifiedName, SourceUnit};
33use bynk_syntax::span::Span;
34
35use crate::project_model::ErrorSink;
36
37pub const UNIT_NOT_EXPORTED: &str = "bynk.package.unit_not_exported";
39pub const UNKNOWN_UNIT: &str = "bynk.package.unknown_unit";
41pub const ALIAS_SHADOWS_UNIT: &str = "bynk.package.alias_shadows_unit";
43pub const UNSUPPORTED_IN_DEPENDENCY: &str = "bynk.package.unsupported_in_dependency";
46pub const DEV_DEPENDENCY_IN_SOURCE: &str = "bynk.package.dev_dependency_in_source";
48pub const UNAPPROVED_REQUIREMENT: &str = "bynk.requires.unapproved";
51
52pub fn apply_graph(
60 project_root: Option<&Path>,
61 overlay: &HashMap<PathBuf, String>,
62 parsed: &mut Vec<ParsedFile>,
63 tests: bool,
64 errors: &mut ErrorSink,
65) {
66 let Some(project_root) = project_root else {
67 return;
68 };
69 let (graph, problems) = bynk_project::resolve_graph(project_root, overlay);
70 let manifest = Path::new(crate::stability::MANIFEST);
71 for p in problems {
72 let mut err = CompileError::new(p.code, Span::default(), p.message);
73 if let Some(note) = p.note {
74 err = err.with_note(note);
75 }
76 errors.push_for(Some(manifest), err);
77 }
78 let Some(graph) = graph else {
79 return;
80 };
81
82 let mut package_of: Vec<Option<usize>> = Vec::with_capacity(parsed.len());
85 for pf in parsed.iter_mut() {
86 let idx = (!pf.is_synthetic())
87 .then(|| pf.abs_path())
88 .flatten()
89 .and_then(|abs| graph.package_of(&abs));
90 if let Some(i) = idx {
91 pf.assign_package(graph.packages[i].id.clone());
92 }
93 package_of.push(idx);
94 }
95 let mut keep = vec![true; parsed.len()];
96 for (k, pf) in parsed.iter().enumerate() {
97 if package_of[k].is_some() && !matches!(pf.kind(), UnitKind::Commons | UnitKind::Adapter) {
98 keep[k] = false;
99 }
100 }
101
102 let own_first: BTreeSet<String> = parsed
104 .iter()
105 .enumerate()
106 .filter(|(k, pf)| {
108 package_of[*k].is_none()
109 && !pf.is_synthetic()
110 && !matches!(pf.kind(), UnitKind::Test | UnitKind::Integration)
111 })
112 .filter_map(|(_, pf)| pf.unit().name().parts.first().map(|i| i.name.clone()))
113 .collect();
114 for alias in graph.root_aliases.keys() {
115 if own_first.contains(alias) {
116 errors.push_for(
117 Some(manifest),
118 CompileError::new(
119 ALIAS_SHADOWS_UNIT,
120 Span::default(),
121 format!(
122 "the dependency alias `{alias}` is also the first segment of one of this project's own units, so `{alias}.…` would be ambiguous"
123 ),
124 )
125 .with_note("rename the alias in `[dependencies]` (the key is the alias), or the unit"),
126 );
127 }
128 }
129
130 let mut declared: Vec<BTreeMap<String, UnitKind>> = vec![BTreeMap::new(); graph.packages.len()];
132 for (k, pf) in parsed.iter().enumerate() {
133 if let Some(i) = package_of[k]
134 && keep[k]
135 {
136 declared[i].insert(pf.unit().name().joined(), pf.kind());
137 }
138 }
139
140 for (k, pf) in parsed.iter_mut().enumerate() {
142 if let Some(i) = package_of[k] {
143 let enc = graph.packages[i].id.encoded();
144 prefix_name(unit_name_mut(pf.unit_mut()), &enc);
145 }
146 }
147
148 for (k, pf) in parsed.iter_mut().enumerate() {
150 if !keep[k] || pf.is_synthetic() {
151 continue;
152 }
153 let file = pf.identity_path();
154 let from = package_of[k];
155 let in_suite = matches!(pf.kind(), UnitKind::Test | UnitKind::Integration);
156 let mut found: Vec<CompileError> = Vec::new();
157 let resolve = |target: &mut QualifiedName, found: &mut Vec<CompileError>| {
158 resolve_target(&graph, &declared, from, in_suite, target, found)
159 };
160 let uses_pass = |uses: &mut Vec<bynk_syntax::ast::UsesDecl>,
161 found: &mut Vec<CompileError>| {
162 uses.retain_mut(|u| resolve(&mut u.target, found) != Resolved::Dropped);
163 };
164 let consumes_pass = |consumes: &mut Vec<bynk_syntax::ast::ConsumesDecl>,
165 found: &mut Vec<CompileError>| {
166 consumes.retain_mut(|cd| {
167 let written = cd.target.clone();
168 match resolve(&mut cd.target, found) {
169 Resolved::Dropped => false,
170 Resolved::Dependency => {
171 if cd.alias.is_none() && cd.selected.is_none() {
175 cd.alias = Some(Ident {
176 name: written.joined(),
177 span: written.span,
178 });
179 }
180 true
181 }
182 Resolved::Unchanged => true,
183 }
184 });
185 };
186 match pf.unit_mut() {
187 SourceUnit::Commons(c) => uses_pass(&mut c.uses, &mut found),
188 SourceUnit::Context(c) => {
189 uses_pass(&mut c.uses, &mut found);
190 consumes_pass(&mut c.consumes, &mut found);
191 }
192 SourceUnit::Adapter(a) => {
193 uses_pass(&mut a.uses, &mut found);
194 consumes_pass(&mut a.consumes, &mut found);
195 }
196 SourceUnit::Suite(s) => {
197 resolve(&mut s.target, &mut found);
198 uses_pass(&mut s.uses, &mut found);
199 }
200 }
201 errors.extend_for(Some(&file), found);
202 }
203
204 for (k, pf) in parsed.iter().enumerate() {
206 let Some(i) = package_of[k] else { continue };
207 if !keep[k] {
208 continue;
209 }
210 let pkg = graph.packages[i].id.name();
211 for item in pf.items() {
215 if let CommonsItem::Asset(a) = item {
216 errors.push_for(
217 Some(&pf.identity_path()),
218 CompileError::new(
219 UNSUPPORTED_IN_DEPENDENCY,
220 a.span,
221 format!(
222 "dependency `{pkg}` declares an `asset`, and a dependency's assets aren't supported yet: it would resolve against the consuming project's `assets/`"
223 ),
224 ),
225 );
226 }
227 if let CommonsItem::Embed(e) = item {
229 errors.push_for(
230 Some(&pf.identity_path()),
231 CompileError::new(
232 UNSUPPORTED_IN_DEPENDENCY,
233 e.span,
234 format!(
235 "dependency `{pkg}` declares an `embed`, and a dependency's embeds aren't supported yet: it would resolve against the consuming project's `embeds/`"
236 ),
237 ),
238 );
239 }
240 }
241 }
242
243 let reached = reached_units(parsed, &keep, &package_of, tests);
245 for (k, pf) in parsed.iter().enumerate() {
246 if package_of[k].is_some() && !reached.contains(&pf.unit().name().joined()) {
247 keep[k] = false;
248 }
249 }
250 for (k, pf) in parsed.iter().enumerate() {
253 let Some(i) = package_of[k] else { continue };
254 if !keep[k] || !graph.packages[i].fetched {
255 continue;
256 }
257 let SourceUnit::Adapter(a) = pf.unit() else {
258 continue;
259 };
260 let Some(binding) = &a.binding else { continue };
261 let pkg = graph.packages[i].id.name();
262 let adapter = a
263 .name
264 .parts
265 .iter()
266 .skip(1)
267 .map(|p| p.name.as_str())
268 .collect::<Vec<_>>()
269 .join(".");
270 for dep in &binding.requires {
271 if graph
272 .npm_approved
273 .contains(&(dep.package.clone(), dep.range.clone()))
274 {
275 continue;
276 }
277 let chain = std::iter::once("this project".to_string())
278 .chain(graph.chain_to(i))
279 .collect::<Vec<_>>()
280 .join(" → ");
281 errors.push_for(
282 Some(&pf.identity_path()),
283 CompileError::new(
284 UNAPPROVED_REQUIREMENT,
285 dep.span,
286 format!(
287 "adapter `{pkg}:{adapter}` requires the npm package `{}` at `{}`, which this project hasn't approved",
288 dep.package, dep.range
289 ),
290 )
291 .with_note(format!("required through {chain}"))
292 .with_note(format!(
293 "review it, then run `bynk approve {}`",
294 dep.package
295 )),
296 );
297 }
298 }
299
300 let mut k = 0;
301 parsed.retain(|_| {
302 let kept = keep[k];
303 k += 1;
304 kept
305 });
306}
307
308#[derive(Clone, Copy, PartialEq, Eq)]
310enum Resolved {
311 Unchanged,
313 Dependency,
315 Dropped,
318}
319
320fn resolve_target(
325 graph: &PackageGraph,
326 declared: &[BTreeMap<String, UnitKind>],
327 from: Option<usize>,
328 in_suite: bool,
329 target: &mut QualifiedName,
330 found: &mut Vec<CompileError>,
331) -> Resolved {
332 let Some(first) = target.parts.first().map(|p| p.name.clone()) else {
333 return Resolved::Unchanged;
334 };
335 if first == "bynk" {
336 return Resolved::Unchanged;
337 }
338 if from.is_none() && target.parts.len() >= 2 && graph.unresolved_aliases.contains(&first) {
339 return Resolved::Dropped;
341 }
342 let aliases = match from {
343 None => &graph.root_aliases,
344 Some(i) => &graph.packages[i].aliases,
345 };
346 if target.parts.len() >= 2
347 && let Some(&dep) = aliases.get(&first)
348 {
349 let pkg = &graph.packages[dep];
350 if from.is_none() && !in_suite && graph.dev_aliases.contains(&first) {
351 found.push(
352 CompileError::new(
353 DEV_DEPENDENCY_IN_SOURCE,
354 target.span,
355 format!(
356 "`{}` reaches dependency `{}` through `{first}`, which is a `[dev-dependencies]` alias: only a suite may use it",
357 target.joined(),
358 pkg.id.name()
359 ),
360 )
361 .with_note("move it to `[dependencies]` to use it here"),
362 );
363 return Resolved::Dropped;
364 }
365 let unit = target.parts[1..]
366 .iter()
367 .map(|p| p.name.as_str())
368 .collect::<Vec<_>>()
369 .join(".");
370 match declared[dep].get(&unit) {
371 None => {
372 found.push(
373 CompileError::new(
374 UNKNOWN_UNIT,
375 target.span,
376 format!(
377 "`{}` names no commons or adapter in dependency `{}` (alias `{first}`)",
378 target.joined(),
379 pkg.id.name()
380 ),
381 )
382 .with_note(exports_note(pkg, &declared[dep])),
383 );
384 return Resolved::Dropped;
385 }
386 Some(_) => {
387 let exported = pkg.exports.as_ref().is_none_or(|list| list.contains(&unit));
388 if !exported {
389 found.push(
390 CompileError::new(
391 UNIT_NOT_EXPORTED,
392 target.span,
393 format!("`{unit}` isn't exported by dependency `{}`", pkg.id.name()),
394 )
395 .with_note(exports_note(pkg, &declared[dep])),
396 );
397 }
398 }
399 }
400 prefix_name_from(target, &pkg.id, 1);
401 return Resolved::Dependency;
402 }
403 if let Some(i) = from {
404 let joined = target.joined();
406 if declared[i].contains_key(&joined) {
407 prefix_name(target, &graph.packages[i].id.encoded());
408 return Resolved::Dependency;
409 }
410 }
411 Resolved::Unchanged
412}
413
414fn exports_note(pkg: &bynk_project::Package, declared: &BTreeMap<String, UnitKind>) -> String {
416 let shared: Vec<String> = match &pkg.exports {
417 Some(list) => list.clone(),
418 None => declared
419 .iter()
420 .filter(|(_, k)| matches!(k, UnitKind::Commons | UnitKind::Adapter))
421 .map(|(n, _)| n.clone())
422 .collect(),
423 };
424 if shared.is_empty() {
425 format!("`{}` shares no units", pkg.id.name())
426 } else {
427 format!(
428 "`{}` shares {}",
429 pkg.id.name(),
430 shared
431 .iter()
432 .map(|n| format!("`{n}`"))
433 .collect::<Vec<_>>()
434 .join(", ")
435 )
436 }
437}
438
439fn reached_units(
442 parsed: &[ParsedFile],
443 keep: &[bool],
444 package_of: &[Option<usize>],
445 tests: bool,
446) -> BTreeSet<String> {
447 let mut edges: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
448 let mut roots: BTreeSet<String> = BTreeSet::new();
449 for (k, pf) in parsed.iter().enumerate() {
450 if !keep[k] {
451 continue;
452 }
453 let name = pf.unit().name().joined();
454 let targets = edges.entry(name.clone()).or_default();
455 for u in pf.uses() {
456 targets.insert(u.target.joined());
457 }
458 for c in pf.consumes() {
459 targets.insert(c.target.joined());
460 }
461 let suite = matches!(pf.kind(), UnitKind::Test | UnitKind::Integration);
462 if package_of[k].is_none() && (tests || !suite) {
463 roots.insert(name);
464 }
465 }
466 let mut seen: BTreeSet<String> = BTreeSet::new();
467 let mut stack: Vec<String> = roots.into_iter().collect();
468 while let Some(n) = stack.pop() {
469 if !seen.insert(n.clone()) {
470 continue;
471 }
472 if let Some(next) = edges.get(&n) {
473 stack.extend(next.iter().cloned());
474 }
475 }
476 seen
477}
478
479fn unit_name_mut(unit: &mut SourceUnit) -> &mut QualifiedName {
481 match unit {
482 SourceUnit::Commons(c) => &mut c.name,
483 SourceUnit::Context(c) => &mut c.name,
484 SourceUnit::Suite(s) => &mut s.target,
485 SourceUnit::Adapter(a) => &mut a.name,
486 }
487}
488
489fn prefix_name(name: &mut QualifiedName, encoded: &str) {
491 let span = name.parts.first().map_or(name.span, |p| p.span);
492 name.parts.insert(
493 0,
494 Ident {
495 name: encoded.to_owned(),
496 span,
497 },
498 );
499}
500
501fn prefix_name_from(name: &mut QualifiedName, id: &PackageId, drop: usize) {
504 let span = name.parts.first().map_or(name.span, |p| p.span);
505 name.parts.drain(..drop);
506 name.parts.insert(
507 0,
508 Ident {
509 name: id.encoded(),
510 span,
511 },
512 );
513}