mirror of
https://github.com/rust-lang/rust.git
synced 2024-11-23 15:23:46 +00:00
lint-docs: Move free functions into methods of LintExtractor.
This helps avoid needing to pass so many parameters around.
This commit is contained in:
parent
e37f25aa3f
commit
f17e6487b2
@ -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<dyn Error>> {
|
||||
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<String, BTreeSet<String>>;
|
||||
|
||||
/// Collects the group names from rustc.
|
||||
fn collect_groups(rustc: crate::Rustc<'_>) -> Result<LintGroups, Box<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<String, Box<dyn Error>> {
|
||||
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<LintGroups, Box<dyn Error>> {
|
||||
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<String, Box<dyn Error>> {
|
||||
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)
|
||||
}
|
||||
|
@ -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<String>,
|
||||
@ -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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<Vec<Lint>, Box<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<Vec<Lint>, Box<dyn Error>> {
|
||||
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<Vec<Lint>, Box<dyn Error>> {
|
||||
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<Vec<Lint>, Box<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
// 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<dyn Error>> {
|
||||
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::<Vec<_>>();
|
||||
}
|
||||
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<String, Box<dyn Error>> {
|
||||
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::<Result<Vec<serde_json::Value>, _>>()?;
|
||||
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<String, &'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<dyn Error>> {
|
||||
// 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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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::<Vec<_>>();
|
||||
}
|
||||
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<String, Box<dyn Error>> {
|
||||
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::<Result<Vec<serde_json::Value>, _>>()?;
|
||||
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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(())
|
||||
}
|
||||
|
@ -57,13 +57,12 @@ fn doit() -> Result<(), Box<dyn Error>> {
|
||||
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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user