Skip to main content

nova_driver/
program.rs

1//! What the driver loads (spec
2//! `docs/superpowers/specs/2026-10-08-phase-3-3a-local-packages-design.md`
3//! §4): a [`Program`] names its roots and carries the package graph, and
4//! [`load_program`] decides what each `import` names.
5
6use std::collections::{HashMap, HashSet, VecDeque};
7use std::path::{Path, PathBuf};
8
9use nova_diagnostics::{Diagnostic, FileDb, FileId, Severity, Span};
10use nova_pm::{Edge, Graph, GraphPackage, Offline, PackageId};
11use nova_resolver::ImportTarget;
12
13use crate::analyze::Sources;
14
15/// The roots a command takes (spec §4.4's table), each only if it exists.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Roots {
18    /// `src/main.nova`: `nova run` and `nova build`.
19    Program,
20    /// `src/main.nova`, then `src/lib.nova`: `nova check`.
21    Check,
22    /// Those, then `tests/*.nova` sorted by name: `nova test` and the
23    /// language server.
24    Test,
25}
26
27/// Where a root is.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum RootKind {
30    /// Directly in the root package's `src/`.
31    Src,
32    /// Directly in the root package's `tests/`.
33    Tests,
34    /// In no package (spec §4.1).
35    Loose,
36    /// A doc test module (spec
37    /// `docs/superpowers/specs/2026-10-11-phase-3-5b-doc-tests-design.md`
38    /// §5), at a path of its own that need not be on disk. In a package's
39    /// program it imports what a `tests/` module imports, and no file
40    /// beside it; without a graph (std's doc tests) it imports nothing. A
41    /// program with one keeps only the test each doc test module adds.
42    Doc,
43}
44
45/// A file the loader starts from.
46#[derive(Debug, Clone)]
47pub struct Root {
48    pub path: PathBuf,
49    pub kind: RootKind,
50}
51
52/// What to load (spec §4.4).
53pub struct Program {
54    /// Every manifest the graph read. The loader adds every source file.
55    pub db: FileDb,
56    /// `None` for a loose program, or when the root's manifest cannot be
57    /// read or parsed.
58    pub graph: Option<Graph>,
59    /// The entry first.
60    pub roots: Vec<Root>,
61    /// Whether MIR runs: the entry is `src/main.nova`, or a loose file.
62    pub runs: bool,
63    /// The graph's diagnostics.
64    pub diagnostics: Vec<Diagnostic>,
65}
66
67impl Program {
68    /// A loose program: `path`, and the files beside it that it imports.
69    pub fn loose(path: &Path) -> Program {
70        Program {
71            db: FileDb::new(),
72            graph: None,
73            roots: vec![Root {
74                path: path.to_path_buf(),
75                kind: RootKind::Loose,
76            }],
77            runs: true,
78            diagnostics: Vec::new(),
79        }
80    }
81
82    /// The program whose entry is `path` (spec §4.1). It is a module of its
83    /// package when it is directly in a package's `src/` or `tests/`, and
84    /// otherwise loose, reading no manifest.
85    pub fn for_file(path: &Path) -> Program {
86        let Some((root, kind)) = package_of(path) else {
87            return Program::loose(path);
88        };
89        let mut db = FileDb::new();
90        let (graph, diagnostics) = nova_pm::graph(&root, &mut db);
91        let runs = kind == RootKind::Src && path.file_stem().is_some_and(|stem| stem == "main");
92        Program {
93            db,
94            graph,
95            roots: vec![Root {
96                path: path.to_path_buf(),
97                kind,
98            }],
99            runs,
100            diagnostics,
101        }
102    }
103
104    /// The package whose `nova.toml` is in `root` (empty for the current
105    /// directory), with the roots `roots` names that exist.
106    pub fn for_package(root: &Path, roots: Roots) -> Program {
107        Program::for_package_in(root, roots, &Offline::from_env())
108    }
109
110    /// [`Program::for_package`], with registry packages found as `offline`
111    /// says (spec 3.3b §5.4): tests pass the registry directory, and
112    /// publishing's verification reads no dev-dependencies.
113    pub fn for_package_in(root: &Path, roots: Roots, offline: &Offline) -> Program {
114        let mut db = FileDb::new();
115        let (graph, diagnostics) = nova_pm::graph_with(root, None, offline, &mut db);
116        let mut list = Vec::new();
117        let main = under(root, "src/main.nova");
118        if main.is_file() {
119            list.push(Root {
120                path: main,
121                kind: RootKind::Src,
122            });
123        }
124        let runs = !list.is_empty();
125        let lib = under(root, "src/lib.nova");
126        if roots != Roots::Program && lib.is_file() {
127            list.push(Root {
128                path: lib,
129                kind: RootKind::Src,
130            });
131        }
132        if roots == Roots::Test {
133            list.extend(
134                test_files(&under(root, "tests"))
135                    .into_iter()
136                    .map(|path| Root {
137                        path,
138                        kind: RootKind::Tests,
139                    }),
140            );
141        }
142        Program {
143            db,
144            graph,
145            roots: list,
146            runs,
147            diagnostics,
148        }
149    }
150
151    /// A program of doc tests (spec 3.5b §5, §6.2), each of `paths` a
152    /// [`RootKind::Doc`] root, read through the `Sources` given to
153    /// `build_test_program_with`: of the package whose `nova.toml` is in
154    /// `package`, or of no package for std's (`None`).
155    ///
156    /// A package's library, `src/lib.nova`, is a root after them. The
157    /// loader then reads it under the path the root names, as `nova doc`'s
158    /// analysis does, rather than the one an import names, which differs on
159    /// Windows under an empty root; a diagnostic's map finds the library in
160    /// the database by that name (spec 3.5b §7). The first doc test stays
161    /// the entry, which names the binary apart from the unit tests'.
162    ///
163    /// With no `paths` there is no root, and a build fails with "nothing
164    /// to compile" rather than build the library's own tests.
165    pub fn doc_tests(package: Option<&Path>, paths: Vec<PathBuf>) -> Program {
166        let mut roots: Vec<Root> = paths
167            .into_iter()
168            .map(|path| Root {
169                path,
170                kind: RootKind::Doc,
171            })
172            .collect();
173        match package {
174            Some(root) => {
175                let lib = under(root, "src/lib.nova");
176                if !roots.is_empty() && lib.is_file() {
177                    roots.push(Root {
178                        path: lib,
179                        kind: RootKind::Src,
180                    });
181                }
182                Program {
183                    roots,
184                    runs: false,
185                    ..Program::for_package(root, Roots::Program)
186                }
187            }
188            None => Program {
189                db: FileDb::new(),
190                graph: None,
191                roots,
192                runs: false,
193                diagnostics: Vec::new(),
194            },
195        }
196    }
197
198    /// Whether the graph found an error.
199    pub fn has_errors(&self) -> bool {
200        self.diagnostics
201            .iter()
202            .any(|d| d.severity == Severity::Error)
203    }
204
205    /// The root package's `nova.toml` in `db`, once read. The language
206    /// server publishes the graph's problems under it (spec §6).
207    pub fn manifest(&self) -> Option<FileId> {
208        match &self.graph {
209            Some(graph) => Some(graph.root().manifest_file),
210            // Without a graph only the root's manifest was read, and each of
211            // its diagnostics is labelled in it.
212            None => self
213                .diagnostics
214                .iter()
215                .flat_map(|d| &d.labels)
216                .map(|label| label.span.file)
217                .next(),
218        }
219    }
220}
221
222/// `relative`, a `/`-separated path, under `root`. Under an empty root it
223/// is `relative` as written, so `nova run` at a project's root still names
224/// `src/main.nova` on every system, as it always has.
225fn under(root: &Path, relative: &str) -> PathBuf {
226    if root.as_os_str().is_empty() {
227        PathBuf::from(relative)
228    } else {
229        relative
230            .split('/')
231            .fold(root.to_path_buf(), |path, part| path.join(part))
232    }
233}
234
235/// The package `path` is a module of, and where in it, when `path` is
236/// directly in the `src/` or `tests/` of a directory holding a `nova.toml`
237/// (spec §4.1). The language server finds its projects the same way.
238pub fn package_of(path: &Path) -> Option<(PathBuf, RootKind)> {
239    let parent = path.parent()?;
240    let kind = match parent.file_name()?.to_str()? {
241        "src" => RootKind::Src,
242        "tests" => RootKind::Tests,
243        _ => return None,
244    };
245    let root = parent.parent()?;
246    root.join(nova_pm::MANIFEST)
247        .is_file()
248        .then(|| (root.to_path_buf(), kind))
249}
250
251/// The `*.nova` files directly in `dir`, sorted by name (spec §4.4). A
252/// missing directory has none.
253fn test_files(dir: &Path) -> Vec<PathBuf> {
254    let Ok(entries) = std::fs::read_dir(dir) else {
255        return Vec::new();
256    };
257    let mut files: Vec<PathBuf> = entries
258        .filter_map(Result::ok)
259        .map(|entry| entry.path())
260        .filter(|path| path.extension().is_some_and(|ext| ext == "nova") && path.is_file())
261        .collect();
262    files.sort();
263    files
264}
265
266/// The files of `modules` that belong to a registry package (spec 3.3b
267/// §5.4). A registry manifest's warnings never leave the graph.
268pub(crate) fn registry_files(graph: Option<&Graph>, modules: &[Loaded]) -> HashSet<FileId> {
269    let Some(graph) = graph else {
270        return HashSet::new();
271    };
272    modules
273        .iter()
274        .filter(|m| m.package.is_some_and(|p| graph.package(p).registry))
275        .map(|m| m.file)
276        .collect()
277}
278
279/// Whether `d` is a warning about a registry package alone, which is not
280/// shown, as Cargo caps a dependency's lints: it has a label, and every
281/// label is in `registry`.
282pub(crate) fn a_dependencys_warning(d: &Diagnostic, registry: &HashSet<FileId>) -> bool {
283    d.severity == Severity::Warning
284        && !d.labels.is_empty()
285        && d.labels
286            .iter()
287            .all(|label| registry.contains(&label.span.file))
288}
289
290/// One module [`load_program`] read.
291pub(crate) struct Loaded {
292    /// A label for messages (spec §4.5): `utils`, `tests/utils`, `geom` or
293    /// `geom/utils`.
294    pub name: String,
295    pub path: PathBuf,
296    pub file: FileId,
297    pub ast: nova_ast::File,
298    /// Its package. `None` for a loose module.
299    pub package: Option<PackageId>,
300    /// Whether it is directly in its package's `tests/`, or a doc test
301    /// module: its tests are named `<file stem>::<function>`.
302    pub test_file: bool,
303    /// Whether it is a doc test module (spec 3.5b §6.2).
304    pub doc: bool,
305    /// What each of its imports names, by first segment (spec §4.3).
306    pub imports: HashMap<String, ImportTarget>,
307    /// Every loaded module it could import, by the name an import of it
308    /// writes (spec 3.4b §4.6).
309    pub importable: Vec<(String, usize)>,
310}
311
312/// What [`load_program`] found.
313#[derive(Default)]
314pub(crate) struct Load {
315    /// In load order: `modules[i]` is `ModuleId(i)`.
316    pub modules: Vec<Loaded>,
317    /// Lex, parse and import diagnostics, in load order.
318    pub diagnostics: Vec<Diagnostic>,
319    /// The names of the items the parser dropped.
320    pub dropped: Vec<String>,
321}
322
323/// A module's identity (spec §4.2).
324#[derive(Debug, Clone, PartialEq, Eq, Hash)]
325enum Place {
326    /// A file directly in a package's `src/`, or `tests/` when `tests`.
327    Package {
328        package: PackageId,
329        tests: bool,
330        stem: String,
331    },
332    /// A loose file.
333    Loose { dir: PathBuf, stem: String },
334    /// A doc test module, by its path (spec 3.5b §5): of the root package,
335    /// or of none for std's. Nothing imports one.
336    Doc {
337        package: Option<PackageId>,
338        path: PathBuf,
339    },
340}
341
342impl Place {
343    fn package(&self) -> Option<PackageId> {
344        match self {
345            Place::Package { package, .. } => Some(*package),
346            Place::Loose { .. } => None,
347            Place::Doc { package, .. } => *package,
348        }
349    }
350
351    fn is_test(&self) -> bool {
352        matches!(self, Place::Package { tests: true, .. } | Place::Doc { .. })
353    }
354
355    fn is_doc(&self) -> bool {
356        matches!(self, Place::Doc { .. })
357    }
358}
359
360/// What an import names, before every module is loaded.
361enum Pending {
362    Place(Place),
363    Reported,
364}
365
366/// What [`Loader::find`] found for one import.
367enum Found {
368    /// A module: its identity and its file.
369    Module(Place, PathBuf),
370    /// An error, reported here (spec §4.3, rules 3–5).
371    Reported(Diagnostic),
372    /// Nothing: the resolver reports "cannot find module".
373    Nothing,
374}
375
376/// Read, lex and parse every module the roots reach, breadth first from the
377/// roots in order (spec §4.4), deciding what each import names (§4.3).
378///
379/// A module that cannot be read is skipped, and an import of it is then
380/// "cannot find module". `Err` only when the entry itself cannot be read.
381/// Without a graph, every root is loose.
382pub(crate) fn load_program(
383    graph: Option<&Graph>,
384    roots: &[Root],
385    sources: &dyn Sources,
386    db: &mut FileDb,
387) -> std::io::Result<Load> {
388    let mut loader = Loader {
389        graph,
390        sources,
391        listings: HashMap::new(),
392        known: HashMap::new(),
393    };
394    let mut load = Load::default();
395    let mut pending: Vec<Vec<(String, Pending)>> = Vec::new();
396    let mut places: HashMap<Place, usize> = HashMap::new();
397    let mut tried: HashSet<Place> = HashSet::new();
398    let mut queue: VecDeque<(Place, PathBuf)> = roots
399        .iter()
400        .map(|root| (loader.root_place(root), root.path.clone()))
401        .collect();
402    let mut is_entry = true;
403
404    while let Some((place, path)) = queue.pop_front() {
405        let entry = std::mem::replace(&mut is_entry, false);
406        if !tried.insert(place.clone()) {
407            continue;
408        }
409        let source = match sources.read(&path) {
410            Ok(source) => source,
411            Err(e) if entry => return Err(e),
412            Err(_) => continue,
413        };
414        let file = db.add(path.display().to_string(), source.as_str());
415
416        let (tokens, lex_errors) = nova_lexer::lex(&source, file);
417        load.diagnostics.extend(lex_errors.iter().map(|e| {
418            Diagnostic::error("L0001", e.to_string()).with_primary_label(e.span(), "here")
419        }));
420        let parsed = nova_parser::parse_recovering(&tokens, file);
421        load.diagnostics.extend(parsed.errors.iter().map(|e| {
422            Diagnostic::error("P0001", e.to_string()).with_primary_label(e.span(), "here")
423        }));
424        load.dropped
425            .extend(parsed.dropped.into_iter().map(|n| n.value));
426
427        // Only a single-segment import names a module; the resolver rejects
428        // the others.
429        let mut imports = Vec::new();
430        for item in &parsed.file.items {
431            let nova_ast::Item::Import(import) = &item.value else {
432                continue;
433            };
434            let [segment] = import.path.value.segments.as_slice() else {
435                continue;
436            };
437            let name = segment.value.clone();
438            match loader.find(&place, &name, import.path.span, db) {
439                Found::Module(target, target_path) => {
440                    queue.push_back((target.clone(), target_path));
441                    imports.push((name, Pending::Place(target)));
442                }
443                Found::Reported(diagnostic) => {
444                    load.diagnostics.push(diagnostic);
445                    imports.push((name, Pending::Reported));
446                }
447                Found::Nothing => {}
448            }
449        }
450        places.insert(place.clone(), load.modules.len());
451        load.modules.push(Loaded {
452            name: loader.label(&place),
453            path,
454            file,
455            ast: parsed.file,
456            package: place.package(),
457            test_file: place.is_test(),
458            doc: place.is_doc(),
459            imports: HashMap::new(),
460            importable: Vec::new(),
461        });
462        pending.push(imports);
463    }
464
465    for (module, imports) in load.modules.iter_mut().zip(pending) {
466        for (name, target) in imports {
467            let target = match target {
468                Pending::Place(place) => match places.get(&place) {
469                    Some(&index) => ImportTarget::Module(index),
470                    // It could not be read.
471                    None => continue,
472                },
473                Pending::Reported => ImportTarget::Reported,
474            };
475            module.imports.insert(name, target);
476        }
477    }
478    // Spec 3.4b §4.6 (plan decision 9): what each loaded module could
479    // import, by the name an import of it writes, through `find` itself.
480    let mut place_of: Vec<Option<Place>> = vec![None; load.modules.len()];
481    for (place, &i) in &places {
482        place_of[i] = Some(place.clone());
483    }
484    let nowhere = Span::point(0, FileId::DUMMY);
485    let mut importables: Vec<Vec<(String, usize)>> = Vec::new();
486    for (i, from) in place_of.iter().enumerate() {
487        let mut importable = Vec::new();
488        if let Some(from) = from {
489            let mut names: Vec<String> = place_of
490                .iter()
491                .flatten()
492                .filter_map(|p| sibling_stem(from, p))
493                .collect();
494            let edges = match (from, graph) {
495                (Place::Package { package, tests, .. }, Some(graph)) => {
496                    visible_edges(graph, *package, *tests)
497                }
498                (
499                    Place::Doc {
500                        package: Some(package),
501                        ..
502                    },
503                    Some(graph),
504                ) => visible_edges(graph, *package, true),
505                _ => Vec::new(),
506            };
507            names.extend(edges.into_iter().map(|e| e.import_name));
508            names.sort();
509            names.dedup();
510            for name in names {
511                if let Found::Module(target, _) = loader.find(from, &name, nowhere, db) {
512                    if let Some(&j) = places.get(&target) {
513                        if j != i {
514                            importable.push((name, j));
515                        }
516                    }
517                }
518            }
519        }
520        importables.push(importable);
521    }
522    for (module, importable) in load.modules.iter_mut().zip(importables) {
523        module.importable = importable;
524    }
525    Ok(load)
526}
527
528struct Loader<'a> {
529    graph: Option<&'a Graph>,
530    sources: &'a dyn Sources,
531    /// Each directory's file names, read once (spec §4.3, decision 5).
532    listings: HashMap<PathBuf, Vec<String>>,
533    /// Each `has` answered, by directory and stem: a name the listing lacks
534    /// costs one read per load, not one per module (spec 3.5a §3,
535    /// follow-up 2).
536    known: HashMap<(PathBuf, String), bool>,
537}
538
539impl<'a> Loader<'a> {
540    fn root_place(&self, root: &Root) -> Place {
541        let stem = crate::FrontendContext::module_name(&root.path);
542        match (root.kind, self.graph) {
543            (RootKind::Src, Some(_)) => Place::Package {
544                package: PackageId(0),
545                tests: false,
546                stem,
547            },
548            (RootKind::Tests, Some(_)) => Place::Package {
549                package: PackageId(0),
550                tests: true,
551                stem,
552            },
553            (RootKind::Doc, graph) => Place::Doc {
554                package: graph.map(|_| PackageId(0)),
555                path: root.path.clone(),
556            },
557            _ => Place::Loose {
558                dir: root
559                    .path
560                    .parent()
561                    .map(Path::to_path_buf)
562                    .unwrap_or_default(),
563                stem,
564            },
565        }
566    }
567
568    /// The module's label (spec §4.5).
569    fn label(&self, place: &Place) -> String {
570        match place {
571            Place::Loose { stem, .. } => stem.clone(),
572            Place::Doc { path, .. } => format!("doc/{}", crate::FrontendContext::module_name(path)),
573            Place::Package {
574                package: PackageId(0),
575                tests,
576                stem,
577            } => {
578                if *tests {
579                    format!("tests/{stem}")
580                } else {
581                    stem.clone()
582                }
583            }
584            Place::Package { package, stem, .. } => {
585                let import = &self.graph().package(*package).import_name;
586                if stem == "lib" {
587                    import.clone()
588                } else {
589                    format!("{import}/{stem}")
590                }
591            }
592        }
593    }
594
595    /// The graph, borrowed for `'a` rather than from `self`, so `has` can
596    /// run while it is held.
597    fn graph(&self) -> &'a Graph {
598        self.graph.expect("a package's module has a graph")
599    }
600
601    /// What `import name`, at `at` in the module at `from`, names (spec
602    /// §4.3).
603    fn find(&mut self, from: &Place, name: &str, at: Span, db: &FileDb) -> Found {
604        let (package, tests, siblings) = match from {
605            Place::Loose { dir, .. } => {
606                if !self.has(dir, name) {
607                    return Found::Nothing;
608                }
609                let place = Place::Loose {
610                    dir: dir.clone(),
611                    stem: name.to_string(),
612                };
613                return Found::Module(place, dir.join(format!("{name}.nova")));
614            }
615            Place::Package { package, tests, .. } => (*package, *tests, true),
616            // What a `tests/` module sees, less the files beside it (spec
617            // 3.5b §5.1).
618            Place::Doc {
619                package: Some(package),
620                ..
621            } => (*package, true, false),
622            // std's doc tests import nothing.
623            Place::Doc { package: None, .. } => return Found::Nothing,
624        };
625        let graph = self.graph();
626        let dir = module_dir(graph.package(package), tests);
627        let edge = visible_edges(graph, package, tests)
628            .into_iter()
629            .find(|edge| edge.import_name == name);
630        let file = (siblings && self.has(&dir, name)).then(|| {
631            (
632                Place::Package {
633                    package,
634                    tests,
635                    stem: name.to_string(),
636                },
637                dir.join(format!("{name}.nova")),
638            )
639        });
640        match (file, edge) {
641            (Some((_, path)), Some(edge)) => Found::Reported(clash(name, &path, &edge, at, db)),
642            (Some((place, path)), None) => Found::Module(place, path),
643            (None, Some(edge)) => {
644                let target = graph.package(edge.package);
645                if !target.has_lib {
646                    // The package itself, seen from its own tests (rule 5).
647                    return Found::Reported(
648                        Diagnostic::error("E0001", format!("cannot find module `{name}`"))
649                            .with_primary_label(at, "no such module")
650                            .with_note(format!("`{}` has no src/lib.nova", target.name)),
651                    );
652                }
653                Found::Module(
654                    Place::Package {
655                        package: edge.package,
656                        tests: false,
657                        stem: "lib".to_string(),
658                    },
659                    module_dir(target, false).join("lib.nova"),
660                )
661            }
662            (None, None) => {
663                let dev = !tests
664                    && package == PackageId(0)
665                    && graph
666                        .root()
667                        .dev_dependencies
668                        .iter()
669                        .any(|edge| edge.import_name == name);
670                if dev {
671                    Found::Reported(
672                        Diagnostic::error("E0001", format!("cannot find module `{name}`"))
673                            .with_primary_label(at, "no such module")
674                            .with_note(format!(
675                                "`{name}` is a dev-dependency, which only `tests/` files can import"
676                            )),
677                    )
678                } else {
679                    Found::Nothing
680                }
681            }
682        }
683    }
684
685    /// Whether `dir` holds `<stem>.nova` ([`Loader::look`]), answered once
686    /// per load (spec 3.5a §3, follow-up 2).
687    fn has(&mut self, dir: &Path, stem: &str) -> bool {
688        let key = (dir.to_path_buf(), stem.to_string());
689        if let Some(&known) = self.known.get(&key) {
690            return known;
691        }
692        let found = self.look(dir, stem);
693        self.known.insert(key, found);
694        found
695    }
696
697    /// Whether `dir` holds `<stem>.nova`, its name matching exactly (spec
698    /// §4.3). A name the disk does not hold in any case may be an editor's
699    /// unsaved buffer, read through `Sources`.
700    ///
701    /// An empty `dir`, a loose entry named without one (`nova run
702    /// main.nova`), is the current directory: `read_dir("")` fails on every
703    /// system, which would leave the case-insensitive read to decide.
704    fn look(&mut self, dir: &Path, stem: &str) -> bool {
705        let name = format!("{stem}.nova");
706        let listed = if dir.as_os_str().is_empty() {
707            Path::new(".")
708        } else {
709            dir
710        };
711        let listing = self.listings.entry(dir.to_path_buf()).or_insert_with(|| {
712            std::fs::read_dir(listed)
713                .map(|entries| {
714                    entries
715                        .filter_map(Result::ok)
716                        .filter_map(|entry| entry.file_name().into_string().ok())
717                        .collect()
718                })
719                .unwrap_or_default()
720        });
721        if listing.contains(&name) {
722            return true;
723        }
724        if listing.iter().any(|n| n.eq_ignore_ascii_case(&name)) {
725            return false;
726        }
727        self.sources.read(&dir.join(&name)).is_ok()
728    }
729}
730
731/// A package's `src/`, or `tests/` when `tests`.
732fn module_dir(package: &GraphPackage, tests: bool) -> PathBuf {
733    package.dir.join(if tests { "tests" } else { "src" })
734}
735
736/// `p`'s stem when it is a module beside `from`: a loose file in the same
737/// directory, or a module of the same package and directory (spec 3.4b
738/// §4.6).
739fn sibling_stem(from: &Place, p: &Place) -> Option<String> {
740    match (from, p) {
741        (Place::Loose { dir: a, .. }, Place::Loose { dir: b, stem }) if a == b => {
742            Some(stem.clone())
743        }
744        (
745            Place::Package {
746                package: a,
747                tests: s,
748                ..
749            },
750            Place::Package {
751                package: b,
752                tests: t,
753                stem,
754            },
755        ) if a == b && s == t => Some(stem.clone()),
756        _ => None,
757    }
758}
759
760/// The dependencies a module of `package` sees (spec §4.3). A test module
761/// also sees the root's dev-dependencies, and the root itself by its import
762/// name. Only the root's test modules are ever loaded.
763fn visible_edges(graph: &Graph, package: PackageId, tests: bool) -> Vec<Edge> {
764    let p = graph.package(package);
765    let mut edges = p.dependencies.clone();
766    if tests {
767        edges.extend(p.dev_dependencies.iter().cloned());
768        edges.push(Edge {
769            import_name: p.import_name.clone(),
770            package,
771            span: p.manifest.package.span,
772        });
773    }
774    edges
775}
776
777/// E0004 (spec §4.3, rule 3): `name` is both `file` and the dependency
778/// `edge`. The message names both, so it reads in full where a related
779/// label cannot show.
780fn clash(name: &str, file: &Path, edge: &Edge, at: Span, db: &FileDb) -> Diagnostic {
781    let manifest = db.get_name(edge.span.file).unwrap_or("nova.toml");
782    let line = db
783        .location(edge.span.file, edge.span.start)
784        .map_or(0, |(line, _)| line);
785    Diagnostic::error(
786        "E0004",
787        format!(
788            "`{name}` is both a module of this package and a dependency: {}, and the entry \
789             at {manifest}:{line}",
790            file.display()
791        ),
792    )
793    .with_primary_label(at, "this import is ambiguous")
794    .with_secondary_label(edge.span, "the dependency")
795}