1
0
Fork 0
mirror of https://github.com/imjasonh/rustvulncheck synced 2026-07-08 04:08:07 +00:00

Improve enrichment quality: titles, test filtering, module paths

- Parse markdown heading as title fallback when TOML title is missing,
  fixing the "(no title)" entries
- Filter out test files (tests/, *_test.rs) from vulnerable symbols
  since test code isn't callable library API
- Fix module path generation to handle workspace layouts
  (crates/foo/src/bar.rs → foo::bar) and strip src/ properly
- Replace hyphens with underscores in module paths to match Rust
  crate naming conventions (pyo3-ffi → pyo3_ffi)

https://claude.ai/code/session_01AeNG3herWfKhos9uZVUY5d
This commit is contained in:
Claude 2026-03-25 01:59:56 +00:00
parent 3aeae98506
commit 44f0b1385e
No known key found for this signature in database
2 changed files with 103 additions and 13 deletions

View file

@ -198,14 +198,17 @@ fn parse_advisory_file(path: &Path) -> Result<Advisory> {
}
}
// Fall back to the first markdown heading if TOML title is missing
let title = file
.advisory
.title
.unwrap_or_else(|| extract_markdown_title(&content).unwrap_or_else(|| "(no title)".to_string()));
Ok(Advisory {
id: file.advisory.id,
package: file.advisory.package,
date: file.advisory.date,
title: file
.advisory
.title
.unwrap_or_else(|| "(no title)".to_string()),
title,
patched_versions: file.versions.patched,
github_urls,
})
@ -217,3 +220,21 @@ fn extract_toml_block(content: &str) -> Option<String> {
let end = content[start..].find("```")? + start;
Some(content[start..end].to_string())
}
/// Extract the first markdown heading (# Title) from the content after the TOML block.
fn extract_markdown_title(content: &str) -> Option<String> {
// Find end of TOML block, then look for first heading
let toml_end = content.find("```toml\n")?;
let after_toml_start = content[toml_end + "```toml\n".len()..].find("```")?;
let after_toml = &content[toml_end + "```toml\n".len() + after_toml_start + 3..];
for line in after_toml.lines() {
let trimmed = line.trim();
if let Some(heading) = trimmed.strip_prefix("# ") {
let title = heading.trim();
if !title.is_empty() {
return Some(title.to_string());
}
}
}
None
}

View file

@ -40,6 +40,10 @@ pub fn extract_symbols(diff: &PatchDiff) -> Vec<VulnerableSymbol> {
Regex::new(r"impl(?:<[^>]*>)?\s+(?:([a-zA-Z_][a-zA-Z0-9_:]*)\s+for\s+)?([a-zA-Z_][a-zA-Z0-9_:<>, ]*)").unwrap();
for file_patch in &diff.files {
// Skip test files — they aren't callable library code
if is_test_file(&file_patch.filename) {
continue;
}
let module_path = file_path_to_module(&file_patch.filename);
let mut current_context_fn: Option<String> = None;
@ -136,14 +140,63 @@ pub fn extract_symbols(diff: &PatchDiff) -> Vec<VulnerableSymbol> {
}
/// Convert a file path like `src/http/request.rs` to a module path like `http::request`.
///
/// Handles workspace layouts like `crates/foo/src/bar.rs` → `foo::bar`
/// and top-level `src/bar.rs` → `bar`. Replaces hyphens with underscores
/// to match Rust crate naming conventions.
fn file_path_to_module(path: &str) -> String {
let path = path
.trim_start_matches("src/")
.trim_end_matches(".rs")
.trim_end_matches("/mod")
.trim_end_matches("/lib");
// Split into components
let parts: Vec<&str> = path.split('/').collect();
path.replace('/', "::")
// Find the `src` component — everything before it is the crate prefix,
// everything after it is the module path.
let (crate_parts, mod_parts) = if let Some(src_idx) = parts.iter().position(|&p| p == "src") {
(&parts[..src_idx], &parts[src_idx + 1..])
} else {
// No src/ directory — use the whole path
(&[][..], &parts[..])
};
// Build module path from the parts after src/
let mut mod_path: Vec<&str> = mod_parts.to_vec();
// Strip .rs extension from last component and handle mod.rs / lib.rs
if let Some(last) = mod_path.last_mut() {
*last = last.trim_end_matches(".rs");
}
// Remove trailing mod or lib (e.g. src/net/mod.rs → net)
if mod_path.last() == Some(&"mod") || mod_path.last() == Some(&"lib") {
mod_path.pop();
}
// For workspace crates, use the last crate directory as prefix
// e.g. crates/algorithms/sha3/src/simd/avx2.rs → sha3::simd::avx2
let crate_name = crate_parts.last().copied().unwrap_or("");
let mut result_parts: Vec<&str> = Vec::new();
if !crate_name.is_empty() {
result_parts.push(crate_name);
}
result_parts.extend(mod_path);
// Join and replace hyphens with underscores (Rust crate convention)
result_parts.join("::").replace('-', "_")
}
/// Returns true if the file path looks like a test file.
fn is_test_file(path: &str) -> bool {
let parts: Vec<&str> = path.split('/').collect();
// Any path component named "tests" or "test"
if parts.iter().any(|&p| p == "tests" || p == "test") {
return true;
}
// Files named *_test.rs or test_*.rs
if let Some(filename) = parts.last() {
if filename.ends_with("_test.rs") || filename.starts_with("test_") {
return true;
}
}
false
}
/// Build a qualified function name from module path, optional impl type, and fn name.
@ -170,11 +223,27 @@ mod tests {
#[test]
fn test_file_path_to_module() {
assert_eq!(file_path_to_module("src/http/request.rs"), "http::request");
assert_eq!(file_path_to_module("src/lib.rs"), "lib");
assert_eq!(file_path_to_module("src/lib.rs"), "");
assert_eq!(file_path_to_module("src/net/tcp/mod.rs"), "net::tcp");
// Workspace crate paths
assert_eq!(
file_path_to_module("src/net/tcp/mod.rs"),
"net::tcp"
file_path_to_module("crates/algorithms/sha3/src/simd/avx2.rs"),
"sha3::simd::avx2"
);
// Hyphens become underscores
assert_eq!(
file_path_to_module("pyo3-ffi/src/object.rs"),
"pyo3_ffi::object"
);
}
#[test]
fn test_is_test_file() {
assert!(is_test_file("tests/integration.rs"));
assert!(is_test_file("crates/foo/tests/rfc7539.rs"));
assert!(is_test_file("src/my_test.rs"));
assert!(!is_test_file("src/lib.rs"));
assert!(!is_test_file("src/http/request.rs"));
}
#[test]