diff --git a/Cargo.toml b/Cargo.toml index 69af69a8..ff73092b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/mdbook-core/src/book.rs b/crates/mdbook-core/src/book.rs index 040612df..11bbb5a6 100644 --- a/crates/mdbook-core/src/book.rs +++ b/crates/mdbook-core/src/book.rs @@ -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, - __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) -> 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 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, diff --git a/crates/mdbook-core/src/config.rs b/crates/mdbook-core/src/config.rs index 686b9465..f4533292 100644 --- a/crates/mdbook-core/src/config.rs +++ b/crates/mdbook-core/src/config.rs @@ -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, @@ -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, } -#[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, @@ -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, @@ -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, diff --git a/crates/mdbook-driver/src/builtin_preprocessors/index.rs b/crates/mdbook-driver/src/builtin_preprocessors/index.rs index 18ac4407..046b20de 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/index.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/index.rs @@ -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 { diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index 127ac3fd..e6466ee9 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -26,6 +26,7 @@ const MAX_LINK_NESTED_DEPTH: usize = 10; /// - `{{# playground}}` - Insert runnable Rust files /// - `{{# title}}` - Override \ of a webpage. #[derive(Default)] +#[non_exhaustive] pub struct LinkPreprocessor; impl LinkPreprocessor { diff --git a/crates/mdbook-driver/src/builtin_renderers/markdown_renderer.rs b/crates/mdbook-driver/src/builtin_renderers/markdown_renderer.rs index 5255bc80..d626cf17 100644 --- a/crates/mdbook-driver/src/builtin_renderers/markdown_renderer.rs +++ b/crates/mdbook-driver/src/builtin_renderers/markdown_renderer.rs @@ -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 { diff --git a/crates/mdbook-driver/src/load.rs b/crates/mdbook-driver/src/load.rs index 77b1a41c..2c970696 100644 --- a/crates/mdbook-driver/src/load.rs +++ b/crates/mdbook-driver/src/load.rs @@ -95,6 +95,7 @@ fn load_summary_item + 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()); diff --git a/crates/mdbook-driver/src/mdbook.rs b/crates/mdbook-driver/src/mdbook.rs index 0655a6f3..38c2bd8d 100644 --- a/crates/mdbook-driver/src/mdbook.rs +++ b/crates/mdbook-driver/src/mdbook.rs @@ -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"), } } diff --git a/crates/mdbook-driver/src/mdbook/tests.rs b/crates/mdbook-driver/src/mdbook/tests.rs index c5caf36f..cdcd4e77 100644 --- a/crates/mdbook-driver/src/mdbook/tests.rs +++ b/crates/mdbook-driver/src/mdbook/tests.rs @@ -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(); diff --git a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs index 3b47235a..b04aca7b 100644 --- a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs +++ b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs @@ -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 { "hidden()\nnothidden():\n hidden()\n hidden()\n nothidden()\n", ), ]; + 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); } } diff --git a/crates/mdbook-html/src/html_handlebars/search.rs b/crates/mdbook-html/src/html_handlebars/search.rs index b14c9c67..25f19d26 100644 --- a/crates/mdbook-html/src/html_handlebars/search.rs +++ b/crates/mdbook-html/src/html_handlebars/search.rs @@ -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 ); } } diff --git a/crates/mdbook-html/src/theme/mod.rs b/crates/mdbook-html/src/theme/mod.rs index 2547d3f9..373ef67e 100644 --- a/crates/mdbook-html/src/theme/mod.rs +++ b/crates/mdbook-html/src/theme/mod.rs @@ -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, pub head: Vec, diff --git a/crates/mdbook-preprocessor/src/lib.rs b/crates/mdbook-preprocessor/src/lib.rs index 898d19a3..c1aa35ea 100644 --- a/crates/mdbook-preprocessor/src/lib.rs +++ b/crates/mdbook-preprocessor/src/lib.rs @@ -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>, - #[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: (), } } } diff --git a/crates/mdbook-renderer/src/lib.rs b/crates/mdbook-renderer/src/lib.rs index 12b0aab4..ba2fe046 100644 --- a/crates/mdbook-renderer/src/lib.rs +++ b/crates/mdbook-renderer/src/lib.rs @@ -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, - #[serde(skip)] - __non_exhaustive: (), } impl RenderContext { @@ -75,7 +74,6 @@ impl RenderContext { root: root.into(), destination: destination.into(), chapter_titles: HashMap::new(), - __non_exhaustive: (), } } diff --git a/crates/mdbook-summary/src/lib.rs b/crates/mdbook-summary/src/lib.rs index f34488b7..24a4bc61 100644 --- a/crates/mdbook-summary/src/lib.rs +++ b/crates/mdbook-summary/src/lib.rs @@ -63,6 +63,7 @@ pub fn parse_summary(summary: &str) -> Result { /// 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, @@ -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), diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs index 91e1f102..3423b481 100644 --- a/examples/nop-preprocessor.rs +++ b/examples/nop-preprocessor.rs @@ -146,8 +146,7 @@ mod nop_lib { "parent_names": [] } } - ], - "__non_exhaustive": null + ] } ]"##; let input_json = input_json.as_bytes(); diff --git a/tests/testsuite/renderer.rs b/tests/testsuite/renderer.rs index 9d74f9ba..43a022d3 100644 --- a/tests/testsuite/renderer.rs +++ b/tests/testsuite/renderer.rs @@ -167,7 +167,6 @@ fn backends_receive_render_context_via_stdin() { str![[r##" { "book": { - "__non_exhaustive": null, "sections": [ { "Chapter": { diff --git a/tests/testsuite/search.rs b/tests/testsuite/search.rs index c3fa0085..0a177276 100644 --- a/tests/testsuite/search.rs +++ b/tests/testsuite/search.rs @@ -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(); }