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}