Auto merge of #8947 - Serial-ATA:lint-produces-output, r=xFrednet

Add lint output to lint list

changelog: Add the ability to show the lint output in the lint list

This just adds the logic to produce the output, it hasn't been added to any lints yet. It did help find some mistakes in some docs though 😄.

### Screenshots

<details>
<summary>A single code block</summary>

![single-code-block](https://user-images.githubusercontent.com/69764315/172013766-145b22b1-1d91-4fb8-9cd0-b967a52d6330.png)
</details>

<details>
<summary>A single code block with a "Use instead" section</summary>

![with-usage](https://user-images.githubusercontent.com/69764315/172013792-d2dd6c9c-defa-41e0-8c27-8e8e311adb63.png)
</details>

<details>
<summary>Multiple code blocks</summary>

![multi-code-block](https://user-images.githubusercontent.com/69764315/172013808-5328f59b-e7c5-4914-a396-253822a6d350.png)
</details>

This is the last task in #7172 🎉.
r? `@xFrednet` (?)
This commit is contained in:
bors 2022-06-14 10:42:09 +00:00
commit 5a45805db5
9 changed files with 246 additions and 32 deletions

View File

@ -59,7 +59,7 @@ rustc_tools_util = { version = "0.2", path = "rustc_tools_util" }
[features]
deny-warnings = ["clippy_lints/deny-warnings"]
integration = ["tempfile"]
internal = ["clippy_lints/internal"]
internal = ["clippy_lints/internal", "tempfile"]
[package.metadata.rust-analyzer]
# This package uses #[feature(rustc_private)]

View File

@ -10,6 +10,7 @@ edition = "2021"
[dependencies]
cargo_metadata = "0.14"
clippy_dev = { path = "../clippy_dev", optional = true }
clippy_utils = { path = "../clippy_utils" }
if_chain = "1.0"
itertools = "0.10.1"
@ -18,6 +19,7 @@ quine-mc_cluskey = "0.2"
regex-syntax = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", optional = true }
tempfile = { version = "3.3.0", optional = true }
toml = "0.5"
unicode-normalization = "0.1"
unicode-script = { version = "0.5", default-features = false }
@ -30,7 +32,7 @@ url = { version = "2.2", features = ["serde"] }
[features]
deny-warnings = ["clippy_utils/deny-warnings"]
# build clippy with internal lints enabled, off by default
internal = ["clippy_utils/internal", "serde_json"]
internal = ["clippy_utils/internal", "serde_json", "tempfile", "clippy_dev"]
[package.metadata.rust-analyzer]
# This crate uses #[feature(rustc_private)]

View File

@ -32,20 +32,20 @@
/// makes code look more complex than it really is.
///
/// ### Example
/// ```rust,ignore
/// ```rust
/// # let (x, y) = (true, true);
/// if x {
/// if y {
///
/// //
/// }
/// }
///
/// ```
///
/// Use instead:
///
/// ```rust,ignore
/// ```rust
/// # let (x, y) = (true, true);
/// if x && y {
///
/// //
/// }
/// ```
#[clippy::version = "pre 1.29.0"]

View File

@ -178,7 +178,7 @@
/// if the `fn main()` is left implicit.
///
/// ### Examples
/// ``````rust
/// ```rust
/// /// An example of a doctest with a `main()` function
/// ///
/// /// # Examples
@ -191,7 +191,7 @@
/// fn needless_main() {
/// unimplemented!();
/// }
/// ``````
/// ```
#[clippy::version = "1.40.0"]
pub NEEDLESS_DOCTEST_MAIN,
style,

View File

@ -22,10 +22,11 @@
/// let included_bytes = include_bytes!("very_large_file.txt");
/// ```
///
/// Instead, you can load the file at runtime:
/// Use instead:
/// ```rust,ignore
/// use std::fs;
///
/// // You can load the file at runtime
/// let string = fs::read_to_string("very_large_file.txt")?;
/// let bytes = fs::read("very_large_file.txt")?;
/// ```

View File

@ -51,6 +51,7 @@
/// - Does not match on `_127` since that is a valid grouping for decimal and octal numbers
///
/// ### Example
/// ```ignore
/// `2_32` => `2_i32`
/// `250_8 => `250_u8`
/// ```

View File

@ -31,6 +31,8 @@
use std::fs::{self, OpenOptions};
use std::io::prelude::*;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
/// This is the output file of the lint collector.
const OUTPUT_FILE: &str = "../util/gh-pages/lints.json";
@ -180,6 +182,7 @@ pub struct MetadataCollector {
lints: BinaryHeap<LintMetadata>,
applicability_info: FxHashMap<String, ApplicabilityInfo>,
config: Vec<ClippyConfiguration>,
clippy_project_root: PathBuf,
}
impl MetadataCollector {
@ -188,6 +191,7 @@ pub fn new() -> Self {
lints: BinaryHeap::<LintMetadata>::default(),
applicability_info: FxHashMap::<String, ApplicabilityInfo>::default(),
config: collect_configs(),
clippy_project_root: clippy_dev::clippy_project_root(),
}
}
@ -215,11 +219,13 @@ fn drop(&mut self) {
// Mapping the final data
let mut lints = std::mem::take(&mut self.lints).into_sorted_vec();
collect_renames(&mut lints);
for x in &mut lints {
x.applicability = Some(applicability_info.remove(&x.id).unwrap_or_default());
replace_produces(&x.id, &mut x.docs, &self.clippy_project_root);
}
collect_renames(&mut lints);
// Outputting
if Path::new(OUTPUT_FILE).exists() {
fs::remove_file(OUTPUT_FILE).unwrap();
@ -263,14 +269,193 @@ fn new(
}
}
fn replace_produces(lint_name: &str, docs: &mut String, clippy_project_root: &Path) {
let mut doc_lines = docs.lines().map(ToString::to_string).collect::<Vec<_>>();
let mut lines = doc_lines.iter_mut();
'outer: loop {
// Find the start of the example
// ```rust
loop {
match lines.next() {
Some(line) if line.trim_start().starts_with("```rust") => {
if line.contains("ignore") || line.contains("no_run") {
// A {{produces}} marker may have been put on a ignored code block by mistake,
// just seek to the end of the code block and continue checking.
if lines.any(|line| line.trim_start().starts_with("```")) {
continue;
}
panic!("lint `{}` has an unterminated code block", lint_name)
}
break;
},
Some(line) if line.trim_start() == "{{produces}}" => {
panic!(
"lint `{}` has marker {{{{produces}}}} with an ignored or missing code block",
lint_name
)
},
Some(line) => {
let line = line.trim();
// These are the two most common markers of the corrections section
if line.eq_ignore_ascii_case("Use instead:") || line.eq_ignore_ascii_case("Could be written as:") {
break 'outer;
}
},
None => break 'outer,
}
}
// Collect the example
let mut example = Vec::new();
loop {
match lines.next() {
Some(line) if line.trim_start() == "```" => break,
Some(line) => example.push(line),
None => panic!("lint `{}` has an unterminated code block", lint_name),
}
}
// Find the {{produces}} and attempt to generate the output
loop {
match lines.next() {
Some(line) if line.is_empty() => {},
Some(line) if line.trim() == "{{produces}}" => {
let output = get_lint_output(lint_name, &example, clippy_project_root);
line.replace_range(
..,
&format!(
"<details>\
<summary>Produces</summary>\n\
\n\
```text\n\
{}\n\
```\n\
</details>",
output
),
);
break;
},
// No {{produces}}, we can move on to the next example
Some(_) => break,
None => break 'outer,
}
}
}
*docs = cleanup_docs(&doc_lines);
}
fn get_lint_output(lint_name: &str, example: &[&mut String], clippy_project_root: &Path) -> String {
let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("failed to create temp dir: {e}"));
let file = dir.path().join("lint_example.rs");
let mut source = String::new();
let unhidden = example
.iter()
.map(|line| line.trim_start().strip_prefix("# ").unwrap_or(line));
// Get any attributes
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;
}
}
let needs_main = !example.iter().any(|line| line.contains("fn main"));
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");
}
if let Err(e) = fs::write(&file, &source) {
panic!("failed to write to `{}`: {e}", file.as_path().to_string_lossy());
}
let prefixed_name = format!("{}{lint_name}", CLIPPY_LINT_GROUP_PREFIX);
let mut cmd = Command::new("cargo");
cmd.current_dir(clippy_project_root)
.env("CARGO_INCREMENTAL", "0")
.env("CLIPPY_ARGS", "")
.env("CLIPPY_DISABLE_DOCS_LINKS", "1")
// We need to disable this to enable all lints
.env("ENABLE_METADATA_COLLECTION", "0")
.args(["run", "--bin", "clippy-driver"])
.args(["--target-dir", "./clippy_lints/target"])
.args(["--", "--error-format=json"])
.args(["--edition", "2021"])
.arg("-Cdebuginfo=0")
.args(["-A", "clippy::all"])
.args(["-W", &prefixed_name])
.args(["-L", "./target/debug"])
.args(["-Z", "no-codegen"]);
let output = cmd
.arg(file.as_path())
.output()
.unwrap_or_else(|e| panic!("failed to run `{:?}`: {e}", cmd));
let tmp_file_path = file.to_string_lossy();
let stderr = std::str::from_utf8(&output.stderr).unwrap();
let msgs = stderr
.lines()
.filter(|line| line.starts_with('{'))
.map(|line| serde_json::from_str(line).unwrap())
.collect::<Vec<serde_json::Value>>();
let mut rendered = String::new();
let iter = msgs
.iter()
.filter(|msg| matches!(&msg["code"]["code"], serde_json::Value::String(s) if s == &prefixed_name));
for message in iter {
let rendered_part = message["rendered"].as_str().expect("rendered field should exist");
rendered.push_str(rendered_part);
}
if rendered.is_empty() {
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();
panic!(
"did not find lint `{}` in output of example, got:\n{}\n{}",
lint_name,
non_json.join("\n"),
rendered.join("\n")
);
}
// The reader doesn't need to see `/tmp/.tmpfiy2Qd/lint_example.rs` :)
rendered.trim_end().replace(&*tmp_file_path, "lint_example.rs")
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)]
struct SerializableSpan {
path: String,
line: usize,
}
impl std::fmt::Display for SerializableSpan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for SerializableSpan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.path.rsplit('/').next().unwrap_or_default(), self.line)
}
}
@ -435,10 +620,10 @@ fn check_item(&mut self, cx: &LateContext<'hir>, item: &'hir Item<'_>) {
if !BLACK_LISTED_LINTS.contains(&lint_name.as_str());
// metadata extraction
if let Some((group, level)) = get_lint_group_and_level_or_lint(cx, &lint_name, item);
if let Some(mut docs) = extract_attr_docs_or_lint(cx, item);
if let Some(mut raw_docs) = extract_attr_docs_or_lint(cx, item);
then {
if let Some(configuration_section) = self.get_lint_configs(&lint_name) {
docs.push_str(&configuration_section);
raw_docs.push_str(&configuration_section);
}
let version = get_lint_version(cx, item);
@ -448,7 +633,7 @@ fn check_item(&mut self, cx: &LateContext<'hir>, item: &'hir Item<'_>) {
group,
level,
version,
docs,
raw_docs,
));
}
}
@ -459,7 +644,7 @@ fn check_item(&mut self, cx: &LateContext<'hir>, item: &'hir Item<'_>) {
let lint_name = sym_to_string(item.ident.name).to_ascii_lowercase();
if !BLACK_LISTED_LINTS.contains(&lint_name.as_str());
// Metadata the little we can get from a deprecated lint
if let Some(docs) = extract_attr_docs_or_lint(cx, item);
if let Some(raw_docs) = extract_attr_docs_or_lint(cx, item);
then {
let version = get_lint_version(cx, item);
@ -469,7 +654,7 @@ fn check_item(&mut self, cx: &LateContext<'hir>, item: &'hir Item<'_>) {
DEPRECATED_LINT_GROUP_STR.to_string(),
DEPRECATED_LINT_LEVEL,
version,
docs,
raw_docs,
));
}
}
@ -535,22 +720,28 @@ fn extract_attr_docs_or_lint(cx: &LateContext<'_>, item: &Item<'_>) -> Option<St
/// ```
///
/// Would result in `Hello world!\n=^.^=\n`
///
/// ---
///
fn extract_attr_docs(cx: &LateContext<'_>, item: &Item<'_>) -> Option<String> {
let attrs = cx.tcx.hir().attrs(item.hir_id());
let mut lines = attrs.iter().filter_map(ast::Attribute::doc_str);
if let Some(line) = lines.next() {
let raw_docs = lines.fold(String::from(line.as_str()) + "\n", |s, line| s + line.as_str() + "\n");
return Some(raw_docs);
}
None
}
/// This function may modify the doc comment to ensure that the string can be displayed using a
/// markdown viewer in Clippy's lint list. The following modifications could be applied:
/// * Removal of leading space after a new line. (Important to display tables)
/// * Ensures that code blocks only contain language information
fn extract_attr_docs(cx: &LateContext<'_>, item: &Item<'_>) -> Option<String> {
let attrs = cx.tcx.hir().attrs(item.hir_id());
let mut lines = attrs.iter().filter_map(ast::Attribute::doc_str);
let mut docs = String::from(lines.next()?.as_str());
fn cleanup_docs(docs_collection: &Vec<String>) -> String {
let mut in_code_block = false;
let mut is_code_block_rust = false;
for line in lines {
let line = line.as_str();
let mut docs = String::new();
for line in docs_collection {
// Rustdoc hides code lines starting with `# ` and this removes them from Clippy's lint list :)
if is_code_block_rust && line.trim_start().starts_with("# ") {
continue;
@ -583,7 +774,8 @@ fn extract_attr_docs(cx: &LateContext<'_>, item: &Item<'_>) -> Option<String> {
docs.push_str(line);
}
}
Some(docs)
docs
}
fn get_lint_version(cx: &LateContext<'_>, item: &Item<'_>) -> String {

View File

@ -21,7 +21,7 @@ fn dogfood_clippy() {
// "" is the root package
for package in &["", "clippy_dev", "clippy_lints", "clippy_utils", "rustc_tools_util"] {
run_clippy_for_package(package, &[]);
run_clippy_for_package(package, &["-D", "clippy::all", "-D", "clippy::pedantic"]);
}
}
@ -77,8 +77,6 @@ fn run_clippy_for_package(project: &str, args: &[&str]) {
.arg("--all-features")
.arg("--")
.args(args)
.args(&["-D", "clippy::all"])
.args(&["-D", "clippy::pedantic"])
.arg("-Cdebuginfo=0"); // disable debuginfo to generate less data in the target dir
if cfg!(feature = "internal") {

View File

@ -206,6 +206,26 @@ Otherwise, have a great day =^.^=
margin: auto 5px;
font-family: monospace;
}
details {
border-radius: 4px;
padding: .5em .5em 0;
}
code {
white-space: pre !important;
}
summary {
font-weight: bold;
margin: -.5em -.5em 0;
padding: .5em;
display: revert;
}
details[open] {
padding: .5em;
}
</style>
<style>
/* Expanding the mdBoom theme*/