Switch all public types to non_exhaustive

This switches all public types to use non_exhaustive to make it easier
to make additions without a semver-breaking change.

Some of the ergonomics are hampered due to the lack of exhaustiveness
checking. Hopefully some day in the future,
non_exhaustive_omitted_patterns_lint or something like it will get
stabilized.

Closes https://github.com/rust-lang/mdBook/issues/1835
This commit is contained in:
Eric Huss 2025-08-09 16:38:22 -07:00
parent c25e866796
commit 5956092b4b
18 changed files with 90 additions and 122 deletions

View file

@ -9,6 +9,9 @@ members = [
all = { level = "allow", priority = -2 }
correctness = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
exhaustive_enums = "warn"
exhaustive_structs = "warn"
manual_non_exhaustive = "warn"
[workspace.lints.rust]
missing_docs = "warn"

View file

@ -16,10 +16,10 @@ use std::path::PathBuf;
/// [`iter()`]: #method.iter
/// [`for_each_mut()`]: #method.for_each_mut
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Book {
/// The sections in this book.
pub sections: Vec<BookItem>,
__non_exhaustive: (),
}
impl Book {
@ -30,10 +30,7 @@ impl Book {
/// Creates a new book with the given items.
pub fn new_with_items(items: Vec<BookItem>) -> Book {
Book {
sections: items,
__non_exhaustive: (),
}
Book { sections: items }
}
/// Get a depth-first iterator over the items in the book.
@ -81,6 +78,7 @@ where
/// Enum representing any type of item which can be added to a book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BookItem {
/// A nested chapter.
Chapter(Chapter),
@ -99,6 +97,7 @@ impl From<Chapter> for BookItem {
/// The representation of a "chapter", usually mapping to a single file on
/// disk however it may contain multiple sub-chapters.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Chapter {
/// The chapter's name.
pub name: String,

View file

@ -64,6 +64,7 @@ use toml::value::Table;
/// The overall configuration object for MDBook, essentially an in-memory
/// representation of `book.toml`.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Config {
/// Metadata about the book.
pub book: BookConfig,
@ -386,6 +387,7 @@ fn is_legacy_format(table: &Value) -> bool {
/// loading it from disk.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct BookConfig {
/// The book's title.
pub title: Option<String>,
@ -429,6 +431,7 @@ impl BookConfig {
/// Text direction to use for HTML output
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum TextDirection {
/// Left to right.
#[serde(rename = "ltr")]
@ -454,6 +457,7 @@ impl TextDirection {
/// Configuration for the build procedure.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct BuildConfig {
/// Where to put built artefacts relative to the book's root directory.
pub build_dir: PathBuf,
@ -481,13 +485,15 @@ impl Default for BuildConfig {
/// Configuration for the Rust compiler(e.g., for playground)
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct RustConfig {
/// Rust edition used in playground
pub edition: Option<RustEdition>,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
/// Rust edition to use for the code.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum RustEdition {
/// The 2024 edition of Rust
#[serde(rename = "2024")]
@ -506,6 +512,7 @@ pub enum RustEdition {
/// Configuration for the HTML renderer.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct HtmlConfig {
/// The theme directory, if specified.
pub theme: Option<PathBuf>,
@ -625,6 +632,7 @@ impl HtmlConfig {
/// Configuration for how to render the print icon, print.html, and print.css.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct Print {
/// Whether print support is enabled.
pub enable: bool,
@ -644,6 +652,7 @@ impl Default for Print {
/// Configuration for how to fold chapters of sidebar.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct Fold {
/// When off, all folds are open. Default: `false`.
pub enable: bool,
@ -656,6 +665,7 @@ pub struct Fold {
/// Configuration for tweaking how the HTML renderer handles the playground.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct Playground {
/// Should playground snippets be editable? Default: `false`.
pub editable: bool,
@ -685,6 +695,7 @@ impl Default for Playground {
/// Configuration for tweaking how the HTML renderer handles code blocks.
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct Code {
/// A prefix string to hide lines per language (one or more chars).
pub hidelines: HashMap<String, String>,
@ -693,6 +704,7 @@ pub struct Code {
/// Configuration of the search functionality of the HTML renderer.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct Search {
/// Enable the search feature. Default: `true`.
pub enable: bool,
@ -750,6 +762,7 @@ impl Default for Search {
/// Search options for chapters (or paths).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct SearchChapterSettings {
/// Whether or not indexing is enabled, default `true`.
pub enable: Option<bool>,

View file

@ -8,6 +8,7 @@ use std::{path::Path, sync::LazyLock};
/// A preprocessor for converting file name `README.md` to `index.md` since
/// `README.md` is the de facto index file in markdown-based documentation.
#[derive(Default)]
#[non_exhaustive]
pub struct IndexPreprocessor;
impl IndexPreprocessor {

View file

@ -26,6 +26,7 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
/// - `{{# playground}}` - Insert runnable Rust files
/// - `{{# title}}` - Override \<title\> of a webpage.
#[derive(Default)]
#[non_exhaustive]
pub struct LinkPreprocessor;
impl LinkPreprocessor {

View file

@ -5,9 +5,10 @@ use mdbook_core::utils;
use mdbook_renderer::{RenderContext, Renderer};
use std::fs;
#[derive(Default)]
/// A renderer to output the Markdown after the preprocessors have run. Mostly useful
/// when debugging preprocessors.
#[derive(Default)]
#[non_exhaustive]
pub struct MarkdownRenderer;
impl MarkdownRenderer {

View file

@ -95,6 +95,7 @@ fn load_summary_item<P: AsRef<Path> + Clone>(
SummaryItem::Separator => Ok(BookItem::Separator),
SummaryItem::Link(link) => load_chapter(link, src_dir, parent_names).map(BookItem::Chapter),
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
_ => panic!("SummaryItem {item:?} not covered"),
}
}
@ -252,28 +253,21 @@ And here is some \
fn load_recursive_link_with_separators() {
let (root, temp) = nested_links();
let nested = Chapter {
name: String::from("Nested Chapter 1"),
content: String::from("Hello World!"),
number: Some(SectionNumber::new([1, 2])),
path: Some(PathBuf::from("second.md")),
source_path: Some(PathBuf::from("second.md")),
parent_names: vec![String::from("Chapter 1")],
sub_items: Vec::new(),
};
let should_be = BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
number: None,
path: Some(PathBuf::from("chapter_1.md")),
source_path: Some(PathBuf::from("chapter_1.md")),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(nested.clone()),
BookItem::Separator,
BookItem::Chapter(nested),
],
});
let mut nested = Chapter::new(
"Nested Chapter 1",
String::from("Hello World!"),
"second.md",
vec![String::from("Chapter 1")],
);
nested.number = Some(SectionNumber::new([1, 2]));
let mut chapter =
Chapter::new("Chapter 1", String::from(DUMMY_SRC), "chapter_1.md", vec![]);
chapter.sub_items = vec![
BookItem::Chapter(nested.clone()),
BookItem::Separator,
BookItem::Chapter(nested),
];
let should_be = BookItem::Chapter(chapter);
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
@ -282,17 +276,15 @@ And here is some \
#[test]
fn load_a_book_with_a_single_chapter() {
let (link, temp) = dummy_link();
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(link)],
..Default::default()
};
let sections = vec![BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
path: Some(PathBuf::from("chapter_1.md")),
source_path: Some(PathBuf::from("chapter_1.md")),
..Default::default()
})];
let mut summary = Summary::default();
summary.numbered_chapters = vec![SummaryItem::Link(link)];
let chapter = Chapter::new(
"Chapter 1",
String::from(DUMMY_SRC),
PathBuf::from("chapter_1.md"),
vec![],
);
let sections = vec![BookItem::Chapter(chapter)];
let should_be = Book::new_with_items(sections);
let got = load_book_from_disk(&summary, temp.path()).unwrap();
@ -303,16 +295,9 @@ And here is some \
#[test]
fn cant_load_chapters_with_an_empty_path() {
let (_, temp) = dummy_link();
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("Empty"),
location: Some(PathBuf::from("")),
..Default::default()
})],
..Default::default()
};
let mut summary = Summary::default();
let link = Link::new("Empty", "");
summary.numbered_chapters = vec![SummaryItem::Link(link)];
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err());
}
@ -323,14 +308,9 @@ And here is some \
let dir = temp.path().join("nested");
fs::create_dir(&dir).unwrap();
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("nested"),
location: Some(dir),
..Default::default()
})],
..Default::default()
};
let mut summary = Summary::default();
let link = Link::new("nested", dir);
summary.numbered_chapters = vec![SummaryItem::Link(link)];
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err());

View file

@ -136,6 +136,7 @@ impl MDBook {
/// BookItem::Chapter(ref chapter) => {},
/// BookItem::Separator => {},
/// BookItem::PartTitle(ref title) => {}
/// _ => {}
/// }
/// }
///
@ -329,6 +330,7 @@ impl MDBook {
RustEdition::E2024 => {
cmd.args(["--edition", "2024"]);
}
_ => panic!("RustEdition {edition:?} not covered"),
}
}

View file

@ -230,7 +230,7 @@ fn config_respects_preprocessor_selection() {
let cfg = Config::from_str(cfg_str).unwrap();
let html_renderer = HtmlHandlebars;
let html_renderer = HtmlHandlebars::default();
let pre = LinkPreprocessor::new();
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg).unwrap();

View file

@ -21,6 +21,7 @@ use std::sync::LazyLock;
/// The HTML renderer for mdBook.
#[derive(Default)]
#[non_exhaustive]
pub struct HtmlHandlebars;
impl HtmlHandlebars {
@ -656,6 +657,7 @@ fn make_data(
BookItem::Separator => {
chapter.insert("spacer".to_owned(), json!("_spacer_"));
}
_ => panic!("BookItem {item:?} not covered"),
}
chapters.push(chapter);
@ -778,6 +780,7 @@ fn add_playground_pre(
Some(RustEdition::E2018) => " edition2018",
Some(RustEdition::E2021) => " edition2021",
Some(RustEdition::E2024) => " edition2024",
Some(_) => panic!("edition {edition:?} not covered"),
None => "",
}
};
@ -1085,14 +1088,9 @@ mod tests {
),
];
for (src, should_be) in &inputs {
let got = add_playground_pre(
src,
&Playground {
editable: true,
..Playground::default()
},
None,
);
let mut p = Playground::default();
p.editable = true;
let got = add_playground_pre(src, &p, None);
assert_eq!(&*got, *should_be);
}
}
@ -1117,14 +1115,9 @@ mod tests {
),
];
for (src, should_be) in &inputs {
let got = add_playground_pre(
src,
&Playground {
editable: true,
..Playground::default()
},
Some(RustEdition::E2015),
);
let mut p = Playground::default();
p.editable = true;
let got = add_playground_pre(src, &p, Some(RustEdition::E2015));
assert_eq!(&*got, *should_be);
}
}
@ -1149,14 +1142,9 @@ mod tests {
),
];
for (src, should_be) in &inputs {
let got = add_playground_pre(
src,
&Playground {
editable: true,
..Playground::default()
},
Some(RustEdition::E2018),
);
let mut p = Playground::default();
p.editable = true;
let got = add_playground_pre(src, &p, Some(RustEdition::E2018));
assert_eq!(&*got, *should_be);
}
}
@ -1181,14 +1169,9 @@ mod tests {
),
];
for (src, should_be) in &inputs {
let got = add_playground_pre(
src,
&Playground {
editable: true,
..Playground::default()
},
Some(RustEdition::E2021),
);
let mut p = Playground::default();
p.editable = true;
let got = add_playground_pre(src, &p, Some(RustEdition::E2021));
assert_eq!(&*got, *should_be);
}
}
@ -1248,17 +1231,10 @@ mod tests {
"<code class=\"language-python hidelines=!!!\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\"> hidden()\n</span><span class=\"boring\"> hidden()\n</span> nothidden()\n</code>",
),
];
let mut code = Code::default();
code.hidelines.insert("python".to_string(), "~".to_string());
for (src, should_be) in &inputs {
let got = hide_lines(
src,
&Code {
hidelines: {
let mut map = HashMap::new();
map.insert("python".to_string(), "~".to_string());
map
},
},
);
let got = hide_lines(src, &code);
assert_eq!(&*got, *should_be);
}
}

View file

@ -409,9 +409,11 @@ fn chapter_settings_priority() {
("cli/inner/index.md", Some(true)),
("cli/inner/foo.md", Some(false)),
] {
let mut settings = SearchChapterSettings::default();
settings.enable = enable;
assert_eq!(
get_chapter_settings(&chapter_configs, Path::new(path)),
SearchChapterSettings { enable }
settings
);
}
}

View file

@ -49,6 +49,7 @@ pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("../../front-end/fonts/FontA
/// You should only ever use the static variables directly if you want to
/// override the user's theme with the defaults.
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub struct Theme {
pub index: Vec<u8>,
pub head: Vec<u8>,

View file

@ -47,6 +47,7 @@ pub trait Preprocessor {
/// Extra information for a `Preprocessor` to give them more context when
/// processing a book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PreprocessorContext {
/// The location of the book directory on disk.
pub root: PathBuf,
@ -62,8 +63,6 @@ pub struct PreprocessorContext {
/// This should not be used outside of mdbook's internals.
#[serde(skip)]
pub chapter_titles: RefCell<HashMap<PathBuf, String>>,
#[serde(skip)]
__non_exhaustive: (),
}
impl PreprocessorContext {
@ -75,7 +74,6 @@ impl PreprocessorContext {
renderer,
mdbook_version: crate::MDBOOK_VERSION.to_string(),
chapter_titles: RefCell::new(HashMap::new()),
__non_exhaustive: (),
}
}
}

View file

@ -36,6 +36,7 @@ pub trait Renderer {
/// The context provided to all renderers.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RenderContext {
/// Which version of `mdbook` did this come from (as written in `mdbook`'s
/// `Cargo.toml`). Useful if you know the renderer is only compatible with
@ -57,8 +58,6 @@ pub struct RenderContext {
/// This should not be used outside of mdbook's internals.
#[serde(skip)]
pub chapter_titles: HashMap<PathBuf, String>,
#[serde(skip)]
__non_exhaustive: (),
}
impl RenderContext {
@ -75,7 +74,6 @@ impl RenderContext {
root: root.into(),
destination: destination.into(),
chapter_titles: HashMap::new(),
__non_exhaustive: (),
}
}

View file

@ -63,6 +63,7 @@ pub fn parse_summary(summary: &str) -> Result<Summary> {
/// The parsed `SUMMARY.md`, specifying how the book should be laid out.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Summary {
/// An optional title for the `SUMMARY.md`, currently just ignored.
pub title: Option<String>,
@ -79,6 +80,7 @@ pub struct Summary {
///
/// This is roughly the equivalent of `[Some section](./path/to/file.md)`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Link {
/// The name of the chapter.
pub name: String,
@ -114,8 +116,9 @@ impl Default for Link {
}
}
/// An item in `SUMMARY.md` which could be either a separator or a `Link`.
/// An item in `SUMMARY.md`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SummaryItem {
/// A link to a chapter.
Link(Link),

View file

@ -146,8 +146,7 @@ mod nop_lib {
"parent_names": []
}
}
],
"__non_exhaustive": null
]
}
]"##;
let input_json = input_json.as_bytes();

View file

@ -167,7 +167,6 @@ fn backends_receive_render_context_via_stdin() {
str![[r##"
{
"book": {
"__non_exhaustive": null,
"sections": [
{
"Chapter": {

View file

@ -3,7 +3,7 @@
use crate::prelude::*;
use mdbook_core::book::{BookItem, Chapter};
use snapbox::file;
use std::path::{Path, PathBuf};
use std::path::Path;
fn read_book_index(root: &Path) -> serde_json::Value {
let index = root.join("book/searchindex.js");
@ -116,15 +116,7 @@ fn can_disable_individual_chapters() {
fn with_no_source_path() {
let test = BookTest::from_dir("search/reasonable_search_index");
let mut book = test.load_book();
let chapter = Chapter {
name: "Sample chapter".to_string(),
content: "".to_string(),
number: None,
sub_items: Vec::new(),
path: Some(PathBuf::from("sample.html")),
source_path: None,
parent_names: Vec::new(),
};
let chapter = Chapter::new("Sample chapter", String::new(), "sample.html", vec![]);
book.book.sections.push(BookItem::Chapter(chapter));
book.build().unwrap();
}