]> git.proxmox.com Git - rustc.git/blame - src/tools/lint-docs/src/lib.rs
New upstream version 1.55.0+dfsg1
[rustc.git] / src / tools / lint-docs / src / lib.rs
CommitLineData
1b1a35ee
XL
1use std::error::Error;
2use std::fmt::Write;
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6use walkdir::WalkDir;
7
8mod groups;
9
fc512014
XL
10pub struct LintExtractor<'a> {
11 /// Path to the `src` directory, where it will scan for `.rs` files to
12 /// find lint declarations.
13 pub src_path: &'a Path,
14 /// Path where to save the output.
15 pub out_path: &'a Path,
16 /// Path to the `rustc` executable.
17 pub rustc_path: &'a Path,
18 /// The target arch to build the docs for.
19 pub rustc_target: &'a str,
20 /// Verbose output.
21 pub verbose: bool,
22 /// Validate the style and the code example.
23 pub validate: bool,
24}
25
1b1a35ee
XL
26struct Lint {
27 name: String,
28 doc: Vec<String>,
29 level: Level,
30 path: PathBuf,
31 lineno: usize,
32}
33
34impl Lint {
35 fn doc_contains(&self, text: &str) -> bool {
36 self.doc.iter().any(|line| line.contains(text))
37 }
38
39 fn is_ignored(&self) -> bool {
40 self.doc
41 .iter()
42 .filter(|line| line.starts_with("```rust"))
43 .all(|line| line.contains(",ignore"))
44 }
fc512014
XL
45
46 /// Checks the doc style of the lint.
47 fn check_style(&self) -> Result<(), Box<dyn Error>> {
48 for &expected in &["### Example", "### Explanation", "{{produces}}"] {
49 if expected == "{{produces}}" && self.is_ignored() {
50 continue;
51 }
52 if !self.doc_contains(expected) {
53 return Err(format!("lint docs should contain the line `{}`", expected).into());
54 }
55 }
56 if let Some(first) = self.doc.first() {
57 if !first.starts_with(&format!("The `{}` lint", self.name)) {
58 return Err(format!(
59 "lint docs should start with the text \"The `{}` lint\" to introduce the lint",
60 self.name
61 )
62 .into());
63 }
64 }
65 Ok(())
66 }
1b1a35ee
XL
67}
68
69#[derive(Clone, Copy, PartialEq)]
70enum Level {
71 Allow,
72 Warn,
73 Deny,
74}
75
76impl Level {
77 fn doc_filename(&self) -> &str {
78 match self {
79 Level::Allow => "allowed-by-default.md",
80 Level::Warn => "warn-by-default.md",
81 Level::Deny => "deny-by-default.md",
82 }
83 }
84}
85
fc512014
XL
86impl<'a> LintExtractor<'a> {
87 /// Collects all lints, and writes the markdown documentation at the given directory.
88 pub fn extract_lint_docs(&self) -> Result<(), Box<dyn Error>> {
89 let mut lints = self.gather_lints()?;
90 for lint in &mut lints {
91 self.generate_output_example(lint).map_err(|e| {
92 format!(
93 "failed to test example in lint docs for `{}` in {}:{}: {}",
94 lint.name,
95 lint.path.display(),
96 lint.lineno,
97 e
98 )
99 })?;
100 }
101 self.save_lints_markdown(&lints)?;
102 self.generate_group_docs(&lints)?;
103 Ok(())
1b1a35ee 104 }
1b1a35ee 105
fc512014
XL
106 /// Collects all lints from all files in the given directory.
107 fn gather_lints(&self) -> Result<Vec<Lint>, Box<dyn Error>> {
108 let mut lints = Vec::new();
109 for entry in WalkDir::new(self.src_path).into_iter().filter_map(|e| e.ok()) {
110 if !entry.path().extension().map_or(false, |ext| ext == "rs") {
111 continue;
112 }
113 lints.extend(self.lints_from_file(entry.path())?);
1b1a35ee 114 }
fc512014
XL
115 if lints.is_empty() {
116 return Err("no lints were found!".into());
117 }
118 Ok(lints)
1b1a35ee 119 }
1b1a35ee 120
fc512014
XL
121 /// Collects all lints from the given file.
122 fn lints_from_file(&self, path: &Path) -> Result<Vec<Lint>, Box<dyn Error>> {
123 let mut lints = Vec::new();
124 let contents = fs::read_to_string(path)
125 .map_err(|e| format!("could not read {}: {}", path.display(), e))?;
126 let mut lines = contents.lines().enumerate();
127 'outer: loop {
128 // Find a lint declaration.
129 let lint_start = loop {
130 match lines.next() {
131 Some((lineno, line)) => {
132 if line.trim().starts_with("declare_lint!") {
133 break lineno + 1;
134 }
135 }
136 None => return Ok(lints),
137 }
138 };
139 // Read the lint.
140 let mut doc_lines = Vec::new();
141 let (doc, name) = loop {
142 match lines.next() {
143 Some((lineno, line)) => {
144 let line = line.trim();
145 if let Some(text) = line.strip_prefix("/// ") {
146 doc_lines.push(text.to_string());
147 } else if line == "///" {
148 doc_lines.push("".to_string());
149 } else if line.starts_with("// ") {
150 // Ignore comments.
151 continue;
152 } else {
153 let name = lint_name(line).map_err(|e| {
154 format!(
155 "could not determine lint name in {}:{}: {}, line was `{}`",
156 path.display(),
157 lineno,
158 e,
159 line
160 )
161 })?;
162 if doc_lines.is_empty() {
163 if self.validate {
164 return Err(format!(
165 "did not find doc lines for lint `{}` in {}",
166 name,
167 path.display()
168 )
169 .into());
170 } else {
171 eprintln!(
172 "warning: lint `{}` in {} does not define any doc lines, \
173 these are required for the lint documentation",
174 name,
175 path.display()
176 );
177 continue 'outer;
178 }
179 }
180 break (doc_lines, name);
181 }
182 }
183 None => {
184 return Err(format!(
185 "unexpected EOF for lint definition at {}:{}",
186 path.display(),
187 lint_start
188 )
189 .into());
1b1a35ee
XL
190 }
191 }
fc512014
XL
192 };
193 // These lints are specifically undocumented. This should be reserved
194 // for internal rustc-lints only.
195 if name == "deprecated_in_future" {
196 continue;
1b1a35ee 197 }
fc512014
XL
198 // Read the level.
199 let level = loop {
200 match lines.next() {
201 // Ignore comments.
202 Some((_, line)) if line.trim().starts_with("// ") => {}
203 Some((lineno, line)) => match line.trim() {
204 "Allow," => break Level::Allow,
205 "Warn," => break Level::Warn,
206 "Deny," => break Level::Deny,
207 _ => {
1b1a35ee 208 return Err(format!(
fc512014
XL
209 "unexpected lint level `{}` in {}:{}",
210 line,
211 path.display(),
212 lineno
1b1a35ee
XL
213 )
214 .into());
215 }
fc512014
XL
216 },
217 None => {
1b1a35ee 218 return Err(format!(
fc512014 219 "expected lint level in {}:{}, got EOF",
1b1a35ee 220 path.display(),
fc512014 221 lint_start
1b1a35ee
XL
222 )
223 .into());
224 }
1b1a35ee 225 }
fc512014
XL
226 };
227 // The rest of the lint definition is ignored.
228 assert!(!doc.is_empty());
229 lints.push(Lint { name, doc, level, path: PathBuf::from(path), lineno: lint_start });
1b1a35ee 230 }
1b1a35ee 231 }
1b1a35ee 232
fc512014
XL
233 /// Mutates the lint definition to replace the `{{produces}}` marker with the
234 /// actual output from the compiler.
235 fn generate_output_example(&self, lint: &mut Lint) -> Result<(), Box<dyn Error>> {
236 // Explicit list of lints that are allowed to not have an example. Please
237 // try to avoid adding to this list.
238 if matches!(
239 lint.name.as_str(),
240 "unused_features" // broken lint
241 | "unstable_features" // deprecated
242 ) {
243 return Ok(());
1b1a35ee 244 }
fc512014
XL
245 if lint.doc_contains("[rustdoc book]") && !lint.doc_contains("{{produces}}") {
246 // Rustdoc lints are documented in the rustdoc book, don't check these.
247 return Ok(());
1b1a35ee 248 }
fc512014
XL
249 if self.validate {
250 lint.check_style()?;
251 }
252 // Unfortunately some lints have extra requirements that this simple test
253 // setup can't handle (like extern crates). An alternative is to use a
254 // separate test suite, and use an include mechanism such as mdbook's
255 // `{{#rustdoc_include}}`.
256 if !lint.is_ignored() {
257 if let Err(e) = self.replace_produces(lint) {
258 if self.validate {
259 return Err(e);
260 }
261 eprintln!(
262 "warning: the code example in lint `{}` in {} failed to \
263 generate the expected output: {}",
264 lint.name,
265 lint.path.display(),
266 e
267 );
268 }
1b1a35ee 269 }
fc512014 270 Ok(())
1b1a35ee 271 }
1b1a35ee 272
fc512014
XL
273 /// Mutates the lint docs to replace the `{{produces}}` marker with the actual
274 /// output from the compiler.
275 fn replace_produces(&self, lint: &mut Lint) -> Result<(), Box<dyn Error>> {
276 let mut lines = lint.doc.iter_mut();
277 loop {
278 // Find start of example.
279 let options = loop {
280 match lines.next() {
281 Some(line) if line.starts_with("```rust") => {
282 break line[7..].split(',').collect::<Vec<_>>();
283 }
284 Some(line) if line.contains("{{produces}}") => {
285 return Err("lint marker {{{{produces}}}} found, \
286 but expected to immediately follow a rust code block"
287 .into());
288 }
289 Some(_) => {}
290 None => return Ok(()),
1b1a35ee 291 }
fc512014
XL
292 };
293 // Find the end of example.
294 let mut example = Vec::new();
295 loop {
296 match lines.next() {
297 Some(line) if line == "```" => break,
298 Some(line) => example.push(line),
299 None => {
300 return Err(format!(
301 "did not find end of example triple ticks ```, docs were:\n{:?}",
302 lint.doc
303 )
1b1a35ee 304 .into());
fc512014 305 }
1b1a35ee
XL
306 }
307 }
fc512014
XL
308 // Find the {{produces}} line.
309 loop {
310 match lines.next() {
311 Some(line) if line.is_empty() => {}
312 Some(line) if line == "{{produces}}" => {
313 let output = self.generate_lint_output(&lint.name, &example, &options)?;
314 line.replace_range(
315 ..,
316 &format!(
317 "This will produce:\n\
318 \n\
319 ```text\n\
320 {}\
321 ```",
322 output
323 ),
324 );
325 break;
326 }
327 // No {{produces}} after example, find next example.
328 Some(_line) => break,
329 None => return Ok(()),
1b1a35ee 330 }
1b1a35ee
XL
331 }
332 }
333 }
1b1a35ee 334
fc512014
XL
335 /// Runs the compiler against the example, and extracts the output.
336 fn generate_lint_output(
337 &self,
338 name: &str,
339 example: &[&mut String],
340 options: &[&str],
341 ) -> Result<String, Box<dyn Error>> {
342 if self.verbose {
343 eprintln!("compiling lint {}", name);
344 }
345 let tempdir = tempfile::TempDir::new()?;
346 let tempfile = tempdir.path().join("lint_example.rs");
347 let mut source = String::new();
348 let needs_main = !example.iter().any(|line| line.contains("fn main"));
349 // Remove `# ` prefix for hidden lines.
350 let unhidden = example.iter().map(|line| line.strip_prefix("# ").unwrap_or(line));
351 let mut lines = unhidden.peekable();
352 while let Some(line) = lines.peek() {
353 if line.starts_with("#!") {
354 source.push_str(line);
355 source.push('\n');
356 lines.next();
357 } else {
358 break;
359 }
360 }
361 if needs_main {
362 source.push_str("fn main() {\n");
363 }
364 for line in lines {
1b1a35ee 365 source.push_str(line);
fc512014
XL
366 source.push('\n')
367 }
368 if needs_main {
369 source.push_str("}\n");
370 }
371 fs::write(&tempfile, source)
372 .map_err(|e| format!("failed to write {}: {}", tempfile.display(), e))?;
373 let mut cmd = Command::new(self.rustc_path);
374 if options.contains(&"edition2015") {
375 cmd.arg("--edition=2015");
1b1a35ee 376 } else {
fc512014
XL
377 cmd.arg("--edition=2018");
378 }
379 cmd.arg("--error-format=json");
380 cmd.arg("--target").arg(self.rustc_target);
381 if options.contains(&"test") {
382 cmd.arg("--test");
383 }
384 cmd.arg("lint_example.rs");
385 cmd.current_dir(tempdir.path());
386 let output = cmd.output().map_err(|e| format!("failed to run command {:?}\n{}", cmd, e))?;
387 let stderr = std::str::from_utf8(&output.stderr).unwrap();
388 let msgs = stderr
389 .lines()
390 .filter(|line| line.starts_with('{'))
391 .map(serde_json::from_str)
392 .collect::<Result<Vec<serde_json::Value>, _>>()?;
393 match msgs
394 .iter()
395 .find(|msg| matches!(&msg["code"]["code"], serde_json::Value::String(s) if s==name))
396 {
397 Some(msg) => {
398 let rendered = msg["rendered"].as_str().expect("rendered field should exist");
399 Ok(rendered.to_string())
400 }
401 None => {
402 match msgs.iter().find(
403 |msg| matches!(&msg["rendered"], serde_json::Value::String(s) if s.contains(name)),
404 ) {
405 Some(msg) => {
406 let rendered = msg["rendered"].as_str().expect("rendered field should exist");
407 Ok(rendered.to_string())
408 }
409 None => {
410 let rendered: Vec<&str> =
411 msgs.iter().filter_map(|msg| msg["rendered"].as_str()).collect();
412 let non_json: Vec<&str> =
413 stderr.lines().filter(|line| !line.starts_with('{')).collect();
414 Err(format!(
415 "did not find lint `{}` in output of example, got:\n{}\n{}",
416 name,
417 non_json.join("\n"),
418 rendered.join("\n")
419 )
420 .into())
421 }
422 }
423 }
1b1a35ee
XL
424 }
425 }
fc512014
XL
426
427 /// Saves the mdbook lint chapters at the given path.
428 fn save_lints_markdown(&self, lints: &[Lint]) -> Result<(), Box<dyn Error>> {
429 self.save_level(lints, Level::Allow, ALLOWED_MD)?;
430 self.save_level(lints, Level::Warn, WARN_MD)?;
431 self.save_level(lints, Level::Deny, DENY_MD)?;
432 Ok(())
1b1a35ee 433 }
fc512014
XL
434
435 fn save_level(&self, lints: &[Lint], level: Level, header: &str) -> Result<(), Box<dyn Error>> {
436 let mut result = String::new();
437 result.push_str(header);
438 let mut these_lints: Vec<_> = lints.iter().filter(|lint| lint.level == level).collect();
439 these_lints.sort_unstable_by_key(|lint| &lint.name);
440 for lint in &these_lints {
441 write!(result, "* [`{}`](#{})\n", lint.name, lint.name.replace("_", "-")).unwrap();
1b1a35ee 442 }
fc512014
XL
443 result.push('\n');
444 for lint in &these_lints {
445 write!(result, "## {}\n\n", lint.name.replace("_", "-")).unwrap();
446 for line in &lint.doc {
447 result.push_str(line);
448 result.push('\n');
449 }
450 result.push('\n');
451 }
452 let out_path = self.out_path.join("listing").join(level.doc_filename());
453 // Delete the output because rustbuild uses hard links in its copies.
454 let _ = fs::remove_file(&out_path);
455 fs::write(&out_path, result)
456 .map_err(|e| format!("could not write to {}: {}", out_path.display(), e))?;
457 Ok(())
458 }
459}
460
461/// Extracts the lint name (removing the visibility modifier, and checking validity).
462fn lint_name(line: &str) -> Result<String, &'static str> {
463 // Skip over any potential `pub` visibility.
464 match line.trim().split(' ').next_back() {
465 Some(name) => {
466 if !name.ends_with(',') {
467 return Err("lint name should end with comma");
1b1a35ee 468 }
fc512014 469 let name = &name[..name.len() - 1];
136023e0
XL
470 if !name.chars().all(|ch| ch.is_uppercase() || ch.is_ascii_digit() || ch == '_')
471 || name.is_empty()
472 {
fc512014
XL
473 return Err("lint name did not have expected format");
474 }
475 Ok(name.to_lowercase().to_string())
1b1a35ee 476 }
fc512014 477 None => Err("could not find lint name"),
1b1a35ee
XL
478 }
479}
480
481static ALLOWED_MD: &str = r#"# Allowed-by-default lints
482
483These lints are all set to the 'allow' level by default. As such, they won't show up
484unless you set them to a higher lint level with a flag or attribute.
485
486"#;
487
488static WARN_MD: &str = r#"# Warn-by-default lints
489
490These lints are all set to the 'warn' level by default.
491
492"#;
493
494static DENY_MD: &str = r#"# Deny-by-default lints
495
496These lints are all set to the 'deny' level by default.
497
498"#;