Skip to content

Commit d5d3a37

Browse files
committed
Implement issue finder for lint names
1 parent d13b678 commit d5d3a37

File tree

3 files changed

+208
-4
lines changed

3 files changed

+208
-4
lines changed

clippy_dev/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ regex = "1"
1111
lazy_static = "1.0"
1212
shell-escape = "0.1"
1313
walkdir = "2"
14+
reqwest = { version = "0.10", features = ["blocking", "json"], optional = true }
15+
serde = { version = "1.0", features = ["derive"], optional = true }
1416

1517
[features]
1618
deny-warnings = []
19+
issues = ["reqwest", "serde"]

clippy_dev/src/issues_for_lint.rs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use crate::gather_all;
2+
use lazy_static::lazy_static;
3+
use regex::Regex;
4+
use reqwest::{
5+
blocking::{Client, Response},
6+
header,
7+
};
8+
use serde::Deserialize;
9+
use std::env;
10+
11+
lazy_static! {
12+
static ref NEXT_PAGE_RE: Regex = Regex::new(r#"<(?P<link>[^;]+)>;\srel="next""#).unwrap();
13+
}
14+
15+
#[derive(Debug, Deserialize)]
16+
struct Issue {
17+
title: String,
18+
number: u32,
19+
body: String,
20+
pull_request: Option<PR>,
21+
}
22+
23+
#[derive(Debug, Deserialize)]
24+
struct PR {}
25+
26+
enum Error {
27+
Reqwest(reqwest::Error),
28+
Env(std::env::VarError),
29+
Http(header::InvalidHeaderValue),
30+
}
31+
32+
impl From<reqwest::Error> for Error {
33+
fn from(err: reqwest::Error) -> Self {
34+
Self::Reqwest(err)
35+
}
36+
}
37+
38+
impl From<std::env::VarError> for Error {
39+
fn from(err: std::env::VarError) -> Self {
40+
Self::Env(err)
41+
}
42+
}
43+
44+
impl From<header::InvalidHeaderValue> for Error {
45+
fn from(err: header::InvalidHeaderValue) -> Self {
46+
Self::Http(err)
47+
}
48+
}
49+
50+
impl std::fmt::Display for Error {
51+
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
52+
match self {
53+
Self::Reqwest(err) => write!(fmt, "reqwest: {}", err),
54+
Self::Env(err) => write!(fmt, "env: {}", err),
55+
Self::Http(err) => write!(fmt, "http: {}", err),
56+
}
57+
}
58+
}
59+
60+
pub fn run(name: &str, filter: &[u32]) {
61+
match open_issues() {
62+
Ok(issues) => {
63+
for (i, issue) in filter_issues(&issues, name, filter).enumerate() {
64+
if i == 0 {
65+
println!("### `{}`\n", name);
66+
}
67+
println!("- [ ] #{} ({})", issue.number, issue.title)
68+
}
69+
},
70+
Err(err) => eprintln!("{}", err),
71+
}
72+
}
73+
74+
pub fn run_all(filter: &[u32]) {
75+
match open_issues() {
76+
Ok(issues) => {
77+
let mut lint_names = gather_all().map(|lint| lint.name).collect::<Vec<_>>();
78+
lint_names.sort();
79+
for name in lint_names {
80+
let mut print_empty_line = false;
81+
for (i, issue) in filter_issues(&issues, &name, filter).enumerate() {
82+
if i == 0 {
83+
println!("### `{}`\n", name);
84+
print_empty_line = true;
85+
}
86+
println!("- [ ] #{} ({})", issue.number, issue.title)
87+
}
88+
if print_empty_line {
89+
println!();
90+
}
91+
}
92+
},
93+
Err(err) => eprintln!("{}", err),
94+
}
95+
}
96+
97+
fn open_issues() -> Result<Vec<Issue>, Error> {
98+
let github_token = env::var("GITHUB_TOKEN")?;
99+
100+
let mut headers = header::HeaderMap::new();
101+
headers.insert(
102+
header::AUTHORIZATION,
103+
header::HeaderValue::from_str(&format!("token {}", github_token))?,
104+
);
105+
headers.insert(header::USER_AGENT, header::HeaderValue::from_static("ghost"));
106+
let client = Client::builder().default_headers(headers).build()?;
107+
108+
let issues_base = "https://api.github.com/repos/rust-lang/rust-clippy/issues";
109+
110+
let mut issues = vec![];
111+
let mut response = client
112+
.get(issues_base)
113+
.query(&[("per_page", "100"), ("state", "open"), ("direction", "asc")])
114+
.send()?;
115+
while let Some(link) = next_link(&response) {
116+
issues.extend(
117+
response
118+
.json::<Vec<Issue>>()?
119+
.into_iter()
120+
.filter(|i| i.pull_request.is_none()),
121+
);
122+
response = client.get(&link).send()?;
123+
}
124+
125+
Ok(issues)
126+
}
127+
128+
fn filter_issues<'a>(issues: &'a [Issue], name: &str, filter: &'a [u32]) -> impl Iterator<Item = &'a Issue> {
129+
let name = name.to_lowercase();
130+
let separated_name = name.chars().map(|c| if c == '_' { ' ' } else { c }).collect::<String>();
131+
let dash_separated_name = name.chars().map(|c| if c == '_' { '-' } else { c }).collect::<String>();
132+
133+
issues.iter().filter(move |i| {
134+
let title = i.title.to_lowercase();
135+
let body = i.body.to_lowercase();
136+
!filter.contains(&i.number)
137+
&& (title.contains(&name)
138+
|| title.contains(&separated_name)
139+
|| title.contains(&dash_separated_name)
140+
|| body.contains(&name)
141+
|| body.contains(&separated_name)
142+
|| body.contains(&dash_separated_name))
143+
})
144+
}
145+
146+
fn next_link(response: &Response) -> Option<String> {
147+
if let Some(links) = response.headers().get("Link").and_then(|l| l.to_str().ok()) {
148+
if let Some(cap) = NEXT_PAGE_RE.captures_iter(links).next() {
149+
return Some(cap["link"].to_string());
150+
}
151+
}
152+
153+
None
154+
}

clippy_dev/src/main.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#![cfg_attr(feature = "deny-warnings", deny(warnings))]
22

3-
use clap::{App, Arg, SubCommand};
3+
use clap::{App, Arg, ArgMatches, SubCommand};
44
use clippy_dev::*;
55

66
mod fmt;
7+
#[cfg(feature = "issues")]
8+
mod issues_for_lint;
79
mod stderr_length_check;
810

911
#[derive(PartialEq)]
@@ -13,7 +15,7 @@ enum UpdateMode {
1315
}
1416

1517
fn main() {
16-
let matches = App::new("Clippy developer tooling")
18+
let mut app = App::new("Clippy developer tooling")
1719
.subcommand(
1820
SubCommand::with_name("fmt")
1921
.about("Run rustfmt on all projects and tests")
@@ -55,8 +57,31 @@ fn main() {
5557
Arg::with_name("limit-stderr-length")
5658
.long("limit-stderr-length")
5759
.help("Ensures that stderr files do not grow longer than a certain amount of lines."),
58-
)
59-
.get_matches();
60+
);
61+
if cfg!(feature = "issues") {
62+
app = app.subcommand(
63+
SubCommand::with_name("issues_for_lint")
64+
.about(
65+
"Prints all issues where the specified lint is mentioned either in the title or in the description",
66+
)
67+
.arg(
68+
Arg::with_name("name")
69+
.short("n")
70+
.long("name")
71+
.help("The name of the lint")
72+
.takes_value(true)
73+
.required_unless("all"),
74+
)
75+
.arg(Arg::with_name("all").long("all").help("Create a list for all lints"))
76+
.arg(
77+
Arg::with_name("filter")
78+
.long("filter")
79+
.takes_value(true)
80+
.help("Comma separated list of issue numbers, that should be filtered out"),
81+
),
82+
);
83+
}
84+
let matches = app.get_matches();
6085

6186
if matches.is_present("limit-stderr-length") {
6287
stderr_length_check::check();
@@ -75,10 +100,32 @@ fn main() {
75100
update_lints(&UpdateMode::Change);
76101
}
77102
},
103+
("issues_for_lint", Some(matches)) => issues_for_lint(matches),
78104
_ => {},
79105
}
80106
}
81107

108+
fn issues_for_lint(_matches: &ArgMatches<'_>) {
109+
#[cfg(feature = "issues")]
110+
{
111+
let filter = if let Some(filter) = _matches.value_of("filter") {
112+
let mut issue_nbs = vec![];
113+
for nb in filter.split(',') {
114+
issue_nbs.push(nb.trim().parse::<u32>().expect("only numbers are allowed as filter"));
115+
}
116+
issue_nbs
117+
} else {
118+
vec![]
119+
};
120+
if _matches.is_present("all") {
121+
issues_for_lint::run_all(&filter);
122+
} else {
123+
let name = _matches.value_of("name").expect("checked by clap");
124+
issues_for_lint::run(&name, &filter);
125+
}
126+
}
127+
}
128+
82129
fn print_lints() {
83130
let lint_list = gather_all();
84131
let usable_lints: Vec<Lint> = Lint::usable_lints(lint_list).collect();

0 commit comments

Comments
 (0)