Skip to content

Commit 156dd4a

Browse files
committed
Implement issue finder for lint names
1 parent a6f310e commit 156dd4a

File tree

3 files changed

+208
-4
lines changed

3 files changed

+208
-4
lines changed

clippy_dev/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ regex = "1"
1212
lazy_static = "1.0"
1313
shell-escape = "0.1"
1414
walkdir = "2"
15+
reqwest = { version = "0.10", features = ["blocking", "json"], optional = true }
16+
serde = { version = "1.0", features = ["derive"], optional = true }
1517

1618
[features]
1719
deny-warnings = []
20+
issues = ["reqwest", "serde"]

clippy_dev/src/issues_for_lint.rs

+154
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

+51-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
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
use std::path::Path;
66

77
mod fmt;
8+
#[cfg(feature = "issues")]
9+
mod issues_for_lint;
810
mod new_lint;
911
mod stderr_length_check;
1012

@@ -15,7 +17,7 @@ enum UpdateMode {
1517
}
1618

1719
fn main() {
18-
let matches = App::new("Clippy developer tooling")
20+
let mut app = App::new("Clippy developer tooling")
1921
.subcommand(
2022
SubCommand::with_name("fmt")
2123
.about("Run rustfmt on all projects and tests")
@@ -98,8 +100,31 @@ fn main() {
98100
Arg::with_name("limit-stderr-length")
99101
.long("limit-stderr-length")
100102
.help("Ensures that stderr files do not grow longer than a certain amount of lines."),
101-
)
102-
.get_matches();
103+
);
104+
if cfg!(feature = "issues") {
105+
app = app.subcommand(
106+
SubCommand::with_name("issues_for_lint")
107+
.about(
108+
"Prints all issues where the specified lint is mentioned either in the title or in the description",
109+
)
110+
.arg(
111+
Arg::with_name("name")
112+
.short("n")
113+
.long("name")
114+
.help("The name of the lint")
115+
.takes_value(true)
116+
.required_unless("all"),
117+
)
118+
.arg(Arg::with_name("all").long("all").help("Create a list for all lints"))
119+
.arg(
120+
Arg::with_name("filter")
121+
.long("filter")
122+
.takes_value(true)
123+
.help("Comma separated list of issue numbers, that should be filtered out"),
124+
),
125+
);
126+
}
127+
let matches = app.get_matches();
103128

104129
if matches.is_present("limit-stderr-length") {
105130
stderr_length_check::check();
@@ -128,10 +153,32 @@ fn main() {
128153
Err(e) => eprintln!("Unable to create lint: {}", e),
129154
}
130155
},
156+
("issues_for_lint", Some(matches)) => issues_for_lint(matches),
131157
_ => {},
132158
}
133159
}
134160

161+
fn issues_for_lint(_matches: &ArgMatches<'_>) {
162+
#[cfg(feature = "issues")]
163+
{
164+
let filter = if let Some(filter) = _matches.value_of("filter") {
165+
let mut issue_nbs = vec![];
166+
for nb in filter.split(',') {
167+
issue_nbs.push(nb.trim().parse::<u32>().expect("only numbers are allowed as filter"));
168+
}
169+
issue_nbs
170+
} else {
171+
vec![]
172+
};
173+
if _matches.is_present("all") {
174+
issues_for_lint::run_all(&filter);
175+
} else {
176+
let name = _matches.value_of("name").expect("checked by clap");
177+
issues_for_lint::run(&name, &filter);
178+
}
179+
}
180+
}
181+
135182
fn print_lints() {
136183
let lint_list = gather_all();
137184
let usable_lints: Vec<Lint> = Lint::usable_lints(lint_list).collect();

0 commit comments

Comments
 (0)