arborist-metrics 0.1.3

Multi-language code complexity metrics (cognitive, cyclomatic, SLOC) powered by tree-sitter
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2026 Strange Days Tech S.A.S. de C.V. <https://strangedays.tech>

use crate::languages::LanguageProfile;
use tree_sitter::Node;

/// Compute cyclomatic complexity for a function body.
///
/// Starts at 1 (base path). Each decision point adds +1:
/// if, else if, for, while, do-while, match/switch arm,
/// catch/except, &&, ||, ternary operator.
///
/// Note: `else` is NOT a decision point and does not increment cyclomatic complexity.
pub fn compute_cyclomatic(node: &Node, source: &[u8], profile: &dyn LanguageProfile) -> u64 {
    let mut complexity: u64 = 1; // base path
    walk_cyclomatic(node, source, profile, &mut complexity);
    complexity
}

fn walk_cyclomatic(
    node: &Node,
    _source: &[u8],
    profile: &dyn LanguageProfile,
    complexity: &mut u64,
) {
    let kind = node.kind();
    let control_flow = profile.control_flow_nodes();
    let boolean_ops = profile.boolean_operators();
    let match_constructs = profile.match_construct_nodes();
    let match_arms = profile.match_arm_nodes();

    // Count control flow nodes as decision points, but NOT any else clauses
    // and NOT match/switch constructs (those are counted per-arm instead).
    if control_flow.contains(&kind) && !is_any_else(kind) && !match_constructs.contains(&kind) {
        *complexity += 1;
    }

    // Each match/switch arm is a separate decision point (SonarSource/McCabe).
    if match_arms.contains(&kind) {
        *complexity += 1;
    }

    if boolean_ops.contains(&kind) {
        *complexity += 1;
    }

    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        walk_cyclomatic(&child, _source, profile, complexity);
    }
}

/// Check if a node kind is any form of `else` clause.
/// No else clause is a decision point in cyclomatic complexity.
fn is_any_else(kind: &str) -> bool {
    matches!(kind, "else_clause" | "else_statement" | "else")
}