ontocore-diagnostics 0.14.0

Ontology lint rules and diagnostic collection for OntoCore (ontocore-*)
Documentation
//! Workspace diagnostic rule configuration (`.ontocore/diagnostics.toml`).

use ontocore_core::{DiagnosticCode, DiagnosticSeverity};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;

#[derive(Debug, Clone, Default, Deserialize)]
pub struct DiagnosticConfig {
    #[serde(default)]
    pub rules: HashMap<String, RuleConfig>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RuleConfig {
    #[serde(default = "default_enabled")]
    pub enabled: bool,
    pub severity: Option<String>,
}

fn default_enabled() -> bool {
    true
}

impl DiagnosticConfig {
    pub fn load(path: &Path) -> Result<Self, String> {
        let text = std::fs::read_to_string(path)
            .map_err(|e| format!("failed to read {}: {e}", path.display()))?;
        toml::from_str(&text).map_err(|e| format!("invalid diagnostics config: {e}"))
    }

    pub fn is_rule_enabled(&self, code: DiagnosticCode) -> bool {
        let key = code.as_str();
        self.rules.get(key).map(|r| r.enabled).unwrap_or(true)
    }

    pub fn severity_override(&self, code: DiagnosticCode) -> Option<DiagnosticSeverity> {
        let key = code.as_str();
        let sev = self.rules.get(key)?.severity.as_deref()?;
        match sev.to_ascii_lowercase().as_str() {
            "error" => Some(DiagnosticSeverity::Error),
            "warning" => Some(DiagnosticSeverity::Warning),
            "info" | "hint" => Some(DiagnosticSeverity::Info),
            _ => None,
        }
    }
}

pub fn find_config(workspace: &Path) -> Option<DiagnosticConfig> {
    let path = workspace.join(".ontocore").join("diagnostics.toml");
    DiagnosticConfig::load(&path).ok()
}

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

    #[test]
    fn parses_rule_toggles() {
        let cfg: DiagnosticConfig = toml::from_str(
            r#"
[rules.missing_label]
enabled = false
[rules.broken_import]
severity = "error"
"#,
        )
        .unwrap();
        assert!(!cfg.is_rule_enabled(DiagnosticCode::MissingLabel));
        assert_eq!(
            cfg.severity_override(DiagnosticCode::BrokenImport),
            Some(DiagnosticSeverity::Error)
        );
    }

    #[test]
    fn find_config_loads_from_workspace_dot_ontocore() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::create_dir_all(dir.path().join(".ontocore")).unwrap();
        std::fs::write(
            dir.path().join(".ontocore/diagnostics.toml"),
            "[rules.orphan_class]\nenabled = false\n",
        )
        .unwrap();
        let cfg = find_config(dir.path()).expect("config");
        assert!(!cfg.is_rule_enabled(DiagnosticCode::OrphanClass));
    }

    #[test]
    fn hint_severity_maps_to_info() {
        let cfg: DiagnosticConfig = toml::from_str(
            r#"
[rules.duplicate_label]
severity = "hint"
"#,
        )
        .unwrap();
        assert_eq!(
            cfg.severity_override(DiagnosticCode::DuplicateLabel),
            Some(DiagnosticSeverity::Info)
        );
    }
}