big-code-analysis 2.0.0

Tool to compute and export code metrics
Documentation
#![allow(
    clippy::enum_glob_use,
    clippy::too_many_lines,
    clippy::wildcard_imports
)]
#![allow(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss
)]

use super::{Abc, Stats, for_each_named_child};
use crate::macros::go_bool_terminal_kinds;
use crate::*;

// Go ABC unary-conditional walker (issue #403; see `rust_inspect_container`
// for the cross-language rationale). Terminal-bool kinds include calls,
// selector access (`r.Field`), index access (`xs[i]`), and type
// assertions (`x.(*T)`) — every kind whose evaluated value is implicitly
// boolean in idiomatic Go for `if` / `for` conditions.
fn go_inspect_container(container_node: &Node, conditions: &mut f64) {
    use Go as G;

    let mut node = *container_node;
    let mut node_kind = node.kind_id().into();
    let Some(parent) = node.parent() else { return };
    let mut has_boolean_content = matches!(
        parent.kind_id().into(),
        G::BinaryExpression | G::IfStatement | G::ForStatement
    );

    loop {
        let is_parens = matches!(node_kind, G::ParenthesizedExpression);
        let is_not = matches!(node_kind, G::UnaryExpression)
            && node.child(0).is_some_and(|c| c.kind_id() == G::BANG as u16);

        if !is_parens && !is_not {
            break;
        }
        if !has_boolean_content && is_not {
            has_boolean_content = true;
        }

        let Some(child) = node.child(1) else { break };
        node = child;
        node_kind = node.kind_id().into();

        if matches!(node_kind, go_bool_terminal_kinds!()) {
            if has_boolean_content {
                *conditions += 1.;
            }
            break;
        }
    }
}

// Phase-2B (issue #403): condition-slot dispatcher for Go.
fn go_count_condition(condition: &Node, conditions: &mut f64) {
    use Go as G;
    let kind = condition.kind_id().into();
    if matches!(kind, go_bool_terminal_kinds!()) {
        *conditions += 1.;
    } else if matches!(kind, G::ParenthesizedExpression | G::UnaryExpression) {
        go_inspect_container(condition, conditions);
    }
}

fn go_count_unary_conditions(list_node: &Node, conditions: &mut f64) {
    use Go as G;

    let list_kind = list_node.kind_id().into();
    let mut cursor = list_node.cursor();

    if cursor.goto_first_child() {
        loop {
            let node = cursor.node();
            let node_kind = node.kind_id().into();

            if matches!(node_kind, go_bool_terminal_kinds!())
                && matches!(list_kind, G::BinaryExpression)
            {
                *conditions += 1.;
            } else if node.is_named() {
                go_inspect_container(&node, conditions);
            }

            if !cursor.goto_next_sibling() {
                break;
            }
        }
    }
}

impl Abc for GoCode {
    fn compute<'a>(node: &Node<'a>, _code: &'a [u8], stats: &mut Stats) {
        // Aliased because `Go::Go` (the `go` keyword variant) collides
        // with the bare enum name in pattern position under
        // `use Go::*;` (same workaround as in cyclomatic / cognitive).
        use Go as G;

        match node.kind_id().into() {
            // Plain `=`, augmented `+=`, `-=`, … all parse as
            // `assignment_statement`. `:=` is a short variable
            // declaration. `x++` / `x--` rebind too.
            G::AssignmentStatement | G::ShortVarDeclaration | G::IncStatement | G::DecStatement => {
                stats.assignments += 1.;
            }
            // Every call expression — including method calls
            // (`r.Method()` parses as `call_expression` whose callee is
            // a `selector_expression`) — contributes one branch.
            // Composite literals (`Point{X: 1}`) are NOT calls.
            G::CallExpression => {
                stats.branches += 1.;
            }
            // Comparison operators emitted as token children of a
            // `binary_expression`, `else`, and each non-default switch
            // / type-switch / select arm all contribute one condition.
            // `<` / `>` double as type-argument delimiters in generic
            // instantiations (`f[T any]`, `List[int]`); the
            // `BinaryExpression` parent guard filters those out
            // without inspecting siblings. `default_case` is
            // intentionally excluded — like Java / C# `default:`, it
            // does not introduce a new decision point.
            G::EQEQ
            | G::BANGEQ
            | G::LTEQ
            | G::GTEQ
            | G::Else
            | G::ExpressionCase
            | G::TypeCase
            | G::CommunicationCase => {
                stats.conditions += 1.;
            }
            G::LT | G::GT
                if node
                    .parent()
                    .is_some_and(|p| matches!(p.kind_id().into(), G::BinaryExpression)) =>
            {
                stats.conditions += 1.;
            }
            // Fitzpatrick Rule 7: each operand of a `&&` / `||` chain is
            // one condition (issue #403). The walker iterates immediate
            // children of the parent `binary_expression`.
            G::AMPAMP | G::PIPEPIPE => {
                if let Some(parent) = node.parent() {
                    go_count_unary_conditions(&parent, &mut stats.conditions);
                }
            }
            // Phase-2B (issue #403): Rule 6 / 7 condition slots.
            // `if true {}` / `if !a {}` / `if (a) {}` count once.
            // Use `child_by_field_name("condition")` so the
            // `if x := f(); x { ... }` init-statement form is
            // handled correctly — its `condition` field is at
            // child(2) (not child(1), which is the init slot).
            G::IfStatement => {
                if let Some(cond) = node.child_by_field_name("condition") {
                    go_count_condition(&cond, &mut stats.conditions);
                }
            }
            // Phase-2B follow-up (findings.md #1): Go's `for` is its
            // only loop with a bare-condition slot. Children:
            //   `for cond {}`           → child(1) = condition
            //   `for init; cond; post`  → child(1) = `for_clause`
            //   `for range items {}`    → child(1) = `range_clause`
            // `go_count_condition` filters non-terminal /
            // non-paren / non-unary kinds, so `for_clause` and
            // `range_clause` fall through harmlessly without a
            // dedicated guard.
            G::ForStatement => {
                if let Some(cond) = node.child(1) {
                    go_count_condition(&cond, &mut stats.conditions);
                }
            }
            // `return value` — Go wraps the return values in an
            // `expression_list` at child(1). Iterate the list's
            // children and route each through `inspect_container`
            // (NOT the terminal-at-top form): `return !x` counts
            // the wrapped Identifier once, while `return x` (bare
            // identifier in the return slot) reports zero
            // conditions. Matches Java's policy in
            // `java_return_without_conditions`. Bare `return`
            // (no values) has no child(1).
            G::ReturnStatement => {
                if let Some(expr_list) = node.child(1) {
                    for_each_named_child(&expr_list, &mut stats.conditions, go_inspect_container);
                }
            }
            // Method-argument-list walker for `f(!a, !b)`. Two
            // aliases — `argument_list` is emitted as ArgumentList
            // or ArgumentList2 depending on production rule path.
            G::ArgumentList | G::ArgumentList2 => {
                go_count_unary_conditions(node, &mut stats.conditions);
            }
            _ => {}
        }
    }
}