1use std::collections::{HashMap, VecDeque};
10use std::path::{Path, PathBuf};
11
12use nova_diagnostics::{Diagnostic, FileDb, FileId, Severity, Span};
13use semver::VersionReq;
14
15use crate::lock::{parse_lock, Lock, LOCKFILE};
16use crate::manifest::{Dependency, Manifest};
17use crate::resolve::Requirement;
18use crate::{check_name, import_name, index_dir_name, real_path, registry_dir, MANIFEST};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct PackageId(pub u32);
23
24#[derive(Debug, Clone)]
26pub struct Edge {
27 pub import_name: String,
29 pub package: PackageId,
30 pub span: Span,
32}
33
34#[derive(Debug, Clone)]
36pub struct GraphPackage {
37 pub name: String,
38 pub import_name: String,
39 pub dir: PathBuf,
42 pub canonical: PathBuf,
44 pub manifest: Manifest,
45 pub manifest_file: FileId,
47 pub has_lib: bool,
48 pub has_main: bool,
49 pub registry: bool,
52 pub dependencies: Vec<Edge>,
53 pub dev_dependencies: Vec<Edge>,
55}
56
57#[derive(Debug, Clone)]
59pub struct Graph {
60 pub packages: Vec<GraphPackage>,
62}
63
64impl Graph {
65 pub fn root(&self) -> &GraphPackage {
66 &self.packages[0]
67 }
68
69 pub fn package(&self, id: PackageId) -> &GraphPackage {
70 &self.packages[id.0 as usize]
71 }
72
73 pub fn dirs(&self) -> Vec<PathBuf> {
75 self.packages.iter().map(|p| p.canonical.clone()).collect()
76 }
77
78 pub fn reached_through(&self, id: PackageId) -> Option<Span> {
82 let root = self.root();
83 let mut via: HashMap<PackageId, Span> = HashMap::new();
84 let mut queue = VecDeque::new();
85 for edge in root.dependencies.iter().chain(&root.dev_dependencies) {
86 if edge.package != PackageId(0) && !via.contains_key(&edge.package) {
87 via.insert(edge.package, edge.span);
88 queue.push_back(edge.package);
89 }
90 }
91 while let Some(package) = queue.pop_front() {
92 let span = via[&package];
93 for edge in &self.package(package).dependencies {
94 if edge.package != PackageId(0) && !via.contains_key(&edge.package) {
95 via.insert(edge.package, span);
96 queue.push_back(edge.package);
97 }
98 }
99 }
100 via.get(&id).copied()
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct Offline {
107 pub registry: Option<PathBuf>,
109 pub lock: Option<Lock>,
112 pub dev: bool,
115}
116
117impl Offline {
118 pub fn from_env() -> Offline {
121 Offline {
122 registry: registry_dir(),
123 lock: None,
124 dev: true,
125 }
126 }
127}
128
129pub fn graph(root: &Path, db: &mut FileDb) -> (Option<Graph>, Vec<Diagnostic>) {
137 graph_with(root, None, &Offline::from_env(), db)
138}
139
140pub fn graph_from(
143 root: &Path,
144 text: Option<&str>,
145 db: &mut FileDb,
146) -> (Option<Graph>, Vec<Diagnostic>) {
147 graph_with(root, text, &Offline::from_env(), db)
148}
149
150pub fn graph_with(
152 root: &Path,
153 text: Option<&str>,
154 offline: &Offline,
155 db: &mut FileDb,
156) -> (Option<Graph>, Vec<Diagnostic>) {
157 let mut builder = Builder::new(root, db, offline, Mode::Graph);
158 let Some(package) = builder.read(root, text, false) else {
159 return (None, builder.diagnostics);
160 };
161 builder.check_root(&package);
162 builder.packages.push(package);
163 builder.walk();
164 let Builder {
165 diagnostics,
166 packages,
167 ..
168 } = builder;
169 (Some(Graph { packages }), diagnostics)
170}
171
172pub fn requirements(
181 root: &Path,
182 text: Option<&str>,
183 dev: bool,
184 db: &mut FileDb,
185) -> (Vec<Requirement>, Vec<Diagnostic>) {
186 let offline = Offline {
187 registry: None,
188 lock: None,
189 dev,
190 };
191 let mut builder = Builder::new(root, db, &offline, Mode::Collect);
192 let Some(package) = builder.read(root, text, false) else {
193 return (Vec::new(), builder.diagnostics);
194 };
195 builder.check_root(&package);
196 builder.packages.push(package);
197 builder.walk();
198 let graph = Graph {
199 packages: std::mem::take(&mut builder.packages),
200 };
201 let found = builder
202 .requirements
203 .into_iter()
204 .map(|(mut requirement, from)| {
205 if let Some(span) = graph.reached_through(from) {
206 requirement.span = span;
207 }
208 requirement
209 })
210 .collect();
211 (found, builder.diagnostics)
212}
213
214fn is_keyword(name: &str) -> bool {
216 nova_lexer::KEYWORDS.contains(&name)
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220enum Visit {
221 New,
222 OnPath,
223 Done,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228enum Mode {
229 Graph,
231 Collect,
233}
234
235enum LockState {
237 Missing,
238 Unreadable,
239 Read(Lock),
240}
241
242struct Builder<'a> {
243 db: &'a mut FileDb,
244 offline: &'a Offline,
245 mode: Mode,
246 root: PathBuf,
248 diagnostics: Vec<Diagnostic>,
249 packages: Vec<GraphPackage>,
250 lock: Option<LockState>,
252 requirements: Vec<(Requirement, PackageId)>,
255}
256
257impl<'a> Builder<'a> {
258 fn new(root: &Path, db: &'a mut FileDb, offline: &'a Offline, mode: Mode) -> Builder<'a> {
259 Builder {
260 db,
261 offline,
262 mode,
263 root: root.to_path_buf(),
264 diagnostics: Vec::new(),
265 packages: Vec::new(),
266 lock: None,
267 requirements: Vec::new(),
268 }
269 }
270}
271
272impl Builder<'_> {
273 fn error(&mut self, code: &str, message: String, at: Span, label: &str) {
274 self.diagnostics
275 .push(Diagnostic::error(code, message).with_primary_label(at, label));
276 }
277
278 fn check_root(&mut self, package: &GraphPackage) {
281 if self.mode == Mode::Graph && !package.has_lib && !package.has_main {
282 self.error(
283 "M0013",
284 format!(
285 "package `{}` has neither src/lib.nova nor src/main.nova",
286 package.name
287 ),
288 package.manifest.package.span,
289 "a package needs a library, a program, or both",
290 );
291 }
292 if package.has_lib && is_keyword(&package.import_name) {
293 self.error(
294 "M0012",
295 format!(
296 "library `{}` would be imported as `{}`, which is a keyword",
297 package.name, package.import_name
298 ),
299 package.manifest.package.span,
300 "choose another name",
301 );
302 }
303 }
304
305 fn walk(&mut self) {
308 let mut queue = VecDeque::from([PackageId(0)]);
309 while let Some(id) = queue.pop_front() {
310 self.expand(id, &mut queue);
311 }
312 self.cycles();
313 }
314
315 fn read(&mut self, dir: &Path, text: Option<&str>, registry: bool) -> Option<GraphPackage> {
320 let path = dir.join(MANIFEST);
321 let source = match text {
322 Some(text) => text.to_string(),
323 None => match std::fs::read_to_string(&path) {
324 Ok(source) => source,
325 Err(error) => {
326 self.diagnostics.push(Diagnostic::error(
327 "M0001",
328 format!("cannot read {}: {error}", path.display()),
329 ));
330 return None;
331 }
332 },
333 };
334 let file = self.db.add(path.display().to_string(), source.as_str());
335 let (manifest, diagnostics) = crate::manifest::parse(&source, file);
336 self.diagnostics.extend(
337 diagnostics
338 .into_iter()
339 .filter(|d| !registry || d.severity == Severity::Error),
340 );
341 let manifest = manifest?;
342 let canonical = real_path(if dir.as_os_str().is_empty() {
343 Path::new(".")
344 } else {
345 dir
346 });
347 let src = dir.join("src");
348 Some(GraphPackage {
349 name: manifest.package.name.clone(),
350 import_name: import_name(&manifest.package.name),
351 dir: dir.to_path_buf(),
352 canonical,
353 has_lib: src.join("lib.nova").is_file(),
354 has_main: src.join("main.nova").is_file(),
355 registry,
356 manifest,
357 manifest_file: file,
358 dependencies: Vec::new(),
359 dev_dependencies: Vec::new(),
360 })
361 }
362
363 fn expand(&mut self, id: PackageId, queue: &mut VecDeque<PackageId>) {
367 let package = &self.packages[id.0 as usize];
368 let mut entries: Vec<(Dependency, bool)> = package
369 .manifest
370 .dependencies
371 .iter()
372 .cloned()
373 .map(|d| (d, false))
374 .collect();
375 if id == PackageId(0) && self.offline.dev {
376 entries.extend(
377 package
378 .manifest
379 .dev_dependencies
380 .iter()
381 .cloned()
382 .map(|d| (d, true)),
383 );
384 }
385 let usable = self.names_ok(id, &entries);
386 for ((dependency, dev), ok) in entries.iter().zip(usable) {
387 if !ok {
388 continue;
389 }
390 if let Some(edge) = self.edge(id, dependency, queue) {
391 let package = &mut self.packages[id.0 as usize];
392 if *dev {
393 package.dev_dependencies.push(edge);
394 } else {
395 package.dependencies.push(edge);
396 }
397 }
398 }
399 }
400
401 fn names_ok(&mut self, id: PackageId, entries: &[(Dependency, bool)]) -> Vec<bool> {
405 let own = self.packages[id.0 as usize].import_name.clone();
406 let mut seen: Vec<(String, String)> = Vec::new();
407 let mut ok = Vec::new();
408 for (dependency, _) in entries {
409 let name = import_name(&dependency.name);
410 let problem = if is_keyword(&name) {
411 Some(format!(
412 "dependency `{}` would be imported as `{name}`, which is a keyword",
413 dependency.name
414 ))
415 } else if let Some((_, key)) = seen.iter().find(|(n, _)| *n == name) {
416 Some(if *key == dependency.name {
417 format!(
418 "`{}` is in both [dependencies] and [dev-dependencies]",
419 dependency.name
420 )
421 } else {
422 format!(
423 "`{key}` and `{}` are both imported as `{name}`",
424 dependency.name
425 )
426 })
427 } else if id == PackageId(0) && name == own {
428 Some(format!(
429 "dependency `{}` would be imported as `{name}`, this package's own name",
430 dependency.name
431 ))
432 } else {
433 None
434 };
435 match problem {
436 Some(message) => {
437 self.error("M0012", message, dependency.span, "choose another name");
438 ok.push(false);
439 }
440 None => {
441 seen.push((name, dependency.name.clone()));
442 ok.push(true);
443 }
444 }
445 }
446 ok
447 }
448
449 fn edge(
452 &mut self,
453 from: PackageId,
454 dependency: &Dependency,
455 queue: &mut VecDeque<PackageId>,
456 ) -> Option<Edge> {
457 let path = match (&dependency.path, &dependency.version) {
458 (Some(path), _) => path,
459 (None, Some(req)) => return self.registry_edge(from, dependency, &req.clone(), queue),
460 (None, None) => return None,
462 };
463 let at = dependency.path_span.unwrap_or(dependency.span);
464 let declarer = &self.packages[from.0 as usize];
465 let dir = declarer.dir.join(path);
466 let declarer_name = declarer.name.clone();
467 let declarer_canonical = declarer.canonical.clone();
468 if !dir.join(MANIFEST).is_file() {
469 self.error(
470 "M0007",
471 format!(
472 "dependency `{}`: {} is not a directory holding a nova.toml",
473 dependency.name,
474 dir.display()
475 ),
476 at,
477 "no nova.toml here",
478 );
479 return None;
480 }
481 let canonical = real_path(&dir);
482 if canonical == declarer_canonical {
483 self.error(
484 "M0010",
485 format!("dependency cycle: `{declarer_name}` depends on itself"),
486 at,
487 "this is the package's own directory",
488 );
489 return None;
490 }
491 let package = match self.packages.iter().position(|p| p.canonical == canonical) {
492 Some(index) => {
493 let found = &self.packages[index].name;
494 if *found != dependency.name {
495 let message =
496 format!("dependency `{}` is the package `{found}`", dependency.name);
497 self.error(
498 "M0008",
499 message,
500 dependency.span,
501 "an entry's key must be the package's name",
502 );
503 return None;
504 }
505 if !self.packages[index].has_lib {
508 self.error(
509 "M0009",
510 format!(
511 "dependency `{}` is not a library: {} has no src/lib.nova",
512 dependency.name,
513 dir.display()
514 ),
515 at,
516 "a dependency needs src/lib.nova",
517 );
518 return None;
519 }
520 PackageId(index as u32)
521 }
522 None => {
523 let new = self.read(&dir, None, false)?;
524 self.admit(new, dependency, at, queue)?
525 }
526 };
527 Some(Edge {
528 import_name: import_name(&dependency.name),
529 package,
530 span: dependency.span,
531 })
532 }
533
534 fn admit(
537 &mut self,
538 new: GraphPackage,
539 dependency: &Dependency,
540 at: Span,
541 queue: &mut VecDeque<PackageId>,
542 ) -> Option<PackageId> {
543 if new.name != dependency.name {
544 let message = format!(
545 "dependency `{}` is the package `{}`",
546 dependency.name, new.name
547 );
548 self.error(
549 "M0008",
550 message,
551 dependency.span,
552 "an entry's key must be the package's name",
553 );
554 return None;
555 }
556 if !new.has_lib {
557 self.error(
558 "M0009",
559 format!(
560 "dependency `{}` is not a library: {} has no src/lib.nova",
561 dependency.name,
562 new.dir.display()
563 ),
564 at,
565 "a dependency needs src/lib.nova",
566 );
567 return None;
568 }
569 if let Some(other) = self.packages.iter().find(|p| p.name == new.name) {
570 let message = format!(
571 "two packages named `{}` in one build: {} and {}",
572 new.name,
573 other.dir.display(),
574 new.dir.display()
575 );
576 self.error(
577 "M0011",
578 message,
579 dependency.span,
580 "the second package of that name",
581 );
582 return None;
583 }
584 self.packages.push(new);
585 let id = PackageId(self.packages.len() as u32 - 1);
586 queue.push_back(id);
587 Some(id)
588 }
589
590 fn registry_edge(
594 &mut self,
595 from: PackageId,
596 dependency: &Dependency,
597 req: &VersionReq,
598 queue: &mut VecDeque<PackageId>,
599 ) -> Option<Edge> {
600 if let Err(why) = check_name(&dependency.name) {
603 self.error("M0003", why, dependency.span, "not a valid package name");
604 return None;
605 }
606 if self.mode == Mode::Collect {
607 let by = self.packages[from.0 as usize].name.clone();
608 let requirement = Requirement {
609 name: dependency.name.clone(),
610 req: req.clone(),
611 by,
612 span: dependency.span,
613 };
614 self.requirements.push((requirement, from));
615 return None;
616 }
617 let name = &dependency.name;
618 let found = self
619 .lock()
620 .map(|lock| (lock.index.clone(), lock.find(name).cloned()));
621 let Some((index, Some(locked))) = found else {
622 self.missing(
623 dependency,
624 format!("dependency `{name}` is not downloaded yet; run `nova fetch`"),
625 );
626 return None;
627 };
628 if !req.matches(&locked.version) {
629 self.missing(
630 dependency,
631 format!(
632 "dependency `{name}` is locked at {}, which does not meet `{req}`; run \
633 `nova fetch`",
634 locked.version
635 ),
636 );
637 return None;
638 }
639 let Some(registry) = self.offline.registry.clone() else {
640 self.missing(
641 dependency,
642 format!(
643 "dependency `{name}` cannot be found: cannot place the package cache: set \
644 NOVA_HOME to a writable directory"
645 ),
646 );
647 return None;
648 };
649 let dir = registry
650 .join("src")
651 .join(index_dir_name(&index))
652 .join(format!("{name}-{}", locked.version));
653 if !dir.join(MANIFEST).is_file() {
654 self.missing(
655 dependency,
656 format!("dependency `{name}` is not downloaded yet; run `nova fetch`"),
657 );
658 return None;
659 }
660 let canonical = real_path(&dir);
661 let package = match self.packages.iter().position(|p| p.canonical == canonical) {
662 Some(index) => PackageId(index as u32),
663 None => {
664 let new = self.read(&dir, None, true)?;
665 if let Some(entry) = new.manifest.dependencies.iter().find(|d| d.path.is_some()) {
668 self.error(
669 "M0017",
670 format!(
671 "downloaded package `{name}` {} has a path dependency `{}`",
672 locked.version, entry.name
673 ),
674 dependency.span,
675 "declared here",
676 );
677 return None;
678 }
679 let mut names: Vec<String> = new
680 .manifest
681 .dependencies
682 .iter()
683 .map(|d| d.name.clone())
684 .collect();
685 names.sort();
686 if names != locked.dependencies {
687 self.missing(
688 dependency,
689 format!(
690 "the downloaded copy of `{name}` {} does not match nova.lock; delete \
691 {} and run `nova fetch`",
692 locked.version,
693 dir.display()
694 ),
695 );
696 return None;
697 }
698 self.admit(new, dependency, dependency.span, queue)?
699 }
700 };
701 Some(Edge {
702 import_name: import_name(name),
703 package,
704 span: dependency.span,
705 })
706 }
707
708 fn missing(&mut self, dependency: &Dependency, message: String) {
710 self.diagnostics.push(
711 Diagnostic::error("M0005", message)
712 .with_primary_label(dependency.span, "declared here"),
713 );
714 }
715
716 fn lock(&mut self) -> Option<&Lock> {
719 if self.lock.is_none() {
720 let state = match &self.offline.lock {
721 Some(lock) => LockState::Read(lock.clone()),
722 None => {
723 let path = self.root.join(LOCKFILE);
724 match std::fs::read_to_string(&path) {
725 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
726 LockState::Missing
727 }
728 Err(error) => {
729 self.diagnostics.push(
730 Diagnostic::error(
731 "M0016",
732 format!("nova.lock cannot be read: {error}"),
733 )
734 .with_note("delete nova.lock, or run `nova update`"),
735 );
736 LockState::Unreadable
737 }
738 Ok(text) => {
739 let file = self.db.add(path.display().to_string(), text.as_str());
740 match parse_lock(&text, file) {
741 Ok(lock) => LockState::Read(lock),
742 Err(diagnostic) => {
743 self.diagnostics.push(diagnostic);
744 LockState::Unreadable
745 }
746 }
747 }
748 }
749 }
750 };
751 self.lock = Some(state);
752 }
753 match &self.lock {
754 Some(LockState::Read(lock)) => Some(lock),
755 _ => None,
756 }
757 }
758
759 fn cycles(&mut self) {
762 let count = self.packages.len();
763 let mut state = vec![Visit::New; count];
764 let mut path = Vec::new();
765 for start in 0..count {
766 if state[start] == Visit::New {
767 self.visit(PackageId(start as u32), &mut state, &mut path);
768 }
769 }
770 }
771
772 fn visit(&mut self, id: PackageId, state: &mut [Visit], path: &mut Vec<PackageId>) {
773 state[id.0 as usize] = Visit::OnPath;
774 path.push(id);
775 for edge in self.packages[id.0 as usize].dependencies.clone() {
776 match state[edge.package.0 as usize] {
777 Visit::OnPath => {
778 let start = path.iter().position(|p| *p == edge.package).unwrap_or(0);
779 let names: Vec<&str> = path[start..]
780 .iter()
781 .chain([&edge.package])
782 .map(|p| self.packages[p.0 as usize].name.as_str())
783 .collect();
784 let message = format!("dependency cycle: {}", names.join(" -> "));
785 self.error("M0010", message, edge.span, "this entry closes the cycle");
786 }
787 Visit::New => self.visit(edge.package, state, path),
788 Visit::Done => {}
789 }
790 }
791 path.pop();
792 state[id.0 as usize] = Visit::Done;
793 }
794}