about summary refs log tree commit diff
diff options
context:
space:
mode:
authorxFrednet <xFrednet@gmail.com>2024-07-10 00:13:33 +0200
committerxFrednet <xFrednet@gmail.com>2024-07-18 15:28:14 +0200
commit0e3d19799ccd2279ac3f1e672d954566ce2e77df (patch)
tree7cbf3f70a0efdbd5bcc61b31c91157efd34a569d
parent601a61fe276a92bbde6e74d60fc493e064677d9d (diff)
downloadrust-0e3d19799ccd2279ac3f1e672d954566ce2e77df.tar.gz
rust-0e3d19799ccd2279ac3f1e672d954566ce2e77df.zip
Lintcheck: Construct links for sources
-rw-r--r--lintcheck/lintcheck_crates.toml2
-rw-r--r--lintcheck/src/driver.rs1
-rw-r--r--lintcheck/src/input.rs45
-rw-r--r--lintcheck/src/json.rs12
-rw-r--r--lintcheck/src/main.rs3
-rw-r--r--lintcheck/src/output.rs20
-rw-r--r--lintcheck/src/recursive.rs10
7 files changed, 83 insertions, 10 deletions
diff --git a/lintcheck/lintcheck_crates.toml b/lintcheck/lintcheck_crates.toml
index ff608e6f935..6e8e1e726d6 100644
--- a/lintcheck/lintcheck_crates.toml
+++ b/lintcheck/lintcheck_crates.toml
@@ -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'}
diff --git a/lintcheck/src/driver.rs b/lintcheck/src/driver.rs
index 041be5081f2..2fda2b00f87 100644
--- a/lintcheck/src/driver.rs
+++ b/lintcheck/src/driver.rs
@@ -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());
diff --git a/lintcheck/src/input.rs b/lintcheck/src/input.rs
index 8e048615f43..5aa261d7f6d 100644
--- a/lintcheck/src/input.rs
+++ b/lintcheck/src/input.rs
@@ -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 {
@@ -33,6 +37,39 @@ 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
@@ -41,6 +78,7 @@ struct TomlCrate {
 pub struct CrateWithSource {
     pub name: String,
     pub source: CrateSource,
+    pub file_link: String,
     pub options: Option<Vec<String>>,
 }
 
@@ -70,6 +108,7 @@ pub fn read_crates(toml_path: &Path) -> (Vec<CrateWithSource>, RecursiveOptions)
                 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 {
@@ -78,6 +117,7 @@ pub fn read_crates(toml_path: &Path) -> (Vec<CrateWithSource>, RecursiveOptions)
                 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() {
@@ -88,6 +128,7 @@ pub fn read_crates(toml_path: &Path) -> (Vec<CrateWithSource>, RecursiveOptions)
                     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 {
@@ -141,6 +182,7 @@ impl CrateWithSource {
         }
         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);
@@ -173,6 +215,7 @@ impl CrateWithSource {
                     name: name.clone(),
                     path: extract_dir.join(format!("{name}-{version}/")),
                     options: options.clone(),
+                    base_url: file_link.clone(),
                 }
             },
             CrateSource::Git { url, commit } => {
@@ -214,6 +257,7 @@ impl CrateWithSource {
                     name: name.clone(),
                     path: repo_path,
                     options: options.clone(),
+                    base_url: file_link.clone(),
                 }
             },
             CrateSource::Path { path } => {
@@ -253,6 +297,7 @@ impl CrateWithSource {
                     name: name.clone(),
                     path: dest_crate_root,
                     options: options.clone(),
+                    base_url: file_link.clone(),
                 }
             },
         }
diff --git a/lintcheck/src/json.rs b/lintcheck/src/json.rs
index 1a652927988..410f19f27b8 100644
--- a/lintcheck/src/json.rs
+++ b/lintcheck/src/json.rs
@@ -11,6 +11,7 @@ struct LintJson {
     lint: String,
     file_name: String,
     byte_pos: (u32, u32),
+    file_link: String,
     rendered: String,
 }
 
@@ -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(),
             }
@@ -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)]) {
@@ -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};
 
@@ -80,8 +84,8 @@ fn print_changed_diff(changed: &[(LintJson, LintJson)]) {
                 },
             }
         }
+        println!("```");
     }
-    println!("```");
 }
 
 pub(crate) fn diff(old_path: &Path, new_path: &Path) {
diff --git a/lintcheck/src/main.rs b/lintcheck/src/main.rs
index e85d70f11ed..ba4ced4e469 100644
--- a/lintcheck/src/main.rs
+++ b/lintcheck/src/main.rs
@@ -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 {
@@ -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)
diff --git a/lintcheck/src/output.rs b/lintcheck/src/output.rs
index 4bfc554ef9e..2fd09242f8a 100644
--- a/lintcheck/src/output.rs
+++ b/lintcheck/src/output.rs
@@ -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")
@@ -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 {
diff --git a/lintcheck/src/recursive.rs b/lintcheck/src/recursive.rs
index 373ca6f9918..6817d917b93 100644
--- a/lintcheck/src/recursive.rs
+++ b/lintcheck/src/recursive.rs
@@ -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)
@@ -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();