Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lintcheck/lintcheck_crates.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[crates]
# some of these are from cargotest
cargo = {name = "cargo", version = '0.64.0'}
cargo = {name = "cargo", version = '0.64.0', online_link = 'https://docs.rs/cargo/{version}/src/{file}.html#{line}'}
iron = {name = "iron", version = '0.6.1'}
ripgrep = {name = "ripgrep", version = '12.1.1'}
xsv = {name = "xsv", version = '0.13.0'}
Expand Down
1 change: 1 addition & 0 deletions lintcheck/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::{env, mem};
fn run_clippy(addr: &str) -> Option<i32> {
let driver_info = DriverInfo {
package_name: env::var("CARGO_PKG_NAME").ok()?,
version: env::var("CARGO_PKG_VERSION").ok()?,
};

let mut stream = BufReader::new(TcpStream::connect(addr).unwrap());
Expand Down
110 changes: 76 additions & 34 deletions lintcheck/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ use walkdir::{DirEntry, WalkDir};

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

const DEFAULT_DOCS_LINK: &str = "https://docs.rs/{krate}/{version}/src/{krate}/{file}.html#{line}";
const DEFAULT_GITHUB_LINK: &str = "{url}/blob/{hash}/src/{file}#L{line}";
const DEFAULT_PATH_LINK: &str = "{path}/src/{file}:{line}";

/// List of sources to check, loaded from a .toml file
#[derive(Debug, Deserialize)]
pub struct SourceList {
Expand All @@ -33,32 +37,60 @@ struct TomlCrate {
git_hash: Option<String>,
path: Option<String>,
options: Option<Vec<String>>,
/// Magic values:
/// * `{krate}` will be replaced by `self.name`
/// * `{version}` will be replaced by `self.version`
/// * `{url}` will be replaced with `self.git_url`
/// * `{hash}` will be replaced with `self.git_hash`
/// * `{path}` will be replaced with `self.path`
/// * `{file}` will be replaced by the path after `src/`
/// * `{line}` will be replaced by the line
///
/// If unset, this will be filled by [`read_crates`] since it depends on
/// the source.
online_link: Option<String>,
}

impl TomlCrate {
fn file_link(&self, default: &str) -> String {
let mut link = self.online_link.clone().unwrap_or_else(|| default.to_string());
link = link.replace("{krate}", &self.name);

if let Some(version) = &self.version {
link = link.replace("{version}", version);
}
if let Some(url) = &self.git_url {
link = link.replace("{url}", url);
}
if let Some(hash) = &self.git_hash {
link = link.replace("{hash}", hash);
}
if let Some(path) = &self.path {
link = link.replace("{path}", path);
}
link
}
}

/// Represents an archive we download from crates.io, or a git repo, or a local repo/folder
/// Once processed (downloaded/extracted/cloned/copied...), this will be translated into a `Crate`
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Ord, PartialOrd)]
pub struct CrateWithSource {
pub name: String,
pub source: CrateSource,
pub file_link: String,
pub options: Option<Vec<String>>,
}

#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Ord, PartialOrd)]
pub enum CrateSource {
CratesIo {
name: String,
version: String,
options: Option<Vec<String>>,
},
Git {
name: String,
url: String,
commit: String,
options: Option<Vec<String>>,
},
Path {
name: String,
path: PathBuf,
options: Option<Vec<String>>,
},
CratesIo { version: String },
Git { url: String, commit: String },
Path { path: PathBuf },
}

/// Read a `lintcheck_crates.toml` file
pub fn read_crates(toml_path: &Path) -> (Vec<CrateSource>, RecursiveOptions) {
pub fn read_crates(toml_path: &Path) -> (Vec<CrateWithSource>, RecursiveOptions) {
let toml_content: String =
fs::read_to_string(toml_path).unwrap_or_else(|_| panic!("Failed to read {}", toml_path.display()));
let crate_list: SourceList =
Expand All @@ -71,23 +103,32 @@ pub fn read_crates(toml_path: &Path) -> (Vec<CrateSource>, RecursiveOptions) {
let mut crate_sources = Vec::new();
for tk in tomlcrates {
if let Some(ref path) = tk.path {
crate_sources.push(CrateSource::Path {
crate_sources.push(CrateWithSource {
name: tk.name.clone(),
path: PathBuf::from(path),
source: CrateSource::Path {
path: PathBuf::from(path),
},
file_link: tk.file_link(DEFAULT_PATH_LINK),
options: tk.options.clone(),
});
} else if let Some(ref version) = tk.version {
crate_sources.push(CrateSource::CratesIo {
crate_sources.push(CrateWithSource {
name: tk.name.clone(),
version: version.to_string(),
source: CrateSource::CratesIo {
version: version.to_string(),
},
file_link: tk.file_link(DEFAULT_DOCS_LINK),
options: tk.options.clone(),
});
} else if tk.git_url.is_some() && tk.git_hash.is_some() {
// otherwise, we should have a git source
crate_sources.push(CrateSource::Git {
crate_sources.push(CrateWithSource {
name: tk.name.clone(),
url: tk.git_url.clone().unwrap(),
commit: tk.git_hash.clone().unwrap(),
source: CrateSource::Git {
url: tk.git_url.clone().unwrap(),
commit: tk.git_hash.clone().unwrap(),
},
file_link: tk.file_link(DEFAULT_GITHUB_LINK),
options: tk.options.clone(),
});
} else {
Expand Down Expand Up @@ -117,7 +158,7 @@ pub fn read_crates(toml_path: &Path) -> (Vec<CrateSource>, RecursiveOptions) {
(crate_sources, crate_list.recursive)
}

impl CrateSource {
impl CrateWithSource {
/// Makes the sources available on the disk for clippy to check.
/// Clones a git repo and checks out the specified commit or downloads a crate from crates.io or
/// copies a local folder
Expand All @@ -139,8 +180,11 @@ impl CrateSource {
retries += 1;
}
}
match self {
CrateSource::CratesIo { name, version, options } => {
let name = &self.name;
let options = &self.options;
let file_link = &self.file_link;
match &self.source {
CrateSource::CratesIo { version } => {
let extract_dir = PathBuf::from(LINTCHECK_SOURCES);
let krate_download_dir = PathBuf::from(LINTCHECK_DOWNLOADS);

Expand Down Expand Up @@ -171,14 +215,10 @@ impl CrateSource {
name: name.clone(),
path: extract_dir.join(format!("{name}-{version}/")),
options: options.clone(),
base_url: file_link.clone(),
}
},
CrateSource::Git {
name,
url,
commit,
options,
} => {
CrateSource::Git { url, commit } => {
let repo_path = {
let mut repo_path = PathBuf::from(LINTCHECK_SOURCES);
// add a -git suffix in case we have the same crate from crates.io and a git repo
Expand Down Expand Up @@ -217,9 +257,10 @@ impl CrateSource {
name: name.clone(),
path: repo_path,
options: options.clone(),
base_url: file_link.clone(),
}
},
CrateSource::Path { name, path, options } => {
CrateSource::Path { path } => {
fn is_cache_dir(entry: &DirEntry) -> bool {
fs::read(entry.path().join("CACHEDIR.TAG"))
.map(|x| x.starts_with(b"Signature: 8a477f597d28d172789f06886806bc55"))
Expand Down Expand Up @@ -256,6 +297,7 @@ impl CrateSource {
name: name.clone(),
path: dest_crate_root,
options: options.clone(),
base_url: file_link.clone(),
}
},
}
Expand Down
12 changes: 8 additions & 4 deletions lintcheck/src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct LintJson {
lint: String,
file_name: String,
byte_pos: (u32, u32),
Comment on lines 12 to 13
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could remove these now since the file_link would cover it well enough

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The byte position is still used in the key() function on line 19. We could replace it with the link, but I think having it separately would make more sense. While all links currently contain line labels, there is no guarantee that they will for manually configured lints. And links also don't contain the column, which is included in this field.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since they're displayed under [lint] at [url] sections I think those making up the key makes sense. Removing information from the key has a benefit - it would mean tweaks to a span's start column/end line/end column show as a change rather than

Added `x` at y#5
...
Removed `x` at y#5

I didn't think about links without line numbers, if that happened more things would appear as changes than necessary but reasonably I don't think we'll have such links

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not critical though, either way it's ready for r+ after ditching the test commit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing information from the key has a benefit - it would mean tweaks to a span's start column/end line/end column show as a change rather than

The tweaked span would still result in a added and removed message, since the lint message displays the span and is also used as part of the span. I think here it's safer to display them separately and leave it to the reviewer to merge them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message isn't part of the key

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I mixed it up with the lint name 😅 But I'd still prefer it to be separate. Some lints might also be emitted multiple times per line.

file_link: String,
rendered: String,
}

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

println!("### {title}");
println!("```");
for warning in warnings {
println!("{title} `{}` at {}", warning.lint, warning.file_link);
println!("```");
print!("{}", warning.rendered);
println!("```");
}
println!("```");
}

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

println!("### Changed");
println!("```diff");
for (old, new) in changed {
println!("Changed `{}` at {}", new.lint, new.file_link);
println!("```diff");
for change in diff::lines(&old.rendered, &new.rendered) {
use diff::Result::{Both, Left, Right};

Expand All @@ -80,8 +84,8 @@ fn print_changed_diff(changed: &[(LintJson, LintJson)]) {
},
}
}
println!("```");
}
println!("```");
}

pub(crate) fn diff(old_path: &Path, new_path: &Path) {
Expand Down
13 changes: 4 additions & 9 deletions lintcheck/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::{env, fs};

use cargo_metadata::Message;
use input::{read_crates, CrateSource};
use input::read_crates;
use output::{ClippyCheckOutput, ClippyWarning, RustcIce};
use rayon::prelude::*;

Expand All @@ -53,6 +53,7 @@ struct Crate {
// path to the extracted sources that clippy can check
path: PathBuf,
options: Option<Vec<String>>,
base_url: String,
}

impl Crate {
Expand Down Expand Up @@ -185,7 +186,7 @@ impl Crate {
// get all clippy warnings and ICEs
let mut entries: Vec<ClippyCheckOutput> = Message::parse_stream(stdout.as_bytes())
.filter_map(|msg| match msg {
Ok(Message::CompilerMessage(message)) => ClippyWarning::new(message.message),
Ok(Message::CompilerMessage(message)) => ClippyWarning::new(message.message, &self.base_url),
_ => None,
})
.map(ClippyCheckOutput::ClippyWarning)
Expand Down Expand Up @@ -292,13 +293,7 @@ fn lintcheck(config: LintcheckConfig) {
.into_iter()
.filter(|krate| {
if let Some(only_one_crate) = &config.only {
let name = match krate {
CrateSource::CratesIo { name, .. }
| CrateSource::Git { name, .. }
| CrateSource::Path { name, .. } => name,
};

name == only_one_crate
krate.name == *only_one_crate
} else {
true
}
Expand Down
20 changes: 17 additions & 3 deletions lintcheck/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,12 @@ impl RustcIce {
pub struct ClippyWarning {
pub lint: String,
pub diag: Diagnostic,
/// The URL that points to the file and line of the lint emission
pub url: String,
}

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

Some(Self { lint, diag })
let span = diag.spans.iter().find(|span| span.is_primary).unwrap();
let file = &span.file_name;
let url = if let Some(src_split) = file.find("/src/") {
// This removes the inital `target/lintcheck/sources/<crate>-<version>/`
let src_split = src_split + "/src/".len();
let (_, file) = file.split_at(src_split);

let line_no = span.line_start;
base_url.replace("{file}", file).replace("{line}", &line_no.to_string())
} else {
file.clone()
};

Some(Self { lint, diag, url })
}

pub fn span(&self) -> &DiagnosticSpan {
Expand Down
10 changes: 9 additions & 1 deletion lintcheck/src/recursive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Eq, Hash, PartialEq, Clone, Serialize, Deserialize)]
pub(crate) struct DriverInfo {
pub package_name: String,
pub version: String,
}

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

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

for message in messages {
sender.send(message).unwrap();
Expand Down