big-code-analysis 2.1.0

Tool to compute and export code metrics
Documentation
//! `Npa` implementation for Go.
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
#![allow(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss
)]

use super::*;

/// The `(declared, exported)` attribute counts one `field_declaration`
/// contributes: named fields carry one or more `FieldIdentifier` children
/// (`A, b int` → two names), while an embedded field carries none and
/// contributes the embedded type's base name. `exported` applies Go's
/// lexical export rule to each of those names.
fn go_count_declaration_names(declaration: &Node, code: &[u8]) -> (usize, usize) {
    use Go as G;

    let mut names = declaration
        .children()
        .filter(|c| matches!(c.kind_id().into(), G::FieldIdentifier))
        .peekable();
    if names.peek().is_none() {
        let Some(name) = go_embedded_field_name(declaration) else {
            return (0, 0);
        };
        return (
            1,
            usize::from(name.utf8_text(code).is_some_and(go_is_exported)),
        );
    }
    let mut declared = 0usize;
    let mut exported = 0usize;
    for name in names {
        declared += 1;
        if name.utf8_text(code).is_some_and(go_is_exported) {
            exported += 1;
        }
    }
    (declared, exported)
}

// Go attribute counting.
//
// Go has no `class` concept; struct types declared at file scope
// (`type Foo struct { … }`) play that role. Methods live separately
// as `MethodDeclaration` nodes attached to a receiver type. Because
// `StructType` is NOT a func_space (per `Checker::is_func_space`),
// the iterator visits it with the enclosing func_space's stats
// (typically the file-level `Unit`). Each declared field NAME counts
// as one attribute: a grouped declaration `A, b int` declares two
// names (two attributes), and an embedded type (`io.Reader`, `*Foo`)
// declares one attribute whose name is the embedded type's base
// identifier (`Reader`, `Foo`).
//
// Visibility (issue #458): Go's export rule is purely lexical — an
// identifier is exported (public) iff its first character is an
// uppercase Unicode letter (`unicode.IsUpper`). `Npa::compute`
// receives the source bytes as `code`, so we read each field name's
// text and gate the public count (`class_npa`) on `go_is_exported`.
// The total count (`class_na`) stays unconditional. The blank
// identifier `_` is never exported.
//
// Limitations:
// - Struct fields are attributed to the enclosing func_space (the
//   file's `Unit`, or a local function space for `type T struct{…}`
//   declared inside a function body). Multiple structs at the same
//   level contribute to the same `class_na` bucket. This mirrors the
//   Rust impl's "fields land on the enclosing space" approach.
// - Interface methods (`interface { Foo() }`) are not attributes —
//   they are method signatures, counted by Npm under
//   `interface_nm`, not by Npa.
impl Npa for GoCode {
    fn compute<'a>(
        node: &Node<'a>,
        code: &'a [u8],
        _ancestors: Ancestors<'a, '_>,
        stats: &mut Stats,
    ) {
        use Go as G;

        if !matches!(node.kind_id().into(), G::StructType) {
            return;
        }

        // The struct body is the `field_declaration_list` direct
        // child. An empty struct (`struct{}`) has the list with no
        // FieldDeclaration children → 0 attributes.
        let Some(body) = node
            .children()
            .find(|c| matches!(c.kind_id().into(), G::FieldDeclarationList))
        else {
            return;
        };

        // Tally per declared NAME: named fields carry one or more
        // `FieldIdentifier` children (`A, b int` → two names), while
        // an embedded field carries none and contributes the embedded
        // type's base name. Public count is gated on Go's lexical
        // export rule; the total is unconditional.
        let mut total = 0usize;
        let mut public = 0usize;
        for declaration in body
            .children()
            .filter(|c| matches!(c.kind_id().into(), G::FieldDeclaration))
        {
            let (declared, exported) = go_count_declaration_names(&declaration, code);
            total += declared;
            public += exported;
        }

        stats.class_na += total;
        stats.class_npa += public;
    }
}