#[cfg(feature = "lang-ts")]
use heal_cli::observer::complexity::extract_functions;
#[cfg(any(feature = "lang-ts", feature = "lang-rust"))]
use heal_cli::observer::complexity::{analyze, parse, FunctionMetric};
#[cfg(any(feature = "lang-ts", feature = "lang-rust"))]
use heal_cli::observer::lang::Language;
#[cfg(feature = "lang-ts")]
fn analyze_ts(source: &str) -> Vec<FunctionMetric> {
let parsed = parse(source.to_string(), Language::TypeScript).expect("parse ok");
analyze(&parsed)
}
#[cfg(feature = "lang-rust")]
fn analyze_rust(source: &str) -> Vec<FunctionMetric> {
let parsed = parse(source.to_string(), Language::Rust).expect("rust parse ok");
analyze(&parsed)
}
#[cfg(any(feature = "lang-ts", feature = "lang-rust"))]
fn metric<'a>(metrics: &'a [FunctionMetric], name: &str) -> &'a FunctionMetric {
metrics
.iter()
.find(|m| m.name == name)
.unwrap_or_else(|| panic!("no metric named {name}; have {metrics:?}"))
}
#[cfg(feature = "lang-ts")]
#[test]
fn extracts_all_function_shaped_scopes() {
let source = r"
function topLevel() { return 1; }
class Box {
open() { return true; }
close() { return false; }
}
const arrow = (n: number) => n * 2;
[1, 2, 3].forEach(function (n) { console.log(n); });
";
let parsed = parse(source.to_string(), Language::TypeScript).expect("parse ok");
let scopes = extract_functions(&parsed);
let names: Vec<&str> = scopes.iter().map(|s| s.name.as_str()).collect();
assert!(
names.contains(&"topLevel"),
"missing function_declaration: {names:?}"
);
assert!(
names.contains(&"open"),
"missing first method_definition: {names:?}"
);
assert!(
names.contains(&"close"),
"missing second method_definition: {names:?}"
);
assert!(
names.contains(&"arrow"),
"missing arrow_function via variable_declarator: {names:?}"
);
assert!(
names.iter().any(|n| n.starts_with("<anonymous@")),
"missing anonymous function_expression: {names:?}"
);
assert_eq!(scopes.len(), 5, "expected exactly 5 scopes, got {scopes:?}");
}
#[cfg(feature = "lang-ts")]
#[test]
fn ccn_baseline_for_empty_function_is_one() {
let metrics = analyze_ts("function noop() {}");
let m = metric(&metrics, "noop");
assert_eq!(m.ccn, 1);
}
#[cfg(feature = "lang-ts")]
#[test]
fn ccn_sums_decision_points() {
let source = r#"
function mixed(xs: number[], flag: boolean) {
if (flag && xs.length > 0) { // if + &&
for (const x of xs) { // for
console.log(x > 0 ? "pos" : "neg"); // ternary
}
}
}
"#;
let metrics = analyze_ts(source);
let m = metric(&metrics, "mixed");
assert_eq!(m.ccn, 5, "got {m:?}");
}
#[cfg(feature = "lang-ts")]
#[test]
fn cognitive_baseline_for_straight_line_is_zero() {
let source = "function add(a: number, b: number): number { return a + b; }";
let metrics = analyze_ts(source);
let m = metric(&metrics, "add");
assert_eq!(m.cognitive, 0);
}
#[cfg(feature = "lang-ts")]
#[test]
fn cognitive_nests_with_depth() {
let source = r"
function deep(a: boolean, b: boolean, c: boolean) {
if (a) { // +1 (depth 0)
if (b) { // +2 (depth 1)
if (c) { // +3 (depth 2)
return 1;
}
}
}
return 0;
}
";
let metrics = analyze_ts(source);
let m = metric(&metrics, "deep");
assert_eq!(m.cognitive, 6, "got {m:?}");
}
#[cfg(feature = "lang-ts")]
#[test]
fn cognitive_else_if_chain_does_not_double_nest() {
let source = r#"
function classify(n: number): string {
if (n < 0) {
return "neg";
} else if (n === 0) {
return "zero";
} else if (n < 10) {
return "small";
} else {
return "large";
}
}
"#;
let metrics = analyze_ts(source);
let m = metric(&metrics, "classify");
assert_eq!(m.cognitive, 4, "got {m:?}");
}
#[cfg(feature = "lang-ts")]
#[test]
fn cognitive_logical_chain_counts_operator_switches() {
let source = "function chain(a: boolean, b: boolean, c: boolean, d: boolean) { return a && b || c && d; }";
let metrics = analyze_ts(source);
let m = metric(&metrics, "chain");
assert_eq!(m.cognitive, 3, "got {m:?}");
let source = "function same(a: boolean, b: boolean, c: boolean) { return a && b && c; }";
let metrics = analyze_ts(source);
let m = metric(&metrics, "same");
assert_eq!(m.cognitive, 1, "got {m:?}");
}
#[cfg(feature = "lang-ts")]
#[test]
fn nested_function_isolation() {
let source = r"
function outer(a: boolean, b: boolean, c: boolean) {
function inner(x: boolean, y: boolean) {
if (x) {
if (y) {
return 1;
}
}
return 0;
}
if (a) {
return inner(b, c);
}
return -1;
}
";
let metrics = analyze_ts(source);
let outer = metric(&metrics, "outer");
let inner = metric(&metrics, "inner");
assert_eq!(
outer.ccn, 2,
"outer CCN should exclude inner's decisions: {outer:?}"
);
assert_eq!(
outer.cognitive, 1,
"outer Cognitive should exclude inner: {outer:?}"
);
assert_eq!(inner.ccn, 3, "inner CCN: {inner:?}");
assert_eq!(inner.cognitive, 3, "inner Cognitive (1 + 2): {inner:?}");
}
#[cfg(feature = "lang-ts")]
#[test]
fn tsx_grammar_parses_components() {
let source = r"
type Props = { items: string[] };
function List({ items }: Props) {
if (items.length === 0) {
return <p>empty</p>;
}
return (
<ul>
{items.map((it) => (
<li key={it}>{it}</li>
))}
</ul>
);
}
";
let parsed = parse(source.to_string(), Language::Tsx).expect("tsx parse ok");
let metrics = analyze(&parsed);
let list = metric(&metrics, "List");
assert_eq!(list.ccn, 2, "if adds 1 to baseline 1: {list:?}");
assert_eq!(list.cognitive, 1, "single if at depth 0: {list:?}");
let anon = metrics
.iter()
.find(|m| m.name.starts_with("<anonymous@"))
.expect("anonymous arrow inside .map should be its own scope");
assert_eq!(anon.ccn, 1);
assert_eq!(anon.cognitive, 0);
}
#[cfg(feature = "lang-rust")]
#[test]
fn rust_ccn_baseline_for_empty_function_is_one() {
let metrics = analyze_rust("fn noop() {}");
let m = metric(&metrics, "noop");
assert_eq!(m.ccn, 1);
}
#[cfg(feature = "lang-rust")]
#[test]
fn rust_ccn_sums_decision_points() {
let source = r#"
fn mixed(xs: &[i32], flag: bool) -> Result<(), std::io::Error> {
if flag && xs.len() > 0 {
for x in xs {
std::fs::write("/tmp/x", x.to_string())?;
let kind = match *x {
0 => "zero",
n if n > 0 => "pos",
_ => "neg",
};
println!("{kind}");
}
}
Ok(())
}
"#;
let metrics = analyze_rust(source);
let m = metric(&metrics, "mixed");
assert_eq!(m.ccn, 8, "got {m:?}");
}
#[cfg(feature = "lang-rust")]
#[test]
fn rust_cognitive_baseline_for_straight_line_is_zero() {
let metrics = analyze_rust("fn add(a: i32, b: i32) -> i32 { a + b }");
let m = metric(&metrics, "add");
assert_eq!(m.cognitive, 0);
}
#[cfg(feature = "lang-rust")]
#[test]
fn rust_cognitive_nests_with_depth() {
let source = r"
fn deep(a: bool, b: bool, c: bool) -> i32 {
if a {
if b {
if c {
return 1;
}
}
}
0
}
";
let metrics = analyze_rust(source);
let m = metric(&metrics, "deep");
assert_eq!(m.cognitive, 6, "got {m:?}");
}
#[cfg(feature = "lang-rust")]
#[test]
fn rust_cognitive_else_if_chain_does_not_double_nest() {
let source = r#"
fn classify(n: i32) -> &'static str {
if n < 0 {
"neg"
} else if n == 0 {
"zero"
} else if n < 10 {
"small"
} else {
"large"
}
}
"#;
let metrics = analyze_rust(source);
let m = metric(&metrics, "classify");
assert_eq!(m.cognitive, 4, "got {m:?}");
}
#[cfg(feature = "lang-rust")]
#[test]
fn rust_cognitive_match_treats_arms_as_one_increment() {
let source = r"
fn classify(opt: Option<i32>) -> i32 {
match opt {
Some(0) => 0,
Some(n) => n,
None => {
if true {
-1
} else {
-2
}
}
}
}
";
let metrics = analyze_rust(source);
let m = metric(&metrics, "classify");
assert_eq!(m.cognitive, 4, "got {m:?}");
}
#[cfg(feature = "lang-rust")]
#[test]
fn rust_cognitive_try_operator_counts_as_flat_increment() {
let source = r"
fn read_two() -> Result<String, std::io::Error> {
let a = std::fs::read_to_string('a')?;
let b = std::fs::read_to_string(&a)?;
Ok(b)
}
";
let metrics = analyze_rust(source);
let m = metric(&metrics, "read_two");
assert_eq!(m.cognitive, 2, "got {m:?}");
}
#[cfg(feature = "lang-rust")]
#[test]
fn rust_closure_isolation_from_outer_function() {
let source = r"
fn outer(a: bool) -> i32 {
let inner = |x: i32| if x > 0 { 1 } else { 0 };
if a { inner(1) } else { inner(-1) }
}
";
let metrics = analyze_rust(source);
let outer = metric(&metrics, "outer");
assert_eq!(
outer.ccn, 2,
"outer CCN should exclude closure's decisions: {outer:?}"
);
assert_eq!(
outer.cognitive, 2,
"outer Cognitive: 1 (if) + 1 (else block) = 2: {outer:?}"
);
let inner = metric(&metrics, "inner");
assert_eq!(inner.ccn, 2);
assert_eq!(
inner.cognitive, 2,
"closure Cognitive: 1 (if) + 1 (else block) = 2: {inner:?}"
);
}
#[cfg(feature = "lang-rust")]
#[test]
fn rust_if_let_and_while_let_smoke() {
let source = r"
fn process(opt: Option<i32>) -> i32 {
let mut iter = opt.into_iter();
while let Some(x) = iter.next() {
if let Some(y) = Some(x).filter(|n| *n > 0) {
return y;
}
}
0
}
";
let metrics = analyze_rust(source);
let m = metric(&metrics, "process");
assert_eq!(m.ccn, 3, "got {m:?}");
assert_eq!(m.cognitive, 3, "got {m:?}");
}