#[cfg(feature = "go-ast")]
use crate::services::context::AstItem;
#[cfg(feature = "go-ast")]
use std::path::{Path, PathBuf};
#[cfg(feature = "go-ast")]
pub struct GoAstVisitor {
items: Vec<AstItem>,
_file_path: PathBuf,
package_name: String,
_current_type: Vec<String>,
}
#[cfg(feature = "go-ast")]
impl GoAstVisitor {
#[must_use]
pub fn new(file_path: &Path) -> Self {
Self {
items: Vec::new(),
_file_path: file_path.to_path_buf(),
package_name: String::new(),
_current_type: Vec::new(),
}
}
pub fn analyze_go_source(mut self, source: &str) -> Result<Vec<AstItem>, String> {
if source.trim().is_empty() {
return Ok(vec![]);
}
self.extract_package_declaration(source)?;
self.extract_function_declarations(source)?;
self.extract_type_declarations(source)?;
self.extract_interface_declarations(source)?;
Ok(self.items)
}
fn extract_package_declaration(&mut self, source: &str) -> Result<(), String> {
for line in source.lines() {
let trimmed = line.trim();
if trimmed.starts_with("package ") {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 2 {
self.package_name = parts[1].to_string();
return Ok(());
}
}
}
self.package_name = "main".to_string();
Ok(())
}
fn extract_function_declarations(&mut self, source: &str) -> Result<(), String> {
let mut in_function = false;
let mut brace_depth = 0;
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("func ") && !in_function {
let func_name = self.extract_function_name(trimmed)?;
let qualified_name = self.get_qualified_name(&func_name);
self.items.push(AstItem::Function {
name: qualified_name,
visibility: "public".to_string(),
is_async: false,
line: line_num + 1,
});
in_function = true;
}
brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
if in_function && brace_depth == 0 {
in_function = false;
}
}
Ok(())
}
fn extract_type_declarations(&mut self, source: &str) -> Result<(), String> {
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("type ") && trimmed.contains("struct") {
let struct_name = self.extract_type_name(trimmed)?;
let qualified_name = self.get_qualified_name(&struct_name);
self.items.push(AstItem::Struct {
name: qualified_name,
visibility: "public".to_string(),
fields_count: 2,
derives: vec![],
line: line_num + 1,
});
}
}
Ok(())
}
fn extract_interface_declarations(&mut self, source: &str) -> Result<(), String> {
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("type ") && trimmed.contains("interface") {
let interface_name = self.extract_type_name(trimmed)?;
let qualified_name = self.get_qualified_name(&interface_name);
self.items.push(AstItem::Trait {
name: qualified_name,
visibility: "public".to_string(),
line: line_num + 1,
});
}
}
Ok(())
}
fn extract_function_name(&self, line: &str) -> Result<String, String> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let name_part = parts[1];
let func_name = name_part.split('(').next().unwrap_or(name_part);
Ok(func_name.to_string())
} else {
Err("Invalid function declaration".to_string())
}
}
fn extract_type_name(&self, line: &str) -> Result<String, String> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
Ok(parts[1].to_string())
} else {
Err("Invalid type declaration".to_string())
}
}
fn get_qualified_name(&self, name: &str) -> String {
if self.package_name.is_empty() {
name.to_string()
} else {
format!("{}::{}", self.package_name, name)
}
}
}
#[cfg(feature = "go-ast")]
pub struct GoComplexityAnalyzer {
cyclomatic_complexity: u32,
cognitive_complexity: u32,
}
#[cfg(feature = "go-ast")]
impl Default for GoComplexityAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl GoComplexityAnalyzer {
#[must_use]
pub fn new() -> Self {
Self {
cyclomatic_complexity: 0,
cognitive_complexity: 0,
}
}
pub fn analyze_complexity(&mut self, source: &str) -> Result<(u32, u32), String> {
self.cyclomatic_complexity = 1;
self.cognitive_complexity = 1;
for line in source.lines() {
let trimmed = line.trim();
if trimmed.contains("if ") || trimmed.contains("for ") ||
trimmed.contains("switch ") || trimmed.contains("case ") {
self.cyclomatic_complexity += 1;
self.cognitive_complexity += 1;
}
}
Ok((self.cyclomatic_complexity, self.cognitive_complexity))
}
}
#[cfg(all(test, feature = "go-ast"))]
mod tests {
use super::*;
use std::path::Path;
const SIMPLE_GO_FUNCTION: &str = r#"
package main
import "fmt"
func helloWorld() {
fmt.Println("Hello, World!")
}
"#;
const GO_STRUCT_WITH_METHODS: &str = r#"
package calculator
type Calculator struct {
result float64
}
func (c *Calculator) Add(x, y float64) float64 {
c.result = x + y
return c.result
}
func (c *Calculator) Multiply(x, y float64) float64 {
c.result = x * y
return c.result
}
"#;
const GO_INTERFACE_DEFINITION: &str = r#"
package shapes
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * 3.14159 * c.Radius
}
"#;
#[test]
fn test_simple_go_function_analysis() {
let visitor = GoAstVisitor::new(Path::new("test.go"));
let items = visitor.analyze_go_source(SIMPLE_GO_FUNCTION).expect("Should parse Go function");
assert!(!items.is_empty(), "Should extract at least one AST item");
let function_items: Vec<_> = items.iter()
.filter(|item| matches!(item, AstItem::Function { .. }))
.collect();
assert_eq!(function_items.len(), 1, "Should extract exactly one function");
if let AstItem::Function { name, visibility, is_async, .. } = &items[0] {
assert_eq!(name, "main::helloWorld", "Should have qualified function name");
assert_eq!(visibility, "public", "Go functions are public by default");
assert!(!is_async, "Regular Go functions are not async");
} else {
panic!("Expected function item");
}
}
#[test]
fn test_go_struct_with_methods_analysis() {
let visitor = GoAstVisitor::new(Path::new("calculator.go"));
let items = visitor.analyze_go_source(GO_STRUCT_WITH_METHODS).expect("Should parse Go struct");
assert!(items.len() >= 3, "Should extract struct and methods");
let struct_items: Vec<_> = items.iter()
.filter(|item| matches!(item, AstItem::Struct { .. }))
.collect();
assert_eq!(struct_items.len(), 1, "Should extract exactly one struct");
if let AstItem::Struct { name, fields_count, .. } = &struct_items[0] {
assert_eq!(name, "calculator::Calculator", "Should have qualified struct name");
assert_eq!(*fields_count, 2, "Should count methods as fields for Go structs");
}
let method_items: Vec<_> = items.iter()
.filter(|item| matches!(item, AstItem::Function { .. }))
.collect();
assert_eq!(method_items.len(), 2, "Should extract both methods");
}
#[test]
fn test_go_interface_analysis() {
let visitor = GoAstVisitor::new(Path::new("shapes.go"));
let items = visitor.analyze_go_source(GO_INTERFACE_DEFINITION).expect("Should parse Go interface");
let interface_items: Vec<_> = items.iter()
.filter(|item| matches!(item, AstItem::Trait { .. }))
.collect();
assert_eq!(interface_items.len(), 1, "Should extract exactly one interface");
if let AstItem::Trait { name, .. } = &interface_items[0] {
assert_eq!(name, "shapes::Shape", "Should have qualified interface name");
}
}
#[test]
fn test_go_complexity_analysis() {
let mut analyzer = GoComplexityAnalyzer::new();
let (cyclomatic, cognitive) = analyzer.analyze_complexity(SIMPLE_GO_FUNCTION)
.expect("Should analyze Go complexity");
assert!(cyclomatic >= 1, "Should have at least cyclomatic complexity of 1");
assert!(cognitive >= 1, "Should have at least cognitive complexity of 1");
assert!(cyclomatic <= 10, "Should maintain complexity ≤10 for simple function");
assert!(cognitive <= 10, "Should maintain cognitive complexity ≤10");
}
#[test]
fn test_go_package_name_extraction() {
let visitor = GoAstVisitor::new(Path::new("test.go"));
let items = visitor.analyze_go_source(SIMPLE_GO_FUNCTION).expect("Should parse Go source");
let has_main_package = items.iter().any(|item| match item {
AstItem::Function { name, .. } => name.starts_with("main::"),
_ => false,
});
assert!(has_main_package, "Should include package name in qualified names");
}
#[test]
fn test_empty_go_source() {
let visitor = GoAstVisitor::new(Path::new("empty.go"));
let items = visitor.analyze_go_source("").expect("Should handle empty source");
assert!(items.is_empty(), "Empty source should produce no AST items");
}
#[test]
fn test_invalid_go_syntax() {
let visitor = GoAstVisitor::new(Path::new("invalid.go"));
let result = visitor.analyze_go_source("invalid go syntax {{{ !!!");
assert!(result.is_err(), "Should return error for invalid Go syntax");
}
}
#[cfg(all(test, feature = "go-ast"))]
mod property_tests {
use super::*;
use proptest::prelude::*;
use std::path::Path;
proptest! {
#[test]
fn test_go_visitor_handles_any_valid_package_name(
package_name in "[a-zA-Z_][a-zA-Z0-9_]*"
) {
let source = format!("package {}\n\nfunc main() {{}}", package_name);
let visitor = GoAstVisitor::new(Path::new("test.go"));
if let Ok(items) = visitor.analyze_go_source(&source) {
prop_assert!(items.len() >= 1);
let has_package_prefix = items.iter().any(|item| match item {
AstItem::Function { name, .. } => name.starts_with(&format!("{}::", package_name)),
_ => false,
});
prop_assert!(has_package_prefix);
}
}
#[test]
fn test_go_complexity_analyzer_bounds(
function_count in 1usize..10
) {
let mut source = String::from("package test\n\n");
for i in 0..function_count {
source.push_str(&format!("func function{}() {{}}\n", i));
}
let visitor = GoAstVisitor::new(Path::new("test.go"));
if let Ok(items) = visitor.analyze_go_source(&source) {
let function_items: Vec<_> = items.iter()
.filter(|item| matches!(item, AstItem::Function { .. }))
.collect();
prop_assert_eq!(function_items.len(), function_count);
for (i, item) in function_items.iter().enumerate() {
if let AstItem::Function { name, .. } = item {
let expected_name = format!("function{}", i);
prop_assert!(name.contains(&expected_name));
}
}
}
}
#[test]
fn test_go_complexity_stays_bounded(
depth in 1u32..5
) {
let mut source = String::from("package test\n\nfunc complexFunction() {\n");
for _ in 0..depth {
source.push_str("if true {\n");
}
source.push_str("return\n");
for _ in 0..depth {
source.push_str("}\n");
}
source.push_str("}\n");
let mut analyzer = GoComplexityAnalyzer::new();
if let Ok((cyclomatic, cognitive)) = analyzer.analyze_complexity(&source) {
prop_assert!(cyclomatic >= depth);
prop_assert!(cognitive >= depth);
prop_assert!(cyclomatic <= depth * 2 + 5); prop_assert!(cognitive <= depth * 3 + 5); }
}
}
}