1use 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
23pub struct Documented {
26 pub site: Site,
28 pub db: FileDb,
30 pub warnings: Vec<Diagnostic>,
32}
33
34pub enum Failure {
36 Diagnostics {
38 db: FileDb,
40 diagnostics: Vec<Diagnostic>,
42 },
43 Other(String),
45}
46
47pub struct Site {
49 pub(crate) packages: Vec<Package>,
50 pub(crate) start: String,
52}
53
54pub(crate) struct Package {
56 pub name: String,
57 pub version: String,
58 pub description: Option<String>,
59 pub std: bool,
60 pub modules: Vec<Module>,
62 pub primitives: Vec<Primitive>,
64 pub builtins: Vec<BuiltinDoc>,
66}
67
68pub(crate) struct Module {
70 pub name: String,
71 pub qual: String,
73 pub page: String,
75 pub root: bool,
78 pub docs: Docs,
79 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 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 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
117pub(crate) struct Item {
119 pub kind: ItemKind,
120 pub name: String,
121 pub anchor: String,
122 pub signature: String,
124 pub docs: Docs,
125 pub members: Vec<Member>,
127 pub hidden_fields: bool,
129 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 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 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
163pub(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
172pub(crate) struct Impl {
174 pub header: String,
175 pub docs: Docs,
176 pub methods: Vec<Member>,
177 pub on: String,
179}
180
181pub(crate) struct Primitive {
183 pub name: &'static str,
184 pub page: String,
185 pub about: Docs,
186 pub impls: Vec<Impl>,
187}
188
189pub(crate) struct BuiltinDoc {
191 pub name: &'static str,
192 pub signature: String,
193 pub docs: Docs,
194}
195
196const 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
214const 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
240const STD_ABOUT: &str = "Nova's standard library, which every module sees without an import.";
242
243const STD_ENTRY: &str = "<nova doc>/main.nova";
246
247pub 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
264pub(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
281pub(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
288fn 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
325pub 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
338struct 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
351fn 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
370fn 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 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#[derive(Clone, Copy)]
411enum Ns {
412 Value,
413 Type,
414 Trait,
415}
416
417struct Pending<'a> {
419 module: ModuleId,
420 span: Span,
421 block: &'a ImplBlock,
422}
423
424enum Dest {
426 Item(String, String),
428 Primitive(&'static str),
430 Own,
432}
433
434struct Ctx<'a> {
436 a: &'a Analysis,
437 defs: &'a Definitions,
438 file: &'a nova_ast::File,
439 users: usize,
441 std: bool,
443 program: bool,
446 uses: HashMap<Span, Target>,
448 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 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 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 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 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 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 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 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
659pub(crate) fn relative(page: &str, url: &str) -> String {
661 format!("{}{url}", "../".repeat(page.matches('/').count()))
662}
663
664fn docs_of(lines: &[Spanned<String>], level: u8) -> Docs {
667 markdown::render_source(markdown::join(lines), level)
668}
669
670fn type_name(ty: &Type) -> Option<&Spanned<String>> {
672 match ty {
673 Type::Path { path, .. } => path.segments.last(),
674 _ => None,
675 }
676}
677
678fn 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 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 #[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 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 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 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 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 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 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
1219fn 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#[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) -> ()".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 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 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 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 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 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}