From 44f0b1385e6c3140b852a36051f03218d777d390 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 01:59:56 +0000 Subject: [PATCH] Improve enrichment quality: titles, test filtering, module paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/advisory.rs | 29 +++++++++++++-- src/diff_analyzer.rs | 87 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 103 insertions(+), 13 deletions(-) diff --git a/src/advisory.rs b/src/advisory.rs index 8fdf8a7..4566242 100644 --- a/src/advisory.rs +++ b/src/advisory.rs @@ -198,14 +198,17 @@ fn parse_advisory_file(path: &Path) -> Result { } } + // 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 { 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 { + // 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 +} diff --git a/src/diff_analyzer.rs b/src/diff_analyzer.rs index 3acc5b0..45dc8ba 100644 --- a/src/diff_analyzer.rs +++ b/src/diff_analyzer.rs @@ -40,6 +40,10 @@ pub fn extract_symbols(diff: &PatchDiff) -> Vec { 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 = None; @@ -136,14 +140,63 @@ pub fn extract_symbols(diff: &PatchDiff) -> Vec { } /// 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]