From f17e6487b2315d3cc3826fb8badeb7d4959b3ffd Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sat, 28 Nov 2020 11:25:42 -0800 Subject: [PATCH] lint-docs: Move free functions into methods of LintExtractor. This helps avoid needing to pass so many parameters around. --- src/tools/lint-docs/src/groups.rs | 196 ++++---- src/tools/lint-docs/src/lib.rs | 754 +++++++++++++++--------------- src/tools/lint-docs/src/main.rs | 15 +- 3 files changed, 483 insertions(+), 482 deletions(-) diff --git a/src/tools/lint-docs/src/groups.rs b/src/tools/lint-docs/src/groups.rs index 6b32ebdc284f..db667264d2fd 100644 --- a/src/tools/lint-docs/src/groups.rs +++ b/src/tools/lint-docs/src/groups.rs @@ -1,9 +1,8 @@ -use crate::Lint; +use crate::{Lint, LintExtractor}; use std::collections::{BTreeMap, BTreeSet}; use std::error::Error; use std::fmt::Write; use std::fs; -use std::path::Path; use std::process::Command; static GROUP_DESCRIPTIONS: &[(&str, &str)] = &[ @@ -15,100 +14,113 @@ static GROUP_DESCRIPTIONS: &[(&str, &str)] = &[ ("rust-2018-compatibility", "Lints used to transition code from the 2015 edition to 2018"), ]; -/// Updates the documentation of lint groups. -pub(crate) fn generate_group_docs( - lints: &[Lint], - rustc: crate::Rustc<'_>, - out_path: &Path, -) -> Result<(), Box> { - let groups = collect_groups(rustc)?; - let groups_path = out_path.join("groups.md"); - let contents = fs::read_to_string(&groups_path) - .map_err(|e| format!("could not read {}: {}", groups_path.display(), e))?; - let new_contents = contents.replace("{{groups-table}}", &make_groups_table(lints, &groups)?); - // Delete the output because rustbuild uses hard links in its copies. - let _ = fs::remove_file(&groups_path); - fs::write(&groups_path, new_contents) - .map_err(|e| format!("could not write to {}: {}", groups_path.display(), e))?; - Ok(()) -} - type LintGroups = BTreeMap>; -/// Collects the group names from rustc. -fn collect_groups(rustc: crate::Rustc<'_>) -> Result> { - let mut result = BTreeMap::new(); - let mut cmd = Command::new(rustc.path); - cmd.arg("-Whelp"); - let output = cmd.output().map_err(|e| format!("failed to run command {:?}\n{}", cmd, e))?; - if !output.status.success() { - return Err(format!( - "failed to collect lint info: {:?}\n--- stderr\n{}--- stdout\n{}\n", - output.status, - std::str::from_utf8(&output.stderr).unwrap(), - std::str::from_utf8(&output.stdout).unwrap(), - ) - .into()); +impl<'a> LintExtractor<'a> { + /// Updates the documentation of lint groups. + pub(crate) fn generate_group_docs(&self, lints: &[Lint]) -> Result<(), Box> { + let groups = self.collect_groups()?; + let groups_path = self.out_path.join("groups.md"); + let contents = fs::read_to_string(&groups_path) + .map_err(|e| format!("could not read {}: {}", groups_path.display(), e))?; + let new_contents = + contents.replace("{{groups-table}}", &self.make_groups_table(lints, &groups)?); + // Delete the output because rustbuild uses hard links in its copies. + let _ = fs::remove_file(&groups_path); + fs::write(&groups_path, new_contents) + .map_err(|e| format!("could not write to {}: {}", groups_path.display(), e))?; + Ok(()) } - let stdout = std::str::from_utf8(&output.stdout).unwrap(); - let lines = stdout.lines(); - let group_start = lines.skip_while(|line| !line.contains("groups provided")).skip(1); - let table_start = group_start.skip_while(|line| !line.contains("----")).skip(1); - for line in table_start { - if line.is_empty() { - break; - } - let mut parts = line.trim().splitn(2, ' '); - let name = parts.next().expect("name in group"); - if name == "warnings" { - // This is special. - continue; - } - let lints = - parts.next().ok_or_else(|| format!("expected lints following name, got `{}`", line))?; - let lints = lints.split(',').map(|l| l.trim().to_string()).collect(); - assert!(result.insert(name.to_string(), lints).is_none()); - } - if result.is_empty() { - return Err( - format!("expected at least one group in -Whelp output, got:\n{}", stdout).into() - ); - } - Ok(result) -} -fn make_groups_table(lints: &[Lint], groups: &LintGroups) -> Result> { - let mut result = String::new(); - let mut to_link = Vec::new(); - result.push_str("| Group | Description | Lints |\n"); - result.push_str("|-------|-------------|-------|\n"); - result.push_str("| warnings | All lints that are set to issue warnings | See [warn-by-default] for the default set of warnings |\n"); - for (group_name, group_lints) in groups { - let description = GROUP_DESCRIPTIONS.iter().find(|(n, _)| n == group_name) - .ok_or_else(|| format!("lint group `{}` does not have a description, please update the GROUP_DESCRIPTIONS list", group_name))? - .1; - to_link.extend(group_lints); - let brackets: Vec<_> = group_lints.iter().map(|l| format!("[{}]", l)).collect(); - write!(result, "| {} | {} | {} |\n", group_name, description, brackets.join(", ")).unwrap(); + /// Collects the group names from rustc. + fn collect_groups(&self) -> Result> { + let mut result = BTreeMap::new(); + let mut cmd = Command::new(self.rustc_path); + cmd.arg("-Whelp"); + let output = cmd.output().map_err(|e| format!("failed to run command {:?}\n{}", cmd, e))?; + if !output.status.success() { + return Err(format!( + "failed to collect lint info: {:?}\n--- stderr\n{}--- stdout\n{}\n", + output.status, + std::str::from_utf8(&output.stderr).unwrap(), + std::str::from_utf8(&output.stdout).unwrap(), + ) + .into()); + } + let stdout = std::str::from_utf8(&output.stdout).unwrap(); + let lines = stdout.lines(); + let group_start = lines.skip_while(|line| !line.contains("groups provided")).skip(1); + let table_start = group_start.skip_while(|line| !line.contains("----")).skip(1); + for line in table_start { + if line.is_empty() { + break; + } + let mut parts = line.trim().splitn(2, ' '); + let name = parts.next().expect("name in group"); + if name == "warnings" { + // This is special. + continue; + } + let lints = parts + .next() + .ok_or_else(|| format!("expected lints following name, got `{}`", line))?; + let lints = lints.split(',').map(|l| l.trim().to_string()).collect(); + assert!(result.insert(name.to_string(), lints).is_none()); + } + if result.is_empty() { + return Err( + format!("expected at least one group in -Whelp output, got:\n{}", stdout).into() + ); + } + Ok(result) } - result.push('\n'); - result.push_str("[warn-by-default]: listing/warn-by-default.md\n"); - for lint_name in to_link { - let lint_def = - lints.iter().find(|l| l.name == lint_name.replace("-", "_")).ok_or_else(|| { - format!( - "`rustc -W help` defined lint `{}` but that lint does not appear to exist", - lint_name - ) - })?; - write!( - result, - "[{}]: listing/{}#{}\n", - lint_name, - lint_def.level.doc_filename(), - lint_name - ) - .unwrap(); + + fn make_groups_table( + &self, + lints: &[Lint], + groups: &LintGroups, + ) -> Result> { + let mut result = String::new(); + let mut to_link = Vec::new(); + result.push_str("| Group | Description | Lints |\n"); + result.push_str("|-------|-------------|-------|\n"); + result.push_str("| warnings | All lints that are set to issue warnings | See [warn-by-default] for the default set of warnings |\n"); + for (group_name, group_lints) in groups { + let description = GROUP_DESCRIPTIONS + .iter() + .find(|(n, _)| n == group_name) + .ok_or_else(|| { + format!( + "lint group `{}` does not have a description, \ + please update the GROUP_DESCRIPTIONS list", + group_name + ) + })? + .1; + to_link.extend(group_lints); + let brackets: Vec<_> = group_lints.iter().map(|l| format!("[{}]", l)).collect(); + write!(result, "| {} | {} | {} |\n", group_name, description, brackets.join(", ")) + .unwrap(); + } + result.push('\n'); + result.push_str("[warn-by-default]: listing/warn-by-default.md\n"); + for lint_name in to_link { + let lint_def = + lints.iter().find(|l| l.name == lint_name.replace("-", "_")).ok_or_else(|| { + format!( + "`rustc -W help` defined lint `{}` but that lint does not appear to exist", + lint_name + ) + })?; + write!( + result, + "[{}]: listing/{}#{}\n", + lint_name, + lint_def.level.doc_filename(), + lint_name + ) + .unwrap(); + } + Ok(result) } - Ok(result) } diff --git a/src/tools/lint-docs/src/lib.rs b/src/tools/lint-docs/src/lib.rs index 6ca71dcaf3cd..aafd33301ea8 100644 --- a/src/tools/lint-docs/src/lib.rs +++ b/src/tools/lint-docs/src/lib.rs @@ -7,6 +7,20 @@ use walkdir::WalkDir; mod groups; +pub struct LintExtractor<'a> { + /// Path to the `src` directory, where it will scan for `.rs` files to + /// find lint declarations. + pub src_path: &'a Path, + /// Path where to save the output. + pub out_path: &'a Path, + /// Path to the `rustc` executable. + pub rustc_path: &'a Path, + /// The target arch to build the docs for. + pub rustc_target: &'a str, + /// Verbose output. + pub verbose: bool, +} + struct Lint { name: String, doc: Vec, @@ -26,6 +40,28 @@ impl Lint { .filter(|line| line.starts_with("```rust")) .all(|line| line.contains(",ignore")) } + + /// Checks the doc style of the lint. + fn check_style(&self) -> Result<(), Box> { + for &expected in &["### Example", "### Explanation", "{{produces}}"] { + if expected == "{{produces}}" && self.is_ignored() { + continue; + } + if !self.doc_contains(expected) { + return Err(format!("lint docs should contain the line `{}`", expected).into()); + } + } + if let Some(first) = self.doc.first() { + if !first.starts_with(&format!("The `{}` lint", self.name)) { + return Err(format!( + "lint docs should start with the text \"The `{}` lint\" to introduce the lint", + self.name + ) + .into()); + } + } + Ok(()) + } } #[derive(Clone, Copy, PartialEq)] @@ -45,150 +81,356 @@ impl Level { } } -#[derive(Copy, Clone)] -pub struct Rustc<'a> { - pub path: &'a Path, - pub target: &'a str, -} - -/// Collects all lints, and writes the markdown documentation at the given directory. -pub fn extract_lint_docs( - src_path: &Path, - out_path: &Path, - rustc: Rustc<'_>, - verbose: bool, -) -> Result<(), Box> { - let mut lints = gather_lints(src_path)?; - for lint in &mut lints { - generate_output_example(lint, rustc, verbose).map_err(|e| { - format!( - "failed to test example in lint docs for `{}` in {}:{}: {}", - lint.name, - lint.path.display(), - lint.lineno, - e - ) - })?; - } - save_lints_markdown(&lints, &out_path.join("listing"))?; - groups::generate_group_docs(&lints, rustc, out_path)?; - Ok(()) -} - -/// Collects all lints from all files in the given directory. -fn gather_lints(src_path: &Path) -> Result, Box> { - let mut lints = Vec::new(); - for entry in WalkDir::new(src_path).into_iter().filter_map(|e| e.ok()) { - if !entry.path().extension().map_or(false, |ext| ext == "rs") { - continue; +impl<'a> LintExtractor<'a> { + /// Collects all lints, and writes the markdown documentation at the given directory. + pub fn extract_lint_docs(&self) -> Result<(), Box> { + let mut lints = self.gather_lints()?; + for lint in &mut lints { + self.generate_output_example(lint).map_err(|e| { + format!( + "failed to test example in lint docs for `{}` in {}:{}: {}", + lint.name, + lint.path.display(), + lint.lineno, + e + ) + })?; } - lints.extend(lints_from_file(entry.path())?); + self.save_lints_markdown(&lints)?; + self.generate_group_docs(&lints)?; + Ok(()) } - if lints.is_empty() { - return Err("no lints were found!".into()); - } - Ok(lints) -} -/// Collects all lints from the given file. -fn lints_from_file(path: &Path) -> Result, Box> { - let mut lints = Vec::new(); - let contents = fs::read_to_string(path) - .map_err(|e| format!("could not read {}: {}", path.display(), e))?; - let mut lines = contents.lines().enumerate(); - loop { - // Find a lint declaration. - let lint_start = loop { - match lines.next() { - Some((lineno, line)) => { - if line.trim().starts_with("declare_lint!") { - break lineno + 1; - } - } - None => return Ok(lints), + /// Collects all lints from all files in the given directory. + fn gather_lints(&self) -> Result, Box> { + let mut lints = Vec::new(); + for entry in WalkDir::new(self.src_path).into_iter().filter_map(|e| e.ok()) { + if !entry.path().extension().map_or(false, |ext| ext == "rs") { + continue; } - }; - // Read the lint. - let mut doc_lines = Vec::new(); - let (doc, name) = loop { - match lines.next() { - Some((lineno, line)) => { - let line = line.trim(); - if line.starts_with("/// ") { - doc_lines.push(line.trim()[4..].to_string()); - } else if line.starts_with("///") { - doc_lines.push("".to_string()); - } else if line.starts_with("// ") { - // Ignore comments. - continue; - } else { - let name = lint_name(line).map_err(|e| { - format!( - "could not determine lint name in {}:{}: {}, line was `{}`", - path.display(), - lineno, - e, - line - ) - })?; - if doc_lines.is_empty() { - return Err(format!( - "did not find doc lines for lint `{}` in {}", - name, - path.display() - ) - .into()); + lints.extend(self.lints_from_file(entry.path())?); + } + if lints.is_empty() { + return Err("no lints were found!".into()); + } + Ok(lints) + } + + /// Collects all lints from the given file. + fn lints_from_file(&self, path: &Path) -> Result, Box> { + let mut lints = Vec::new(); + let contents = fs::read_to_string(path) + .map_err(|e| format!("could not read {}: {}", path.display(), e))?; + let mut lines = contents.lines().enumerate(); + loop { + // Find a lint declaration. + let lint_start = loop { + match lines.next() { + Some((lineno, line)) => { + if line.trim().starts_with("declare_lint!") { + break lineno + 1; } - break (doc_lines, name); } + None => return Ok(lints), } - None => { - return Err(format!( - "unexpected EOF for lint definition at {}:{}", - path.display(), - lint_start - ) - .into()); - } - } - }; - // These lints are specifically undocumented. This should be reserved - // for internal rustc-lints only. - if name == "deprecated_in_future" { - continue; - } - // Read the level. - let level = loop { - match lines.next() { - // Ignore comments. - Some((_, line)) if line.trim().starts_with("// ") => {} - Some((lineno, line)) => match line.trim() { - "Allow," => break Level::Allow, - "Warn," => break Level::Warn, - "Deny," => break Level::Deny, - _ => { + }; + // Read the lint. + let mut doc_lines = Vec::new(); + let (doc, name) = loop { + match lines.next() { + Some((lineno, line)) => { + let line = line.trim(); + if line.starts_with("/// ") { + doc_lines.push(line.trim()[4..].to_string()); + } else if line.starts_with("///") { + doc_lines.push("".to_string()); + } else if line.starts_with("// ") { + // Ignore comments. + continue; + } else { + let name = lint_name(line).map_err(|e| { + format!( + "could not determine lint name in {}:{}: {}, line was `{}`", + path.display(), + lineno, + e, + line + ) + })?; + if doc_lines.is_empty() { + return Err(format!( + "did not find doc lines for lint `{}` in {}", + name, + path.display() + ) + .into()); + } + break (doc_lines, name); + } + } + None => { return Err(format!( - "unexpected lint level `{}` in {}:{}", - line, + "unexpected EOF for lint definition at {}:{}", path.display(), - lineno + lint_start + ) + .into()); + } + } + }; + // These lints are specifically undocumented. This should be reserved + // for internal rustc-lints only. + if name == "deprecated_in_future" { + continue; + } + // Read the level. + let level = loop { + match lines.next() { + // Ignore comments. + Some((_, line)) if line.trim().starts_with("// ") => {} + Some((lineno, line)) => match line.trim() { + "Allow," => break Level::Allow, + "Warn," => break Level::Warn, + "Deny," => break Level::Deny, + _ => { + return Err(format!( + "unexpected lint level `{}` in {}:{}", + line, + path.display(), + lineno + ) + .into()); + } + }, + None => { + return Err(format!( + "expected lint level in {}:{}, got EOF", + path.display(), + lint_start + ) + .into()); + } + } + }; + // The rest of the lint definition is ignored. + assert!(!doc.is_empty()); + lints.push(Lint { name, doc, level, path: PathBuf::from(path), lineno: lint_start }); + } + } + + /// Mutates the lint definition to replace the `{{produces}}` marker with the + /// actual output from the compiler. + fn generate_output_example(&self, lint: &mut Lint) -> Result<(), Box> { + // Explicit list of lints that are allowed to not have an example. Please + // try to avoid adding to this list. + if matches!( + lint.name.as_str(), + "unused_features" // broken lint + | "unstable_features" // deprecated + ) { + return Ok(()); + } + if lint.doc_contains("[rustdoc book]") && !lint.doc_contains("{{produces}}") { + // Rustdoc lints are documented in the rustdoc book, don't check these. + return Ok(()); + } + lint.check_style()?; + // Unfortunately some lints have extra requirements that this simple test + // setup can't handle (like extern crates). An alternative is to use a + // separate test suite, and use an include mechanism such as mdbook's + // `{{#rustdoc_include}}`. + if !lint.is_ignored() { + self.replace_produces(lint)?; + } + Ok(()) + } + + /// Mutates the lint docs to replace the `{{produces}}` marker with the actual + /// output from the compiler. + fn replace_produces(&self, lint: &mut Lint) -> Result<(), Box> { + let mut lines = lint.doc.iter_mut(); + loop { + // Find start of example. + let options = loop { + match lines.next() { + Some(line) if line.starts_with("```rust") => { + break line[7..].split(',').collect::>(); + } + Some(line) if line.contains("{{produces}}") => { + return Err("lint marker {{{{produces}}}} found, \ + but expected to immediately follow a rust code block" + .into()); + } + Some(_) => {} + None => return Ok(()), + } + }; + // Find the end of example. + let mut example = Vec::new(); + loop { + match lines.next() { + Some(line) if line == "```" => break, + Some(line) => example.push(line), + None => { + return Err(format!( + "did not find end of example triple ticks ```, docs were:\n{:?}", + lint.doc ) .into()); } - }, - None => { - return Err(format!( - "expected lint level in {}:{}, got EOF", - path.display(), - lint_start - ) - .into()); } } - }; - // The rest of the lint definition is ignored. - assert!(!doc.is_empty()); - lints.push(Lint { name, doc, level, path: PathBuf::from(path), lineno: lint_start }); + // Find the {{produces}} line. + loop { + match lines.next() { + Some(line) if line.is_empty() => {} + Some(line) if line == "{{produces}}" => { + let output = self.generate_lint_output(&lint.name, &example, &options)?; + line.replace_range( + .., + &format!( + "This will produce:\n\ + \n\ + ```text\n\ + {}\ + ```", + output + ), + ); + break; + } + // No {{produces}} after example, find next example. + Some(_line) => break, + None => return Ok(()), + } + } + } + } + + /// Runs the compiler against the example, and extracts the output. + fn generate_lint_output( + &self, + name: &str, + example: &[&mut String], + options: &[&str], + ) -> Result> { + if self.verbose { + eprintln!("compiling lint {}", name); + } + let tempdir = tempfile::TempDir::new()?; + let tempfile = tempdir.path().join("lint_example.rs"); + let mut source = String::new(); + let needs_main = !example.iter().any(|line| line.contains("fn main")); + // Remove `# ` prefix for hidden lines. + let unhidden = + example.iter().map(|line| if line.starts_with("# ") { &line[2..] } else { line }); + let mut lines = unhidden.peekable(); + while let Some(line) = lines.peek() { + if line.starts_with("#!") { + source.push_str(line); + source.push('\n'); + lines.next(); + } else { + break; + } + } + if needs_main { + source.push_str("fn main() {\n"); + } + for line in lines { + source.push_str(line); + source.push('\n') + } + if needs_main { + source.push_str("}\n"); + } + fs::write(&tempfile, source) + .map_err(|e| format!("failed to write {}: {}", tempfile.display(), e))?; + let mut cmd = Command::new(self.rustc_path); + if options.contains(&"edition2015") { + cmd.arg("--edition=2015"); + } else { + cmd.arg("--edition=2018"); + } + cmd.arg("--error-format=json"); + cmd.arg("--target").arg(self.rustc_target); + if options.contains(&"test") { + cmd.arg("--test"); + } + cmd.arg("lint_example.rs"); + cmd.current_dir(tempdir.path()); + let output = cmd.output().map_err(|e| format!("failed to run command {:?}\n{}", cmd, e))?; + let stderr = std::str::from_utf8(&output.stderr).unwrap(); + let msgs = stderr + .lines() + .filter(|line| line.starts_with('{')) + .map(serde_json::from_str) + .collect::, _>>()?; + match msgs + .iter() + .find(|msg| matches!(&msg["code"]["code"], serde_json::Value::String(s) if s==name)) + { + Some(msg) => { + let rendered = msg["rendered"].as_str().expect("rendered field should exist"); + Ok(rendered.to_string()) + } + None => { + match msgs.iter().find( + |msg| matches!(&msg["rendered"], serde_json::Value::String(s) if s.contains(name)), + ) { + Some(msg) => { + let rendered = msg["rendered"].as_str().expect("rendered field should exist"); + Ok(rendered.to_string()) + } + None => { + let rendered: Vec<&str> = + msgs.iter().filter_map(|msg| msg["rendered"].as_str()).collect(); + let non_json: Vec<&str> = + stderr.lines().filter(|line| !line.starts_with('{')).collect(); + Err(format!( + "did not find lint `{}` in output of example, got:\n{}\n{}", + name, + non_json.join("\n"), + rendered.join("\n") + ) + .into()) + } + } + } + } + } + + /// Saves the mdbook lint chapters at the given path. + fn save_lints_markdown(&self, lints: &[Lint]) -> Result<(), Box> { + self.save_level(lints, Level::Allow, ALLOWED_MD)?; + self.save_level(lints, Level::Warn, WARN_MD)?; + self.save_level(lints, Level::Deny, DENY_MD)?; + Ok(()) + } + + fn save_level(&self, lints: &[Lint], level: Level, header: &str) -> Result<(), Box> { + let mut result = String::new(); + result.push_str(header); + let mut these_lints: Vec<_> = lints.iter().filter(|lint| lint.level == level).collect(); + these_lints.sort_unstable_by_key(|lint| &lint.name); + for lint in &these_lints { + write!(result, "* [`{}`](#{})\n", lint.name, lint.name.replace("_", "-")).unwrap(); + } + result.push('\n'); + for lint in &these_lints { + write!(result, "## {}\n\n", lint.name.replace("_", "-")).unwrap(); + for line in &lint.doc { + result.push_str(line); + result.push('\n'); + } + result.push('\n'); + } + let out_path = self.out_path.join("listing").join(level.doc_filename()); + // Delete the output because rustbuild uses hard links in its copies. + let _ = fs::remove_file(&out_path); + fs::write(&out_path, result) + .map_err(|e| format!("could not write to {}: {}", out_path.display(), e))?; + Ok(()) } } @@ -210,220 +452,6 @@ fn lint_name(line: &str) -> Result { } } -/// Mutates the lint definition to replace the `{{produces}}` marker with the -/// actual output from the compiler. -fn generate_output_example( - lint: &mut Lint, - rustc: Rustc<'_>, - verbose: bool, -) -> Result<(), Box> { - // Explicit list of lints that are allowed to not have an example. Please - // try to avoid adding to this list. - if matches!( - lint.name.as_str(), - "unused_features" // broken lint - | "unstable_features" // deprecated - ) { - return Ok(()); - } - if lint.doc_contains("[rustdoc book]") && !lint.doc_contains("{{produces}}") { - // Rustdoc lints are documented in the rustdoc book, don't check these. - return Ok(()); - } - check_style(lint)?; - // Unfortunately some lints have extra requirements that this simple test - // setup can't handle (like extern crates). An alternative is to use a - // separate test suite, and use an include mechanism such as mdbook's - // `{{#rustdoc_include}}`. - if !lint.is_ignored() { - replace_produces(lint, rustc, verbose)?; - } - Ok(()) -} - -/// Checks the doc style of the lint. -fn check_style(lint: &Lint) -> Result<(), Box> { - for &expected in &["### Example", "### Explanation", "{{produces}}"] { - if expected == "{{produces}}" && lint.is_ignored() { - continue; - } - if !lint.doc_contains(expected) { - return Err(format!("lint docs should contain the line `{}`", expected).into()); - } - } - if let Some(first) = lint.doc.first() { - if !first.starts_with(&format!("The `{}` lint", lint.name)) { - return Err(format!( - "lint docs should start with the text \"The `{}` lint\" to introduce the lint", - lint.name - ) - .into()); - } - } - Ok(()) -} - -/// Mutates the lint docs to replace the `{{produces}}` marker with the actual -/// output from the compiler. -fn replace_produces( - lint: &mut Lint, - rustc: Rustc<'_>, - verbose: bool, -) -> Result<(), Box> { - let mut lines = lint.doc.iter_mut(); - loop { - // Find start of example. - let options = loop { - match lines.next() { - Some(line) if line.starts_with("```rust") => { - break line[7..].split(',').collect::>(); - } - Some(line) if line.contains("{{produces}}") => { - return Err("lint marker {{{{produces}}}} found, \ - but expected to immediately follow a rust code block" - .into()); - } - Some(_) => {} - None => return Ok(()), - } - }; - // Find the end of example. - let mut example = Vec::new(); - loop { - match lines.next() { - Some(line) if line == "```" => break, - Some(line) => example.push(line), - None => { - return Err(format!( - "did not find end of example triple ticks ```, docs were:\n{:?}", - lint.doc - ) - .into()); - } - } - } - // Find the {{produces}} line. - loop { - match lines.next() { - Some(line) if line.is_empty() => {} - Some(line) if line == "{{produces}}" => { - let output = - generate_lint_output(&lint.name, &example, &options, rustc, verbose)?; - line.replace_range( - .., - &format!( - "This will produce:\n\ - \n\ - ```text\n\ - {}\ - ```", - output - ), - ); - break; - } - // No {{produces}} after example, find next example. - Some(_line) => break, - None => return Ok(()), - } - } - } -} - -/// Runs the compiler against the example, and extracts the output. -fn generate_lint_output( - name: &str, - example: &[&mut String], - options: &[&str], - rustc: Rustc<'_>, - verbose: bool, -) -> Result> { - if verbose { - eprintln!("compiling lint {}", name); - } - let tempdir = tempfile::TempDir::new()?; - let tempfile = tempdir.path().join("lint_example.rs"); - let mut source = String::new(); - let needs_main = !example.iter().any(|line| line.contains("fn main")); - // Remove `# ` prefix for hidden lines. - let unhidden = - example.iter().map(|line| if line.starts_with("# ") { &line[2..] } else { line }); - let mut lines = unhidden.peekable(); - while let Some(line) = lines.peek() { - if line.starts_with("#!") { - source.push_str(line); - source.push('\n'); - lines.next(); - } else { - break; - } - } - if needs_main { - source.push_str("fn main() {\n"); - } - for line in lines { - source.push_str(line); - source.push('\n') - } - if needs_main { - source.push_str("}\n"); - } - fs::write(&tempfile, source) - .map_err(|e| format!("failed to write {}: {}", tempfile.display(), e))?; - let mut cmd = Command::new(rustc.path); - if options.contains(&"edition2015") { - cmd.arg("--edition=2015"); - } else { - cmd.arg("--edition=2018"); - } - cmd.arg("--error-format=json"); - cmd.arg("--target").arg(rustc.target); - if options.contains(&"test") { - cmd.arg("--test"); - } - cmd.arg("lint_example.rs"); - cmd.current_dir(tempdir.path()); - let output = cmd.output().map_err(|e| format!("failed to run command {:?}\n{}", cmd, e))?; - let stderr = std::str::from_utf8(&output.stderr).unwrap(); - let msgs = stderr - .lines() - .filter(|line| line.starts_with('{')) - .map(serde_json::from_str) - .collect::, _>>()?; - match msgs - .iter() - .find(|msg| matches!(&msg["code"]["code"], serde_json::Value::String(s) if s==name)) - { - Some(msg) => { - let rendered = msg["rendered"].as_str().expect("rendered field should exist"); - Ok(rendered.to_string()) - } - None => { - match msgs.iter().find( - |msg| matches!(&msg["rendered"], serde_json::Value::String(s) if s.contains(name)), - ) { - Some(msg) => { - let rendered = msg["rendered"].as_str().expect("rendered field should exist"); - Ok(rendered.to_string()) - } - None => { - let rendered: Vec<&str> = - msgs.iter().filter_map(|msg| msg["rendered"].as_str()).collect(); - let non_json: Vec<&str> = - stderr.lines().filter(|line| !line.starts_with('{')).collect(); - Err(format!( - "did not find lint `{}` in output of example, got:\n{}\n{}", - name, - non_json.join("\n"), - rendered.join("\n") - ) - .into()) - } - } - } - } -} - static ALLOWED_MD: &str = r#"# Allowed-by-default lints These lints are all set to the 'allow' level by default. As such, they won't show up @@ -442,41 +470,3 @@ static DENY_MD: &str = r#"# Deny-by-default lints These lints are all set to the 'deny' level by default. "#; - -/// Saves the mdbook lint chapters at the given path. -fn save_lints_markdown(lints: &[Lint], out_dir: &Path) -> Result<(), Box> { - save_level(lints, Level::Allow, out_dir, ALLOWED_MD)?; - save_level(lints, Level::Warn, out_dir, WARN_MD)?; - save_level(lints, Level::Deny, out_dir, DENY_MD)?; - Ok(()) -} - -fn save_level( - lints: &[Lint], - level: Level, - out_dir: &Path, - header: &str, -) -> Result<(), Box> { - let mut result = String::new(); - result.push_str(header); - let mut these_lints: Vec<_> = lints.iter().filter(|lint| lint.level == level).collect(); - these_lints.sort_unstable_by_key(|lint| &lint.name); - for lint in &these_lints { - write!(result, "* [`{}`](#{})\n", lint.name, lint.name.replace("_", "-")).unwrap(); - } - result.push('\n'); - for lint in &these_lints { - write!(result, "## {}\n\n", lint.name.replace("_", "-")).unwrap(); - for line in &lint.doc { - result.push_str(line); - result.push('\n'); - } - result.push('\n'); - } - let out_path = out_dir.join(level.doc_filename()); - // Delete the output because rustbuild uses hard links in its copies. - let _ = fs::remove_file(&out_path); - fs::write(&out_path, result) - .map_err(|e| format!("could not write to {}: {}", out_path.display(), e))?; - Ok(()) -} diff --git a/src/tools/lint-docs/src/main.rs b/src/tools/lint-docs/src/main.rs index 5db49007d375..9b75ab45fca8 100644 --- a/src/tools/lint-docs/src/main.rs +++ b/src/tools/lint-docs/src/main.rs @@ -57,13 +57,12 @@ fn doit() -> Result<(), Box> { if rustc_target.is_none() { return Err("--rustc-target must be specified to the rustc target".into()); } - lint_docs::extract_lint_docs( - &src_path.unwrap(), - &out_path.unwrap(), - lint_docs::Rustc { - path: rustc_path.as_deref().unwrap(), - target: rustc_target.as_deref().unwrap(), - }, + let le = lint_docs::LintExtractor { + src_path: &src_path.unwrap(), + out_path: &out_path.unwrap(), + rustc_path: &rustc_path.unwrap(), + rustc_target: &rustc_target.unwrap(), verbose, - ) + }; + le.extract_lint_docs() }