From d5d3a373cf85fc77dff020cca1f3fe23adf5ae4e Mon Sep 17 00:00:00 2001 From: flip1995 Date: Tue, 14 Jan 2020 20:05:24 +0100 Subject: [PATCH] Implement issue finder for lint names --- clippy_dev/Cargo.toml | 3 + clippy_dev/src/issues_for_lint.rs | 154 ++++++++++++++++++++++++++++++ clippy_dev/src/main.rs | 55 ++++++++++- 3 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 clippy_dev/src/issues_for_lint.rs 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();