use std::collections::BTreeMap;
use std::io;
use std::path::Path;
use crate::cache::ScanCache;
use crate::extract::Vis;
use crate::graph::Ranking;
use crate::place::Placement;
const MAX_LINES: usize = 5000;
pub fn write(
root: &Path,
cache: &ScanCache,
_placement: &Placement,
ranking: &Ranking,
) -> io::Result<usize> {
let mut lines: Vec<(String, u64)> = Vec::new();
for (rel, entry) in &cache.files {
let Some(lang) = entry.lang else { continue };
let Some(x) = cache.parses.get(&(lang, entry.hash)) else {
continue;
};
for d in &x.defs {
if d.vis != Vis::Pub {
continue;
}
let rank = ranking.name_refs.get(&d.name).copied().unwrap_or(0);
lines.push((format!("{}\t{rel}#{}", d.name, d.line), rank));
}
}
lines.sort();
lines.dedup();
let total = lines.len();
let mut kept: Vec<String> = if total > MAX_LINES {
let mut by_rank = lines.clone();
by_rank.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
let cutoff: std::collections::BTreeSet<&String> =
by_rank.iter().take(MAX_LINES).map(|(l, _)| l).collect();
lines
.iter()
.filter(|(l, _)| cutoff.contains(l))
.map(|(l, _)| l.clone())
.collect()
} else {
lines.into_iter().map(|(l, _)| l).collect()
};
let mut doc = String::from(
"# SYMBOLS - exact public-symbol index\n\
# symbol\tanchors\n",
);
if total > kept.len() {
doc.push_str(&format!(
"# note: {} lower-ranked symbols omitted - search source within the routed scope\n",
total - kept.len()
));
}
let mut grouped: BTreeMap<String, Vec<String>> = BTreeMap::new();
for line in kept.drain(..) {
if let Some((symbol, anchor)) = line.split_once('\t') {
grouped
.entry(symbol.to_string())
.or_default()
.push(anchor.to_string());
}
}
for (symbol, anchors) in grouped {
doc.push_str(&symbol);
doc.push('\t');
doc.push_str(&anchors.join(" "));
doc.push('\n');
}
let dir = crate::cache::radar_dir(root);
std::fs::create_dir_all(&dir)?;
std::fs::write(dir.join("SYMBOLS.md"), doc)?;
Ok(total)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::FileEntry;
use crate::extract::{Extraction, SymKind, Symbol};
use crate::lang::Lang;
#[test]
fn writes_sorted_exact_index() {
let dir = std::env::temp_dir().join(format!("radar-symbols-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("mkdir");
let mut cache = ScanCache::default();
let hash = [1u8; 32];
cache.files.insert(
"pkg/a.py".into(),
FileEntry {
mtime: (1, 0),
size: 1,
ino: 0,
hash,
lang: Some(Lang::Python),
},
);
cache.files.insert(
"pkg/b.py".into(),
FileEntry {
mtime: (1, 0),
size: 1,
ino: 0,
hash: [2u8; 32],
lang: Some(Lang::Python),
},
);
cache.parses.insert(
(Lang::Python, hash),
Extraction {
defs: vec![
Symbol {
line: 3,
end_line: 4,
name: "zeta".into(),
kind: SymKind::Fn,
vis: Vis::Pub,
sig: "def zeta()".into(),
terms: Vec::new(),
},
Symbol {
line: 9,
end_line: 10,
name: "alpha".into(),
kind: SymKind::Fn,
vis: Vis::Pub,
sig: "def alpha()".into(),
terms: Vec::new(),
},
Symbol {
line: 12,
end_line: 13,
name: "_hidden".into(),
kind: SymKind::Fn,
vis: Vis::Priv,
sig: "def _hidden()".into(),
terms: Vec::new(),
},
],
refs: vec![],
},
);
cache.parses.insert(
(Lang::Python, [2u8; 32]),
Extraction {
defs: vec![Symbol {
line: 2,
end_line: 3,
name: "alpha".into(),
kind: SymKind::Fn,
vis: Vis::Pub,
sig: "def alpha()".into(),
terms: Vec::new(),
}],
refs: vec![],
},
);
let placement = crate::place::place(&cache);
let ranking = crate::graph::rank(&cache);
let total = write(&dir, &cache, &placement, &ranking).expect("write");
assert_eq!(total, 3, "private symbols excluded");
let doc = std::fs::read_to_string(dir.join(".radar/SYMBOLS.md")).expect("read");
let alpha = doc.find("alpha\t").expect("alpha indexed");
let zeta = doc.find("zeta\t").expect("zeta indexed");
assert!(alpha < zeta, "sorted");
assert!(doc.contains("# symbol\tanchors"), "compact header: {doc}");
assert!(!doc.contains("\tMAP.md\t"), "map column omitted: {doc}");
assert!(
doc.contains("alpha\tpkg/a.py#9 pkg/b.py#2"),
"duplicate symbol anchors grouped: {doc}"
);
let _ = std::fs::remove_dir_all(&dir);
}
}