diff --git a/clippy_dev/Cargo.toml b/clippy_dev/Cargo.toml
index b30ba61d2991..82ac5409479d 100644
--- a/clippy_dev/Cargo.toml
+++ b/clippy_dev/Cargo.toml
@@ -11,6 +11,9 @@ regex = "1"
lazy_static = "1.0"
shell-escape = "0.1"
walkdir = "2"
+reqwest = { version = "0.10", features = ["blocking", "json"], optional = true }
+serde = { version = "1.0", features = ["derive"], optional = true }
[features]
deny-warnings = []
+issues = ["reqwest", "serde"]
diff --git a/clippy_dev/src/issues_for_lint.rs b/clippy_dev/src/issues_for_lint.rs
new file mode 100644
index 000000000000..b458037b39e1
--- /dev/null
+++ b/clippy_dev/src/issues_for_lint.rs
@@ -0,0 +1,154 @@
+use crate::gather_all;
+use lazy_static::lazy_static;
+use regex::Regex;
+use reqwest::{
+ blocking::{Client, Response},
+ header,
+};
+use serde::Deserialize;
+use std::env;
+
+lazy_static! {
+ static ref NEXT_PAGE_RE: Regex = Regex::new(r#"<(?P[^;]+)>;\srel="next""#).unwrap();
+}
+
+#[derive(Debug, Deserialize)]
+struct Issue {
+ title: String,
+ number: u32,
+ body: String,
+ pull_request: Option,
+}
+
+#[derive(Debug, Deserialize)]
+struct PR {}
+
+enum Error {
+ Reqwest(reqwest::Error),
+ Env(std::env::VarError),
+ Http(header::InvalidHeaderValue),
+}
+
+impl From for Error {
+ fn from(err: reqwest::Error) -> Self {
+ Self::Reqwest(err)
+ }
+}
+
+impl From for Error {
+ fn from(err: std::env::VarError) -> Self {
+ Self::Env(err)
+ }
+}
+
+impl From for Error {
+ fn from(err: header::InvalidHeaderValue) -> Self {
+ Self::Http(err)
+ }
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::Reqwest(err) => write!(fmt, "reqwest: {}", err),
+ Self::Env(err) => write!(fmt, "env: {}", err),
+ Self::Http(err) => write!(fmt, "http: {}", err),
+ }
+ }
+}
+
+pub fn run(name: &str, filter: &[u32]) {
+ match open_issues() {
+ Ok(issues) => {
+ for (i, issue) in filter_issues(&issues, name, filter).enumerate() {
+ if i == 0 {
+ println!("### `{}`\n", name);
+ }
+ println!("- [ ] #{} ({})", issue.number, issue.title)
+ }
+ },
+ Err(err) => eprintln!("{}", err),
+ }
+}
+
+pub fn run_all(filter: &[u32]) {
+ match open_issues() {
+ Ok(issues) => {
+ let mut lint_names = gather_all().map(|lint| lint.name).collect::>();
+ lint_names.sort();
+ for name in lint_names {
+ let mut print_empty_line = false;
+ for (i, issue) in filter_issues(&issues, &name, filter).enumerate() {
+ if i == 0 {
+ println!("### `{}`\n", name);
+ print_empty_line = true;
+ }
+ println!("- [ ] #{} ({})", issue.number, issue.title)
+ }
+ if print_empty_line {
+ println!();
+ }
+ }
+ },
+ Err(err) => eprintln!("{}", err),
+ }
+}
+
+fn open_issues() -> Result, Error> {
+ let github_token = env::var("GITHUB_TOKEN")?;
+
+ let mut headers = header::HeaderMap::new();
+ headers.insert(
+ header::AUTHORIZATION,
+ header::HeaderValue::from_str(&format!("token {}", github_token))?,
+ );
+ headers.insert(header::USER_AGENT, header::HeaderValue::from_static("ghost"));
+ let client = Client::builder().default_headers(headers).build()?;
+
+ let issues_base = "https://api.github.com/repos/rust-lang/rust-clippy/issues";
+
+ let mut issues = vec![];
+ let mut response = client
+ .get(issues_base)
+ .query(&[("per_page", "100"), ("state", "open"), ("direction", "asc")])
+ .send()?;
+ while let Some(link) = next_link(&response) {
+ issues.extend(
+ response
+ .json::>()?
+ .into_iter()
+ .filter(|i| i.pull_request.is_none()),
+ );
+ response = client.get(&link).send()?;
+ }
+
+ Ok(issues)
+}
+
+fn filter_issues<'a>(issues: &'a [Issue], name: &str, filter: &'a [u32]) -> impl Iterator- {
+ let name = name.to_lowercase();
+ let separated_name = name.chars().map(|c| if c == '_' { ' ' } else { c }).collect::();
+ let dash_separated_name = name.chars().map(|c| if c == '_' { '-' } else { c }).collect::();
+
+ issues.iter().filter(move |i| {
+ let title = i.title.to_lowercase();
+ let body = i.body.to_lowercase();
+ !filter.contains(&i.number)
+ && (title.contains(&name)
+ || title.contains(&separated_name)
+ || title.contains(&dash_separated_name)
+ || body.contains(&name)
+ || body.contains(&separated_name)
+ || body.contains(&dash_separated_name))
+ })
+}
+
+fn next_link(response: &Response) -> Option {
+ if let Some(links) = response.headers().get("Link").and_then(|l| l.to_str().ok()) {
+ if let Some(cap) = NEXT_PAGE_RE.captures_iter(links).next() {
+ return Some(cap["link"].to_string());
+ }
+ }
+
+ None
+}
diff --git a/clippy_dev/src/main.rs b/clippy_dev/src/main.rs
index 22a2f2ca79c7..8d04b66a9893 100644
--- a/clippy_dev/src/main.rs
+++ b/clippy_dev/src/main.rs
@@ -1,9 +1,11 @@
#![cfg_attr(feature = "deny-warnings", deny(warnings))]
-use clap::{App, Arg, SubCommand};
+use clap::{App, Arg, ArgMatches, SubCommand};
use clippy_dev::*;
mod fmt;
+#[cfg(feature = "issues")]
+mod issues_for_lint;
mod stderr_length_check;
#[derive(PartialEq)]
@@ -13,7 +15,7 @@ enum UpdateMode {
}
fn main() {
- let matches = App::new("Clippy developer tooling")
+ let mut app = App::new("Clippy developer tooling")
.subcommand(
SubCommand::with_name("fmt")
.about("Run rustfmt on all projects and tests")
@@ -55,8 +57,31 @@ fn main() {
Arg::with_name("limit-stderr-length")
.long("limit-stderr-length")
.help("Ensures that stderr files do not grow longer than a certain amount of lines."),
- )
- .get_matches();
+ );
+ if cfg!(feature = "issues") {
+ app = app.subcommand(
+ SubCommand::with_name("issues_for_lint")
+ .about(
+ "Prints all issues where the specified lint is mentioned either in the title or in the description",
+ )
+ .arg(
+ Arg::with_name("name")
+ .short("n")
+ .long("name")
+ .help("The name of the lint")
+ .takes_value(true)
+ .required_unless("all"),
+ )
+ .arg(Arg::with_name("all").long("all").help("Create a list for all lints"))
+ .arg(
+ Arg::with_name("filter")
+ .long("filter")
+ .takes_value(true)
+ .help("Comma separated list of issue numbers, that should be filtered out"),
+ ),
+ );
+ }
+ let matches = app.get_matches();
if matches.is_present("limit-stderr-length") {
stderr_length_check::check();
@@ -75,10 +100,32 @@ fn main() {
update_lints(&UpdateMode::Change);
}
},
+ ("issues_for_lint", Some(matches)) => issues_for_lint(matches),
_ => {},
}
}
+fn issues_for_lint(_matches: &ArgMatches<'_>) {
+ #[cfg(feature = "issues")]
+ {
+ let filter = if let Some(filter) = _matches.value_of("filter") {
+ let mut issue_nbs = vec![];
+ for nb in filter.split(',') {
+ issue_nbs.push(nb.trim().parse::().expect("only numbers are allowed as filter"));
+ }
+ issue_nbs
+ } else {
+ vec![]
+ };
+ if _matches.is_present("all") {
+ issues_for_lint::run_all(&filter);
+ } else {
+ let name = _matches.value_of("name").expect("checked by clap");
+ issues_for_lint::run(&name, &filter);
+ }
+ }
+}
+
fn print_lints() {
let lint_list = gather_all();
let usable_lints: Vec = Lint::usable_lints(lint_list).collect();