diff --git a/mdbook-spec/src/lib.rs b/mdbook-spec/src/lib.rs index 37f65508b..e52ca400e 100644 --- a/mdbook-spec/src/lib.rs +++ b/mdbook-spec/src/lib.rs @@ -9,6 +9,7 @@ use mdbook::BookItem; use once_cell::sync::Lazy; use regex::{Captures, Regex}; use semver::{Version, VersionReq}; +use std::fmt; use std::io; use std::ops::Range; use std::path::PathBuf; @@ -46,15 +47,58 @@ pub fn handle_preprocessing() -> Result<(), Error> { Ok(()) } -pub struct Spec { +/// Handler for errors and warnings. +pub struct Diagnostics { /// Whether or not warnings should be errors (set by SPEC_DENY_WARNINGS /// environment variable). deny_warnings: bool, + /// Number of messages generated. + count: u32, +} + +impl Diagnostics { + fn new() -> Diagnostics { + let deny_warnings = std::env::var("SPEC_DENY_WARNINGS").as_deref() == Ok("1"); + Diagnostics { + deny_warnings, + count: 0, + } + } + + /// Displays a warning or error (depending on whether warnings are denied). + /// + /// Usually you want the [`warn_or_err!`] macro. + fn warn_or_err(&mut self, args: fmt::Arguments<'_>) { + if self.deny_warnings { + eprintln!("error: {args}"); + } else { + eprintln!("warning: {args}"); + } + self.count += 1; + } +} + +/// Displays a warning or error (depending on whether warnings are denied). +#[macro_export] +macro_rules! warn_or_err { + ($diag:expr, $($arg:tt)*) => { + $diag.warn_or_err(format_args!($($arg)*)); + }; +} + +/// Displays a message for an internal error, and immediately exits. +#[macro_export] +macro_rules! bug { + ($($arg:tt)*) => { + eprintln!("mdbook-spec internal error: {}", format_args!($($arg)*)); + std::process::exit(1); + }; +} + +pub struct Spec { /// Path to the rust-lang/rust git repository (set by SPEC_RUST_ROOT /// environment variable). rust_root: Option, - /// The git ref that can be used in a URL to the rust-lang/rust repository. - git_ref: String, } impl Spec { @@ -64,30 +108,10 @@ impl Spec { /// the rust git checkout. If `None`, it will use the `SPEC_RUST_ROOT` /// environment variable. If the root is not specified, then no tests will /// be linked unless `SPEC_DENY_WARNINGS` is set in which case this will - /// return an error.. + /// return an error. pub fn new(rust_root: Option) -> Result { - let deny_warnings = std::env::var("SPEC_DENY_WARNINGS").as_deref() == Ok("1"); let rust_root = rust_root.or_else(|| std::env::var_os("SPEC_RUST_ROOT").map(PathBuf::from)); - if deny_warnings && rust_root.is_none() { - bail!("SPEC_RUST_ROOT environment variable must be set"); - } - let git_ref = match git_ref(&rust_root) { - Ok(s) => s, - Err(e) => { - if deny_warnings { - eprintln!("error: {e:?}"); - std::process::exit(1); - } else { - eprintln!("warning: {e:?}"); - "master".into() - } - } - }; - Ok(Spec { - deny_warnings, - rust_root, - git_ref, - }) + Ok(Spec { rust_root }) } /// Generates link references to all rules on all pages, so you can easily @@ -180,9 +204,20 @@ impl Preprocessor for Spec { } fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result { - let rules = self.collect_rules(&book); + let mut diag = Diagnostics::new(); + if diag.deny_warnings && self.rust_root.is_none() { + bail!("error: SPEC_RUST_ROOT environment variable must be set"); + } + let rules = self.collect_rules(&book, &mut diag); let tests = self.collect_tests(&rules); let summary_table = test_links::make_summary_table(&book, &tests, &rules); + let git_ref = match git_ref(&self.rust_root) { + Ok(s) => s, + Err(e) => { + warn_or_err!(&mut diag, "{e:?}"); + "master".into() + } + }; book.for_each_mut(|item| { let BookItem::Chapter(ch) = item else { @@ -193,7 +228,7 @@ impl Preprocessor for Spec { } ch.content = self.admonitions(&ch); ch.content = self.auto_link_references(&ch, &rules); - ch.content = self.render_rule_definitions(&ch.content, &tests); + ch.content = self.render_rule_definitions(&ch.content, &tests, &git_ref); if ch.name == "Test summary" { ch.content = ch.content.replace("{{summary-table}}", &summary_table); } @@ -201,7 +236,15 @@ impl Preprocessor for Spec { // Final pass will resolve everything as a std link (or error if the // link is unknown). - std_links::std_links(&mut book); + std_links::std_links(&mut book, &mut diag); + + if diag.count > 0 { + if diag.deny_warnings { + eprintln!("mdbook-spec exiting due to {} errors", diag.count); + std::process::exit(1); + } + eprintln!("mdbook-spec generated {} warnings", diag.count); + } Ok(book) } diff --git a/mdbook-spec/src/rules.rs b/mdbook-spec/src/rules.rs index 1149826ae..dcab26d1b 100644 --- a/mdbook-spec/src/rules.rs +++ b/mdbook-spec/src/rules.rs @@ -1,7 +1,7 @@ //! Handling for rule identifiers. use crate::test_links::RuleToTests; -use crate::Spec; +use crate::{warn_or_err, Diagnostics, Spec}; use mdbook::book::Book; use mdbook::BookItem; use once_cell::sync::Lazy; @@ -34,7 +34,7 @@ pub struct Rules { impl Spec { /// Collects all rule definitions in the book. - pub fn collect_rules(&self, book: &Book) -> Rules { + pub fn collect_rules(&self, book: &Book, diag: &mut Diagnostics) -> Rules { let mut rules = Rules::default(); for item in book.iter() { let BookItem::Chapter(ch) = item else { @@ -53,16 +53,12 @@ impl Spec { .def_paths .insert(rule_id.to_string(), (source_path.clone(), path.clone())) { - let message = format!( + warn_or_err!( + diag, "rule `{rule_id}` defined multiple times\n\ First location: {old:?}\n\ Second location: {source_path:?}" ); - if self.deny_warnings { - panic!("error: {message}"); - } else { - eprintln!("warning: {message}"); - } } let mut parts: Vec<_> = rule_id.split('.').collect(); while !parts.is_empty() { @@ -78,7 +74,12 @@ impl Spec { /// Converts lines that start with `r[…]` into a "rule" which has special /// styling and can be linked to. - pub fn render_rule_definitions(&self, content: &str, tests: &RuleToTests) -> String { + pub fn render_rule_definitions( + &self, + content: &str, + tests: &RuleToTests, + git_ref: &str, + ) -> String { RULE_RE .replace_all(content, |caps: &Captures<'_>| { let rule_id = &caps[1]; @@ -96,7 +97,6 @@ impl Spec { test_html, "
  • {test_path}
  • ", test_path = test.path, - git_ref = self.git_ref ) .unwrap(); } diff --git a/mdbook-spec/src/std_links.rs b/mdbook-spec/src/std_links.rs index fc3d8f93e..8a80388e5 100644 --- a/mdbook-spec/src/std_links.rs +++ b/mdbook-spec/src/std_links.rs @@ -1,7 +1,8 @@ //! Support for translating links to the standard library. -use mdbook::book::Book; -use mdbook::book::Chapter; +use crate::{bug, warn_or_err, Diagnostics}; +use anyhow::{bail, Result}; +use mdbook::book::{Book, Chapter}; use mdbook::BookItem; use once_cell::sync::Lazy; use pulldown_cmark::{BrokenLink, CowStr, Event, LinkType, Options, Parser, Tag}; @@ -9,10 +10,9 @@ use regex::Regex; use std::collections::HashMap; use std::fmt::Write as _; use std::fs; -use std::io::{self, Write as _}; use std::ops::Range; use std::path::PathBuf; -use std::process::{self, Command}; +use std::process::Command; use tempfile::TempDir; /// The Regex used to extract the std links from the HTML generated by rustdoc. @@ -31,7 +31,7 @@ static MD_LINK_SHORTCUT: Lazy = Lazy::new(|| Regex::new(r"(?s)(\[.+\])"). /// Converts links to the standard library to the online documentation in a /// fashion similar to rustdoc intra-doc links. -pub fn std_links(book: &mut Book) { +pub fn std_links(book: &mut Book, diag: &mut Diagnostics) { // Collect all links in all chapters. let mut chapter_links = HashMap::new(); for item in book.iter() { @@ -42,15 +42,18 @@ pub fn std_links(book: &mut Book) { continue; } let key = ch.source_path.as_ref().unwrap(); - chapter_links.insert(key, collect_markdown_links(&ch)); + chapter_links.insert(key, collect_markdown_links(&ch, diag)); } // Write a Rust source file to use with rustdoc to generate intra-doc links. let tmp = TempDir::with_prefix("mdbook-spec-").unwrap(); - run_rustdoc(&tmp, &chapter_links); + if let Err(e) = run_rustdoc(&tmp, &chapter_links, diag) { + warn_or_err!(diag, "{e:?}"); + return; + } // Extract the links from the generated html. - let generated = - fs::read_to_string(tmp.path().join("doc/a/index.html")).expect("index.html generated"); + let generated = fs::read_to_string(tmp.path().join("doc/a/index.html")) + .expect("index.html failed to generate"); let mut urls: Vec<_> = STD_LINK_EXTRACT_RE .captures_iter(&generated) .map(|cap| cap.get(1).unwrap().as_str()) @@ -58,12 +61,11 @@ pub fn std_links(book: &mut Book) { let mut urls = &mut urls[..]; let expected_len: usize = chapter_links.values().map(|l| l.len()).sum(); if urls.len() != expected_len { - eprintln!( - "error: expected rustdoc to generate {} links, but found {}", + bug!( + "expected rustdoc to generate {} links, but found {}", expected_len, urls.len(), ); - process::exit(1); } // Unflatten the urls list so that it is split back by chapter. let mut ch_urls: HashMap<&PathBuf, Vec<_>> = HashMap::new(); @@ -84,7 +86,7 @@ pub fn std_links(book: &mut Book) { } let key = ch.source_path.as_ref().unwrap(); // Create a list of replacements to make in the raw markdown to point to the new url. - let replacements = compute_replacements(&ch, &chapter_links[key], &ch_urls[key]); + let replacements = compute_replacements(&ch, &chapter_links[key], &ch_urls[key], diag); let mut new_contents = ch.content.clone(); for (md_link, url, range) in replacements { @@ -133,7 +135,7 @@ struct Link<'a> { } /// Collects all markdown links that look like they might be standard library links. -fn collect_markdown_links(chapter: &Chapter) -> Vec> { +fn collect_markdown_links<'a>(chapter: &'a Chapter, diag: &mut Diagnostics) -> Vec> { let mut opts = Options::empty(); opts.insert(Options::ENABLE_TABLES); opts.insert(Options::ENABLE_FOOTNOTES); @@ -180,13 +182,13 @@ fn collect_markdown_links(chapter: &Chapter) -> Vec> { continue; } if !title.is_empty() { - eprintln!( - "error: titles in links are not supported\n\ + warn_or_err!( + diag, + "titles in links are not supported\n\ Link {dest_url} has title `{title}` found in chapter {} ({:?})", chapter.name, chapter.source_path.as_ref().unwrap() ); - process::exit(1); } links.push(Link { link_type, @@ -208,14 +210,19 @@ fn collect_markdown_links(chapter: &Chapter) -> Vec> { /// generate intra-doc links on them. /// /// The output will be in the given `tmp` directory. -fn run_rustdoc(tmp: &TempDir, chapter_links: &HashMap<&PathBuf, Vec>>) { +fn run_rustdoc( + tmp: &TempDir, + chapter_links: &HashMap<&PathBuf, Vec>>, + diag: &mut Diagnostics, +) -> Result<()> { let src_path = tmp.path().join("a.rs"); // Allow redundant since there could some in-scope things that are // technically not necessary, but we don't care about (like // [`Option`](std::option::Option)). let mut src = format!( - "#![deny(rustdoc::broken_intra_doc_links)]\n\ - #![allow(rustdoc::redundant_explicit_links)]\n" + "#![{}(rustdoc::broken_intra_doc_links)]\n\ + #![allow(rustdoc::redundant_explicit_links)]\n", + if diag.deny_warnings { "deny" } else { "warn" } ); // This uses a list to make easy to pull the links out of the generated HTML. for (_ch_path, links) in chapter_links { @@ -231,10 +238,10 @@ fn run_rustdoc(tmp: &TempDir, chapter_links: &HashMap<&PathBuf, Vec>>) | LinkType::CollapsedUnknown | LinkType::ShortcutUnknown => { // These should only happen due to broken link replacements. - panic!("unexpected link type unknown {link:?}"); + bug!("unexpected link type unknown {link:?}"); } LinkType::Autolink | LinkType::Email => { - panic!("link type should have been filtered {link:?}"); + bug!("link type should have been filtered {link:?}"); } } } @@ -256,10 +263,13 @@ fn run_rustdoc(tmp: &TempDir, chapter_links: &HashMap<&PathBuf, Vec>>) .output() .expect("rustdoc installed"); if !output.status.success() { - eprintln!("error: failed to extract std links ({:?})\n", output.status,); - io::stderr().write_all(&output.stderr).unwrap(); - process::exit(1); + let stderr = String::from_utf8_lossy(&output.stderr); + bail!( + "failed to extract std links ({:?})\n{stderr}", + output.status + ); } + Ok(()) } static DOC_URL: Lazy = Lazy::new(|| { @@ -271,8 +281,7 @@ fn relative_url(url: &str, chapter: &Chapter) -> String { // Set SPEC_RELATIVE=0 to disable this, which can be useful for working locally. if std::env::var("SPEC_RELATIVE").as_deref() != Ok("0") { let Some(url_start) = DOC_URL.shortest_match(url) else { - eprintln!("error: expected rustdoc URL to start with {DOC_URL:?}, got {url}"); - std::process::exit(1); + bug!("expected rustdoc URL to start with {DOC_URL:?}, got {url}"); }; let url_path = &url[url_start..]; let num_dots = chapter.path.as_ref().unwrap().components().count(); @@ -294,21 +303,23 @@ fn compute_replacements<'a>( chapter: &'a Chapter, links: &[Link<'_>], urls: &[&'a str], + diag: &mut Diagnostics, ) -> Vec<(&'a str, &'a str, Range)> { let mut replacements = Vec::new(); for (url, link) in urls.iter().zip(links) { let Some(cap) = ANCHOR_URL.captures(url) else { let line = super::line_from_range(&chapter.content, &link.range); - eprintln!( - "error: broken markdown link found in {}\n\ + warn_or_err!( + diag, + "broken markdown link found in {}\n\ Line is: {line}\n\ Link to `{}` could not be resolved by rustdoc to a known URL (result was `{}`).\n", chapter.source_path.as_ref().unwrap().display(), link.dest_url, url ); - process::exit(1); + continue; }; let url = cap.get(1).unwrap().as_str(); let md_link = &chapter.content[link.range.clone()]; @@ -316,11 +327,10 @@ fn compute_replacements<'a>( let range = link.range.clone(); let add_link = |re: &Regex| { let Some(cap) = re.captures(md_link) else { - eprintln!( - "error: expected link `{md_link}` of type {:?} to match regex {re}", + bug!( + "expected link `{md_link}` of type {:?} to match regex {re}", link.link_type ); - process::exit(1); }; let md_link = cap.get(1).unwrap().as_str(); replacements.push((md_link, url, range)); @@ -337,7 +347,7 @@ fn compute_replacements<'a>( add_link(&MD_LINK_SHORTCUT); } _ => { - panic!("unexpected link type: {link:#?}"); + bug!("unexpected link type: {link:#?}"); } } }