debtmap 0.16.5

Code complexity and technical debt analyzer
Documentation
//! Language-specific code analyzers for parsing and extracting metrics.
//!
//! This module contains analyzers for different programming languages (Rust, Python,
//! JavaScript, TypeScript) that parse source code into ASTs and extract metrics like
//! complexity, function signatures, and call graphs.
//!
//! The module provides:
//! - Language-specific parsers using tree-sitter grammars
//! - Function and method extraction with purity detection
//! - Call graph analysis and construction
//! - Effect tracking (I/O, state mutations)
//! - Test and macro detection
//!
//! # Architecture
//!
//! Analyzers implement the [`Analyzer`] trait for unified file analysis, or
//! [`FileAnalyzer`] for debt-focused metrics. The enhanced analyzer provides
//! comprehensive analysis including call graphs and effect tracking.

use crate::core::{ast::Ast, FileMetrics, FunctionMetrics};
use crate::priority::file_metrics::FileDebtMetrics;
use anyhow::Result;
use std::path::Path;

pub mod batch;
pub mod call_graph;
pub mod call_graph_integration;
pub mod closure_analyzer;
pub mod context_aware;
pub mod custom_macro_analyzer;
pub mod effects;
pub mod enhanced_analyzer;
pub mod file_analyzer;
pub mod function_registry;
pub mod io_detector;
pub mod macro_definition_collector;
pub mod purity_detector;
pub mod python;
pub mod rust;
pub mod rust_call_graph;
pub mod rust_complexity_calculation;
pub mod rust_constructor_detector;
pub mod rust_data_flow_analyzer;
pub mod rust_enum_converter_detector;
pub mod scope_tracker;
pub mod signature_extractor;
pub mod state_field_detector;
pub mod state_machine_pattern_detector;
pub mod test_detector;
pub mod trait_implementation_tracker;
pub mod trait_resolver;
pub mod traits;
pub mod type_registry;
pub mod type_tracker;
pub mod typescript;
pub mod validation_pattern_detector;

pub use enhanced_analyzer::{AnalysisResult, EnhancedAnalyzer};

pub trait Analyzer: Send + Sync {
    fn parse(&self, content: &str, path: std::path::PathBuf) -> Result<Ast>;
    fn analyze(&self, ast: &Ast) -> FileMetrics;
    fn language(&self) -> crate::core::Language;
}

pub trait FileAnalyzer {
    fn analyze_file(&self, path: &Path, content: &str) -> Result<FileDebtMetrics>;
    fn aggregate_functions(&self, functions: &[FunctionMetrics]) -> FileDebtMetrics;
}

pub fn analyze_file(
    content: String,
    path: std::path::PathBuf,
    analyzer: &dyn Analyzer,
) -> Result<FileMetrics> {
    analyzer
        .parse(&content, path.clone())
        .map(transform_ast)
        .map(|ast| analyzer.analyze(&ast))
        .map(apply_filters)
}

fn transform_ast(ast: Ast) -> Ast {
    ast.transform(|a| a)
}

fn apply_filters(metrics: FileMetrics) -> FileMetrics {
    metrics
}

type Parser = Box<dyn Fn(&str) -> Result<Ast>>;
type Transformer = Box<dyn Fn(Ast) -> Ast>;
type Calculator = Box<dyn Fn(&Ast) -> FileMetrics>;

pub fn compose_analyzers(
    parsers: Vec<Parser>,
    transformers: Vec<Transformer>,
    calculators: Vec<Calculator>,
) -> impl Fn(&str) -> Result<FileMetrics> {
    move |content: &str| {
        let ast = parsers[0](content)?;
        let transformed = transformers.iter().fold(ast, |acc, f| f(acc));
        Ok(calculators[0](&transformed))
    }
}

pub fn get_analyzer(language: crate::core::Language) -> Box<dyn Analyzer> {
    use crate::core::Language;

    type AnalyzerFactory = fn() -> Box<dyn Analyzer>;

    static ANALYZER_MAP: &[(Language, AnalyzerFactory)] = &[
        (Language::Rust, || {
            let mut analyzer = rust::RustAnalyzer::new();
            // Check environment variable for functional analysis (spec 111)
            if std::env::var("DEBTMAP_FUNCTIONAL_ANALYSIS").is_ok() {
                analyzer = analyzer.with_functional_analysis(true);
            }
            Box::new(analyzer)
        }),
        (Language::Python, || {
            let mut analyzer = python::PythonAnalyzer::new();
            if std::env::var("DEBTMAP_FUNCTIONAL_ANALYSIS").is_ok() {
                analyzer = analyzer.with_functional_analysis(true);
            }
            Box::new(analyzer)
        }),
        (Language::JavaScript, || {
            let mut analyzer = typescript::TypeScriptAnalyzer::javascript();
            if std::env::var("DEBTMAP_FUNCTIONAL_ANALYSIS").is_ok() {
                analyzer = analyzer.with_functional_analysis(true);
            }
            Box::new(analyzer)
        }),
        (Language::TypeScript, || {
            let mut analyzer = typescript::TypeScriptAnalyzer::new();
            if std::env::var("DEBTMAP_FUNCTIONAL_ANALYSIS").is_ok() {
                analyzer = analyzer.with_functional_analysis(true);
            }
            Box::new(analyzer)
        }),
    ];

    ANALYZER_MAP
        .iter()
        .find(|(lang, _)| *lang == language)
        .map(|(_, factory)| factory())
        .unwrap_or_else(|| Box::new(NullAnalyzer))
}

pub fn get_analyzer_with_context(
    language: crate::core::Language,
    context_aware: bool,
) -> Box<dyn Analyzer> {
    let base_analyzer = get_analyzer(language);

    if context_aware {
        Box::new(context_aware::ContextAwareAnalyzer::new(base_analyzer))
    } else {
        base_analyzer
    }
}

struct NullAnalyzer;

impl Analyzer for NullAnalyzer {
    fn parse(&self, _content: &str, _path: std::path::PathBuf) -> Result<Ast> {
        Ok(Ast::Unknown)
    }

    fn analyze(&self, _ast: &Ast) -> FileMetrics {
        FileMetrics {
            path: std::path::PathBuf::new(),
            language: crate::core::Language::Unknown,
            complexity: crate::core::ComplexityMetrics::default(),
            debt_items: vec![],
            dependencies: vec![],
            duplications: vec![],
            total_lines: 0,
            module_scope: None,
            classes: None,
        }
    }

    fn language(&self) -> crate::core::Language {
        crate::core::Language::Unknown
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn test_transform_ast() {
        let ast = Ast::Unknown;
        let result = transform_ast(ast);
        assert!(matches!(result, Ast::Unknown));
    }

    #[test]
    fn test_apply_filters() {
        let metrics = FileMetrics {
            path: PathBuf::from("test.rs"),
            language: crate::core::Language::Rust,
            complexity: crate::core::ComplexityMetrics::default(),
            debt_items: vec![],
            dependencies: vec![],
            duplications: vec![],
            total_lines: 0,
            module_scope: None,
            classes: None,
        };
        let result = apply_filters(metrics.clone());
        assert_eq!(result.path, metrics.path);
        assert_eq!(result.language, metrics.language);
    }

    #[test]
    fn test_get_analyzer_rust() {
        let analyzer = get_analyzer(crate::core::Language::Rust);
        assert_eq!(analyzer.language(), crate::core::Language::Rust);
    }

    #[test]
    fn test_get_analyzer_unknown() {
        let analyzer = get_analyzer(crate::core::Language::Unknown);
        assert_eq!(analyzer.language(), crate::core::Language::Unknown);
    }

    #[test]
    fn test_null_analyzer_parse() {
        let analyzer = NullAnalyzer;
        let result = analyzer
            .parse("test content", PathBuf::from("test.txt"))
            .unwrap();
        assert!(matches!(result, Ast::Unknown));
    }

    #[test]
    fn test_null_analyzer_analyze() {
        let analyzer = NullAnalyzer;
        let ast = Ast::Unknown;
        let metrics = analyzer.analyze(&ast);
        assert_eq!(metrics.path, PathBuf::new());
        assert_eq!(metrics.language, crate::core::Language::Unknown);
        assert_eq!(metrics.complexity.functions.len(), 0);
        assert_eq!(metrics.debt_items.len(), 0);
        assert_eq!(metrics.dependencies.len(), 0);
        assert_eq!(metrics.duplications.len(), 0);
    }

    #[test]
    fn test_null_analyzer_language() {
        let analyzer = NullAnalyzer;
        assert_eq!(analyzer.language(), crate::core::Language::Unknown);
    }

    #[test]
    fn test_analyze_file() {
        let analyzer = NullAnalyzer;
        let content = String::from("test content");
        let path = PathBuf::from("test.txt");
        let result = analyze_file(content, path.clone(), &analyzer).unwrap();
        assert_eq!(result.language, crate::core::Language::Unknown);
    }

    #[test]
    fn test_compose_analyzers() {
        let parsers: Vec<Parser> = vec![Box::new(|_| Ok(Ast::Unknown))];
        let transformers: Vec<Transformer> = vec![Box::new(|ast| ast)];
        let calculators: Vec<Calculator> = vec![Box::new(|_| FileMetrics {
            path: PathBuf::from("test.rs"),
            language: crate::core::Language::Rust,
            complexity: crate::core::ComplexityMetrics::default(),
            debt_items: vec![],
            dependencies: vec![],
            duplications: vec![],
            total_lines: 0,
            module_scope: None,
            classes: None,
        })];

        let analyzer = compose_analyzers(parsers, transformers, calculators);
        let result = analyzer("test content").unwrap();
        assert_eq!(result.path, PathBuf::from("test.rs"));
        assert_eq!(result.language, crate::core::Language::Rust);
    }
}