Make find dead-symbols linear and add code_only_line tests
find_dead_symbols previously re-scanned every candidate file's lines once per unique symbol name (O(names × lines)). Replace with a per-file word → line-number index built once per file and shared across all names, bounded to tokens that are actual symbol names. Also add unit coverage for code_only_line's string/comment blanking, including Rust raw strings with multi-hash delimiters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
f1531a64d4
commit
b5389341a1
1 file changed
+63
-15
+63
-15
@@ -260,8 +260,14 @@ pub fn find_dead_symbols(project: &Path) -> Result<Value> {
|
|||||||
for s in &symbols {
|
for s in &symbols {
|
||||||
by_name.entry(s.name.to_lowercase()).or_default().push(s);
|
by_name.entry(s.name.to_lowercase()).or_default().push(s);
|
||||||
}
|
}
|
||||||
|
// Lowercased symbol names, used to bound the per-file word index to tokens
|
||||||
|
// that actually matter (avoids indexing every word in every file).
|
||||||
|
let names: HashSet<&str> = by_name.keys().map(|k| k.as_str()).collect();
|
||||||
|
|
||||||
let mut line_cache: HashMap<String, Vec<String>> = HashMap::new();
|
// Per-file word → line-number index, built once per file and shared across
|
||||||
|
// all names. Without this, a file's lines are re-scanned once per symbol
|
||||||
|
// name — O(names × lines) instead of O(lines).
|
||||||
|
let mut word_sites: HashMap<String, HashMap<String, Vec<i64>>> = HashMap::new();
|
||||||
let mut dead: Vec<Value> = Vec::new();
|
let mut dead: Vec<Value> = Vec::new();
|
||||||
|
|
||||||
for (name_lc, syms) in &by_name {
|
for (name_lc, syms) in &by_name {
|
||||||
@@ -269,15 +275,12 @@ pub fn find_dead_symbols(project: &Path) -> Result<Value> {
|
|||||||
// contains it (a superset), narrowed by whole-word match.
|
// contains it (a superset), narrowed by whole-word match.
|
||||||
let mut sites: Vec<(String, i64)> = Vec::new();
|
let mut sites: Vec<(String, i64)> = Vec::new();
|
||||||
for rel in search::files_containing(&conn, name_lc, 100_000)? {
|
for rel in search::files_containing(&conn, name_lc, 100_000)? {
|
||||||
let lines = match line_cache.entry(rel.clone()) {
|
let index = word_sites
|
||||||
std::collections::hash_map::Entry::Occupied(e) => e.get().clone(),
|
.entry(rel.clone())
|
||||||
std::collections::hash_map::Entry::Vacant(e) => e
|
.or_insert_with(|| index_words(project, &rel, &names));
|
||||||
.insert(read_all_lines(project, &rel).unwrap_or_default())
|
if let Some(lines) = index.get(name_lc.as_str()) {
|
||||||
.clone(),
|
for &ln in lines {
|
||||||
};
|
sites.push((rel.clone(), ln));
|
||||||
for (i, line) in lines.iter().enumerate() {
|
|
||||||
if word_matches_ci(line, name_lc) && !is_import_line(line) {
|
|
||||||
sites.push((rel.clone(), (i as i64) + 1));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,10 +305,29 @@ pub fn find_dead_symbols(project: &Path) -> Result<Value> {
|
|||||||
Ok(serde_json::json!({"count": dead.len(), "symbols": dead}))
|
Ok(serde_json::json!({"count": dead.len(), "symbols": dead}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whole-word, case-insensitive match (Python/Rust names may vary in case).
|
/// Build a lowercased word → line-number index for one file, keeping only
|
||||||
fn word_matches_ci(line: &str, word: &str) -> bool {
|
/// tokens that are symbol names (from `names`) and skipping import/declaration
|
||||||
line.split(|c: char| !c.is_alphanumeric() && c != '_')
|
/// lines (which reference, but don't count as call sites).
|
||||||
.any(|tok| tok.eq_ignore_ascii_case(word))
|
fn index_words(project: &Path, rel: &str, names: &HashSet<&str>) -> HashMap<String, Vec<i64>> {
|
||||||
|
let mut m: HashMap<String, Vec<i64>> = HashMap::new();
|
||||||
|
let Ok(lines) = read_all_lines(project, rel) else {
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
if is_import_line(line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for tok in line.split(|c: char| !c.is_alphanumeric() && c != '_') {
|
||||||
|
if tok.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let lc = tok.to_lowercase();
|
||||||
|
if names.contains(lc.as_str()) {
|
||||||
|
m.entry(lc).or_default().push((i as i64) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decorator keywords that wire a symbol to an external framework. Such symbols
|
/// Decorator keywords that wire a symbol to an external framework. Such symbols
|
||||||
@@ -828,7 +850,7 @@ fn raw_to_nodes(node: &RawNode, base: &str, depth: Option<usize>) -> Vec<TreeNod
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::is_entry_point;
|
use super::{code_only_line, is_entry_point};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn entry_point_is_language_scoped() {
|
fn entry_point_is_language_scoped() {
|
||||||
@@ -849,4 +871,30 @@ mod tests {
|
|||||||
assert!(is_entry_point("tests/integration.rs", "rust"));
|
assert!(is_entry_point("tests/integration.rs", "rust"));
|
||||||
assert!(is_entry_point("src/bin/tool.rs", "rust"));
|
assert!(is_entry_point("src/bin/tool.rs", "rust"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn code_only_line_blanks_strings_and_comments() {
|
||||||
|
// Code references survive.
|
||||||
|
assert!(code_only_line("let x = foo();").contains("foo"));
|
||||||
|
assert!(code_only_line("let s = \"x\"; foo();").contains("foo"));
|
||||||
|
|
||||||
|
// String literals are blanked.
|
||||||
|
assert!(!code_only_line("let x = \"foo\";").contains("foo"));
|
||||||
|
assert!(!code_only_line("let x = 'foo';").contains("foo"));
|
||||||
|
assert!(!code_only_line("let x = `foo`;").contains("foo"));
|
||||||
|
assert!(!code_only_line("let x = \"\"\"foo\"\"\";").contains("foo"));
|
||||||
|
|
||||||
|
// Rust raw strings, including multi-hash delimiters.
|
||||||
|
assert!(!code_only_line("let x = r#\"...foo...\"#;").contains("foo"));
|
||||||
|
assert!(!code_only_line("let x = r##\"...foo...\"##;").contains("foo"));
|
||||||
|
assert!(!code_only_line("let x = br#\"foo\"#;").contains("foo"));
|
||||||
|
|
||||||
|
// Comments (whole-line and trailing) are blanked.
|
||||||
|
assert!(!code_only_line("// foo bar").contains("foo"));
|
||||||
|
assert!(!code_only_line("# foo bar").contains("foo"));
|
||||||
|
assert!(!code_only_line("-- foo bar").contains("foo"));
|
||||||
|
assert!(!code_only_line("/* foo bar */").contains("foo"));
|
||||||
|
assert!(!code_only_line("x = 1; // foo").contains("foo"));
|
||||||
|
assert!(!code_only_line("x = 1; /* foo */ y = 2;").contains("foo"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user