Skip to content

Commit

Permalink
Implement issue finder for lint names
Browse files Browse the repository at this point in the history
  • Loading branch information
flip1995 committed Jan 15, 2020
1 parent d13b678 commit d5d3a37
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 4 deletions.
3 changes: 3 additions & 0 deletions clippy_dev/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
154 changes: 154 additions & 0 deletions clippy_dev/src/issues_for_lint.rs
Original file line number Diff line number Diff line change
@@ -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<link>[^;]+)>;\srel="next""#).unwrap();
}

#[derive(Debug, Deserialize)]
struct Issue {
title: String,
number: u32,
body: String,
pull_request: Option<PR>,
}

#[derive(Debug, Deserialize)]
struct PR {}

enum Error {
Reqwest(reqwest::Error),
Env(std::env::VarError),
Http(header::InvalidHeaderValue),
}

impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
Self::Reqwest(err)
}
}

impl From<std::env::VarError> for Error {
fn from(err: std::env::VarError) -> Self {
Self::Env(err)
}
}

impl From<header::InvalidHeaderValue> 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::<Vec<_>>();
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<Vec<Issue>, 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::<Vec<Issue>>()?
.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<Item = &'a Issue> {
let name = name.to_lowercase();
let separated_name = name.chars().map(|c| if c == '_' { ' ' } else { c }).collect::<String>();
let dash_separated_name = name.chars().map(|c| if c == '_' { '-' } else { c }).collect::<String>();

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<String> {
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
}
55 changes: 51 additions & 4 deletions clippy_dev/src/main.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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")
Expand Down Expand Up @@ -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();
Expand All @@ -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::<u32>().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> = Lint::usable_lints(lint_list).collect();
Expand Down

0 comments on commit d5d3a37

Please sign in to comment.