debtmap 0.18.0

Code complexity and technical debt analyzer
Documentation
//! Go analyzer implementation.

use crate::analyzers::Analyzer;
use crate::analyzers::go::orchestration::analyze_go_file;
use crate::analyzers::go::parser::parse_source;
use crate::core::ast::Ast;
use crate::core::{ComplexityMetrics, FileMetrics, Language};
use anyhow::Result;
use std::path::PathBuf;
use tracing::{debug, debug_span};

pub struct GoAnalyzer {
    complexity_threshold: u32,
}

impl GoAnalyzer {
    pub fn new() -> Self {
        Self {
            complexity_threshold: 10,
        }
    }

    pub fn with_threshold(mut self, threshold: u32) -> Self {
        self.complexity_threshold = threshold;
        self
    }
}

impl Default for GoAnalyzer {
    fn default() -> Self {
        Self::new()
    }
}

impl Analyzer for GoAnalyzer {
    fn parse(&self, content: &str, path: PathBuf) -> Result<Ast> {
        let _span = debug_span!("parse_go_file", path = %path.display()).entered();
        let go_ast = parse_source(content, &path)?;

        debug!(
            path = %path.display(),
            bytes = content.len(),
            "Parsed Go file"
        );

        Ok(Ast::Go(go_ast))
    }

    fn analyze(&self, ast: &Ast) -> FileMetrics {
        match ast {
            Ast::Go(go_ast) => analyze_go_file(go_ast, self.complexity_threshold),
            _ => empty_go_metrics(),
        }
    }

    fn language(&self) -> Language {
        Language::Go
    }
}

fn empty_go_metrics() -> FileMetrics {
    FileMetrics {
        path: PathBuf::new(),
        language: Language::Go,
        complexity: ComplexityMetrics::default(),
        debt_items: vec![],
        dependencies: vec![],
        duplications: vec![],
        total_lines: 0,
        module_scope: None,
        classes: None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::analyzers::get_analyzer;

    #[test]
    fn test_go_analyzer_language() {
        let analyzer = GoAnalyzer::new();
        assert_eq!(analyzer.language(), Language::Go);
    }

    #[test]
    fn test_go_analyzer_factory() {
        let analyzer = get_analyzer(Language::Go);
        assert_eq!(analyzer.language(), Language::Go);
    }

    #[test]
    fn test_analyze_simple_go_file() {
        let analyzer = GoAnalyzer::new();
        let source = r#"package main

import "fmt"

func main() {
    fmt.Println("hello")
}
"#;
        let ast = analyzer.parse(source, PathBuf::from("main.go")).unwrap();
        let metrics = analyzer.analyze(&ast);

        assert_eq!(metrics.language, Language::Go);
        assert_eq!(metrics.dependencies[0].name, "fmt");
        assert_eq!(metrics.complexity.functions.len(), 1);
        assert_eq!(metrics.complexity.functions[0].name, "main");
    }

    #[test]
    fn test_analyze_go_method_visibility_and_tests() {
        let analyzer = GoAnalyzer::new();
        let source = r#"package service

func (h *Handler) Serve() {}
func TestServe(t *testing.T) {}
"#;
        let ast = analyzer
            .parse(source, PathBuf::from("handler_test.go"))
            .unwrap();
        let metrics = analyzer.analyze(&ast);

        assert_eq!(metrics.complexity.functions[0].name, "Handler.Serve");
        assert_eq!(
            metrics.complexity.functions[0].visibility,
            Some("public".to_string())
        );
        assert!(metrics.complexity.functions[1].is_test);
    }

    #[test]
    fn test_analyze_go_complexity_debt() {
        let analyzer = GoAnalyzer::new().with_threshold(2);
        let source = r#"package service

func decide(a, b bool) int {
    if a || b {
        return 1
    }
    for i := 0; i < 3; i++ {
        if b {
            return i
        }
    }
    return 0
}
"#;
        let ast = analyzer.parse(source, PathBuf::from("service.go")).unwrap();
        let metrics = analyzer.analyze(&ast);

        assert!(metrics.complexity.functions[0].cyclomatic > 2);
        assert!(!metrics.debt_items.is_empty());
    }

    #[test]
    fn test_go_test_function_skips_complexity_debt() {
        let analyzer = GoAnalyzer::new().with_threshold(1);
        let source = r#"package service

func TestDecide(t *testing.T) {
    if true {
        t.Fatal("failed")
    }
}
"#;
        let ast = analyzer
            .parse(source, PathBuf::from("service_test.go"))
            .unwrap();
        let metrics = analyzer.analyze(&ast);

        assert!(metrics.complexity.functions[0].is_test);
        assert!(metrics.debt_items.is_empty());
    }

    #[test]
    fn test_generated_go_file_suppresses_debt() {
        let analyzer = GoAnalyzer::new().with_threshold(1);
        let source = r#"// Code generated by protoc. DO NOT EDIT.
package api

func Generated(a bool) int {
    if a {
        return 1
    }
    return 0
}
"#;
        let ast = analyzer
            .parse(source, PathBuf::from("service.pb.go"))
            .unwrap();
        let metrics = analyzer.analyze(&ast);

        assert_eq!(metrics.complexity.functions.len(), 1);
        assert!(metrics.debt_items.is_empty());
    }

    #[test]
    fn test_go_analyzer_records_raw_call_dependencies() {
        let analyzer = GoAnalyzer::new();
        let source = r#"package service

func Serve() {
    helper()
    fmt.Println("hello")
}

func helper() {}
"#;
        let ast = analyzer.parse(source, PathBuf::from("service.go")).unwrap();
        let metrics = analyzer.analyze(&ast);
        let serve = metrics
            .complexity
            .functions
            .iter()
            .find(|function| function.name == "Serve")
            .unwrap();

        assert_eq!(
            serve.call_dependencies,
            Some(vec!["helper".to_string(), "fmt.Println".to_string()])
        );
    }

    #[test]
    fn test_go_purity_signals() {
        let analyzer = GoAnalyzer::new();
        let source = r#"package service

func add(a int) int {
    return a + 1
}

func run(ch chan int) {
    go add(1)
    ch <- 1
}
"#;
        let ast = analyzer.parse(source, PathBuf::from("service.go")).unwrap();
        let metrics = analyzer.analyze(&ast);
        let add = metrics
            .complexity
            .functions
            .iter()
            .find(|function| function.name == "add")
            .unwrap();
        let run = metrics
            .complexity
            .functions
            .iter()
            .find(|function| function.name == "run")
            .unwrap();

        assert_eq!(add.is_pure, Some(true));
        assert_eq!(run.is_pure, Some(false));
        assert!(
            run.detected_patterns
                .as_ref()
                .unwrap()
                .contains(&"go-statement".to_string())
        );
    }
}