All checks were successful
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 4m19s
CI / Security Audit (push) Successful in 1m44s
CI / Tests (push) Successful in 5m15s
CI / Detect Changes (push) Successful in 5s
CI / Deploy Agent (push) Successful in 2s
CI / Deploy Dashboard (push) Successful in 2s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2s
252 lines
7.0 KiB
Rust
252 lines
7.0 KiB
Rust
use std::path::Path;
|
|
|
|
use compliance_core::models::{Finding, ScanType, Severity};
|
|
use compliance_core::CoreError;
|
|
use tokio::process::Command;
|
|
|
|
use crate::pipeline::dedup;
|
|
|
|
use super::run_with_timeout;
|
|
|
|
pub(super) async fn run_clippy(repo_path: &Path, repo_id: &str) -> Result<Vec<Finding>, CoreError> {
|
|
let child = Command::new("cargo")
|
|
.args([
|
|
"clippy",
|
|
"--message-format=json",
|
|
"--quiet",
|
|
"--",
|
|
"-W",
|
|
"clippy::all",
|
|
])
|
|
.current_dir(repo_path)
|
|
.env("RUSTC_WRAPPER", "")
|
|
.stdout(std::process::Stdio::piped())
|
|
.stderr(std::process::Stdio::piped())
|
|
.spawn()
|
|
.map_err(|e| CoreError::Scanner {
|
|
scanner: "clippy".to_string(),
|
|
source: Box::new(e),
|
|
})?;
|
|
|
|
let output = run_with_timeout(child, "clippy").await?;
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let mut findings = Vec::new();
|
|
|
|
for line in stdout.lines() {
|
|
let msg: serde_json::Value = match serde_json::from_str(line) {
|
|
Ok(v) => v,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
if msg.get("reason").and_then(|v| v.as_str()) != Some("compiler-message") {
|
|
continue;
|
|
}
|
|
|
|
let message = match msg.get("message") {
|
|
Some(m) => m,
|
|
None => continue,
|
|
};
|
|
|
|
let level = message.get("level").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
if level != "warning" && level != "error" {
|
|
continue;
|
|
}
|
|
|
|
let text = message
|
|
.get("message")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
let code = message
|
|
.get("code")
|
|
.and_then(|v| v.get("code"))
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
if text.starts_with("aborting due to") || code.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let (file_path, line_number) = extract_primary_span(message);
|
|
|
|
let severity = if level == "error" {
|
|
Severity::High
|
|
} else {
|
|
Severity::Low
|
|
};
|
|
|
|
let fingerprint = dedup::compute_fingerprint(&[
|
|
repo_id,
|
|
"clippy",
|
|
&code,
|
|
&file_path,
|
|
&line_number.to_string(),
|
|
]);
|
|
|
|
let mut finding = Finding::new(
|
|
repo_id.to_string(),
|
|
fingerprint,
|
|
"clippy".to_string(),
|
|
ScanType::Lint,
|
|
format!("[clippy] {text}"),
|
|
text,
|
|
severity,
|
|
);
|
|
finding.rule_id = Some(code);
|
|
if !file_path.is_empty() {
|
|
finding.file_path = Some(file_path);
|
|
}
|
|
if line_number > 0 {
|
|
finding.line_number = Some(line_number);
|
|
}
|
|
findings.push(finding);
|
|
}
|
|
|
|
Ok(findings)
|
|
}
|
|
|
|
fn extract_primary_span(message: &serde_json::Value) -> (String, u32) {
|
|
let spans = match message.get("spans").and_then(|v| v.as_array()) {
|
|
Some(s) => s,
|
|
None => return (String::new(), 0),
|
|
};
|
|
|
|
for span in spans {
|
|
if span.get("is_primary").and_then(|v| v.as_bool()) == Some(true) {
|
|
let file = span
|
|
.get("file_name")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let line = span.get("line_start").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
|
|
return (file, line);
|
|
}
|
|
}
|
|
|
|
(String::new(), 0)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn extract_primary_span_with_primary() {
|
|
let msg = serde_json::json!({
|
|
"spans": [
|
|
{
|
|
"file_name": "src/lib.rs",
|
|
"line_start": 42,
|
|
"is_primary": true
|
|
}
|
|
]
|
|
});
|
|
let (file, line) = extract_primary_span(&msg);
|
|
assert_eq!(file, "src/lib.rs");
|
|
assert_eq!(line, 42);
|
|
}
|
|
|
|
#[test]
|
|
fn extract_primary_span_no_primary() {
|
|
let msg = serde_json::json!({
|
|
"spans": [
|
|
{
|
|
"file_name": "src/lib.rs",
|
|
"line_start": 42,
|
|
"is_primary": false
|
|
}
|
|
]
|
|
});
|
|
let (file, line) = extract_primary_span(&msg);
|
|
assert_eq!(file, "");
|
|
assert_eq!(line, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn extract_primary_span_multiple_spans() {
|
|
let msg = serde_json::json!({
|
|
"spans": [
|
|
{
|
|
"file_name": "src/other.rs",
|
|
"line_start": 10,
|
|
"is_primary": false
|
|
},
|
|
{
|
|
"file_name": "src/main.rs",
|
|
"line_start": 99,
|
|
"is_primary": true
|
|
}
|
|
]
|
|
});
|
|
let (file, line) = extract_primary_span(&msg);
|
|
assert_eq!(file, "src/main.rs");
|
|
assert_eq!(line, 99);
|
|
}
|
|
|
|
#[test]
|
|
fn extract_primary_span_no_spans() {
|
|
let msg = serde_json::json!({});
|
|
let (file, line) = extract_primary_span(&msg);
|
|
assert_eq!(file, "");
|
|
assert_eq!(line, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn extract_primary_span_empty_spans() {
|
|
let msg = serde_json::json!({ "spans": [] });
|
|
let (file, line) = extract_primary_span(&msg);
|
|
assert_eq!(file, "");
|
|
assert_eq!(line, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_clippy_compiler_message_line() {
|
|
let line = r#"{"reason":"compiler-message","message":{"level":"warning","message":"unused variable","code":{"code":"unused_variables"},"spans":[{"file_name":"src/main.rs","line_start":5,"is_primary":true}]}}"#;
|
|
let msg: serde_json::Value = serde_json::from_str(line).unwrap();
|
|
|
|
assert_eq!(
|
|
msg.get("reason").and_then(|v| v.as_str()),
|
|
Some("compiler-message")
|
|
);
|
|
let message = msg.get("message").unwrap();
|
|
assert_eq!(
|
|
message.get("level").and_then(|v| v.as_str()),
|
|
Some("warning")
|
|
);
|
|
assert_eq!(
|
|
message.get("message").and_then(|v| v.as_str()),
|
|
Some("unused variable")
|
|
);
|
|
assert_eq!(
|
|
message
|
|
.get("code")
|
|
.and_then(|v| v.get("code"))
|
|
.and_then(|v| v.as_str()),
|
|
Some("unused_variables")
|
|
);
|
|
|
|
let (file, line_num) = extract_primary_span(message);
|
|
assert_eq!(file, "src/main.rs");
|
|
assert_eq!(line_num, 5);
|
|
}
|
|
|
|
#[test]
|
|
fn skip_non_compiler_message() {
|
|
let line = r#"{"reason":"build-script-executed","package_id":"foo 0.1.0"}"#;
|
|
let msg: serde_json::Value = serde_json::from_str(line).unwrap();
|
|
assert_ne!(
|
|
msg.get("reason").and_then(|v| v.as_str()),
|
|
Some("compiler-message")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn skip_aborting_message() {
|
|
let text = "aborting due to 3 previous errors";
|
|
assert!(text.starts_with("aborting due to"));
|
|
}
|
|
}
|