Skip to main content

zelkova_compiler/
resolve.rs

1//! The packages a build is made from, and the module names each of them sees.
2//!
3//! A package is compiled against its own modules and the public modules of the packages
4//! its manifest lists — [*Imports across a package
5//! boundary*](../../docs/spec/packages.md#imports-across-a-package-boundary). This
6//! module answers the two questions that has to be settled before any module of any
7//! package is compiled:
8//!
9//! 1. **Which packages are in the build**, and in what order they can be compiled.
10//!    [`resolve`] walks the manifests from the root package outwards and hands back the
11//!    set it reached, dependencies first.
12//! 2. **What each module is called** inside the package that imports it.
13//!    [`visible_modules`] builds that one map — the package's own modules, plus each
14//!    direct dependency's public ones under [the namespace](PackageName::namespace) or,
15//!    for an unwrapped dependency, under their own names — and reports a name two
16//!    modules both answer to.
17//!
18//! The collision check is here, rather than in the phase that would trip over the
19//! ambiguity, because the manifest is what creates it and the manifest is what has to
20//! change. A package with no coherent answer for a name is stopped before any of its
21//! modules is canonicalized — its files are read and parsed first, since the map is
22//! keyed by the names the module headers declare.
23//!
24//! # What this obtains, and what it does not
25//!
26//! A `path` dependency is read where it lies and compiled from source like any other
27//! package. A `git` dependency is not obtained at all — fetching a source, caching it
28//! and recording the result in `zelkova.lock` is [the toolchain
29//! appendix](../../docs/spec/toolchain.md#where-a-dependency-comes-from)'s, and none
30//! of it is written yet — so an entry naming one is
31//! [`Error::UnsupportedSource`] rather than a package quietly
32//! missing from the build.
33//!
34//! `zelkova-core` is a dependency of every package and [is not written in
35//! `dependencies`](../../docs/spec/packages.md#zelkova-core-is-a-dependency-of-every-package);
36//! nothing here supplies it, because a compiler that ships its own core has to know
37//! where that copy sits and nothing yet says where. What is honoured is the other half
38//! of that rule: a package named `zelkova-core` is seen unwrapped whatever the entry
39//! naming it says, so `Basics` is `Basics`. What makes `Basics.Int` the scalar is not
40//! that spelling but the package that declares it: [`scalars`](super::scalars) knows a
41//! scalar by a qualified name that includes [`CORE_PACKAGE`] ([`DEC-15` decision
42//! 1](../../docs/decisions/dec-15.md#1--a-scalar-type-is-known-by-its-qualified-name)).
43
44use std::collections::HashMap;
45use std::path::{Path, PathBuf};
46
47use super::default_imports;
48use super::manifest::{Manifest, ManifestError, Source, Version, MANIFEST_FILE_NAME};
49use super::name::Name;
50use super::{PackageName, PhaseError};
51
52/// The package every other package is seen through, and the one package name the
53/// compiler knows on its own.
54///
55/// It is [seen unwrapped in every
56/// package](../../docs/spec/packages.md#zelkova-core-is-a-dependency-of-every-package),
57/// which is what makes `Basics` `Basics` everywhere, and it is the package every
58/// [scalar](super::scalars) is declared in — [`PackageName::core`] is this name.
59pub const CORE_PACKAGE: &str = "zelkova-core";
60
61/// One package of the resolved build: its name, the directory holding its
62/// `zelkova.toml`, and that manifest.
63#[derive(Debug, Clone)]
64pub struct ResolvedPackage {
65    pub name: PackageName,
66    pub root: PathBuf,
67    pub manifest: Manifest,
68}
69
70impl ResolvedPackage {
71    /// The path of this package's manifest, which is the location every error about it
72    /// names.
73    pub fn manifest_path(&self) -> PathBuf {
74        self.root.join(MANIFEST_FILE_NAME)
75    }
76
77    /// Whether this package's modules are seen by their own names in a package that
78    /// depends on it, given what that package's entry for it says.
79    ///
80    /// `wrapped` is the entry's own flag; `zelkova-core` overrides it, since it is seen
81    /// unwrapped in every package whatever any manifest says.
82    fn seen_unwrapped(&self, wrapped: bool) -> bool {
83        !wrapped || self.name.as_str() == CORE_PACKAGE
84    }
85}
86
87/// How a module came to be visible in the package being compiled, which is the whole of
88/// what a collision diagnostic has to say about each of the two modules claiming one
89/// name.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum OriginKind {
92    /// Declared by the package being compiled, in the file named — `src/Model.zel`.
93    ///
94    /// The file is part of the answer because a package's two source roots share one
95    /// set of module names, so naming the package is not enough to say which of two
96    /// local modules a collision is about.
97    Local { file: String },
98    /// A public module of a dependency seen unwrapped, so it is named by its own name.
99    Unwrapped,
100    /// A public module of a wrapped dependency, named through that package's namespace.
101    Namespaced,
102}
103
104/// One entry of the map of names a package can import: which package the module belongs
105/// to, what that package calls it, and how it came to be visible here.
106///
107/// `package` and `module` together are the module's identity — the pair an
108/// [`Interface`](super::Interface) is found by, and `module` is always the module's
109/// name *within its own package*. The spelling this entry is keyed under is a different
110/// thing, and belongs to the importing package alone.
111#[derive(Debug, Clone)]
112pub struct ModuleOrigin {
113    pub package: PackageName,
114    pub version: Version,
115    pub module: Name,
116    pub kind: OriginKind,
117}
118
119impl ModuleOrigin {
120    /// This origin as one line of a collision diagnostic — the package it is in, that
121    /// package's version, and how it reached the package being compiled.
122    ///
123    /// The namespaced case names the module as its own package calls it, because that
124    /// is the one thing the collision's headline — which quotes the spelling — does not
125    /// already say.
126    fn describe(&self) -> String {
127        let how = match &self.kind {
128            OriginKind::Local { file } => format!("`{}`, a module of this package", file),
129            OriginKind::Unwrapped => "imported unwrapped".to_string(),
130            OriginKind::Namespaced => {
131                format!("module `{}`, imported under its namespace", self.module)
132            }
133        };
134
135        format!("{} {}, {}", self.package, self.version, how)
136    }
137}
138
139/// A dependency as the package being compiled sees it: the resolved package, whether
140/// this package sees it wrapped, and the modules it offers.
141///
142/// `modules` holds the dependency's **public** modules only, by their names within it —
143/// [what a package exposes](../../docs/spec/packages.md#what-a-package-exposes) is
144/// every module of `src/` except the ones `private-modules` names and every `module
145/// foreign` facade. A name that would have reached one of those is absent from the map
146/// this builds, so it fails as a module that does not exist rather than as one that is
147/// refused.
148pub struct DependencyModules<'a> {
149    pub package: &'a ResolvedPackage,
150    pub wrapped: bool,
151    pub modules: Vec<Name>,
152}
153
154/// One module the package being compiled declares: the name its header gives it, and
155/// the file it was written in as [`SourceFile::package_path`](super::source::SourceFile::package_path)
156/// renders it — `src/Model.zel`.
157///
158/// The file travels with the name because the package's two source roots share one set
159/// of names: `src/Model.zel` and `tests/Model.zel` are both `Model`, and that is the
160/// same error as declaring one name twice under one root. Saying which files claimed
161/// the name is the whole of what makes that error actionable.
162#[derive(Debug, Clone)]
163pub struct LocalModule {
164    pub name: Name,
165    pub file: String,
166}
167
168/// Every way resolving a build, or naming the modules in one, can fail.
169///
170/// Like a [`ManifestError`], none of these has a span:
171/// what each is about is a `zelkova.toml`, which is not a file the
172/// [`SourceFiles`](super::source::SourceFiles) database holds, so every variant names the
173/// manifest it was found in inside its own message.
174#[derive(Debug)]
175pub enum Error {
176    /// A dependency's own manifest is missing, malformed, or fails one of its rules.
177    ///
178    /// The root package's manifest never reaches here — `compile_package` reads it
179    /// before resolution starts and reports it as
180    /// [`CompilationError::Manifest`](super::CompilationError::Manifest).
181    Manifest {
182        /// The dependency entry that led here, so the error names a package the reader
183        /// wrote rather than only a path they may never have seen.
184        package: PackageName,
185        error: ManifestError,
186    },
187    /// A dependency entry names a source this compiler cannot obtain.
188    UnsupportedSource {
189        manifest_path: PathBuf,
190        package: PackageName,
191    },
192    /// A `path` source names a directory that could not be reached.
193    SourceUnreachable {
194        manifest_path: PathBuf,
195        package: PackageName,
196        path: PathBuf,
197        message: String,
198    },
199    /// The package found at a source declares a name other than the one the entry asked
200    /// for. A dependent derives a namespace from the key it wrote, so a package
201    /// answering to another name would have that namespace put on modules it has never
202    /// heard of.
203    NameMismatch {
204        manifest_path: PathBuf,
205        expected: PackageName,
206        declared: PackageName,
207        path: PathBuf,
208    },
209    /// One package name given two different directories in one build. A name is a
210    /// package's identity for the whole build, not a label local to the manifest that
211    /// writes it.
212    ConflictingSources {
213        package: PackageName,
214        first: PathBuf,
215        second: PathBuf,
216    },
217    /// A package depends on itself, directly or through a chain. The members of a cycle
218    /// have no order they could be compiled in.
219    Cycle(Vec<PackageName>),
220    /// A package was not compiled because one of its dependencies was not.
221    ///
222    /// This never stands alone: whatever went wrong in the dependency is already
223    /// reported, and this says which package was left uncompiled as a result — so a
224    /// user reading a dependency's diagnostics knows why nothing was said about the
225    /// package they pointed the compiler at.
226    DependencyNotCompiled {
227        package: PackageName,
228        dependency: PackageName,
229    },
230    /// Two modules answer to one name in one package.
231    ///
232    /// Reported when the build is resolved, before any module of that package is
233    /// compiled, naming both modules and the packages they come from — [*Two modules
234    /// under one
235    /// name*](../../docs/spec/packages.md#two-modules-under-one-name-is-an-error).
236    ModuleNameCollision {
237        package: PackageName,
238        name: Name,
239        first: ModuleOrigin,
240        second: ModuleOrigin,
241    },
242    /// A package other than `zelkova-core` declares a local module named after one
243    /// of [the eight default imports](default_imports::DEFAULT_IMPORTS) —
244    /// `default_imports::is_default` — under either source root.
245    ///
246    /// Reported when the build is resolved, before any module of the package is
247    /// compiled, the same as [`ModuleNameCollision`](Error::ModuleNameCollision):
248    /// this is what makes [`DEC-17`](../../docs/decisions/dec-17.md)'s premise
249    /// true rather than assumed, ahead of `LANG-62` giving the rest of
250    /// `zelkova-core`'s module names the same protection. The module is reported
251    /// and not claimed, so a build that also contains `zelkova-core` does not
252    /// additionally report a [`ModuleNameCollision`](Error::ModuleNameCollision)
253    /// for the same name.
254    ReservedModuleName {
255        package: PackageName,
256        module: ModuleOrigin,
257    },
258}
259
260impl PhaseError for Error {
261    fn message(&self) -> String {
262        match self {
263            Error::Manifest { package, error } => {
264                format!("`{}` could not be read: {}", package, error.message())
265            }
266            Error::UnsupportedSource {
267                manifest_path,
268                package,
269            } => format!(
270                "`{}` in `{}` names a `git` source, which this compiler cannot obtain",
271                package,
272                manifest_path.display()
273            ),
274            Error::SourceUnreachable {
275                manifest_path,
276                package,
277                path,
278                message,
279            } => format!(
280                "`{}` in `{}` names the directory `{}`, which could not be read: {}",
281                package,
282                manifest_path.display(),
283                path.display(),
284                message
285            ),
286            Error::NameMismatch {
287                manifest_path,
288                expected,
289                declared,
290                path,
291            } => format!(
292                "the dependency `{}` in `{}` resolves to `{}`, which declares the name `{}`",
293                expected,
294                manifest_path.display(),
295                path.display(),
296                declared
297            ),
298            Error::ConflictingSources { package, .. } => {
299                format!("`{}` is given two different sources in this build", package)
300            }
301            Error::Cycle(packages) => format!(
302                "{} package{} depend on each other in a circle",
303                packages.len(),
304                if packages.len() == 1 { "" } else { "s" }
305            ),
306            Error::ModuleNameCollision { package, name, .. } => {
307                format!("two modules are named `{}` in package `{}`", name, package)
308            }
309            Error::DependencyNotCompiled {
310                package,
311                dependency,
312            } => format!(
313                "`{}` was not compiled, because its dependency `{}` was not",
314                package, dependency
315            ),
316            Error::ReservedModuleName { package, module } => format!(
317                "`{}` is reserved for `{}`, and `{}` declares a module of that name",
318                module.module, CORE_PACKAGE, package
319            ),
320        }
321    }
322
323    fn notes(&self) -> Vec<String> {
324        match self {
325            Error::Manifest { error, .. } => error.notes(),
326            Error::UnsupportedSource { .. } => vec![
327                "a `git` dependency is fetched by the toolchain, which is not written yet; \
328                 a `path` dependency is read where it lies and needs no fetching"
329                    .to_string(),
330            ],
331            Error::ConflictingSources { first, second, .. } => vec![
332                format!("one entry resolves to `{}`", first.display()),
333                format!("another resolves to `{}`", second.display()),
334            ],
335            Error::Cycle(packages) => {
336                let mut path: Vec<String> = packages.iter().map(|p| p.to_string()).collect();
337                if let Some(first) = path.first().cloned() {
338                    path.push(first);
339                }
340                vec![format!("cycle: {}", path.join(" -> "))]
341            }
342            Error::ModuleNameCollision { first, second, .. } => {
343                vec![
344                    first.describe(),
345                    second.describe(),
346                    "wrap one of the two, or rename this package's own module".to_string(),
347                ]
348            }
349            Error::ReservedModuleName { .. } => {
350                vec!["it is one of the modules every package imports by default".to_string()]
351            }
352            _ => Vec::new(),
353        }
354    }
355}
356
357/// Walk the build's manifests, starting from the root package, and hand back every
358/// package in it — each one after the packages it depends on, so they can be compiled
359/// in that order.
360///
361/// `root` is the directory holding `manifest`; `manifest` is already read and validated
362/// by the caller, which is why the root package's own manifest errors are not among
363/// what this can report.
364///
365/// The root package's `test-dependencies` are part of the build, and no other
366/// package's are: a package listed there is available to that package's `tests/` and is
367/// [not resolved by anyone depending on
368/// it](../../docs/spec/packages.md#test-dependencies). The rest of the rules apply to
369/// the union of the two maps, so one version of each package and an acyclic graph are
370/// settled once for the whole build rather than again when the tests are run.
371///
372/// A package reached through the root's `test-dependencies` may depend on the root
373/// itself. That edge names the root's `src/`, which the graph counts apart from its
374/// `tests/`, so it is not a cycle. The order handed back still ends with the root, after
375/// such a package; putting the root's `src/` ahead of it is the build loop's job, since
376/// only the build loop compiles the two roots apart.
377///
378/// Every failure is collected rather than returned at the first one: a build whose
379/// manifests name three missing directories says so once.
380pub fn resolve(root: &Path, manifest: Manifest) -> Result<Vec<ResolvedPackage>, Vec<Error>> {
381    let mut resolver = Resolver {
382        order: Vec::new(),
383        resolved: HashMap::new(),
384        errors: Vec::new(),
385    };
386
387    let root_package = ResolvedPackage {
388        name: manifest.name.clone(),
389        root: canonical_dir(root),
390        manifest,
391    };
392
393    resolver.visit(root_package, true, false, &mut Vec::new());
394
395    if resolver.errors.is_empty() {
396        Ok(resolver.order)
397    } else {
398        Err(resolver.errors)
399    }
400}
401
402/// The packages of `build` that nothing but a `test-dependencies` entry reaches.
403///
404/// [`resolve`] walks the union of the two maps whether or not the tests are being
405/// compiled, because one version of each package and an acyclic graph are properties of
406/// the whole build rather than of one entry point
407/// ([*`test-dependencies`*](../../docs/spec/packages.md#test-dependencies)). What
408/// comes back here is the other half of that: which of the packages it found are in the
409/// build for the tests alone, so a build that did not ask for the tests can leave them
410/// uncompiled instead of parsing, checking and reporting on a package it cannot import.
411///
412/// Reachability is followed through `dependencies` only, starting from `root`, so a
413/// package reached through `test-dependencies` that depends on the root stays in this
414/// set: nothing on the walk names it. A package written in both maps is not possible — the manifest rejects that — but a package
415/// reached through both, as a `test-dependency` here and an ordinary dependency of
416/// something else, is: it comes back out of this set, because an ordinary build needs it.
417pub fn test_only_packages<'a>(
418    build: &'a [ResolvedPackage],
419    root: &'a PackageName,
420) -> std::collections::HashSet<PackageName> {
421    let by_name: HashMap<&PackageName, &ResolvedPackage> = build
422        .iter()
423        .map(|package| (&package.name, package))
424        .collect();
425
426    let mut reached: std::collections::HashSet<&PackageName> = std::collections::HashSet::new();
427    let mut queue: Vec<&'a PackageName> = vec![root];
428    while let Some(name) = queue.pop() {
429        if !reached.insert(name) {
430            continue;
431        }
432        if let Some(package) = by_name.get(name) {
433            queue.extend(package.manifest.dependencies.keys());
434        }
435    }
436
437    build
438        .iter()
439        .filter(|package| !reached.contains(&package.name))
440        .map(|package| package.name.clone())
441        .collect()
442}
443
444/// The directory as it will be compared against every other directory in the build.
445///
446/// Two entries reaching one package by different relative paths are the same package,
447/// and `..` segments are what makes that common: `../acme-parser` from two siblings is
448/// one directory. Canonicalizing is what makes them compare equal — and a path that
449/// cannot be canonicalized (it does not exist yet) is kept as written, so the failure
450/// is reported where the directory is actually read rather than here.
451fn canonical_dir(path: &Path) -> PathBuf {
452    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
453}
454
455struct Resolver {
456    /// Every package resolved so far, in the order they can be compiled: a package is
457    /// pushed only once every package it depends on already is.
458    order: Vec<ResolvedPackage>,
459    /// The directory each resolved package name was found in, which is what a second
460    /// entry for that name is checked against.
461    resolved: HashMap<PackageName, PathBuf>,
462    errors: Vec<Error>,
463}
464
465impl Resolver {
466    /// Resolve `package` and everything it depends on, appending each to `order`.
467    ///
468    /// `stack` is the chain of packages currently being visited, each with the
469    /// directory it was found in. Meeting a name already on it is what stops the
470    /// recursion — either because the chain closes on the same package, which is a
471    /// cycle, or because it closes on a *different* package of the same name, which is
472    /// one name given two sources. Either way the walk stops here rather than following
473    /// the chain round forever.
474    ///
475    /// `with_test_dependencies` is true for the root package alone, because only that
476    /// package's tests are ever compiled. A dependency's `test-dependencies` are none of
477    /// this build's business and are not followed.
478    ///
479    /// `through_test_dependency` is true when the chain left the root through one of its
480    /// `test-dependencies`. Such a chain meeting the root at the root's own directory has
481    /// reached the root's `src/`, which is already being resolved, so it stops there
482    /// without an error
483    /// ([*`test-dependencies`*](../../docs/spec/packages.md#test-dependencies)). The
484    /// root is always `stack[0]`, which is how it is told apart from any other package on
485    /// the stack.
486    ///
487    /// The root's `dependencies` are all visited before its `test-dependencies`, and that
488    /// order is what keeps the exemption to test-only chains. Every package the plain
489    /// graph reaches is resolved by the time a test chain starts, so a test chain that
490    /// meets one stops at the `resolved` check below instead of walking on to the root —
491    /// and a plain chain that closes on the root has already been reported as a cycle.
492    fn visit(
493        &mut self,
494        package: ResolvedPackage,
495        with_test_dependencies: bool,
496        through_test_dependency: bool,
497        stack: &mut Vec<(PackageName, PathBuf)>,
498    ) {
499        if let Some(at) = stack.iter().position(|(name, _)| name == &package.name) {
500            if stack[at].1 == package.root {
501                if at == 0 && through_test_dependency {
502                    return;
503                }
504                self.errors.push(Error::Cycle(
505                    stack[at..].iter().map(|(name, _)| name.clone()).collect(),
506                ));
507            } else {
508                self.errors.push(Error::ConflictingSources {
509                    package: package.name.clone(),
510                    first: stack[at].1.clone(),
511                    second: package.root.clone(),
512                });
513            }
514            return;
515        }
516
517        if let Some(previous) = self.resolved.get(&package.name) {
518            if previous != &package.root {
519                self.errors.push(Error::ConflictingSources {
520                    package: package.name.clone(),
521                    first: previous.clone(),
522                    second: package.root.clone(),
523                });
524            }
525            return;
526        }
527
528        stack.push((package.name.clone(), package.root.clone()));
529
530        // Each map sorted, so that a build with two broken entries reports them in the
531        // same order every run — a `HashMap`'s iteration order is not one a user should
532        // see. `dependencies` first, then `test-dependencies`: see this function's doc
533        // comment for why the order of the two maps matters.
534        fn sorted(
535            map: &HashMap<PackageName, super::manifest::Dependency>,
536        ) -> Vec<(&PackageName, &Source)> {
537            let mut entries: Vec<(&PackageName, &Source)> = map
538                .iter()
539                .map(|(name, dependency)| (name, &dependency.source))
540                .collect();
541            entries.sort_by(|left, right| left.0.as_str().cmp(right.0.as_str()));
542            entries
543        }
544
545        let mut entries: Vec<(&PackageName, &Source, bool)> =
546            sorted(&package.manifest.dependencies)
547                .into_iter()
548                .map(|(name, source)| (name, source, through_test_dependency))
549                .collect();
550        if with_test_dependencies {
551            entries.extend(
552                sorted(&package.manifest.test_dependencies)
553                    .into_iter()
554                    .map(|(name, source)| (name, source, true)),
555            );
556        }
557
558        for (name, source, through_test_dependency) in entries {
559            if let Some(dependency) = self.obtain(&package, name, source) {
560                self.visit(dependency, false, through_test_dependency, stack);
561            }
562        }
563
564        stack.pop();
565
566        self.resolved
567            .insert(package.name.clone(), package.root.clone());
568        self.order.push(package);
569    }
570
571    /// Read the package one dependency entry names, or report why it could not be.
572    ///
573    /// `None` means the entry produced an error, which is already recorded: resolution
574    /// carries on with the rest of the build rather than stopping, so one unreachable
575    /// directory does not hide the next.
576    fn obtain(
577        &mut self,
578        dependent: &ResolvedPackage,
579        name: &PackageName,
580        source: &Source,
581    ) -> Option<ResolvedPackage> {
582        let manifest_path = dependent.manifest_path();
583
584        let path = match source {
585            // A `path` is relative to the manifest that names it
586            // (`docs/spec/packages.md#where-a-dependency-comes-from`).
587            Source::Path { path } => dependent.root.join(path),
588            Source::Git { .. } => {
589                self.errors.push(Error::UnsupportedSource {
590                    manifest_path,
591                    package: name.clone(),
592                });
593                return None;
594            }
595        };
596
597        let path = match std::fs::canonicalize(&path) {
598            Ok(path) => path,
599            Err(err) => {
600                self.errors.push(Error::SourceUnreachable {
601                    manifest_path,
602                    package: name.clone(),
603                    path,
604                    message: err.to_string(),
605                });
606                return None;
607            }
608        };
609
610        let manifest = match super::manifest::load(&path) {
611            Ok(manifest) => manifest,
612            Err(errors) => {
613                self.errors
614                    .extend(errors.into_iter().map(|error| Error::Manifest {
615                        package: name.clone(),
616                        error,
617                    }));
618                return None;
619            }
620        };
621
622        // The key is the package's identity for the whole build, so a package answering
623        // to another name is refused rather than being taken under the name that was
624        // asked for — the namespace a dependent derives from the key would otherwise
625        // name modules this package has never heard of.
626        if &manifest.name != name {
627            self.errors.push(Error::NameMismatch {
628                manifest_path,
629                expected: name.clone(),
630                declared: manifest.name.clone(),
631                path,
632            });
633            return None;
634        }
635
636        Some(ResolvedPackage {
637            name: manifest.name.clone(),
638            root: path,
639            manifest,
640        })
641    }
642}
643
644/// Every module name the package being compiled can import, and what each one names.
645///
646/// The map holds one entry per *spelling*: the package's own modules under their own
647/// names, a wrapped dependency's under `<Namespace>.<module>`, and an unwrapped
648/// dependency's under their own names. That is the whole of what an `import` can
649/// reach — [*Imports across a package
650/// boundary*](../../docs/spec/packages.md#imports-across-a-package-boundary) — and
651/// a module has exactly one spelling in it, never two.
652///
653/// A name claimed twice is an error and no map comes back, because there is no
654/// coherent answer to give the modules about to be compiled. Every collision is
655/// reported, not just the first.
656pub fn visible_modules(
657    package: &ResolvedPackage,
658    local_modules: &[LocalModule],
659    dependencies: &[DependencyModules<'_>],
660) -> Result<HashMap<Name, ModuleOrigin>, Vec<Error>> {
661    let mut names: HashMap<Name, ModuleOrigin> = HashMap::new();
662    let mut errors = Vec::new();
663
664    let mut claim =
665        |name: Name, origin: ModuleOrigin, errors: &mut Vec<Error>| match names.get(&name) {
666            Some(first) => errors.push(Error::ModuleNameCollision {
667                package: package.name.clone(),
668                name,
669                first: first.clone(),
670                second: origin,
671            }),
672            None => {
673                names.insert(name, origin);
674            }
675        };
676
677    // The package's own modules first, so that a collision between one of them and a
678    // dependency's reads with the local module as the name already claimed. The sort is
679    // stable, so two modules answering to one name keep the order they were given —
680    // `src/` before `tests/`, which is the order a collision between the two roots reads
681    // best in.
682    let mut local: Vec<&LocalModule> = local_modules.iter().collect();
683    local.sort_by(|left, right| left.name.as_str().cmp(right.name.as_str()));
684    for module in local {
685        let origin = ModuleOrigin {
686            package: package.name.clone(),
687            version: package.manifest.version.clone(),
688            module: module.name.clone(),
689            kind: OriginKind::Local {
690                file: module.file.clone(),
691            },
692        };
693
694        // The eight default imports' names are `zelkova-core`'s alone
695        // (`docs/spec/packages.md#zelkova-core-is-a-dependency-of-every-package`):
696        // reported here, ahead of any collision, and not claimed — a build that
697        // also contains `zelkova-core` then finds this spelling unclaimed and
698        // reports no `ModuleNameCollision` on top of it.
699        if !package.name.is_core() && default_imports::is_default(&module.name) {
700            errors.push(Error::ReservedModuleName {
701                package: package.name.clone(),
702                module: origin,
703            });
704            continue;
705        }
706
707        claim(module.name.clone(), origin, &mut errors);
708    }
709
710    let mut dependencies: Vec<&DependencyModules<'_>> = dependencies.iter().collect();
711    dependencies.sort_by(|left, right| left.package.name.as_str().cmp(right.package.name.as_str()));
712
713    for dependency in dependencies {
714        let unwrapped = dependency.package.seen_unwrapped(dependency.wrapped);
715        let namespace = dependency.package.name.namespace();
716
717        let mut modules: Vec<&Name> = dependency.modules.iter().collect();
718        modules.sort_by(|left, right| left.as_str().cmp(right.as_str()));
719
720        for module in modules {
721            let (spelling, kind) = if unwrapped {
722                (module.clone(), OriginKind::Unwrapped)
723            } else {
724                (
725                    module.clone().qualify_with(namespace.to_string()),
726                    OriginKind::Namespaced,
727                )
728            };
729
730            claim(
731                spelling,
732                ModuleOrigin {
733                    package: dependency.package.name.clone(),
734                    version: dependency.package.manifest.version.clone(),
735                    module: module.clone(),
736                    kind,
737                },
738                &mut errors,
739            );
740        }
741    }
742
743    if errors.is_empty() {
744        Ok(names)
745    } else {
746        Err(errors)
747    }
748}
749
750#[cfg(test)]
751mod tests {
752    use super::*;
753    use crate::manifest::Dependency;
754
755    fn version(major: u64) -> Version {
756        Version {
757            major,
758            minor: 0,
759            patch: 0,
760        }
761    }
762
763    fn package(name: &str, dependencies: Vec<(&str, bool)>) -> ResolvedPackage {
764        let dependencies = dependencies
765            .into_iter()
766            .map(|(name, wrapped)| {
767                (
768                    PackageName::new(name).unwrap(),
769                    Dependency {
770                        source: Source::Path {
771                            path: format!("../{}", name),
772                        },
773                        wrapped,
774                    },
775                )
776            })
777            .collect();
778
779        ResolvedPackage {
780            name: PackageName::new(name).unwrap(),
781            root: PathBuf::from("/tmp").join(name),
782            manifest: Manifest {
783                name: PackageName::new(name).unwrap(),
784                version: version(1),
785                main: None,
786                private_modules: vec![],
787                dependencies,
788                test_dependencies: HashMap::new(),
789            },
790        }
791    }
792
793    /// The same package, with `names` written in its `test-dependencies`. What the
794    /// entries say does not matter to `test_only_packages`, which follows
795    /// `dependencies` alone — they are here so the manifest reads the way the one
796    /// `resolve` walked did.
797    fn with_test_dependencies(mut package: ResolvedPackage, names: Vec<&str>) -> ResolvedPackage {
798        package.manifest.test_dependencies = names
799            .into_iter()
800            .map(|name| {
801                (
802                    PackageName::new(name).unwrap(),
803                    Dependency {
804                        source: Source::Path {
805                            path: format!("../{}", name),
806                        },
807                        wrapped: true,
808                    },
809                )
810            })
811            .collect();
812        package
813    }
814
815    /// A package the root reaches only through `test-dependencies` is in the build for
816    /// the tests alone. One that an ordinary `dependencies` chain also reaches is not,
817    /// whichever map named it first — an ordinary build needs it.
818    ///
819    /// Mutation-checked by seeding the walk with every package of the build rather than
820    /// with the root: the first assertion then comes back empty.
821    #[test]
822    fn a_package_only_the_test_dependencies_reach_is_test_only() {
823        let root = with_test_dependencies(
824            package("todo", vec![("acme-widgets", true)]),
825            vec!["acme-expect"],
826        );
827        let widgets = package("acme-widgets", vec![]);
828        let expect = package("acme-expect", vec![]);
829
830        let build = vec![widgets, expect, root.clone()];
831        let test_only = test_only_packages(&build, &root.name);
832
833        let expected: std::collections::HashSet<PackageName> =
834            std::iter::once(PackageName::new("acme-expect").unwrap()).collect();
835        assert_eq!(test_only, expected);
836
837        // The same build, with the ordinary dependency reaching `acme-expect` too.
838        let widgets = package("acme-widgets", vec![("acme-expect", true)]);
839        let expect = package("acme-expect", vec![]);
840        let build = vec![expect, widgets, root.clone()];
841
842        assert!(test_only_packages(&build, &root.name).is_empty());
843    }
844
845    /// The package's own modules, as `visible_modules` takes them: a name and the file
846    /// it was declared in. The file is only ever read back out of a collision note.
847    fn local(modules: &[&str]) -> Vec<LocalModule> {
848        modules
849            .iter()
850            .map(|name| LocalModule {
851                name: Name::new(*name),
852                file: format!("src/{}.zel", name),
853            })
854            .collect()
855    }
856
857    fn dependency<'a>(
858        package: &'a ResolvedPackage,
859        wrapped: bool,
860        modules: &[&str],
861    ) -> DependencyModules<'a> {
862        DependencyModules {
863            package,
864            wrapped,
865            modules: modules.iter().map(|m| Name::new(*m)).collect(),
866        }
867    }
868
869    /// A wrapped dependency's modules are reachable only under its namespace, and an
870    /// unwrapped one's only under their own names. Both halves matter: a module has
871    /// exactly one spelling in any file.
872    ///
873    /// Mutation-checked by ignoring `unwrapped` in `visible_modules` and always
874    /// qualifying: the `Size` lookup for the unwrapped dependency goes red.
875    #[test]
876    fn a_dependency_is_named_through_its_namespace_unless_unwrapped() {
877        let widgets = package("acme-widgets", vec![]);
878        let todo = package("todo", vec![]);
879
880        let wrapped = visible_modules(
881            &todo,
882            &local(&["Model"]),
883            &[dependency(&widgets, true, &["Size", "Style.Dark"])],
884        )
885        .expect("no collision");
886
887        assert!(wrapped.contains_key(&Name::new("AcmeWidgets.Size")));
888        assert!(wrapped.contains_key(&Name::new("AcmeWidgets.Style.Dark")));
889        assert!(!wrapped.contains_key(&Name::new("Size")));
890        assert!(wrapped.contains_key(&Name::new("Model")));
891
892        let unwrapped = visible_modules(
893            &todo,
894            &local(&["Model"]),
895            &[dependency(&widgets, false, &["Size", "Style.Dark"])],
896        )
897        .expect("no collision");
898
899        assert!(unwrapped.contains_key(&Name::new("Size")));
900        assert!(unwrapped.contains_key(&Name::new("Style.Dark")));
901        assert!(!unwrapped.contains_key(&Name::new("AcmeWidgets.Size")));
902    }
903
904    /// `zelkova-core` is seen unwrapped whatever the entry naming it says, which is
905    /// what keeps `Basics` spelled `Basics`.
906    ///
907    /// Mutation-checked by dropping the `CORE_PACKAGE` arm of `seen_unwrapped`:
908    /// `Basics` is then only reachable as `ZelkovaCore.Basics`.
909    #[test]
910    fn core_is_unwrapped_whatever_its_entry_says() {
911        let core = package("zelkova-core", vec![]);
912        let todo = package("todo", vec![]);
913
914        let names = visible_modules(&todo, &[], &[dependency(&core, true, &["Basics"])])
915            .expect("no collision");
916
917        assert!(names.contains_key(&Name::new("Basics")));
918        assert!(!names.contains_key(&Name::new("ZelkovaCore.Basics")));
919    }
920
921    /// Two unwrapped dependencies claiming one name is an error naming both packages,
922    /// and so is an unwrapped dependency claiming a local module's name.
923    ///
924    /// Mutation-checked by making `claim` overwrite instead of reporting: both
925    /// `expect_err` calls then come back `Ok`.
926    #[test]
927    fn a_name_claimed_twice_names_both_packages() {
928        let widgets = package("acme-widgets", vec![]);
929        let ui = package("fmonniot-ui", vec![]);
930        let todo = package("todo", vec![]);
931
932        let errors = visible_modules(
933            &todo,
934            &[],
935            &[
936                dependency(&widgets, false, &["Size"]),
937                dependency(&ui, false, &["Size"]),
938            ],
939        )
940        .expect_err("both dependencies claim `Size`");
941
942        assert_eq!(errors.len(), 1, "got {:?}", errors);
943        let notes = errors[0].notes();
944        assert!(
945            errors[0].message().contains("`Size`") && errors[0].message().contains("`todo`"),
946            "got {:?}",
947            errors[0].message()
948        );
949        assert!(
950            notes.iter().any(|n| n.contains("acme-widgets"))
951                && notes.iter().any(|n| n.contains("fmonniot-ui")),
952            "both packages must be named, got {:?}",
953            notes
954        );
955
956        let errors = visible_modules(
957            &todo,
958            &local(&["Size"]),
959            &[dependency(&widgets, false, &["Size"])],
960        )
961        .expect_err("the local module and the dependency both claim `Size`");
962
963        assert_eq!(errors.len(), 1, "got {:?}", errors);
964        let notes = errors[0].notes();
965        assert!(
966            notes.iter().any(|n| n.contains("a module of this package")),
967            "the local module must be named as one, got {:?}",
968            notes
969        );
970    }
971
972    /// Two *wrapped* dependencies cannot collide, whatever they contain — that is what
973    /// the namespace is for.
974    #[test]
975    fn two_wrapped_dependencies_never_collide() {
976        let widgets = package("acme-widgets", vec![]);
977        let ui = package("fmonniot-ui", vec![]);
978        let todo = package("todo", vec![]);
979
980        let names = visible_modules(
981            &todo,
982            &local(&["Size"]),
983            &[
984                dependency(&widgets, true, &["Size"]),
985                dependency(&ui, true, &["Size"]),
986            ],
987        )
988        .expect("wrapped dependencies keep out of each other's way");
989
990        assert!(names.contains_key(&Name::new("Size")));
991        assert!(names.contains_key(&Name::new("AcmeWidgets.Size")));
992        assert!(names.contains_key(&Name::new("FmonniotUi.Size")));
993    }
994
995    /// The eight default imports' names are `zelkova-core`'s alone: `zelkova-core`
996    /// declaring `List` is claimed like any other local module, and a package of any
997    /// other name declaring it is reported as `ReservedModuleName` instead of being
998    /// claimed.
999    ///
1000    /// Mutation-checked by dropping the `is_core` check ahead of `claim` in
1001    /// `visible_modules`'s local-module loop: `acme-widgets` then claims `List`
1002    /// instead of being reported for it, and the second `expect_err` panics.
1003    #[test]
1004    fn only_core_may_declare_a_default_imports_name() {
1005        let core = package("zelkova-core", vec![]);
1006        let names =
1007            visible_modules(&core, &local(&["List"]), &[]).expect("zelkova-core may declare List");
1008        assert!(names.contains_key(&Name::new("List")));
1009
1010        let widgets = package("acme-widgets", vec![]);
1011        let errors = visible_modules(&widgets, &local(&["List"]), &[])
1012            .expect_err("a package other than zelkova-core may not declare List");
1013
1014        assert_eq!(errors.len(), 1, "got {:?}", errors);
1015        match &errors[0] {
1016            Error::ReservedModuleName { package, module } => {
1017                assert_eq!(package.as_str(), "acme-widgets");
1018                assert_eq!(module.module.as_str(), "List");
1019            }
1020            other => panic!("expected ReservedModuleName, got {:?}", other),
1021        }
1022    }
1023}