1use crate::db::GraphDb;
20use crate::repograph::facts::{
21 commits_of, evidence_line, int_prop, label_of, list_prop, neighbors, neighbors_both,
22 owner_name, rank, score_of, str_prop, symbol_file,
23};
24use crate::repograph::map::SYNC_KEY;
25use crate::repograph::render::sanitize;
26use crate::Direction;
27use core_storage::fs::Fs;
28use core_storage::Value;
29use serde::Serialize;
30use std::path::Path;
31
32pub const MAX_SOURCE_LINES: usize = 80;
34const MAX_CALLS: usize = 8;
36const MAX_IMPORTS: usize = 8;
38const MAX_PARTNERS: usize = 6;
40const MAX_COMMITS: usize = 5;
42const MAX_NOTES: usize = 3;
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
47#[serde(rename_all = "snake_case")]
48pub enum Target {
49 File {
50 path: String,
51 },
52 Symbol {
53 key: String,
54 },
55 Unknown {
58 target: String,
59 },
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize)]
64pub struct ContextReport {
65 pub target: Target,
66 pub candidates: Vec<String>,
69 pub signature: Option<String>,
70 pub doc: Option<String>,
71 pub lines: Option<(u32, u32)>,
73 pub source: Option<String>,
76 pub file: String,
78 pub owner: Option<String>,
80 pub callers: Vec<(String, u32)>,
82 pub callees: Vec<(String, u32)>,
84 pub importers: Vec<String>,
85 pub imports: Vec<String>,
86 pub partners: Vec<(String, f64)>,
88 pub recent_commits: Vec<(String, i64, String)>,
90 pub notes: Vec<(String, String)>,
92 pub concepts: Vec<(String, String)>,
94}
95
96impl ContextReport {
97 fn empty(target: Target) -> Self {
99 Self {
100 target,
101 candidates: Vec::new(),
102 signature: None,
103 doc: None,
104 lines: None,
105 source: None,
106 file: String::new(),
107 owner: None,
108 callers: Vec::new(),
109 callees: Vec::new(),
110 importers: Vec::new(),
111 imports: Vec::new(),
112 partners: Vec::new(),
113 recent_commits: Vec::new(),
114 notes: Vec::new(),
115 concepts: Vec::new(),
116 }
117 }
118}
119
120#[must_use]
127pub fn context<F: Fs>(db: &GraphDb<F>, repo: Option<&Path>, target: &str) -> ContextReport {
128 match resolve(db, target) {
129 Resolved::File(path) => {
130 let mut report = ContextReport::empty(Target::File {
131 path: sanitize(&path),
132 });
133 let symbols = neighbors(db, &path, "DEFINES", Direction::In);
134 report.callers = callers_of(db, &symbols, &path);
135 report.callees = callees_of(db, &symbols, &path);
136 report.source = read_source(db, repo, &path, None);
137 fill_file(db, &mut report, &path);
138 report.notes = notes_about(db, &[path]);
139 report
140 }
141 Resolved::Symbol(key) => {
142 let mut report = ContextReport::empty(Target::Symbol {
143 key: sanitize(&key),
144 });
145 report.signature = text_prop(db, &key, "signature");
148 report.doc = text_prop(db, &key, "doc");
149 report.lines = symbol_lines(db, &key);
150 report.callers = callers_of(db, std::slice::from_ref(&key), "");
151 report.callees = callees_of(db, std::slice::from_ref(&key), "");
152 let file = symbol_file(db, &key).unwrap_or_default();
153 report.source = read_source(db, repo, &file, report.lines);
154 fill_file(db, &mut report, &file);
155 report.notes = notes_about(db, &[key, file]);
156 report
157 }
158 Resolved::Ambiguous(candidates) => {
159 let mut report = ContextReport::empty(Target::Unknown {
160 target: sanitize(target),
161 });
162 report.candidates = candidates;
163 report
164 }
165 Resolved::Unknown => ContextReport::empty(Target::Unknown {
166 target: sanitize(target),
167 }),
168 }
169}
170
171enum Resolved {
173 File(String),
174 Symbol(String),
175 Ambiguous(Vec<String>),
177 Unknown,
178}
179
180fn resolve<F: Fs>(db: &GraphDb<F>, target: &str) -> Resolved {
183 match label_of(db, target).as_deref() {
184 Some("File") => return Resolved::File(target.to_string()),
185 Some("Symbol") => return Resolved::Symbol(target.to_string()),
186 _ => {}
190 }
191 let mut named = named_symbols(db, target);
192 match named.len() {
193 0 => Resolved::Unknown,
194 1 => Resolved::Symbol(named.remove(0)),
195 _ => Resolved::Ambiguous(named),
196 }
197}
198
199fn named_symbols<F: Fs>(db: &GraphDb<F>, name: &str) -> Vec<String> {
205 let mut out: Vec<String> = db
206 .nodes_with_label("Symbol")
207 .iter()
208 .filter(|n| matches!(n.prop("name"), Some(Value::Str(s)) if s == name))
209 .map(|n| sanitize(n.key()))
210 .collect();
211 out.sort();
212 out
213}
214
215fn text_prop<F: Fs>(db: &GraphDb<F>, key: &str, field: &str) -> Option<String> {
218 str_prop(db, key, field)
219 .map(|s| sanitize(&s))
220 .filter(|s| !s.trim().is_empty())
221}
222
223fn symbol_lines<F: Fs>(db: &GraphDb<F>, key: &str) -> Option<(u32, u32)> {
225 let start = u32::try_from(int_prop(db, key, "line_start")?).ok()?;
226 let end = u32::try_from(int_prop(db, key, "line_end")?).ok()?;
227 Some((start, end.max(start)))
228}
229
230fn callers_of<F: Fs>(
236 db: &GraphDb<F>,
237 symbols: &[String],
238 exclude_file: &str,
239) -> Vec<(String, u32)> {
240 let mut out: Vec<(String, u32)> = Vec::new();
241 for symbol in symbols {
242 for caller in neighbors(db, symbol, "CALLS", Direction::In) {
243 if !exclude_file.is_empty() && symbol_file(db, &caller).as_deref() == Some(exclude_file)
244 {
245 continue;
246 }
247 let line = evidence_line(&list_prop(db, &caller, "call_lines"), symbol).unwrap_or(0);
248 out.push((sanitize(&caller), line));
249 }
250 }
251 out.sort();
252 out.dedup();
253 out.truncate(MAX_CALLS);
254 out
255}
256
257fn callees_of<F: Fs>(
259 db: &GraphDb<F>,
260 symbols: &[String],
261 exclude_file: &str,
262) -> Vec<(String, u32)> {
263 let mut out: Vec<(String, u32)> = Vec::new();
264 for symbol in symbols {
265 let lines = list_prop(db, symbol, "call_lines");
266 for callee in neighbors(db, symbol, "CALLS", Direction::Out) {
267 if !exclude_file.is_empty() && symbol_file(db, &callee).as_deref() == Some(exclude_file)
268 {
269 continue;
270 }
271 out.push((
272 sanitize(&callee),
273 evidence_line(&lines, &callee).unwrap_or(0),
274 ));
275 }
276 }
277 out.sort();
278 out.dedup();
279 out.truncate(MAX_CALLS);
280 out
281}
282
283fn fill_file<F: Fs>(db: &GraphDb<F>, report: &mut ContextReport, file: &str) {
286 report.file = sanitize(file);
287 if file.is_empty() {
288 return;
289 }
290 report.owner = owner_name(db, file).map(|n| sanitize(&n));
291 report.importers = neighbors(db, file, "IMPORTS", Direction::In)
292 .iter()
293 .take(MAX_IMPORTS)
294 .map(|k| sanitize(k))
295 .collect();
296 report.imports = neighbors(db, file, "IMPORTS", Direction::Out)
297 .iter()
298 .take(MAX_IMPORTS)
299 .map(|k| sanitize(k))
300 .collect();
301
302 let mut partners: Vec<(String, f64)> = neighbors_both(db, file, "CO_CHANGED")
303 .into_iter()
304 .map(|other| {
305 let score = score_of(db, "CO_CHANGED", file, &other).unwrap_or(0.0);
306 (sanitize(&other), score)
307 })
308 .collect();
309 rank(&mut partners);
310 partners.truncate(MAX_PARTNERS);
311 report.partners = partners;
312
313 report.recent_commits = commits_of(db, file)
314 .into_iter()
315 .take(MAX_COMMITS)
316 .map(|c| (sanitize(&c.sha), c.ts, sanitize(&c.subject)))
317 .collect();
318
319 report.concepts = neighbors(db, file, "DESCRIBED_IN", Direction::In)
320 .iter()
321 .take(MAX_NOTES)
322 .map(|key| {
323 let name = str_prop(db, key, "name").unwrap_or_else(|| key.clone());
324 (sanitize(key), sanitize(&name))
325 })
326 .collect();
327}
328
329fn notes_about<F: Fs>(db: &GraphDb<F>, keys: &[String]) -> Vec<(String, String)> {
331 let mut out: Vec<(String, String)> = Vec::new();
332 for key in keys.iter().filter(|k| !k.is_empty()) {
333 for note in neighbors(db, key, "ABOUT", Direction::In) {
334 let text = str_prop(db, ¬e, "text").unwrap_or_default();
335 out.push((sanitize(¬e), sanitize(&text)));
336 }
337 }
338 out.sort();
339 out.dedup();
340 out.truncate(MAX_NOTES);
341 out
342}
343
344fn inside_repo(root: &Path, file: &str) -> Option<std::path::PathBuf> {
362 let rel = Path::new(file);
363 if rel
364 .components()
365 .any(|c| !matches!(c, std::path::Component::Normal(_)))
366 {
367 return None;
368 }
369 let real_root = root.canonicalize().ok()?;
370 let real = real_root.join(rel).canonicalize().ok()?;
371 real.starts_with(&real_root).then_some(real)
372}
373
374fn read_source<F: Fs>(
383 db: &GraphDb<F>,
384 repo: Option<&Path>,
385 file: &str,
386 lines: Option<(u32, u32)>,
387) -> Option<String> {
388 if file.is_empty() {
389 return None;
390 }
391 let root = match repo {
392 Some(p) => p.to_path_buf(),
393 None => std::path::PathBuf::from(str_prop(db, SYNC_KEY, "repo")?),
394 };
395 let text = std::fs::read_to_string(inside_repo(&root, file)?).ok()?;
396 let (first, last) = lines.unwrap_or((1, u32::MAX));
397 let skip = first.saturating_sub(1) as usize;
398 let take = (last.saturating_sub(first) as usize).saturating_add(1);
399 let excerpt: Vec<&str> = text
400 .lines()
401 .skip(skip)
402 .take(take.min(MAX_SOURCE_LINES))
403 .collect();
404 (!excerpt.is_empty()).then(|| excerpt.join("\n"))
405}