tidy/rustdoc_templates.rs
1//! Tidy check to ensure that rustdoc templates didn't forget a `{# #}` to strip extra whitespace
2//! characters.
3
4use std::ffi::OsStr;
5use std::path::Path;
6
7use ignore::DirEntry;
8
9use crate::diagnostics::{CheckId, DiagCtx};
10use crate::walk::walk;
11
12// Array containing `("beginning of tag", "end of tag")`.
13const TAGS: &[(&str, &str)] = &[("{#", "#}"), ("{%", "%}"), ("{{", "}}")];
14
15pub fn check(librustdoc_path: &Path, diag_ctx: DiagCtx) {
16 let mut check = diag_ctx.start_check(CheckId::new("rustdoc_templates").path(librustdoc_path));
17
18 walk(
19 &librustdoc_path.join("html/templates"),
20 |path, is_dir| is_dir || path.extension().is_none_or(|ext| ext != OsStr::new("html")),
21 &mut |path: &DirEntry, file_content: &str| {
22 let mut lines = file_content.lines().enumerate().peekable();
23
24 while let Some((pos, line)) = lines.next() {
25 let line = line.trim();
26 if let Some(need_next_line_check) = TAGS.iter().find_map(|(tag, end_tag)| {
27 // We first check if the line ends with a jinja tag.
28 if !line.ends_with(end_tag) {
29 None
30 // Then we check if this a comment tag.
31 } else if *tag != "{#" {
32 Some(false)
33 // And finally we check if the comment is empty (ie, only there to strip
34 // extra whitespace characters).
35 } else if let Some(start_pos) = line.rfind(tag) {
36 Some(line[start_pos + 2..].trim() == "#}")
37 } else {
38 Some(false)
39 }
40 }) {
41 // All good, the line is ending is a jinja tag. But maybe this tag is useless
42 // if the next line starts with a jinja tag as well!
43 //
44 // However, only (empty) comment jinja tags are concerned about it.
45 if need_next_line_check
46 && lines.peek().is_some_and(|(_, next_line)| {
47 let next_line = next_line.trim_start();
48 TAGS.iter().any(|(tag, _)| next_line.starts_with(tag))
49 })
50 {
51 // It seems like ending this line with a jinja tag is not needed after all.
52 check.error(format!(
53 "`{}` at line {}: unneeded `{{# #}}` tag at the end of the line",
54 path.path().display(),
55 pos + 1,
56 ));
57 }
58 continue;
59 }
60 let Some(next_line) = lines.peek().map(|(_, next_line)| next_line.trim()) else {
61 continue;
62 };
63 if TAGS.iter().any(|(tag, _)| next_line.starts_with(tag)) {
64 continue;
65 }
66 // Maybe this is a multi-line tag, let's filter it out then.
67 match TAGS.iter().find_map(|(tag, end_tag)| {
68 if line.rfind(tag).is_some() { Some(end_tag) } else { None }
69 }) {
70 None => {
71 // No it's not, let's error.
72 check.error(format!(
73 "`{}` at line {}: missing `{{# #}}` at the end of the line",
74 path.path().display(),
75 pos + 1,
76 ));
77 }
78 Some(end_tag) => {
79 // We skip the tag.
80 while let Some((_, next_line)) = lines.peek() {
81 if next_line.contains(end_tag) {
82 break;
83 }
84 lines.next();
85 }
86 }
87 }
88 }
89 },
90 );
91}