lean_ctx/core/code_health/
annotate.rs1use super::cognitive::cognitive_per_function;
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ReadAnnotation {
15 pub line: usize,
17 pub name: String,
18 pub note: String,
20}
21
22pub fn annotations_for_file(source: &str, ext: &str, threshold: u32) -> Vec<ReadAnnotation> {
26 let mut out: Vec<ReadAnnotation> = Vec::new();
27 if let Some(fns) = cognitive_per_function(source, ext) {
28 for f in fns {
29 if f.cognitive > threshold {
30 out.push(ReadAnnotation {
31 line: f.line,
32 name: f.name,
33 note: format!("cc={}", f.cognitive),
34 });
35 }
36 }
37 }
38 out.sort_by(|a, b| a.line.cmp(&b.line).then_with(|| a.name.cmp(&b.name)));
39 out
40}
41
42pub fn by_name(annotations: &[ReadAnnotation]) -> HashMap<String, String> {
45 annotations
46 .iter()
47 .map(|a| (a.name.clone(), a.note.clone()))
48 .collect()
49}
50
51pub fn cognitive_for_symbol(source: &str, ext: &str, name: &str, start_line: usize) -> Option<u32> {
57 let fns = cognitive_per_function(source, ext)?;
58 fns.iter()
59 .filter(|f| f.name == name)
60 .min_by_key(|f| f.line.abs_diff(start_line))
61 .map(|f| f.cognitive)
62}
63
64#[cfg(all(test, feature = "tree-sitter"))]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn annotates_only_over_threshold() {
70 let src = "fn flat() {}\nfn deep(a: bool) { if a { if a { if a { if a {} } } } }\n";
72 let anns = annotations_for_file(src, "rs", 5);
73 assert_eq!(anns.len(), 1);
74 assert_eq!(anns[0].name, "deep");
75 assert_eq!(anns[0].note, "cc=10");
76 }
77
78 #[test]
79 fn nothing_when_under_threshold() {
80 let src = "fn small(a: bool) { if a {} }\n";
81 assert!(annotations_for_file(src, "rs", 15).is_empty());
82 }
83
84 #[test]
85 fn deterministic_across_runs() {
86 let src = "fn deep(a: bool) { if a { if a { if a { if a {} } } } }\n";
87 assert_eq!(
88 annotations_for_file(src, "rs", 5),
89 annotations_for_file(src, "rs", 5)
90 );
91 }
92
93 #[test]
94 fn by_name_lookup() {
95 let src = "fn deep(a: bool) { if a { if a { if a { if a {} } } } }\n";
96 let anns = annotations_for_file(src, "rs", 5);
97 let map = by_name(&anns);
98 assert_eq!(map.get("deep").map(String::as_str), Some("cc=10"));
99 }
100
101 #[test]
102 fn cognitive_for_symbol_reports_any_function() {
103 let src = "fn flat() {}\nfn deep(a: bool) { if a { if a { if a {} } } }\n";
104 assert_eq!(cognitive_for_symbol(src, "rs", "flat", 1), Some(0));
106 assert_eq!(cognitive_for_symbol(src, "rs", "deep", 2), Some(6));
107 assert_eq!(cognitive_for_symbol(src, "rs", "missing", 1), None);
108 }
109
110 #[test]
111 fn cognitive_for_symbol_disambiguates_by_line() {
112 let src = "fn dup(a: bool) { if a {} }\nfn other() {}\nfn dup(a: bool) { if a { if a { if a {} } } }\n";
114 assert_eq!(cognitive_for_symbol(src, "rs", "dup", 1), Some(1));
115 assert_eq!(cognitive_for_symbol(src, "rs", "dup", 3), Some(6));
116 }
117}