Skip to content

Commit b31bce4

Browse files
committed
Auto merge of #13104 - xFrednet:00000-lintcheck-better-md-links, r=Alexendoo
Lintcheck: Add URL to lint emission place in diff This PR adds links to the emission code in our lintcheck CI. When reviewing changes, I would like to be able to see the bigger context, which isn't always included in the lint message. This PR adds a nice link to the lintcheck diff that allows for simple investigation of the code in question. At first, I wanted to simply use the doc.rs links and call it a day, but then I figured out that some crates might have their source files remapped. Cargo was the crate that made me notice this. As a response, I made the link configurable. (See rust-lang/docs.rs#2551 for a detailed explanation and possible solution to remove this workaround in the future.) It's probably easiest to review the individual commits. The last one is just a dummy to showcase the change. [:framed_picture: rendered :framed_picture:](https://github.com/rust-lang/rust-clippy/actions/runs/9960834924?pr=13104) --- r? `@Alexendoo` changelog: none --- That's it, I hope that everyone who's reading this has a beautiful day :D
2 parents bc2feea + 0e3d197 commit b31bce4

File tree

7 files changed

+116
-52
lines changed

7 files changed

+116
-52
lines changed

lintcheck/lintcheck_crates.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[crates]
22
# some of these are from cargotest
3-
cargo = {name = "cargo", version = '0.64.0'}
3+
cargo = {name = "cargo", version = '0.64.0', online_link = 'https://docs.rs/cargo/{version}/src/{file}.html#{line}'}
44
iron = {name = "iron", version = '0.6.1'}
55
ripgrep = {name = "ripgrep", version = '12.1.1'}
66
xsv = {name = "xsv", version = '0.13.0'}

lintcheck/src/driver.rs

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::{env, mem};
1111
fn run_clippy(addr: &str) -> Option<i32> {
1212
let driver_info = DriverInfo {
1313
package_name: env::var("CARGO_PKG_NAME").ok()?,
14+
version: env::var("CARGO_PKG_VERSION").ok()?,
1415
};
1516

1617
let mut stream = BufReader::new(TcpStream::connect(addr).unwrap());

lintcheck/src/input.rs

+76-34
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ use walkdir::{DirEntry, WalkDir};
1010

1111
use crate::{Crate, LINTCHECK_DOWNLOADS, LINTCHECK_SOURCES};
1212

13+
const DEFAULT_DOCS_LINK: &str = "https://docs.rs/{krate}/{version}/src/{krate}/{file}.html#{line}";
14+
const DEFAULT_GITHUB_LINK: &str = "{url}/blob/{hash}/src/{file}#L{line}";
15+
const DEFAULT_PATH_LINK: &str = "{path}/src/{file}:{line}";
16+
1317
/// List of sources to check, loaded from a .toml file
1418
#[derive(Debug, Deserialize)]
1519
pub struct SourceList {
@@ -33,32 +37,60 @@ struct TomlCrate {
3337
git_hash: Option<String>,
3438
path: Option<String>,
3539
options: Option<Vec<String>>,
40+
/// Magic values:
41+
/// * `{krate}` will be replaced by `self.name`
42+
/// * `{version}` will be replaced by `self.version`
43+
/// * `{url}` will be replaced with `self.git_url`
44+
/// * `{hash}` will be replaced with `self.git_hash`
45+
/// * `{path}` will be replaced with `self.path`
46+
/// * `{file}` will be replaced by the path after `src/`
47+
/// * `{line}` will be replaced by the line
48+
///
49+
/// If unset, this will be filled by [`read_crates`] since it depends on
50+
/// the source.
51+
online_link: Option<String>,
52+
}
53+
54+
impl TomlCrate {
55+
fn file_link(&self, default: &str) -> String {
56+
let mut link = self.online_link.clone().unwrap_or_else(|| default.to_string());
57+
link = link.replace("{krate}", &self.name);
58+
59+
if let Some(version) = &self.version {
60+
link = link.replace("{version}", version);
61+
}
62+
if let Some(url) = &self.git_url {
63+
link = link.replace("{url}", url);
64+
}
65+
if let Some(hash) = &self.git_hash {
66+
link = link.replace("{hash}", hash);
67+
}
68+
if let Some(path) = &self.path {
69+
link = link.replace("{path}", path);
70+
}
71+
link
72+
}
3673
}
3774

3875
/// Represents an archive we download from crates.io, or a git repo, or a local repo/folder
3976
/// Once processed (downloaded/extracted/cloned/copied...), this will be translated into a `Crate`
77+
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Ord, PartialOrd)]
78+
pub struct CrateWithSource {
79+
pub name: String,
80+
pub source: CrateSource,
81+
pub file_link: String,
82+
pub options: Option<Vec<String>>,
83+
}
84+
4085
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Ord, PartialOrd)]
4186
pub enum CrateSource {
42-
CratesIo {
43-
name: String,
44-
version: String,
45-
options: Option<Vec<String>>,
46-
},
47-
Git {
48-
name: String,
49-
url: String,
50-
commit: String,
51-
options: Option<Vec<String>>,
52-
},
53-
Path {
54-
name: String,
55-
path: PathBuf,
56-
options: Option<Vec<String>>,
57-
},
87+
CratesIo { version: String },
88+
Git { url: String, commit: String },
89+
Path { path: PathBuf },
5890
}
5991

6092
/// Read a `lintcheck_crates.toml` file
61-
pub fn read_crates(toml_path: &Path) -> (Vec<CrateSource>, RecursiveOptions) {
93+
pub fn read_crates(toml_path: &Path) -> (Vec<CrateWithSource>, RecursiveOptions) {
6294
let toml_content: String =
6395
fs::read_to_string(toml_path).unwrap_or_else(|_| panic!("Failed to read {}", toml_path.display()));
6496
let crate_list: SourceList =
@@ -71,23 +103,32 @@ pub fn read_crates(toml_path: &Path) -> (Vec<CrateSource>, RecursiveOptions) {
71103
let mut crate_sources = Vec::new();
72104
for tk in tomlcrates {
73105
if let Some(ref path) = tk.path {
74-
crate_sources.push(CrateSource::Path {
106+
crate_sources.push(CrateWithSource {
75107
name: tk.name.clone(),
76-
path: PathBuf::from(path),
108+
source: CrateSource::Path {
109+
path: PathBuf::from(path),
110+
},
111+
file_link: tk.file_link(DEFAULT_PATH_LINK),
77112
options: tk.options.clone(),
78113
});
79114
} else if let Some(ref version) = tk.version {
80-
crate_sources.push(CrateSource::CratesIo {
115+
crate_sources.push(CrateWithSource {
81116
name: tk.name.clone(),
82-
version: version.to_string(),
117+
source: CrateSource::CratesIo {
118+
version: version.to_string(),
119+
},
120+
file_link: tk.file_link(DEFAULT_DOCS_LINK),
83121
options: tk.options.clone(),
84122
});
85123
} else if tk.git_url.is_some() && tk.git_hash.is_some() {
86124
// otherwise, we should have a git source
87-
crate_sources.push(CrateSource::Git {
125+
crate_sources.push(CrateWithSource {
88126
name: tk.name.clone(),
89-
url: tk.git_url.clone().unwrap(),
90-
commit: tk.git_hash.clone().unwrap(),
127+
source: CrateSource::Git {
128+
url: tk.git_url.clone().unwrap(),
129+
commit: tk.git_hash.clone().unwrap(),
130+
},
131+
file_link: tk.file_link(DEFAULT_GITHUB_LINK),
91132
options: tk.options.clone(),
92133
});
93134
} else {
@@ -117,7 +158,7 @@ pub fn read_crates(toml_path: &Path) -> (Vec<CrateSource>, RecursiveOptions) {
117158
(crate_sources, crate_list.recursive)
118159
}
119160

120-
impl CrateSource {
161+
impl CrateWithSource {
121162
/// Makes the sources available on the disk for clippy to check.
122163
/// Clones a git repo and checks out the specified commit or downloads a crate from crates.io or
123164
/// copies a local folder
@@ -139,8 +180,11 @@ impl CrateSource {
139180
retries += 1;
140181
}
141182
}
142-
match self {
143-
CrateSource::CratesIo { name, version, options } => {
183+
let name = &self.name;
184+
let options = &self.options;
185+
let file_link = &self.file_link;
186+
match &self.source {
187+
CrateSource::CratesIo { version } => {
144188
let extract_dir = PathBuf::from(LINTCHECK_SOURCES);
145189
let krate_download_dir = PathBuf::from(LINTCHECK_DOWNLOADS);
146190

@@ -171,14 +215,10 @@ impl CrateSource {
171215
name: name.clone(),
172216
path: extract_dir.join(format!("{name}-{version}/")),
173217
options: options.clone(),
218+
base_url: file_link.clone(),
174219
}
175220
},
176-
CrateSource::Git {
177-
name,
178-
url,
179-
commit,
180-
options,
181-
} => {
221+
CrateSource::Git { url, commit } => {
182222
let repo_path = {
183223
let mut repo_path = PathBuf::from(LINTCHECK_SOURCES);
184224
// add a -git suffix in case we have the same crate from crates.io and a git repo
@@ -217,9 +257,10 @@ impl CrateSource {
217257
name: name.clone(),
218258
path: repo_path,
219259
options: options.clone(),
260+
base_url: file_link.clone(),
220261
}
221262
},
222-
CrateSource::Path { name, path, options } => {
263+
CrateSource::Path { path } => {
223264
fn is_cache_dir(entry: &DirEntry) -> bool {
224265
fs::read(entry.path().join("CACHEDIR.TAG"))
225266
.map(|x| x.starts_with(b"Signature: 8a477f597d28d172789f06886806bc55"))
@@ -256,6 +297,7 @@ impl CrateSource {
256297
name: name.clone(),
257298
path: dest_crate_root,
258299
options: options.clone(),
300+
base_url: file_link.clone(),
259301
}
260302
},
261303
}

lintcheck/src/json.rs

+8-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ struct LintJson {
1111
lint: String,
1212
file_name: String,
1313
byte_pos: (u32, u32),
14+
file_link: String,
1415
rendered: String,
1516
}
1617

@@ -29,6 +30,7 @@ pub(crate) fn output(clippy_warnings: Vec<ClippyWarning>) -> String {
2930
LintJson {
3031
file_name: span.file_name.clone(),
3132
byte_pos: (span.byte_start, span.byte_end),
33+
file_link: warning.url,
3234
lint: warning.lint,
3335
rendered: warning.diag.rendered.unwrap(),
3436
}
@@ -50,11 +52,12 @@ fn print_warnings(title: &str, warnings: &[LintJson]) {
5052
}
5153

5254
println!("### {title}");
53-
println!("```");
5455
for warning in warnings {
56+
println!("{title} `{}` at {}", warning.lint, warning.file_link);
57+
println!("```");
5558
print!("{}", warning.rendered);
59+
println!("```");
5660
}
57-
println!("```");
5861
}
5962

6063
fn print_changed_diff(changed: &[(LintJson, LintJson)]) {
@@ -63,8 +66,9 @@ fn print_changed_diff(changed: &[(LintJson, LintJson)]) {
6366
}
6467

6568
println!("### Changed");
66-
println!("```diff");
6769
for (old, new) in changed {
70+
println!("Changed `{}` at {}", new.lint, new.file_link);
71+
println!("```diff");
6872
for change in diff::lines(&old.rendered, &new.rendered) {
6973
use diff::Result::{Both, Left, Right};
7074

@@ -80,8 +84,8 @@ fn print_changed_diff(changed: &[(LintJson, LintJson)]) {
8084
},
8185
}
8286
}
87+
println!("```");
8388
}
84-
println!("```");
8589
}
8690

8791
pub(crate) fn diff(old_path: &Path, new_path: &Path) {

lintcheck/src/main.rs

+4-9
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
3838
use std::{env, fs};
3939

4040
use cargo_metadata::Message;
41-
use input::{read_crates, CrateSource};
41+
use input::read_crates;
4242
use output::{ClippyCheckOutput, ClippyWarning, RustcIce};
4343
use rayon::prelude::*;
4444

@@ -53,6 +53,7 @@ struct Crate {
5353
// path to the extracted sources that clippy can check
5454
path: PathBuf,
5555
options: Option<Vec<String>>,
56+
base_url: String,
5657
}
5758

5859
impl Crate {
@@ -185,7 +186,7 @@ impl Crate {
185186
// get all clippy warnings and ICEs
186187
let mut entries: Vec<ClippyCheckOutput> = Message::parse_stream(stdout.as_bytes())
187188
.filter_map(|msg| match msg {
188-
Ok(Message::CompilerMessage(message)) => ClippyWarning::new(message.message),
189+
Ok(Message::CompilerMessage(message)) => ClippyWarning::new(message.message, &self.base_url),
189190
_ => None,
190191
})
191192
.map(ClippyCheckOutput::ClippyWarning)
@@ -292,13 +293,7 @@ fn lintcheck(config: LintcheckConfig) {
292293
.into_iter()
293294
.filter(|krate| {
294295
if let Some(only_one_crate) = &config.only {
295-
let name = match krate {
296-
CrateSource::CratesIo { name, .. }
297-
| CrateSource::Git { name, .. }
298-
| CrateSource::Path { name, .. } => name,
299-
};
300-
301-
name == only_one_crate
296+
krate.name == *only_one_crate
302297
} else {
303298
true
304299
}

lintcheck/src/output.rs

+17-3
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,12 @@ impl RustcIce {
5353
pub struct ClippyWarning {
5454
pub lint: String,
5555
pub diag: Diagnostic,
56+
/// The URL that points to the file and line of the lint emission
57+
pub url: String,
5658
}
5759

58-
#[allow(unused)]
5960
impl ClippyWarning {
60-
pub fn new(mut diag: Diagnostic) -> Option<Self> {
61+
pub fn new(mut diag: Diagnostic, base_url: &str) -> Option<Self> {
6162
let lint = diag.code.clone()?.code;
6263
if !(lint.contains("clippy") || diag.message.contains("clippy"))
6364
|| diag.message.contains("could not read cargo metadata")
@@ -69,7 +70,20 @@ impl ClippyWarning {
6970
let rendered = diag.rendered.as_mut().unwrap();
7071
*rendered = strip_ansi_escapes::strip_str(&rendered);
7172

72-
Some(Self { lint, diag })
73+
let span = diag.spans.iter().find(|span| span.is_primary).unwrap();
74+
let file = &span.file_name;
75+
let url = if let Some(src_split) = file.find("/src/") {
76+
// This removes the inital `target/lintcheck/sources/<crate>-<version>/`
77+
let src_split = src_split + "/src/".len();
78+
let (_, file) = file.split_at(src_split);
79+
80+
let line_no = span.line_start;
81+
base_url.replace("{file}", file).replace("{line}", &line_no.to_string())
82+
} else {
83+
file.clone()
84+
};
85+
86+
Some(Self { lint, diag, url })
7387
}
7488

7589
pub fn span(&self) -> &DiagnosticSpan {

lintcheck/src/recursive.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use serde::{Deserialize, Serialize};
2020
#[derive(Debug, Eq, Hash, PartialEq, Clone, Serialize, Deserialize)]
2121
pub(crate) struct DriverInfo {
2222
pub package_name: String,
23+
pub version: String,
2324
}
2425

2526
pub(crate) fn serialize_line<T, W>(value: &T, writer: &mut W)
@@ -61,10 +62,17 @@ fn process_stream(
6162
let mut stderr = String::new();
6263
stream.read_to_string(&mut stderr).unwrap();
6364

65+
// It's 99% likely that dependencies compiled with recursive mode are on crates.io
66+
// and therefore on docs.rs. This links to the sources directly, do avoid invalid
67+
// links due to remaped paths. See rust-lang/docs.rs#2551 for more details.
68+
let base_url = format!(
69+
"https://docs.rs/crate/{}/{}/source/src/{{file}}#{{line}}",
70+
driver_info.package_name, driver_info.version
71+
);
6472
let messages = stderr
6573
.lines()
6674
.filter_map(|json_msg| serde_json::from_str::<Diagnostic>(json_msg).ok())
67-
.filter_map(ClippyWarning::new);
75+
.filter_map(|diag| ClippyWarning::new(diag, &base_url));
6876

6977
for message in messages {
7078
sender.send(message).unwrap();

0 commit comments

Comments
 (0)