Rollup merge of #79522 - ehuss:lint-check-validate, r=Mark-Simulacrum

Validate lint docs separately.

This addresses some concerns raised in https://github.com/rust-lang/rust/pull/76549#issuecomment-727638552 about errors with the lint docs being confusing and cumbersome. Errors from validating the lint documentation were being generated during `x.py doc` (and `x.py dist`), since extraction and validation are being done in a single step. This changes it so that extraction and validation are separated, so that `x.py doc` will not error if there is a validation problem, and tests are moved to `x.py test src/tools/lint-docs`.

This includes the following changes:

* Separate validation to `x.py test`.
* Added some more documentation on how to more easily modify and test the docs.
* Added more help to the error messages to hopefully provide more information on how to fix things.

The first commit just moves the code around, so you may consider looking at the other commits for a smaller diff.
This commit is contained in:
Mara Bos 2020-12-01 10:50:16 +00:00 committed by GitHub
commit 36ce8db229
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 593 additions and 488 deletions

View File

@ -366,11 +366,25 @@ impl LintBuffer {
/// ```
///
/// The `{{produces}}` tag will be automatically replaced with the output from
/// the example by the build system. You can build and view the rustc book
/// with `x.py doc --stage=1 src/doc/rustc --open`. If the lint example is too
/// complex to run as a simple example (for example, it needs an extern
/// crate), mark it with `ignore` and manually paste the expected output below
/// the example.
/// the example by the build system. If the lint example is too complex to run
/// as a simple example (for example, it needs an extern crate), mark the code
/// block with `ignore` and manually replace the `{{produces}}` line with the
/// expected output in a `text` code block.
///
/// If this is a rustdoc-only lint, then only include a brief introduction
/// with a link with the text `[rustdoc book]` so that the validator knows
/// that this is for rustdoc only (see BROKEN_INTRA_DOC_LINKS as an example).
///
/// Commands to view and test the documentation:
///
/// * `./x.py doc --stage=1 src/doc/rustc --open`: Builds the rustc book and opens it.
/// * `./x.py test src/tools/lint-docs`: Validates that the lint docs have the
/// correct style, and that the code example actually emits the expected
/// lint.
///
/// If you have already built the compiler, and you want to make changes to
/// just the doc comments, then use the `--keep-stage=0` flag with the above
/// commands to avoid rebuilding the compiler.
#[macro_export]
macro_rules! declare_lint {
($(#[$attr:meta])* $vis: vis $NAME: ident, $Level: ident, $desc: expr) => (

View File

@ -413,6 +413,7 @@ impl<'a> Builder<'a> {
test::TheBook,
test::UnstableBook,
test::RustcBook,
test::LintDocs,
test::RustcGuide,
test::EmbeddedBook,
test::EditionGuide,

View File

@ -726,6 +726,7 @@ fn symlink_dir_force(config: &Config, src: &Path, dst: &Path) -> io::Result<()>
pub struct RustcBook {
pub compiler: Compiler,
pub target: TargetSelection,
pub validate: bool,
}
impl Step for RustcBook {
@ -742,6 +743,7 @@ impl Step for RustcBook {
run.builder.ensure(RustcBook {
compiler: run.builder.compiler(run.builder.top_stage, run.builder.config.build),
target: run.target,
validate: false,
});
}
@ -772,6 +774,9 @@ impl Step for RustcBook {
if builder.config.verbose() {
cmd.arg("--verbose");
}
if self.validate {
cmd.arg("--validate");
}
// If the lib directories are in an unusual location (changed in
// config.toml), then this needs to explicitly update the dylib search
// path.

View File

@ -2115,3 +2115,36 @@ impl Step for TierCheck {
try_run(builder, &mut cargo.into());
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct LintDocs {
pub compiler: Compiler,
pub target: TargetSelection,
}
impl Step for LintDocs {
type Output = ();
const DEFAULT: bool = true;
const ONLY_HOSTS: bool = true;
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
run.path("src/tools/lint-docs")
}
fn make_run(run: RunConfig<'_>) {
run.builder.ensure(LintDocs {
compiler: run.builder.compiler(run.builder.top_stage, run.builder.config.build),
target: run.target,
});
}
/// Tests that the lint examples in the rustc book generate the correct
/// lints and have the expected format.
fn run(self, builder: &Builder<'_>) {
builder.ensure(crate::doc::RustcBook {
compiler: self.compiler,
target: self.target,
validate: true,
});
}
}

View File

@ -1,11 +1,11 @@
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;
/// Descriptions of rustc lint groups.
static GROUP_DESCRIPTIONS: &[(&str, &str)] = &[
("unused", "Lints that detect things being declared but not used, or excess syntax"),
("rustdoc", "Rustdoc-specific lints"),
@ -15,100 +15,123 @@ 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 = match GROUP_DESCRIPTIONS.iter().find(|(n, _)| n == group_name) {
Some((_, desc)) => desc,
None if self.validate => {
return Err(format!(
"lint group `{}` does not have a description, \
please update the GROUP_DESCRIPTIONS list in \
src/tools/lint-docs/src/groups.rs",
group_name
)
.into());
}
None => {
eprintln!(
"warning: lint group `{}` is missing from the GROUP_DESCRIPTIONS list\n\
If this is a new lint group, please update the GROUP_DESCRIPTIONS in \
src/tools/lint-docs/src/groups.rs",
group_name
);
continue;
}
};
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)
}

View File

@ -7,6 +7,22 @@ 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,
/// Validate the style and the code example.
pub validate: bool,
}
struct Lint {
name: String,
doc: Vec<String>,
@ -26,6 +42,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 +83,378 @@ 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();
'outer: 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 let Some(text) = line.strip_prefix("/// ") {
doc_lines.push(text.trim().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() {
if self.validate {
return Err(format!(
"did not find doc lines for lint `{}` in {}",
name,
path.display()
)
.into());
} else {
eprintln!(
"warning: lint `{}` in {} does not define any doc lines, \
these are required for the lint documentation",
name,
path.display()
);
continue 'outer;
}
}
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(());
}
if self.validate {
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() {
if let Err(e) = self.replace_produces(lint) {
if self.validate {
return Err(e);
}
eprintln!(
"warning: the code example in lint `{}` in {} failed to \
generate the expected output: {}",
lint.name,
lint.path.display(),
e
);
}
}
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| line.strip_prefix("# ").unwrap_or(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 +476,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 +494,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(())
}

View File

@ -3,7 +3,20 @@ use std::path::PathBuf;
fn main() {
if let Err(e) = doit() {
println!("error: {}", e);
eprintln!("error: {}", e);
eprintln!(
"
This error was generated by the lint-docs tool.
This tool extracts documentation for lints from the source code and places
them in the rustc book. See the declare_lint! documentation
https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint_defs/macro.declare_lint.html
for an example of the format of documentation this tool expects.
To re-run these tests, run: ./x.py test --keep-stage=0 src/tools/lint-docs
The --keep-stage flag should be used if you have already built the compiler
and are only modifying the doc comments to avoid rebuilding the compiler.
"
);
std::process::exit(1);
}
}
@ -15,6 +28,7 @@ fn doit() -> Result<(), Box<dyn Error>> {
let mut rustc_path = None;
let mut rustc_target = None;
let mut verbose = false;
let mut validate = false;
while let Some(arg) = args.next() {
match arg.as_str() {
"--src" => {
@ -42,6 +56,7 @@ fn doit() -> Result<(), Box<dyn Error>> {
};
}
"-v" | "--verbose" => verbose = true,
"--validate" => validate = true,
s => return Err(format!("unexpected argument `{}`", s).into()),
}
}
@ -57,13 +72,13 @@ 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,
)
validate,
};
le.extract_lint_docs()
}