bacon-ls 0.9.0

Bacon Language Server
Documentation
use std::path::Path;

use serde::Deserialize;
use tokio::process::Command;

use crate::LOCATIONS_FILE;

#[derive(Debug, Deserialize)]
struct BaconConfig {
    jobs: Jobs,
    exports: Exports,
}

#[derive(Debug, Deserialize)]
struct Jobs {
    #[serde(rename = "bacon-ls")]
    bacon_ls: BaconLs,
}

#[derive(Debug, Deserialize)]
struct BaconLs {
    analyzer: String,
    need_stdout: bool,
}

#[derive(Debug, Deserialize)]
struct Exports {
    #[serde(rename = "cargo-json-spans")]
    cargo_json_spans: CargoJsonSpans,
}

#[derive(Debug, Deserialize)]
struct CargoJsonSpans {
    auto: bool,
    exporter: String,
    line_format: String,
    path: String,
}

const ERROR_MESSAGE: &str = "bacon configuration is not compatible with bacon-ls: please take a look to https://github.com/crisidev/bacon-ls?tab=readme-ov-file#configuration and adapt your bacon configuration";
const BACON_ANALYZER: &str = "cargo_json";
const BACON_EXPORTER: &str = "analyzer";
const LINE_FORMAT: &str = "{diagnostic.level}|:|{span.file_name}|:|{span.line_start}|:|{span.line_end}|:|{span.column_start}|:|{span.column_end}|:|{diagnostic.message}|:|{span.suggested_replacement}";

async fn validate_bacon_preferences_file(path: &Path) -> Result<(), String> {
    let toml_content = tokio::fs::read_to_string(path)
        .await
        .map_err(|e| format!("{ERROR_MESSAGE}: {e}"))?;
    let config: BaconConfig =
        toml::from_str(&toml_content).map_err(|e| format!("{ERROR_MESSAGE}: {e}"))?;
    tracing::debug!("bacon config is {config:#?}");
    if config.jobs.bacon_ls.analyzer == BACON_ANALYZER
        && config.jobs.bacon_ls.need_stdout
        && config.exports.cargo_json_spans.auto
        && config.exports.cargo_json_spans.exporter == BACON_EXPORTER
        && config.exports.cargo_json_spans.line_format == LINE_FORMAT
        && config.exports.cargo_json_spans.path == LOCATIONS_FILE
    {
        tracing::info!("bacon configuration {} is valid", path.display());
        Ok(())
    } else {
        Err(ERROR_MESSAGE.to_string())
    }
}

pub(crate) async fn validate_bacon_preferences() -> Result<(), String> {
    let bacon_prefs = Command::new("bacon")
        .arg("--prefs")
        .output()
        .await
        .map_err(|e| e.to_string())?;
    let bacon_prefs_files = String::from_utf8_lossy(&bacon_prefs.stdout);
    for prefs_file in bacon_prefs_files.split("\n") {
        let prefs_file_path = Path::new(prefs_file);
        tracing::debug!("skipping non existing bacon preference file {prefs_file}");
        if prefs_file_path.exists() {
            validate_bacon_preferences_file(prefs_file_path).await?;
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::io::Write;

    use super::*;
    use tempdir::TempDir;

    #[tokio::test]
    async fn test_valid_bacon_preferences() {
        let valid_toml = format!(
            r#"
            [jobs.bacon-ls]
            analyzer = "{BACON_ANALYZER}"
            need_stdout = true

            [exports.cargo-json-spans]
            auto = true
            exporter = "{BACON_EXPORTER}"
            line_format = "{LINE_FORMAT}"
            path = "{LOCATIONS_FILE}"
        "#
        );
        let tmp_dir = TempDir::new("bacon").unwrap();
        let file_path = tmp_dir.path().join("prefs.toml");
        let mut file = std::fs::File::create(&file_path).unwrap();
        write!(file, "{}", valid_toml).unwrap();
        assert!(validate_bacon_preferences_file(&file_path).await.is_ok());
    }

    #[tokio::test]
    async fn test_invalid_analyzer() {
        let invalid_toml = format!(
            r#"
            [jobs.bacon-ls]
            analyzer = "incorrect_analyzer"
            need_stdout = true

            [exports.cargo-json-spans]
            auto = true
            exporter = "{BACON_EXPORTER}"
            line_format = "{LINE_FORMAT}"
            path = "{LOCATIONS_FILE}"
        "#
        );

        let tmp_dir = TempDir::new("bacon").unwrap();
        let file_path = tmp_dir.path().join("prefs.toml");
        let mut file = std::fs::File::create(&file_path).unwrap();
        write!(file, "{}", invalid_toml).unwrap();
        assert!(validate_bacon_preferences_file(&file_path).await.is_err());
    }

    #[tokio::test]
    async fn test_invalid_line_format() {
        let invalid_toml = format!(
            r#"
            [jobs.bacon-ls]
            analyzer = "{BACON_ANALYZER}"
            need_stdout = true

            [exports.cargo-json-spans]
            auto = true
            exporter = "{BACON_EXPORTER}"
            line_format = "invalid_line_format"
            path = "{LOCATIONS_FILE}"
        "#
        );

        let tmp_dir = TempDir::new("bacon").unwrap();
        let file_path = tmp_dir.path().join("prefs.toml");
        let mut file = std::fs::File::create(&file_path).unwrap();
        write!(file, "{}", invalid_toml).unwrap();
        assert!(validate_bacon_preferences_file(&file_path).await.is_err());
    }
}