pretender 0.3.1

Multi-language structural code-quality checks — complexity, duplication, mutation testing, and CI integration
// Plugin infrastructure is not yet wired to production code; allow dead_code
// until the plugin loader is implemented.
#![allow(dead_code)]

use anyhow::{Context, Result};
use serde::Deserialize;
use std::collections::BTreeMap;

#[derive(Debug, Clone, Deserialize)]
pub struct PluginManifest {
    pub name: String,
    pub display_name: String,
    pub extensions: Vec<String>,
    #[serde(default)]
    pub shebangs: Vec<String>,
    pub tree_sitter: TreeSitterSource,
    pub query: String,
    #[serde(default)]
    pub branches: BTreeMap<String, BranchWeights>,
    #[serde(default)]
    pub assertions: Assertions,
    #[serde(default)]
    pub smell_weights: BTreeMap<String, f64>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct TreeSitterSource {
    pub source: String,
    pub rev: String,
}

#[derive(Debug, Clone, Copy, Deserialize)]
pub struct BranchWeights {
    #[serde(default = "default_weight")]
    pub cyclomatic: u32,
    #[serde(default = "default_weight")]
    pub cognitive: u32,
}

fn default_weight() -> u32 {
    1
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct Assertions {
    #[serde(default)]
    pub patterns: Vec<String>,
}

impl PluginManifest {
    pub fn from_toml(source: &str) -> Result<Self> {
        toml::from_str(source).context("failed to parse plugin manifest")
    }
}

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

    const PYTHON_MANIFEST: &str = include_str!("../languages/python/plugin.toml");

    #[test]
    fn parses_python_manifest() {
        let manifest = PluginManifest::from_toml(PYTHON_MANIFEST).expect("parses");
        assert_eq!(manifest.name, "python");
        assert_eq!(manifest.display_name, "Python");
        assert_eq!(manifest.extensions, vec![".py"]);
        assert_eq!(manifest.query, "metrics.scm");
    }

    #[test]
    fn python_manifest_has_branch_weights() {
        let manifest = PluginManifest::from_toml(PYTHON_MANIFEST).unwrap();
        let if_weight = manifest
            .branches
            .get("@branch.if")
            .expect("@branch.if entry present");
        assert_eq!(if_weight.cyclomatic, 1);
        assert_eq!(if_weight.cognitive, 1);
    }

    #[test]
    fn python_manifest_has_assertion_patterns() {
        let manifest = PluginManifest::from_toml(PYTHON_MANIFEST).unwrap();
        assert!(!manifest.assertions.patterns.is_empty());
        assert!(manifest
            .assertions
            .patterns
            .iter()
            .any(|p| p.contains("pytest")));
    }

    #[test]
    fn rejects_invalid_toml() {
        let err = PluginManifest::from_toml("not = valid =").unwrap_err();
        assert!(err.to_string().contains("failed to parse plugin manifest"));
    }

    #[test]
    fn missing_required_field_errors() {
        let toml = r#"
name = "x"
display_name = "X"
extensions = [".x"]
"#;
        assert!(PluginManifest::from_toml(toml).is_err());
    }
}