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())
);
}
}