Skip to main content

nova_doc/
model.rs

1//! From analyses to a site (spec §5 to §7; plan decisions 7 to 9, 13 and
2//! 19).
3
4use std::collections::{HashMap, HashSet};
5use std::path::Path;
6
7use nova_ast::item::{
8    ConstDecl, ExternItem, ImplBlock, Record, TraitDecl, TraitItem, TypeDecl, TypeDef, Visibility,
9    WhereBound,
10};
11use nova_ast::{Item as Ast, Type};
12use nova_diagnostics::{Diagnostic, FileDb, FileId, Severity, Span, Spanned};
13use nova_driver::{analyze_program, Analysis, DiskSources, Options, Program, Roots, Sources};
14use nova_lexer::Token;
15use nova_pm::PackageId;
16use nova_resolver::index::rank;
17use nova_resolver::{Builtin, DefKind, Definitions, ModuleId, Role, Target};
18
19use crate::highlight::highlight;
20use crate::markdown::{self, Docs};
21use crate::signature::{self, FileText};
22
23/// A program documented: its site, and the warnings its analysis found,
24/// with the files they point into.
25pub struct Documented {
26    /// The site, for [`crate::write_site`].
27    pub site: Site,
28    /// The files the warnings point into.
29    pub db: FileDb,
30    /// The analysis's warnings, for the caller to print.
31    pub warnings: Vec<Diagnostic>,
32}
33
34/// Why a program was not documented.
35pub enum Failure {
36    /// Its analysis found errors, for the caller to print.
37    Diagnostics {
38        /// The files the diagnostics point into.
39        db: FileDb,
40        /// Every diagnostic the analysis found, errors among them.
41        diagnostics: Vec<Diagnostic>,
42    },
43    /// Anything else, in a sentence.
44    Other(String),
45}
46
47/// A site: its packages, std last (spec §6.1).
48pub struct Site {
49    pub(crate) packages: Vec<Package>,
50    /// The page `--open` opens, from the site's root.
51    pub(crate) start: String,
52}
53
54/// A documented package.
55pub(crate) struct Package {
56    pub name: String,
57    pub version: String,
58    pub description: Option<String>,
59    pub std: bool,
60    /// The root module first, then the rest by name; std's all by name.
61    pub modules: Vec<Module>,
62    /// std's alone.
63    pub primitives: Vec<Primitive>,
64    /// std's alone.
65    pub builtins: Vec<BuiltinDoc>,
66}
67
68/// A documented module, one page (spec §7.2, §7.3).
69pub(crate) struct Module {
70    pub name: String,
71    /// `package`, or `package::module`: its title and its search path.
72    pub qual: String,
73    /// Its page, from the site's root.
74    pub page: String,
75    /// Whether it is its package's root module, whose page is the
76    /// package's.
77    pub root: bool,
78    pub docs: Docs,
79    /// In source order.
80    pub items: Vec<Item>,
81    pub other_impls: Vec<Impl>,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub(crate) enum ItemKind {
86    Record,
87    Sum,
88    Trait,
89    Function,
90    Constant,
91}
92
93impl ItemKind {
94    /// The word its heading and the search index show.
95    pub fn word(self) -> &'static str {
96        match self {
97            ItemKind::Record => "record",
98            ItemKind::Sum => "type",
99            ItemKind::Trait => "trait",
100            ItemKind::Function => "fn",
101            ItemKind::Constant => "const",
102        }
103    }
104
105    /// The section a module page lists it under, and that section's `id`.
106    pub fn section(self) -> (&'static str, &'static str) {
107        match self {
108            ItemKind::Record => ("Records", "records"),
109            ItemKind::Sum => ("Sum types", "sum-types"),
110            ItemKind::Trait => ("Traits", "traits"),
111            ItemKind::Function => ("Functions", "functions"),
112            ItemKind::Constant => ("Constants", "constants"),
113        }
114    }
115}
116
117/// A documented item (spec §7.3).
118pub(crate) struct Item {
119    pub kind: ItemKind,
120    pub name: String,
121    pub anchor: String,
122    /// Its signature's HTML (spec §8).
123    pub signature: String,
124    pub docs: Docs,
125    /// Fields, variants, associated types and methods, as §7.3 orders them.
126    pub members: Vec<Member>,
127    /// Whether it is a record with fields that are not documented.
128    pub hidden_fields: bool,
129    /// A record's or sum type's impls; a trait's, by their headers.
130    pub impls: Vec<Impl>,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub(crate) enum MemberKind {
135    Field,
136    Variant,
137    AssocType,
138    Method,
139}
140
141impl MemberKind {
142    /// The word the search index shows.
143    pub fn word(self) -> &'static str {
144        match self {
145            MemberKind::Field => "field",
146            MemberKind::Variant => "variant",
147            MemberKind::AssocType => "type",
148            MemberKind::Method => "method",
149        }
150    }
151
152    /// The heading a page groups them under.
153    pub fn heading(self) -> &'static str {
154        match self {
155            MemberKind::Field => "Fields",
156            MemberKind::Variant => "Variants",
157            MemberKind::AssocType => "Associated types",
158            MemberKind::Method => "Methods",
159        }
160    }
161}
162
163/// A documented member.
164pub(crate) struct Member {
165    pub kind: MemberKind,
166    pub name: String,
167    pub anchor: String,
168    pub signature: String,
169    pub docs: Docs,
170}
171
172/// An impl: its header, its docs and its documented methods (spec §5.2).
173pub(crate) struct Impl {
174    pub header: String,
175    pub docs: Docs,
176    pub methods: Vec<Member>,
177    /// The type's name, for the search index's paths.
178    pub on: String,
179}
180
181/// A primitive type's page (spec §7.4).
182pub(crate) struct Primitive {
183    pub name: &'static str,
184    pub page: String,
185    pub about: Docs,
186    pub impls: Vec<Impl>,
187}
188
189/// A builtin function, on std's front page (spec §7.4).
190pub(crate) struct BuiltinDoc {
191    pub name: &'static str,
192    pub signature: String,
193    pub docs: Docs,
194}
195
196/// The primitive types, with what their pages say first (plan decision
197/// 19).
198const PRIMITIVES: [(&str, &str); 7] = [
199    (
200        "Int",
201        "A 64-bit signed integer. `+`, `-` and `*` on `Int` wrap on overflow.",
202    ),
203    ("Float", "A 64-bit floating-point number."),
204    ("Bool", "`true` or `false`."),
205    ("Char", "A Unicode scalar value."),
206    ("String", "A string of UTF-8 text."),
207    ("Bytes", "A sequence of bytes."),
208    (
209        "Future",
210        "The work an `async fn` call stands for, which `.await` runs to its value.",
211    ),
212];
213
214/// The builtin functions every module sees, and their docs (plan decision
215/// 19).
216const BUILTINS: [(&str, &str); 5] = [
217    (
218        "println",
219        "Writes its argument and a line break to standard output.",
220    ),
221    (
222        "print",
223        "Writes its argument to standard output, with no line break.",
224    ),
225    (
226        "eprintln",
227        "Writes its argument and a line break to standard error.",
228    ),
229    (
230        "eprint",
231        "Writes its argument to standard error, with no line break.",
232    ),
233    (
234        "panic",
235        "Stops the program at once: writes `nova: panic: ` and its argument to standard \
236         error, and aborts. Under `nova test`, a test marked `@test(should_panic)` expects it.",
237    ),
238];
239
240/// What std's front page says first.
241const STD_ABOUT: &str = "Nova's standard library, which every module sees without an import.";
242
243/// The entry of std's own analysis, which no file holds (plan decision
244/// 8).
245const STD_ENTRY: &str = "<nova doc>/main.nova";
246
247/// Document the package whose `nova.toml` is in `root` (empty for the
248/// current directory), its dependencies and std (spec §5.1).
249pub fn document_package(root: &Path) -> Result<Documented, Failure> {
250    let (analysis, library) = package_analysis(root, false)?;
251    let std = std_analysis()?;
252    let mut packages = Ctx::new(&analysis, false, !library)?.packages();
253    packages.push(Ctx::new(&std, true, false)?.std_package());
254    clash(&packages)?;
255    let start = format!("{}/index.html", packages[0].name);
256    let warnings = analysis.diagnostics.clone();
257    Ok(Documented {
258        site: Site { packages, start },
259        db: analysis.db,
260        warnings,
261    })
262}
263
264/// A library's own package, as its site documents it, and the analysis it
265/// came from; `None` for a package with no library (3.5b spec §4.1). The
266/// analysis is a test build's, as `nova test` reads the library (3.5b
267/// spec §6.1).
268pub(crate) fn library_package(root: &Path) -> Result<Option<(Package, Analysis)>, Failure> {
269    let (analysis, library) = package_analysis(root, true)?;
270    if !library {
271        return Ok(None);
272    }
273    let packages = Ctx::new(&analysis, false, false)?.packages();
274    let name = analysis.graph.as_ref().map(|g| g.root().name.clone());
275    let package = packages
276        .into_iter()
277        .find(|p| Some(&p.name) == name.as_ref());
278    Ok(package.map(|p| (p, analysis)))
279}
280
281/// std's package, as its site documents it, and std's analysis.
282pub(crate) fn std_package() -> Result<(Package, Analysis), Failure> {
283    let std = std_analysis()?;
284    let package = Ctx::new(&std, true, false)?.std_package();
285    Ok((package, std))
286}
287
288/// The analysis a package's site reads, of its library when it has one,
289/// else of its program (spec §5.1), and whether it is a library. Errors
290/// are a failure. Under `tests`, `std/test` is in scope, as in a test
291/// build.
292fn package_analysis(root: &Path, tests: bool) -> Result<(Analysis, bool), Failure> {
293    let mut program = Program::for_package(root, Roots::Check);
294    let stem = |p: &Path| p.file_stem().and_then(|s| s.to_str()).map(str::to_string);
295    let library = program
296        .roots
297        .iter()
298        .any(|r| stem(&r.path).as_deref() == Some("lib"));
299    let keep = if library { "lib" } else { "main" };
300    program
301        .roots
302        .retain(|r| stem(&r.path).as_deref() == Some(keep));
303    program.runs = false;
304    let options = Options {
305        tests,
306        index: true,
307        module_only: true,
308        ..Options::default()
309    };
310    let analysis = analyze_program(program, &DiskSources, &options)
311        .map_err(|e| Failure::Other(format!("cannot read the program: {e}")))?;
312    if analysis
313        .diagnostics
314        .iter()
315        .any(|d| d.severity == Severity::Error)
316    {
317        return Err(Failure::Diagnostics {
318            db: analysis.db,
319            diagnostics: analysis.diagnostics,
320        });
321    }
322    Ok((analysis, library))
323}
324
325/// Document std alone (spec §4.1, `--std`).
326pub fn document_std() -> Result<Documented, Failure> {
327    let (package, _) = std_package()?;
328    Ok(Documented {
329        site: Site {
330            packages: vec![package],
331            start: "std/index.html".to_string(),
332        },
333        db: FileDb::new(),
334        warnings: Vec::new(),
335    })
336}
337
338/// Serves std's analysis its entry, which no file holds.
339struct StdEntry;
340
341impl Sources for StdEntry {
342    fn read(&self, path: &Path) -> std::io::Result<String> {
343        if path == Path::new(STD_ENTRY) {
344            Ok("fn main() {}\n".to_string())
345        } else {
346            Err(std::io::Error::from(std::io::ErrorKind::NotFound))
347        }
348    }
349}
350
351/// An analysis of std alone, `std/test` included (plan decision 8).
352fn std_analysis() -> Result<Analysis, Failure> {
353    let options = Options {
354        tests: true,
355        index: true,
356        module_only: true,
357        ..Options::default()
358    };
359    let a = analyze_program(Program::loose(Path::new(STD_ENTRY)), &StdEntry, &options)
360        .map_err(|e| Failure::Other(format!("cannot analyse std: {e}")))?;
361    if a.diagnostics.iter().any(|d| d.severity == Severity::Error) {
362        return Err(Failure::Diagnostics {
363            db: a.db,
364            diagnostics: a.diagnostics,
365        });
366    }
367    Ok(a)
368}
369
370/// Spec §4.2: two packages, or two modules of one package, that would
371/// share a directory, names that differ in ASCII case alone included.
372fn clash(packages: &[Package]) -> Result<(), Failure> {
373    for (k, p) in packages.iter().enumerate() {
374        if !p.std && p.name.eq_ignore_ascii_case("std") {
375            return Err(Failure::Other(format!(
376                "the package `{}` would share std's pages in target/doc/std/, so nova doc \
377                 cannot document it",
378                p.name
379            )));
380        }
381        if let Some(q) = packages[..k]
382            .iter()
383            .find(|q| q.name.eq_ignore_ascii_case(&p.name))
384        {
385            return Err(Failure::Other(format!(
386                "the packages `{}` and `{}` would share one directory in target/doc/, so nova \
387                 doc cannot document them",
388                q.name, p.name
389            )));
390        }
391        // The root module's page is the package's own.
392        let modules: Vec<&Module> = p.modules.iter().filter(|m| !m.root).collect();
393        for (j, m) in modules.iter().enumerate() {
394            if let Some(n) = modules[..j]
395                .iter()
396                .find(|n| n.name.eq_ignore_ascii_case(&m.name))
397            {
398                return Err(Failure::Other(format!(
399                    "the modules `{}` and `{}` would share one directory in target/doc/{}/, so \
400                     nova doc cannot document them",
401                    n.qual, m.qual, p.name
402                )));
403            }
404        }
405    }
406    Ok(())
407}
408
409/// Which namespace an item is exported in.
410#[derive(Clone, Copy)]
411enum Ns {
412    Value,
413    Type,
414    Trait,
415}
416
417/// An impl met while building modules, placed once all are built.
418struct Pending<'a> {
419    module: ModuleId,
420    span: Span,
421    block: &'a ImplBlock,
422}
423
424/// Where an impl goes (spec §5.2).
425enum Dest {
426    /// With its type: the type's page and anchor.
427    Item(String, String),
428    /// On a primitive's page.
429    Primitive(&'static str),
430    /// On its own module's page, under "Implementations on other types".
431    Own,
432}
433
434/// One analysis, read for its site.
435struct Ctx<'a> {
436    a: &'a Analysis,
437    defs: &'a Definitions,
438    file: &'a nova_ast::File,
439    /// The program's own modules, which come before std's.
440    users: usize,
441    /// Whether this is std's own analysis, whose entry has no page.
442    std: bool,
443    /// Whether the root package documents every item and member (spec
444    /// §5.1; plan decision 7).
445    program: bool,
446    /// Each name's target, by its span (spec §6.3).
447    uses: HashMap<Span, Target>,
448    /// Each rendered module's file, lexed once.
449    texts: HashMap<FileId, FileText<'a>>,
450}
451
452impl<'a> Ctx<'a> {
453    fn new(a: &'a Analysis, std: bool, program: bool) -> Result<Ctx<'a>, Failure> {
454        let incomplete =
455            || Failure::Other("the analysis is incomplete; this is a nova doc bug".to_string());
456        let defs = a.definitions.as_ref().ok_or_else(incomplete)?;
457        let file = a.file.as_ref().ok_or_else(incomplete)?;
458        let index = a.index.as_ref().ok_or_else(incomplete)?;
459        let mut uses: HashMap<Span, Target> = HashMap::new();
460        for o in index.occurrences.iter().filter(|o| o.role == Role::Use) {
461            let keep = uses
462                .get(&o.span)
463                .map_or(true, |t| rank(defs, &o.target) < rank(defs, t));
464            if keep {
465                uses.insert(o.span, o.target);
466            }
467        }
468        let mut ctx = Ctx {
469            a,
470            defs,
471            file,
472            users: a.modules.len(),
473            std,
474            program,
475            uses,
476            texts: HashMap::new(),
477        };
478        for m in 0..defs.module_count() {
479            let m = ModuleId(m as u32);
480            if !ctx.renders(m) {
481                continue;
482            }
483            if let Some(id) = ctx.module_file(m) {
484                if let Some(text) = a.db.get_source(id) {
485                    ctx.texts.insert(id, FileText::new(text, id));
486                }
487            }
488        }
489        Ok(ctx)
490    }
491
492    /// Whether this analysis renders module `m`'s page: std's analysis its
493    /// std modules, a project's analysis its own.
494    fn renders(&self, m: ModuleId) -> bool {
495        ((m.0 as usize) >= self.users) == self.std
496    }
497
498    fn std_name(&self, m: ModuleId) -> Option<&str> {
499        self.defs.module_name(m).strip_prefix("$std.")
500    }
501
502    fn module_file(&self, m: ModuleId) -> Option<FileId> {
503        let i = m.0 as usize;
504        if i < self.users {
505            self.a.modules.get(i).map(|(f, _)| *f)
506        } else {
507            self.a.db.id_of(&format!("<std/{}>", self.std_name(m)?))
508        }
509    }
510
511    fn package_id(&self, m: ModuleId) -> Option<PackageId> {
512        self.a.module_packages.get(m.0 as usize).copied().flatten()
513    }
514
515    fn package_name(&self, m: ModuleId) -> Option<&str> {
516        Some(&self.a.graph.as_ref()?.package(self.package_id(m)?).name)
517    }
518
519    /// The stem of the root module of `m`'s package (plan decision 9).
520    fn root_stem(&self, m: ModuleId) -> &'static str {
521        if self.program && self.package_id(m) == Some(PackageId(0)) {
522            "main"
523        } else {
524            "lib"
525        }
526    }
527
528    /// Module `m`'s page, from the site's root (spec §6.1). Every std
529    /// module has one, whichever analysis asks.
530    fn page(&self, m: ModuleId) -> Option<String> {
531        let i = m.0 as usize;
532        if i >= self.users {
533            return Some(format!("std/{}/index.html", self.std_name(m)?));
534        }
535        if self.std {
536            return None;
537        }
538        let stem = self.a.modules.get(i)?.1.file_stem()?.to_str()?;
539        let package = self.package_name(m)?;
540        Some(if stem == self.root_stem(m) {
541            format!("{package}/index.html")
542        } else {
543            format!("{package}/{stem}/index.html")
544        })
545    }
546
547    /// Whether module `m` documents every item and member: it is the root
548    /// package's, and that package is only a program.
549    fn all(&self, m: ModuleId) -> bool {
550        self.program && (m.0 as usize) < self.users && self.package_id(m) == Some(PackageId(0))
551    }
552
553    fn same_package(&self, a: ModuleId, b: ModuleId) -> bool {
554        let std = |m: ModuleId| m.0 as usize >= self.users;
555        if std(a) || std(b) {
556            std(a) && std(b)
557        } else {
558            self.package_id(a) == self.package_id(b)
559        }
560    }
561
562    /// Whether module `m` documents its item `name` (spec §5.1).
563    fn shown(&self, m: ModuleId, ns: Ns, name: &str) -> bool {
564        if self.all(m) {
565            return true;
566        }
567        let e = self.defs.exported(m, name);
568        match ns {
569            Ns::Value => e.value.is_some(),
570            Ns::Type => e.ty.is_some(),
571            Ns::Trait => e.trait_def.is_some(),
572        }
573    }
574
575    fn member_shown(&self, m: ModuleId, vis: Visibility) -> bool {
576        self.all(m) || vis == Visibility::Pub
577    }
578
579    /// `target`'s page and anchor from the site's root, when the site
580    /// documents it (spec §6.3).
581    fn url(&self, target: &Target) -> Option<String> {
582        match *target {
583            Target::Primitive(p) => Some(format!("std/primitive.{p}.html")),
584            Target::Def(id) => {
585                let def = self.defs.def(id);
586                let item = |item_index: usize, ns: Ns, prefix: &str| {
587                    let m = self.defs.module_of(item_index);
588                    if !self.shown(m, ns, &def.name) {
589                        return None;
590                    }
591                    Some(format!("{}#{prefix}.{}", self.page(m)?, def.name))
592                };
593                match &def.kind {
594                    DefKind::Fn { item_index } => item(*item_index, Ns::Value, "fn"),
595                    DefKind::Const { item_index } => item(*item_index, Ns::Value, "const"),
596                    DefKind::Record { item_index } => item(*item_index, Ns::Type, "record"),
597                    DefKind::Sum { item_index, .. } => item(*item_index, Ns::Type, "type"),
598                    DefKind::Trait { item_index } => item(*item_index, Ns::Trait, "trait"),
599                    DefKind::ExternFn { item_index, .. } => {
600                        let m = self.defs.module_of(*item_index);
601                        if !self.all(m) {
602                            return None;
603                        }
604                        Some(format!("{}#fn.{}", self.page(m)?, def.name))
605                    }
606                    DefKind::AssocType { trait_def } => {
607                        let t = self.url(&Target::Def(*trait_def))?;
608                        Some(format!("{t}.type.{}", def.name))
609                    }
610                    DefKind::Method { .. } => None,
611                }
612            }
613            Target::Variant(sum, vi) => {
614                let base = self.url(&Target::Def(sum))?;
615                let DefKind::Sum { variants, .. } = &self.defs.def(sum).kind else {
616                    return None;
617                };
618                Some(format!(
619                    "{base}.variant.{}",
620                    variants.get(vi as usize)?.name
621                ))
622            }
623            Target::Field(rec, fi) => {
624                let base = self.url(&Target::Def(rec))?;
625                let DefKind::Record { item_index } = self.defs.def(rec).kind else {
626                    return None;
627                };
628                let Ast::Record(r) = &self.file.items.get(item_index)?.value else {
629                    return None;
630                };
631                let field = r.fields.get(fi as usize)?;
632                if !self.member_shown(self.defs.module_of(item_index), field.vis) {
633                    return None;
634                }
635                Some(format!("{base}.field.{}", field.name.value))
636            }
637            _ => None,
638        }
639    }
640
641    /// `text[start..end]`'s signature for a page at `page` (spec §8).
642    fn sig(
643        &self,
644        ft: &FileText,
645        page: &str,
646        start: usize,
647        end: usize,
648        own: Option<Span>,
649    ) -> String {
650        signature::render(ft, start, end, own, &|span| {
651            self.uses
652                .get(&span)
653                .and_then(|t| self.url(t))
654                .map(|url| relative(page, &url))
655        })
656    }
657}
658
659/// `url`, from the site's root, as seen from `page` (plan decision 13).
660pub(crate) fn relative(page: &str, url: &str) -> String {
661    format!("{}{url}", "../".repeat(page.matches('/').count()))
662}
663
664/// Doc lines rendered, their headings moved down `level` levels, keeping
665/// their source (3.5b spec §11.1).
666fn docs_of(lines: &[Spanned<String>], level: u8) -> Docs {
667    markdown::render_source(markdown::join(lines), level)
668}
669
670/// The last segment of a path type: what an impl is on.
671fn type_name(ty: &Type) -> Option<&Spanned<String>> {
672    match ty {
673        Type::Path { path, .. } => path.segments.last(),
674        _ => None,
675    }
676}
677
678/// `base`, or `base-2`, `base-3` and so on: the first not yet on its page
679/// (spec §6.2).
680fn unique(taken: &mut HashSet<String>, base: String) -> String {
681    if taken.insert(base.clone()) {
682        return base;
683    }
684    (2..)
685        .map(|n| format!("{base}-{n}"))
686        .find(|a| taken.insert(a.clone()))
687        .expect("an unused id")
688}
689
690impl<'a> Ctx<'a> {
691    /// Module `m` and its items; its impls go to `pending` (spec §7.3).
692    fn module(&self, m: ModuleId, pending: &mut Vec<Pending<'a>>) -> Option<Module> {
693        let page = self.page(m)?;
694        let ft = self.texts.get(&self.module_file(m)?)?;
695        let (name, qual, root) = if m.0 as usize >= self.users {
696            let n = self.std_name(m)?.to_string();
697            let qual = format!("std::{n}");
698            (n, qual, false)
699        } else {
700            let stem = self
701                .a
702                .modules
703                .get(m.0 as usize)?
704                .1
705                .file_stem()?
706                .to_str()?
707                .to_string();
708            let package = self.package_name(m)?.to_string();
709            let root = stem == self.root_stem(m);
710            let qual = if root {
711                package
712            } else {
713                format!("{package}::{stem}")
714            };
715            (stem, qual, root)
716        };
717        let mut items = Vec::new();
718        for (i, item) in self.file.items.iter().enumerate() {
719            if self.defs.module_of(i) != m {
720                continue;
721            }
722            match &item.value {
723                Ast::Function(f) if self.shown(m, Ns::Value, &f.name.value) => {
724                    items.push(self.function(
725                        ft,
726                        &page,
727                        &f.name,
728                        &f.docs,
729                        f.return_ty.as_ref(),
730                        &f.where_clause,
731                    ))
732                }
733                Ast::Extern(block) if self.all(m) => {
734                    for e in &block.items {
735                        let ExternItem::Fn(sig) = e;
736                        items.push(self.function(
737                            ft,
738                            &page,
739                            &sig.name,
740                            &sig.docs,
741                            sig.return_ty.as_ref(),
742                            &sig.where_clause,
743                        ));
744                    }
745                }
746                Ast::Record(r) if self.shown(m, Ns::Type, &r.name.value) => {
747                    items.push(self.record(ft, &page, m, r))
748                }
749                Ast::Type(t)
750                    if matches!(t.def, TypeDef::Sum(_))
751                        && self.shown(m, Ns::Type, &t.name.value) =>
752                {
753                    items.push(self.sum(ft, &page, t))
754                }
755                Ast::Trait(t) if self.shown(m, Ns::Trait, &t.name.value) => {
756                    items.push(self.trait_item(ft, &page, t))
757                }
758                Ast::Const(c) if self.shown(m, Ns::Value, &c.name.value) => {
759                    items.push(self.constant(ft, &page, c))
760                }
761                Ast::Impl(block) => pending.push(Pending {
762                    module: m,
763                    span: item.span,
764                    block,
765                }),
766                _ => {}
767            }
768        }
769        Some(Module {
770            name,
771            qual,
772            page,
773            root,
774            docs: markdown::render_source(ft.module_docs(), 1),
775            items,
776            other_impls: Vec::new(),
777        })
778    }
779
780    fn function(
781        &self,
782        ft: &FileText,
783        page: &str,
784        name: &Spanned<String>,
785        docs: &[Spanned<String>],
786        ret: Option<&Spanned<Type>>,
787        wheres: &[WhereBound],
788    ) -> Item {
789        let start = signature::decl_start(ft.text, name.span.start as usize);
790        let end = signature::fn_end(ft, name.span, ret, wheres);
791        Item {
792            kind: ItemKind::Function,
793            name: name.value.clone(),
794            anchor: format!("fn.{}", name.value),
795            signature: self.sig(ft, page, start, end, Some(name.span)),
796            docs: docs_of(docs, 3),
797            members: Vec::new(),
798            hidden_fields: false,
799            impls: Vec::new(),
800        }
801    }
802
803    fn record(&self, ft: &FileText, page: &str, m: ModuleId, r: &Record) -> Item {
804        let start = signature::decl_start(ft.text, r.name.span.start as usize);
805        let end = ft
806            .next(r.name.span.end as usize, |t| *t == Token::LBrace)
807            .unwrap_or(r.name.span.end as usize);
808        let anchor = format!("record.{}", r.name.value);
809        let mut members = Vec::new();
810        let mut hidden = false;
811        for f in &r.fields {
812            if !self.member_shown(m, f.vis) {
813                hidden = true;
814                continue;
815            }
816            let start = signature::decl_start(ft.text, f.name.span.start as usize);
817            members.push(Member {
818                kind: MemberKind::Field,
819                name: f.name.value.clone(),
820                anchor: format!("{anchor}.field.{}", f.name.value),
821                signature: self.sig(ft, page, start, f.ty.span.end as usize, Some(f.name.span)),
822                docs: docs_of(&f.docs, 4),
823            });
824        }
825        Item {
826            kind: ItemKind::Record,
827            name: r.name.value.clone(),
828            signature: self.sig(ft, page, start, end, Some(r.name.span)),
829            anchor,
830            docs: docs_of(&r.docs, 3),
831            members,
832            hidden_fields: hidden,
833            impls: Vec::new(),
834        }
835    }
836
837    fn sum(&self, ft: &FileText, page: &str, t: &TypeDecl) -> Item {
838        let start = signature::decl_start(ft.text, t.name.span.start as usize);
839        let end = ft
840            .next(t.name.span.end as usize, |t| *t == Token::Eq)
841            .unwrap_or(t.name.span.end as usize);
842        let anchor = format!("type.{}", t.name.value);
843        let members = match &t.def {
844            TypeDef::Sum(variants) => variants
845                .iter()
846                .map(|v| {
847                    let end = if v.fields.is_empty() {
848                        v.name.span.end as usize
849                    } else {
850                        ft.group_end(v.name.span.end as usize)
851                            .unwrap_or(v.name.span.end as usize)
852                    };
853                    Member {
854                        kind: MemberKind::Variant,
855                        name: v.name.value.clone(),
856                        anchor: format!("{anchor}.variant.{}", v.name.value),
857                        signature: self.sig(
858                            ft,
859                            page,
860                            v.name.span.start as usize,
861                            end,
862                            Some(v.name.span),
863                        ),
864                        docs: docs_of(&v.docs, 4),
865                    }
866                })
867                .collect(),
868            TypeDef::Alias(_) => Vec::new(),
869        };
870        Item {
871            kind: ItemKind::Sum,
872            name: t.name.value.clone(),
873            signature: self.sig(ft, page, start, end, Some(t.name.span)),
874            anchor,
875            docs: docs_of(&t.docs, 3),
876            members,
877            hidden_fields: false,
878            impls: Vec::new(),
879        }
880    }
881
882    fn trait_item(&self, ft: &FileText, page: &str, t: &TraitDecl) -> Item {
883        let start = signature::decl_start(ft.text, t.name.span.start as usize);
884        let end = ft
885            .next(t.name.span.end as usize, |t| *t == Token::LBrace)
886            .unwrap_or(t.name.span.end as usize);
887        let anchor = format!("trait.{}", t.name.value);
888        let mut types = Vec::new();
889        let mut methods = Vec::new();
890        for item in &t.items {
891            match item {
892                TraitItem::AssocType { docs, name, bounds } => {
893                    let end = bounds.last().map_or(name.span.end, |b| b.span.end) as usize;
894                    let start = signature::decl_start(ft.text, name.span.start as usize);
895                    types.push(Member {
896                        kind: MemberKind::AssocType,
897                        name: name.value.clone(),
898                        anchor: format!("{anchor}.type.{}", name.value),
899                        signature: self.sig(ft, page, start, end, Some(name.span)),
900                        docs: docs_of(docs, 4),
901                    });
902                }
903                TraitItem::Required(s) => methods.push(self.method(
904                    ft,
905                    page,
906                    &format!("{anchor}.method"),
907                    &s.name,
908                    &s.docs,
909                    s.return_ty.as_ref(),
910                    &s.where_clause,
911                )),
912                TraitItem::Provided(f) => methods.push(self.method(
913                    ft,
914                    page,
915                    &format!("{anchor}.method"),
916                    &f.name,
917                    &f.docs,
918                    f.return_ty.as_ref(),
919                    &f.where_clause,
920                )),
921            }
922        }
923        types.extend(methods);
924        Item {
925            kind: ItemKind::Trait,
926            name: t.name.value.clone(),
927            signature: self.sig(ft, page, start, end, Some(t.name.span)),
928            anchor,
929            docs: docs_of(&t.docs, 3),
930            members: types,
931            hidden_fields: false,
932            impls: Vec::new(),
933        }
934    }
935
936    /// A method, its anchor `<prefix>.<name>`.
937    #[allow(clippy::too_many_arguments)]
938    fn method(
939        &self,
940        ft: &FileText,
941        page: &str,
942        prefix: &str,
943        name: &Spanned<String>,
944        docs: &[Spanned<String>],
945        ret: Option<&Spanned<Type>>,
946        wheres: &[WhereBound],
947    ) -> Member {
948        let start = signature::decl_start(ft.text, name.span.start as usize);
949        let end = signature::fn_end(ft, name.span, ret, wheres);
950        Member {
951            kind: MemberKind::Method,
952            name: name.value.clone(),
953            anchor: format!("{prefix}.{}", name.value),
954            signature: self.sig(ft, page, start, end, Some(name.span)),
955            docs: docs_of(docs, 4),
956        }
957    }
958
959    fn constant(&self, ft: &FileText, page: &str, c: &ConstDecl) -> Item {
960        let start = signature::decl_start(ft.text, c.name.span.start as usize);
961        Item {
962            kind: ItemKind::Constant,
963            name: c.name.value.clone(),
964            anchor: format!("const.{}", c.name.value),
965            signature: self.sig(
966                ft,
967                page,
968                start,
969                c.value.span.end as usize,
970                Some(c.name.span),
971            ),
972            docs: docs_of(&c.docs, 3),
973            members: Vec::new(),
974            hidden_fields: false,
975            impls: Vec::new(),
976        }
977    }
978}
979
980impl<'a> Ctx<'a> {
981    /// Each impl in `pending`, where spec §5.2 puts it.
982    fn place(
983        &self,
984        modules: &mut [Module],
985        primitives: &mut [Primitive],
986        pending: Vec<Pending<'a>>,
987    ) {
988        let mut taken: HashMap<String, HashSet<String>> = HashMap::new();
989        for m in modules.iter() {
990            let set = taken.entry(m.page.clone()).or_default();
991            for item in &m.items {
992                set.insert(item.anchor.clone());
993                for member in &item.members {
994                    set.insert(member.anchor.clone());
995                }
996            }
997        }
998        for p in pending {
999            let block = p.block;
1000            let Some(ft) = self.module_file(p.module).and_then(|f| self.texts.get(&f)) else {
1001                continue;
1002            };
1003            let Some(own_page) = self.page(p.module) else {
1004                continue;
1005            };
1006            let on = type_name(&block.ty.value);
1007            let on_name = on.map_or_else(|| "type".to_string(), |s| s.value.clone());
1008            let ty = on.and_then(|s| self.uses.get(&s.span)).copied();
1009            let dest = match ty {
1010                Some(Target::Def(id)) => {
1011                    let item_index = match self.defs.def(id).kind {
1012                        DefKind::Record { item_index } | DefKind::Sum { item_index, .. } => {
1013                            Some(item_index)
1014                        }
1015                        _ => None,
1016                    };
1017                    match item_index {
1018                        Some(ii) if self.same_package(self.defs.module_of(ii), p.module) => {
1019                            // A type of this package that is not documented
1020                            // takes its impls with it.
1021                            let Some(url) = self.url(&Target::Def(id)) else {
1022                                continue;
1023                            };
1024                            let (page, anchor) = url.split_once('#').unwrap_or((url.as_str(), ""));
1025                            Dest::Item(page.to_string(), anchor.to_string())
1026                        }
1027                        _ => Dest::Own,
1028                    }
1029                }
1030                Some(Target::Primitive(name)) if self.std => Dest::Primitive(name),
1031                _ => Dest::Own,
1032            };
1033            let page = match &dest {
1034                Dest::Item(page, _) => page.clone(),
1035                Dest::Primitive(name) => format!("std/primitive.{name}.html"),
1036                Dest::Own => own_page.clone(),
1037            };
1038            let header = self.impl_header(ft, &page, p.span, block);
1039            let mut methods = Vec::new();
1040            if block.trait_.is_none() {
1041                let prefix = match &dest {
1042                    Dest::Item(_, anchor) => format!("{anchor}.method"),
1043                    Dest::Primitive(_) => "method".to_string(),
1044                    Dest::Own => format!("impl.{on_name}.method"),
1045                };
1046                for f in block
1047                    .functions
1048                    .iter()
1049                    .filter(|f| self.member_shown(p.module, f.vis))
1050                {
1051                    let mut m = self.method(
1052                        ft,
1053                        &page,
1054                        &prefix,
1055                        &f.name,
1056                        &f.docs,
1057                        f.return_ty.as_ref(),
1058                        &f.where_clause,
1059                    );
1060                    m.anchor = unique(taken.entry(page.clone()).or_default(), m.anchor);
1061                    methods.push(m);
1062                }
1063            }
1064            let built = Impl {
1065                header,
1066                docs: docs_of(&block.docs, 4),
1067                methods,
1068                on: on_name.clone(),
1069            };
1070            match &dest {
1071                Dest::Item(page, anchor) => {
1072                    if let Some(item) = find_item(modules, page, anchor) {
1073                        item.impls.push(built);
1074                    }
1075                }
1076                Dest::Primitive(name) => {
1077                    if let Some(pr) = primitives.iter_mut().find(|pr| pr.name == *name) {
1078                        pr.impls.push(built);
1079                    }
1080                }
1081                Dest::Own => {
1082                    if let Some(m) = modules.iter_mut().find(|m| m.page == own_page) {
1083                        m.other_impls.push(built);
1084                    }
1085                }
1086            }
1087            // A trait of this package also lists the impl, by its header.
1088            let tr = block
1089                .trait_
1090                .as_ref()
1091                .and_then(|t| t.value.segments.last())
1092                .and_then(|s| self.uses.get(&s.span))
1093                .copied();
1094            if let Some(Target::Def(t)) = tr {
1095                if let DefKind::Trait { item_index } = self.defs.def(t).kind {
1096                    if self.same_package(self.defs.module_of(item_index), p.module) {
1097                        if let Some(url) = self.url(&Target::Def(t)) {
1098                            let (page, anchor) = url.split_once('#').unwrap_or((url.as_str(), ""));
1099                            let header = self.impl_header(ft, page, p.span, block);
1100                            if let Some(item) = find_item(modules, page, anchor) {
1101                                item.impls.push(Impl {
1102                                    header,
1103                                    docs: Docs::default(),
1104                                    methods: Vec::new(),
1105                                    on: on_name,
1106                                });
1107                            }
1108                        }
1109                    }
1110                }
1111            }
1112        }
1113    }
1114
1115    /// An impl's header, `impl … {` without its brace (plan decision 10).
1116    fn impl_header(&self, ft: &FileText, page: &str, span: Span, block: &ImplBlock) -> String {
1117        let start = ft
1118            .next(span.start as usize, |t| *t == Token::Impl)
1119            .unwrap_or(span.start as usize);
1120        let end = ft
1121            .next(start, |t| *t == Token::LBrace)
1122            .unwrap_or(block.ty.span.end as usize);
1123        self.sig(ft, page, start, end, None)
1124    }
1125
1126    /// A project's packages: the root first, then the rest by name (spec
1127    /// §7.1).
1128    fn packages(&self) -> Vec<Package> {
1129        let Some(graph) = self.a.graph.as_ref() else {
1130            return Vec::new();
1131        };
1132        let mut pending = Vec::new();
1133        let mut modules = Vec::new();
1134        let mut owners = Vec::new();
1135        for i in 0..self.users {
1136            let m = ModuleId(i as u32);
1137            let Some(pid) = self.package_id(m) else {
1138                continue;
1139            };
1140            if let Some(module) = self.module(m, &mut pending) {
1141                modules.push(module);
1142                owners.push(pid);
1143            }
1144        }
1145        self.place(&mut modules, &mut [], pending);
1146        let mut packages: Vec<Package> = Vec::new();
1147        for (module, pid) in modules.into_iter().zip(owners) {
1148            let gp = graph.package(pid);
1149            let at = match packages.iter().position(|p| p.name == gp.name) {
1150                Some(at) => at,
1151                None => {
1152                    packages.push(Package {
1153                        name: gp.name.clone(),
1154                        version: gp.manifest.package.version.to_string(),
1155                        description: gp.manifest.package.description.clone(),
1156                        std: false,
1157                        modules: Vec::new(),
1158                        primitives: Vec::new(),
1159                        builtins: Vec::new(),
1160                    });
1161                    packages.len() - 1
1162                }
1163            };
1164            packages[at].modules.push(module);
1165        }
1166        for p in &mut packages {
1167            p.modules
1168                .sort_by(|a, b| (!a.root, &a.name).cmp(&(!b.root, &b.name)));
1169        }
1170        let root = graph.root().name.clone();
1171        packages.sort_by(|a, b| (a.name != root, &a.name).cmp(&(b.name != root, &b.name)));
1172        packages
1173    }
1174
1175    /// std's package, from std's own analysis (spec §5.3, §7.4).
1176    fn std_package(&self) -> Package {
1177        let mut pending = Vec::new();
1178        let mut modules: Vec<Module> = (self.users..self.defs.module_count())
1179            .filter_map(|i| self.module(ModuleId(i as u32), &mut pending))
1180            .collect();
1181        let mut primitives: Vec<Primitive> = PRIMITIVES
1182            .iter()
1183            .map(|&(name, about)| Primitive {
1184                name,
1185                page: format!("std/primitive.{name}.html"),
1186                about: markdown::render(about, 1),
1187                impls: Vec::new(),
1188            })
1189            .collect();
1190        self.place(&mut modules, &mut primitives, pending);
1191        modules.sort_by(|a, b| a.name.cmp(&b.name));
1192        let builtins = Builtin::ALL
1193            .iter()
1194            .filter(|b| !Builtin::STD_ONLY.contains(*b))
1195            .map(|b| BuiltinDoc {
1196                name: b.name(),
1197                signature: highlight(&nova_typeck::builtin_text(*b, self.defs)),
1198                docs: markdown::render(
1199                    BUILTINS
1200                        .iter()
1201                        .find(|(n, _)| *n == b.name())
1202                        .map_or("", |(_, d)| *d),
1203                    3,
1204                ),
1205            })
1206            .collect();
1207        Package {
1208            name: "std".to_string(),
1209            version: env!("CARGO_PKG_VERSION").to_string(),
1210            description: Some(STD_ABOUT.to_string()),
1211            std: true,
1212            modules,
1213            primitives,
1214            builtins,
1215        }
1216    }
1217}
1218
1219/// The item at `page`'s `anchor`.
1220fn find_item<'m>(modules: &'m mut [Module], page: &str, anchor: &str) -> Option<&'m mut Item> {
1221    modules
1222        .iter_mut()
1223        .find(|m| m.page == page)?
1224        .items
1225        .iter_mut()
1226        .find(|i| i.anchor == anchor)
1227}
1228
1229/// A small site for the renderer's, the search index's and the writer's
1230/// tests.
1231#[cfg(test)]
1232pub(crate) fn sample_site() -> Site {
1233    let docs = |text: &str| markdown::render(text, 3);
1234    let point = Item {
1235        kind: ItemKind::Record,
1236        name: "Point".to_string(),
1237        anchor: "record.Point".to_string(),
1238        signature: "<span class=\"kw\">pub</span> <span class=\"kw\">record</span> \
1239                    <span class=\"name\">Point</span>"
1240            .to_string(),
1241        docs: docs("A point."),
1242        members: vec![Member {
1243            kind: MemberKind::Field,
1244            name: "x".to_string(),
1245            anchor: "record.Point.field.x".to_string(),
1246            signature: "<span class=\"kw\">pub</span> x: \
1247                        <a href=\"../std/primitive.Int.html\">Int</a>"
1248                .to_string(),
1249            docs: docs("Across \"x\"."),
1250        }],
1251        hidden_fields: true,
1252        impls: Vec::new(),
1253    };
1254    let module = |name: &str, qual: &str, page: &str, root: bool, items: Vec<Item>| Module {
1255        name: name.to_string(),
1256        qual: qual.to_string(),
1257        page: page.to_string(),
1258        root,
1259        docs: markdown::render("A module.", 1),
1260        items,
1261        other_impls: Vec::new(),
1262    };
1263    let app = Package {
1264        name: "app".to_string(),
1265        version: "0.1.0".to_string(),
1266        description: Some("An app.".to_string()),
1267        std: false,
1268        modules: vec![
1269            module("main", "app", "app/index.html", true, vec![point]),
1270            module(
1271                "util",
1272                "app::util",
1273                "app/util/index.html",
1274                false,
1275                Vec::new(),
1276            ),
1277        ],
1278        primitives: Vec::new(),
1279        builtins: Vec::new(),
1280    };
1281    let std = Package {
1282        name: "std".to_string(),
1283        version: "0.2.0".to_string(),
1284        description: Some(STD_ABOUT.to_string()),
1285        std: true,
1286        modules: vec![module(
1287            "core",
1288            "std::core",
1289            "std/core/index.html",
1290            false,
1291            Vec::new(),
1292        )],
1293        primitives: vec![Primitive {
1294            name: "Int",
1295            page: "std/primitive.Int.html".to_string(),
1296            about: markdown::render("An integer.", 1),
1297            impls: Vec::new(),
1298        }],
1299        builtins: vec![BuiltinDoc {
1300            name: "println",
1301            signature: "<span class=\"kw\">fn</span> println(String) -&gt; ()".to_string(),
1302            docs: markdown::render("Writes a line.", 3),
1303        }],
1304    };
1305    Site {
1306        packages: vec![app, std],
1307        start: "app/index.html".to_string(),
1308    }
1309}
1310
1311#[cfg(test)]
1312mod tests {
1313    use super::*;
1314    use std::path::PathBuf;
1315
1316    /// A fresh, empty directory with a fixed name, so each run replaces the
1317    /// last.
1318    fn fresh(name: &str) -> PathBuf {
1319        let dir = std::env::temp_dir().join(format!("nova-doc-model-{name}"));
1320        let _ = std::fs::remove_dir_all(&dir);
1321        std::fs::create_dir_all(&dir).unwrap();
1322        dir
1323    }
1324
1325    fn write(dir: &Path, files: &[(&str, &str)]) {
1326        for (path, text) in files {
1327            let path = dir.join(path);
1328            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
1329            std::fs::write(path, text).unwrap();
1330        }
1331    }
1332
1333    fn manifest(name: &str, extra: &str) -> String {
1334        format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2026\"\n{extra}")
1335    }
1336
1337    fn documented(root: &Path) -> Documented {
1338        match document_package(root) {
1339            Ok(d) => d,
1340            Err(Failure::Diagnostics { db, diagnostics }) => panic!(
1341                "{}",
1342                nova_diagnostics::render::render_to_string(&db, &diagnostics)
1343            ),
1344            Err(Failure::Other(why)) => panic!("{why}"),
1345        }
1346    }
1347
1348    fn module<'s>(site: &'s Site, page: &str) -> &'s Module {
1349        site.packages
1350            .iter()
1351            .flat_map(|p| &p.modules)
1352            .find(|m| m.page == page)
1353            .unwrap_or_else(|| panic!("no page {page}"))
1354    }
1355
1356    fn item<'m>(m: &'m Module, anchor: &str) -> &'m Item {
1357        m.items
1358            .iter()
1359            .find(|i| i.anchor == anchor)
1360            .unwrap_or_else(|| panic!("no {anchor} on {}", m.page))
1361    }
1362
1363    const GEOM_LIB: &str = "import utils\n\n/// A point.\npub record Point {\n    /// Across.\n    pub x: Int,\n    y: Int,\n}\n\n/// The area of a square of side `width()`.\npub fn area() -> Int { width() * width() }\n\nfn secret() -> Int { 1 }\n\nrecord Hidden {\n    n: Int,\n}\n\n/// Makes a thing of a private type.\npub fn make() -> Hidden { Hidden { n: 1 } }\n";
1364    const GEOM_UTILS: &str = "/// Three.\npub fn width() -> Int { 3 }\n";
1365    const APP_MAIN: &str = "import geom\n\n/// Shows `p`.\nfn show(p: Point) -> String { \"${p.x}\" }\n\n/// A record of the app's own.\nrecord Local {\n    hidden: Int,\n}\n\nfn main() {\n    println(show(Point { x: area(), y: 0 }))\n}\n";
1366
1367    #[test]
1368    fn std_is_one_package_of_modules_primitives_and_builtins() {
1369        let Ok(d) = document_std() else {
1370            panic!("std is not documented")
1371        };
1372        assert_eq!(d.site.packages.len(), 1);
1373        assert_eq!(d.site.start, "std/index.html");
1374        let std = &d.site.packages[0];
1375        assert!(std.std);
1376        let names: Vec<&str> = std.modules.iter().map(|m| m.name.as_str()).collect();
1377        assert_eq!(
1378            names,
1379            [
1380                "bytes",
1381                "collections",
1382                "core",
1383                "crypto",
1384                "fmt",
1385                "fs",
1386                "http",
1387                "io",
1388                "json",
1389                "log",
1390                "net",
1391                "process",
1392                "strings",
1393                "sync",
1394                "task",
1395                "test",
1396                "time"
1397            ]
1398        );
1399        let core = module(&d.site, "std/core/index.html");
1400        let option = item(core, "type.Option");
1401        let variants: Vec<&str> = option.members.iter().map(|m| m.anchor.as_str()).collect();
1402        assert_eq!(
1403            variants,
1404            ["type.Option.variant.Some", "type.Option.variant.None"]
1405        );
1406        assert!(option.impls.iter().any(|i| i
1407            .methods
1408            .iter()
1409            .any(|m| m.anchor == "type.Option.method.unwrap")));
1410        let string = std
1411            .primitives
1412            .iter()
1413            .find(|p| p.name == "String")
1414            .expect("a String page");
1415        assert!(string.impls.iter().any(|i| !i.methods.is_empty()));
1416        let mut builtins: Vec<&str> = std.builtins.iter().map(|b| b.name).collect();
1417        builtins.sort_unstable();
1418        assert_eq!(
1419            builtins,
1420            ["eprint", "eprintln", "panic", "print", "println"]
1421        );
1422    }
1423
1424    #[test]
1425    fn a_program_documents_every_item_and_its_library_only_its_exports() {
1426        let dir = fresh("program");
1427        write(
1428            &dir.join("geom"),
1429            &[
1430                ("nova.toml", &manifest("geom", "")),
1431                ("src/lib.nova", GEOM_LIB),
1432                ("src/utils.nova", GEOM_UTILS),
1433            ],
1434        );
1435        write(
1436            &dir.join("app"),
1437            &[
1438                (
1439                    "nova.toml",
1440                    &manifest("app", "\n[dependencies]\ngeom = { path = \"../geom\" }\n"),
1441                ),
1442                ("src/main.nova", APP_MAIN),
1443            ],
1444        );
1445        let d = documented(&dir.join("app"));
1446        let names: Vec<&str> = d.site.packages.iter().map(|p| p.name.as_str()).collect();
1447        assert_eq!(names, ["app", "geom", "std"]);
1448        assert_eq!(d.site.start, "app/index.html");
1449        let app = module(&d.site, "app/index.html");
1450        assert!(app.root);
1451        let anchors: Vec<&str> = app.items.iter().map(|i| i.anchor.as_str()).collect();
1452        assert_eq!(anchors, ["fn.show", "record.Local", "fn.main"]);
1453        assert!(!item(app, "record.Local").hidden_fields);
1454        let show = item(app, "fn.show");
1455        assert!(
1456            show.signature
1457                .contains("<a href=\"../geom/index.html#record.Point\">Point</a>"),
1458            "{}",
1459            show.signature
1460        );
1461        assert!(
1462            show.signature
1463                .contains("<a href=\"../std/primitive.String.html\">String</a>"),
1464            "{}",
1465            show.signature
1466        );
1467        let geom = module(&d.site, "geom/index.html");
1468        let anchors: Vec<&str> = geom.items.iter().map(|i| i.anchor.as_str()).collect();
1469        assert_eq!(anchors, ["record.Point", "fn.area", "fn.make"]);
1470        // A private type in a public signature is plain text (mutant 1).
1471        let make = item(geom, "fn.make");
1472        assert!(!make.signature.contains("<a "), "{}", make.signature);
1473        let point = item(geom, "record.Point");
1474        assert!(point.hidden_fields);
1475        let fields: Vec<&str> = point.members.iter().map(|m| m.anchor.as_str()).collect();
1476        assert_eq!(fields, ["record.Point.field.x"]);
1477        module(&d.site, "geom/utils/index.html");
1478    }
1479
1480    const IMPLS: &str = "/// A rectangle.\npub record Rect {\n    pub w: Int,\n    pub h: Int,\n}\n\nimpl Rect {\n    /// Its area.\n    pub fn area(self) -> Int { self.w * self.h }\n\n    fn hidden(self) -> Int { 0 }\n}\n\n/// What can be measured.\npub trait Measure {\n    /// The size.\n    fn size(self) -> Int\n}\n\nimpl Measure for Rect {\n    fn size(self) -> Int { self.area() }\n}\n\nimpl Int {\n    /// Twice it.\n    pub fn doubled(self) -> Int { self * 2 }\n}\n";
1481
1482    #[test]
1483    fn an_impl_goes_with_its_type_and_a_trait_lists_its_impls() {
1484        let dir = fresh("impls");
1485        write(
1486            &dir,
1487            &[
1488                ("nova.toml", &manifest("shapes", "")),
1489                ("src/lib.nova", IMPLS),
1490            ],
1491        );
1492        let d = documented(&dir);
1493        let lib = module(&d.site, "shapes/index.html");
1494        let rect = item(lib, "record.Rect");
1495        assert_eq!(rect.impls.len(), 2, "the inherent impl and the trait impl");
1496        let methods: Vec<&str> = rect
1497            .impls
1498            .iter()
1499            .flat_map(|i| &i.methods)
1500            .map(|m| m.anchor.as_str())
1501            .collect();
1502        assert_eq!(methods, ["record.Rect.method.area"]);
1503        let measure = item(lib, "trait.Measure");
1504        assert_eq!(measure.impls.len(), 1);
1505        assert!(
1506            measure.impls[0]
1507                .header
1508                .contains("<a href=\"../shapes/index.html#record.Rect\">Rect</a>"),
1509            "{}",
1510            measure.impls[0].header
1511        );
1512        assert_eq!(lib.other_impls.len(), 1, "the impl on Int");
1513        let doubled: Vec<&str> = lib.other_impls[0]
1514            .methods
1515            .iter()
1516            .map(|m| m.anchor.as_str())
1517            .collect();
1518        assert_eq!(doubled, ["impl.Int.method.doubled"]);
1519    }
1520
1521    const PAIRS: &str = "/// Two of a kind.\npub record Pair<T> {\n    pub a: T,\n    pub b: T,\n}\n\nimpl Pair<Int> {\n    /// Their sum.\n    pub fn total(self) -> Int { self.a + self.b }\n}\n\nimpl Pair<String> {\n    /// The two, joined.\n    pub fn total(self) -> String { \"${self.a}${self.b}\" }\n}\n";
1522
1523    #[test]
1524    fn two_impls_of_one_type_number_a_method_they_share() {
1525        // Spec §6.2: both impls go on `Pair`'s anchor, so the later
1526        // `total` is numbered.
1527        let dir = fresh("pairs");
1528        write(
1529            &dir,
1530            &[
1531                ("nova.toml", &manifest("pairs", "")),
1532                ("src/lib.nova", PAIRS),
1533            ],
1534        );
1535        let d = documented(&dir);
1536        let lib = module(&d.site, "pairs/index.html");
1537        let methods: Vec<&str> = item(lib, "record.Pair")
1538            .impls
1539            .iter()
1540            .flat_map(|i| &i.methods)
1541            .map(|m| m.anchor.as_str())
1542            .collect();
1543        assert_eq!(
1544            methods,
1545            ["record.Pair.method.total", "record.Pair.method.total-2"]
1546        );
1547    }
1548
1549    #[test]
1550    fn an_error_is_a_failure_that_carries_its_diagnostics() {
1551        let dir = fresh("error");
1552        write(
1553            &dir,
1554            &[
1555                ("nova.toml", &manifest("app", "")),
1556                ("src/main.nova", "fn main() {\n    let x: Int = \"s\"\n}\n"),
1557            ],
1558        );
1559        match document_package(&dir) {
1560            Err(Failure::Diagnostics { diagnostics, .. }) => {
1561                assert!(diagnostics.iter().any(|d| d.severity == Severity::Error))
1562            }
1563            _ => panic!("a program with an error was documented"),
1564        }
1565    }
1566
1567    #[test]
1568    fn a_package_named_std_stops_it() {
1569        let dir = fresh("named-std");
1570        write(
1571            &dir,
1572            &[
1573                ("nova.toml", &manifest("std", "")),
1574                ("src/main.nova", "fn main() {}\n"),
1575            ],
1576        );
1577        match document_package(&dir) {
1578            Err(Failure::Other(why)) => assert!(why.contains("`std`"), "{why}"),
1579            _ => panic!("a package named std was documented"),
1580        }
1581    }
1582
1583    #[test]
1584    fn two_modules_whose_names_differ_in_case_alone_stop_it() {
1585        // Their pages would share a directory on macOS's and Windows's
1586        // file systems, which ignore case.
1587        let mut site = sample_site();
1588        assert!(clash(&site.packages).is_ok());
1589        let mut util = sample_site().packages.remove(0).modules.remove(1);
1590        util.name = "Util".to_string();
1591        util.qual = "app::Util".to_string();
1592        util.page = "app/Util/index.html".to_string();
1593        site.packages[0].modules.insert(1, util);
1594        match clash(&site.packages) {
1595            Err(Failure::Other(why)) => assert_eq!(
1596                why,
1597                "the modules `app::Util` and `app::util` would share one directory in \
1598                 target/doc/app/, so nova doc cannot document them"
1599            ),
1600            _ => panic!("two modules that differ in case alone were documented"),
1601        }
1602    }
1603
1604    #[test]
1605    fn a_clashing_id_gets_a_number() {
1606        // Spec §6.2: the later of two equal ids on one page.
1607        let mut taken = HashSet::new();
1608        assert_eq!(
1609            unique(&mut taken, "impl.Vec.method.a".to_string()),
1610            "impl.Vec.method.a"
1611        );
1612        assert_eq!(
1613            unique(&mut taken, "impl.Vec.method.a".to_string()),
1614            "impl.Vec.method.a-2"
1615        );
1616        assert_eq!(
1617            unique(&mut taken, "impl.Vec.method.a".to_string()),
1618            "impl.Vec.method.a-3"
1619        );
1620    }
1621
1622    #[test]
1623    fn every_builtin_a_module_sees_has_docs() {
1624        for b in Builtin::ALL
1625            .iter()
1626            .filter(|b| !Builtin::STD_ONLY.contains(*b))
1627        {
1628            assert!(BUILTINS.iter().any(|(n, _)| *n == b.name()), "{}", b.name());
1629        }
1630    }
1631}