pub mod annotate;
#[cfg(feature = "tree-sitter")]
pub mod astutil;
#[allow(dead_code)]
pub mod cognitive;
#[allow(dead_code)]
pub mod coupling;
pub mod delta;
pub mod fabric;
pub mod gate;
pub mod naming;
pub mod persist;
pub mod report;
pub mod scan;
pub mod score;
pub use cognitive::{FunctionCognitive, cognitive_per_function};
pub use delta::{cognitive_delta, format_gate_notice, worst_regression};
pub use naming::{NamingFinding, naming_findings};
pub use scan::{ProjectHealth, scan_project};
pub use score::{Hotspot, NavigabilityInputs, NavigabilityScore, grade, navigability};
use serde::Serialize;
pub const DEFAULT_COGNITIVE_THRESHOLD: u32 = 15;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GateMode {
Off,
#[default]
Warn,
Block,
}
impl GateMode {
pub fn parse(value: &str) -> Self {
match value.trim().to_ascii_lowercase().as_str() {
"off" | "false" | "none" | "disabled" => GateMode::Off,
"block" | "hard" | "error" => GateMode::Block,
_ => GateMode::Warn,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct FileHealth {
pub functions: Vec<FunctionCognitive>,
pub naming: Vec<NamingFinding>,
}
impl FileHealth {
pub fn over_threshold(&self, threshold: u32) -> impl Iterator<Item = &FunctionCognitive> {
self.functions
.iter()
.filter(move |f| f.cognitive > threshold)
}
pub fn worst_cognitive(&self) -> u32 {
self.functions
.iter()
.map(|f| f.cognitive)
.max()
.unwrap_or(0)
}
}
pub fn analyze_file(source: &str, extension: &str) -> Option<FileHealth> {
let functions = cognitive_per_function(source, extension)?;
let naming = naming_findings(source, extension).unwrap_or_default();
Some(FileHealth { functions, naming })
}
#[cfg(all(test, feature = "tree-sitter"))]
pub mod tests {
use super::*;
#[test]
fn analyze_file_combines_signals() {
let src = "fn _xfm_q2(a: bool, b: bool) { if a { if b {} } }\n";
let health = analyze_file(src, "rs").unwrap();
assert_eq!(health.functions.len(), 1);
assert_eq!(health.worst_cognitive(), 3);
assert_eq!(health.naming.len(), 1, "cryptic name flagged");
}
#[test]
fn analyze_file_unsupported_ext_is_none() {
assert!(analyze_file("plain text", "txt").is_none());
}
#[test]
fn over_threshold_filters() {
let src = "fn deep(a: bool) { if a { if a { if a { if a {} } } } }\n";
let health = analyze_file(src, "rs").unwrap();
assert_eq!(health.over_threshold(5).count(), 1);
assert_eq!(health.over_threshold(15).count(), 0);
}
}