barad-dur 0.18.0

The all-seeing repository analyzer
Documentation
use std::path::Path;

use crate::snapshot::FileComplexity;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Language {
    Rust,
    JsTs,
    Python,
    Go,
    Java,
    Kotlin,
    CSharp,
    Generic,
}

pub fn detect_language(path: &str) -> Language {
    match Path::new(path)
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("")
    {
        "rs" => Language::Rust,
        "js" | "ts" | "jsx" | "tsx" | "mjs" | "cjs" => Language::JsTs,
        "py" => Language::Python,
        "go" => Language::Go,
        "java" => Language::Java,
        "kt" | "kts" => Language::Kotlin,
        "cs" => Language::CSharp,
        _ => Language::Generic,
    }
}

pub fn analyse_content(content: &str, lang: Language) -> FileComplexity {
    let lines: Vec<&str> = content.lines().collect();
    let total_lines = lines.len();

    let loc = lines
        .iter()
        .filter(|l| {
            let t = l.trim();
            !t.is_empty() && !is_comment_line(t, lang)
        })
        .count();

    let cyclomatic_complexity = count_complexity(content);
    let public_methods = count_public_methods(content, lang);
    let properties = count_properties(content, lang);

    FileComplexity {
        total_lines,
        loc,
        cyclomatic_complexity,
        public_methods,
        properties,
        ..Default::default()
    }
}

fn is_comment_line(trimmed: &str, lang: Language) -> bool {
    match lang {
        Language::Rust
        | Language::JsTs
        | Language::Go
        | Language::Java
        | Language::Kotlin
        | Language::CSharp => {
            trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with('*')
        }
        Language::Python => trimmed.starts_with('#'),
        Language::Generic => trimmed.starts_with("//") || trimmed.starts_with('#'),
    }
}

fn count_complexity(content: &str) -> u32 {
    let keywords = [
        " if ", "\tif ", "(if ", "else if", "elif ", " for ", "\tfor ", " while ", "\twhile ",
        " match ", " switch ", " case ", " catch ", " except ", " loop ", "&&", "||", " ?? ",
    ];
    let mut count = 0u32;
    for kw in &keywords {
        count += content.matches(kw).count() as u32;
    }
    count
}

fn pub_methods_rust(content: &str) -> u32 {
    content
        .lines()
        .filter(|line| {
            let t = line.trim();
            t.starts_with("pub fn ") || t.starts_with("pub async fn ")
        })
        .count() as u32
}

fn pub_methods_jsts(content: &str) -> u32 {
    content
        .lines()
        .filter(|line| {
            let t = line.trim();
            !t.starts_with("//")
                && (t.starts_with("export function ")
                    || t.starts_with("export async function ")
                    || t.starts_with("export const ")
                    || (t.contains("public ") && t.contains('(')))
        })
        .count() as u32
}

fn pub_methods_python(content: &str) -> u32 {
    content
        .lines()
        .filter(|line| {
            let t = line.trim();
            t.starts_with("def ") && !t.starts_with("def _")
        })
        .count() as u32
}

fn pub_methods_go(content: &str) -> u32 {
    let mut count = 0u32;
    for line in content.lines() {
        let t = line.trim();
        if let Some(rest) = t.strip_prefix("func ") {
            let name_start = if rest.starts_with('(') {
                rest.find(')').map(|i| rest[i + 1..].trim()).unwrap_or("")
            } else {
                rest
            };
            if name_start
                .chars()
                .next()
                .map(|c| c.is_uppercase())
                .unwrap_or(false)
            {
                count += 1;
            }
        }
    }
    count
}

fn pub_methods_jvm(content: &str) -> u32 {
    content
        .lines()
        .filter(|line| {
            let t = line.trim();
            t.starts_with("public ")
                && t.contains('(')
                && !t.starts_with("public class")
                && !t.starts_with("public interface")
                && !t.starts_with("//")
        })
        .count() as u32
}

fn count_public_methods(content: &str, lang: Language) -> u32 {
    match lang {
        Language::Rust => pub_methods_rust(content),
        Language::JsTs => pub_methods_jsts(content),
        Language::Python => pub_methods_python(content),
        Language::Go => pub_methods_go(content),
        Language::Java | Language::Kotlin | Language::CSharp => pub_methods_jvm(content),
        Language::Generic => 0,
    }
}

fn props_rust(content: &str) -> u32 {
    content
        .lines()
        .filter(|line| {
            let t = line.trim();
            t.starts_with("pub ")
                && t.contains(':')
                && !t.starts_with("pub fn")
                && !t.starts_with("pub async fn")
                && !t.starts_with("pub struct")
                && !t.starts_with("pub enum")
                && !t.starts_with("pub mod")
                && !t.starts_with("pub use")
                && !t.starts_with("pub type")
                && !t.starts_with("pub trait")
                && !t.starts_with("pub const")
                && !t.starts_with("pub static")
                && !t.starts_with("//")
        })
        .count() as u32
}

fn props_jsts(content: &str) -> u32 {
    content
        .lines()
        .filter(|line| {
            let t = line.trim();
            (t.starts_with("this.") && t.contains(" = "))
                || ((t.starts_with("private ")
                    || t.starts_with("public ")
                    || t.starts_with("protected ")
                    || t.starts_with("readonly "))
                    && t.contains(':')
                    && !t.contains('('))
        })
        .count() as u32
}

fn props_python(content: &str) -> u32 {
    content
        .lines()
        .filter(|line| {
            let t = line.trim();
            t.starts_with("self.") && t.contains(" = ")
        })
        .count() as u32
}

fn props_go(content: &str) -> u32 {
    content
        .lines()
        .filter(|line| {
            let t = line.trim();
            t.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
                && (t.contains("string")
                    || t.contains("int")
                    || t.contains("bool")
                    || t.contains("float")
                    || t.contains("[]")
                    || t.contains("map["))
                && !t.contains('(')
                && !t.starts_with("//")
        })
        .count() as u32
}

fn props_jvm(content: &str) -> u32 {
    content
        .lines()
        .filter(|line| {
            let t = line.trim();
            (t.starts_with("private ") || t.starts_with("public ") || t.starts_with("protected "))
                && !t.contains('(')
                && t.contains(';')
                && !t.starts_with("//")
        })
        .count() as u32
}

fn count_properties(content: &str, lang: Language) -> u32 {
    match lang {
        Language::Rust => props_rust(content),
        Language::JsTs => props_jsts(content),
        Language::Python => props_python(content),
        Language::Go => props_go(content),
        Language::Java | Language::Kotlin | Language::CSharp => props_jvm(content),
        Language::Generic => 0,
    }
}

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

    #[test]
    fn detects_rust_language() {
        assert!(matches!(detect_language("src/main.rs"), Language::Rust));
        assert!(matches!(detect_language("lib.rs"), Language::Rust));
    }

    #[test]
    fn detects_js_ts() {
        assert!(matches!(detect_language("app.ts"), Language::JsTs));
        assert!(matches!(detect_language("index.jsx"), Language::JsTs));
    }

    #[test]
    fn detects_python() {
        assert!(matches!(detect_language("script.py"), Language::Python));
    }

    #[test]
    fn detects_go() {
        assert!(matches!(detect_language("main.go"), Language::Go));
    }

    #[test]
    fn detects_java() {
        assert!(matches!(detect_language("Foo.java"), Language::Java));
    }

    #[test]
    fn detects_kotlin() {
        assert!(matches!(detect_language("Bar.kt"), Language::Kotlin));
        assert!(matches!(detect_language("App.kts"), Language::Kotlin));
    }

    #[test]
    fn detects_csharp() {
        assert!(matches!(detect_language("Baz.cs"), Language::CSharp));
    }

    #[test]
    fn loc_skips_blank_and_comment_lines() {
        let content = "// comment\n\nfn main() {}\n    // indented comment\nlet x = 1;\n";
        let result = analyse_content(content, Language::Rust);
        assert_eq!(result.total_lines, 5);
        assert_eq!(result.loc, 2); // fn main(){} and let x = 1;
    }

    #[test]
    fn cyclomatic_complexity_counts_decision_points() {
        let content = " if x { } else if y { } for i in v { } while z { } match a { _ => {} }";
        let result = analyse_content(content, Language::Rust);
        // if, else if, for, while, match = at least 5
        assert!(result.cyclomatic_complexity >= 5);
    }

    #[test]
    fn public_methods_rust() {
        let content = "pub fn foo() {}\nfn bar() {}\npub fn baz() {}\n";
        let result = analyse_content(content, Language::Rust);
        assert_eq!(result.public_methods, 2);
    }

    #[test]
    fn public_methods_typescript() {
        let content = "export function foo() {}\nfunction bar() {}\nexport const baz = () => {}\n";
        let result = analyse_content(content, Language::JsTs);
        assert_eq!(result.public_methods, 2);
    }

    #[test]
    fn properties_rust_struct_fields() {
        let content = "struct Foo {\n    pub x: i32,\n    pub y: String,\n    z: bool,\n}\n";
        let result = analyse_content(content, Language::Rust);
        assert_eq!(result.properties, 2);
    }
}