Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new tidy check to ensure that rustdoc DOM IDs are all declared as expected #86178

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 59 additions & 44 deletions src/librustdoc/html/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1436,50 +1436,65 @@ pub struct IdMap {
}

fn init_id_map() -> FxHashMap<String, usize> {
let mut map = FxHashMap::default();
// This is the list of IDs used in Javascript.
map.insert("help".to_owned(), 1);
// This is the list of IDs used in HTML generated in Rust (including the ones
// used in tera template files).
map.insert("mainThemeStyle".to_owned(), 1);
map.insert("themeStyle".to_owned(), 1);
map.insert("theme-picker".to_owned(), 1);
map.insert("theme-choices".to_owned(), 1);
map.insert("settings-menu".to_owned(), 1);
map.insert("help-button".to_owned(), 1);
map.insert("main".to_owned(), 1);
map.insert("search".to_owned(), 1);
map.insert("crate-search".to_owned(), 1);
map.insert("render-detail".to_owned(), 1);
map.insert("toggle-all-docs".to_owned(), 1);
map.insert("all-types".to_owned(), 1);
map.insert("default-settings".to_owned(), 1);
map.insert("rustdoc-vars".to_owned(), 1);
map.insert("sidebar-vars".to_owned(), 1);
map.insert("copy-path".to_owned(), 1);
map.insert("TOC".to_owned(), 1);
// This is the list of IDs used by rustdoc sections (but still generated by
// rustdoc).
map.insert("fields".to_owned(), 1);
map.insert("variants".to_owned(), 1);
map.insert("implementors-list".to_owned(), 1);
map.insert("synthetic-implementors-list".to_owned(), 1);
map.insert("foreign-impls".to_owned(), 1);
map.insert("implementations".to_owned(), 1);
map.insert("trait-implementations".to_owned(), 1);
map.insert("synthetic-implementations".to_owned(), 1);
map.insert("blanket-implementations".to_owned(), 1);
map.insert("associated-types".to_owned(), 1);
map.insert("associated-const".to_owned(), 1);
map.insert("required-methods".to_owned(), 1);
map.insert("provided-methods".to_owned(), 1);
map.insert("implementors".to_owned(), 1);
map.insert("synthetic-implementors".to_owned(), 1);
map.insert("trait-implementations-list".to_owned(), 1);
map.insert("synthetic-implementations-list".to_owned(), 1);
map.insert("blanket-implementations-list".to_owned(), 1);
map.insert("deref-methods".to_owned(), 1);
map
// We declare the ID map this way to make it simpler for the HTML IDs tidy check to extract
// the IDs.
macro_rules! html_id_map {
($($id:literal,)+) => {{
let mut map = FxHashMap::default();

$(
map.insert($id.to_owned(), 1);
)+
map
}}
}

// IMPORTANT: Do NOT change the formatting or name of this macro
// without updating the tidy check.
html_id_map!(
GuillaumeGomez marked this conversation as resolved.
Show resolved Hide resolved
// This is the list of IDs used in Javascript.
"help",
// This is the list of IDs used in HTML generated in Rust (including the ones
// used in tera template files).
"mainThemeStyle",
"themeStyle",
"theme-picker",
"theme-choices",
"settings-menu",
"help-button",
"main",
"search",
"crate-search",
"render-detail",
"toggle-all-docs",
"all-types",
"default-settings",
"rustdoc-vars",
"sidebar-vars",
"copy-path",
"TOC",
// This is the list of IDs used by rustdoc sections (but still generated by
// rustdoc).
"fields",
"variants",
"implementors-list",
"synthetic-implementors-list",
"foreign-impls",
"implementations",
"trait-implementations",
"synthetic-implementations",
"blanket-implementations",
"associated-types",
"associated-const",
"required-methods",
"provided-methods",
"implementors",
"synthetic-implementors",
"trait-implementations-list",
"synthetic-implementations-list",
"blanket-implementations-list",
"deref-methods",
)
}

impl IdMap {
Expand Down
2 changes: 2 additions & 0 deletions src/librustdoc/html/render/print_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,8 @@ fn item_trait(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean::Tra
// Trait documentation
document(w, cx, it, None, HeadingOffset::H2);

// This function is checked in tidy for rustdoc IDs. If you rename/update it, don't forget
// to update the `src/tools/tidy/rustdoc_html_ids.rs` file.
fn write_small_section_header(w: &mut Buffer, id: &str, title: &str, extra_content: &str) {
write!(
w,
Expand Down
1 change: 1 addition & 0 deletions src/tools/tidy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub mod extdeps;
pub mod features;
pub mod pal;
pub mod primitive_docs;
pub mod rustdoc_html_ids;
pub mod style;
pub mod target_specific_tests;
pub mod ui_tests;
Expand Down
3 changes: 3 additions & 0 deletions src/tools/tidy/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ fn main() {
check!(errors, &compiler_path);
check!(error_codes_check, &[&src_path, &compiler_path]);

// Checks for rustdoc.
check!(rustdoc_html_ids, &src_path);
GuillaumeGomez marked this conversation as resolved.
Show resolved Hide resolved

// Checks that only make sense for the std libs.
check!(pal, &library_path);
check!(primitive_docs, &library_path);
Expand Down
173 changes: 173 additions & 0 deletions src/tools/tidy/src/rustdoc_html_ids.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//! Checks that the rustdoc ID map is up-to-date. The goal here is to check a few things:
//!
//! * All IDs created by rustdoc (through JS or `.html` files generation) are declared in the
//! ID map.
//! * There are no unused IDs.

use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;

use regex::Regex;

const ID_MAP_PATH: &str = "librustdoc/html/markdown.rs";
GuillaumeGomez marked this conversation as resolved.
Show resolved Hide resolved
const IDS_USED_IN_JS: &[&str] = &[
// This one is created in the JS and therefore cannot be found in rust files.
"help",
// This one is used when we need to use a "default" ID.
"deref-methods",
];

fn extract_ids(path: &Path, bad: &mut bool) -> HashMap<String, usize> {
let file = File::open(path).expect("failed to open file to extract rustdoc IDs");
let buf_reader = BufReader::new(file);
let mut iter = buf_reader.lines();
let mut ids = HashMap::new();

while let Some(Ok(line)) = iter.next() {
if line.trim_start().starts_with("html_id_map!(") {
break;
}
}
// We're now in the function body, time to retrieve the IDs!
while let Some(line) = iter.next() {
let line = line.unwrap();
let line = line.trim_start();
if line.starts_with("// ") {
// It's a comment, ignoring this line...
continue;
} else if line.starts_with(")") {
// We reached the end of the IDs declaration list.
break;
}
GuillaumeGomez marked this conversation as resolved.
Show resolved Hide resolved
let id = line.split('"').skip(1).next().unwrap();
if ids.insert(id.to_owned(), 0).is_some() {
eprintln!(
"=> ID `{}` is defined more than once in the ID map in file `{}`",
id,
path.display(),
);
*bad = true;
}
}
if ids.is_empty() {
eprintln!("=> No IDs were found in rustdoc in file `{}`...", path.display());
*bad = true;
}
ids
}

fn check_id(
path: &Path,
id: &str,
ids: &mut HashMap<String, usize>,
line_nb: usize,
bad: &mut bool,
) {
if id.contains('{') {
// This is a formatted ID, no need to check it!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Is it that there's no need to check it, or we're incapable of checking it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incapable.

return;
}
let id = id.to_owned();
GuillaumeGomez marked this conversation as resolved.
Show resolved Hide resolved
match ids.get_mut(&id) {
Some(nb) => *nb += 1,
None => {
eprintln!(
"=> ID `{}` in file `{}` at line {} is missing from `init_id_map`",
id,
path.display(),
line_nb + 1,
);
*bad = true;
}
}
}

fn check_ids(
path: &Path,
f: &str,
ids: &mut HashMap<String, usize>,
regex: &Regex,
bad: &mut bool,
small_section_header_checked: &mut usize,
) {
let mut is_checking_small_section_header = None;

for (line_nb, line) in f.lines().enumerate() {
let trimmed = line.trim_start();
// We're not interested in comments or doc comments.
if trimmed.starts_with("//") {
continue;
} else if let Some(start_line) = is_checking_small_section_header {
if line_nb == start_line + 2 {
check_id(path, trimmed.split('"').skip(1).next().unwrap(), ids, line_nb, bad);
is_checking_small_section_header = None;
}
} else if trimmed.contains("write_small_section_header(")
&& !trimmed.contains("fn write_small_section_header(")
{
// First we extract the arguments.
let trimmed = trimmed.split("write_small_section_header(").skip(1).next().unwrap_or("");
// This function is used to create section: the second argument of the function is an
// ID and we need to check it as well, hence this specific check...
if trimmed.contains(',') {
// This is a call made on one line, so we can simply check it!
check_id(path, trimmed.split('"').skip(1).next().unwrap(), ids, line_nb, bad);
} else {
is_checking_small_section_header = Some(line_nb);
}
*small_section_header_checked += 1;
continue;
}
for cap in regex.captures_iter(line) {
check_id(path, &cap[1], ids, line_nb, bad);
}
}
}

pub fn check(path: &Path, bad: &mut bool) {
// matches ` id="blabla"`
let regex = Regex::new(r#"[\s"]id=\\?["']([^\s\\]+)\\?["'][\s\\>"{]"#).unwrap();

println!("Checking rustdoc IDs...");
let mut ids = extract_ids(&path.join(ID_MAP_PATH), bad);
let mut small_section_header_checked = 0;
if *bad {
return;
}
super::walk(
&path.join("librustdoc/html"),
GuillaumeGomez marked this conversation as resolved.
Show resolved Hide resolved
&mut |path| super::filter_dirs(path),
&mut |entry, contents| {
let path = entry.path();
let file_name = entry.file_name();
if path.extension() == Some(OsStr::new("html"))
|| (path.extension() == Some(OsStr::new("rs")) && file_name != "tests.rs")
{
check_ids(path, contents, &mut ids, &regex, bad, &mut small_section_header_checked);
}
},
);
if small_section_header_checked == 0 {
eprintln!(
"=> No call to the `write_small_section_header` function was found. Was it renamed?",
);
*bad = true;
}
for (id, nb) in ids {
if IDS_USED_IN_JS.contains(&id.as_str()) {
if nb != 0 {
eprintln!("=> ID `{}` is not supposed to be used in Rust code but in the JS!", id);
*bad = true;
}
} else if nb == 0 {
eprintln!(
"=> ID `{}` is unused, it should be removed from `init_id_map` in file `{}`",
id, ID_MAP_PATH
);
*bad = true;
}
}
}