df_ls_diagnostics 0.3.0-rc.1

A language server for Dwarf Fortress RAW files
Documentation
use anyhow::Error;
use lsp_types::*;
pub use lsp_types::{Position, Range};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum DiagnosticMessageSet {
    Lexer,
    Syntax,
    Semantic,
}

#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct DiagnosticsInfo {
    /// The list of all the diagnostic messages in the file.
    /// All warnings, errors and other messages.
    diagnostics: Vec<Diagnostic>,
    /// A list of all error messages available
    diagnostics_messages: HashMap<String, DiagnosticsMessage>,
    /// A human-readable string describing the source of this diagnostic,
    /// e.g. 'typescript' or 'super lint'.
    source: Option<String>,
    /// Limit the amount of diagnostic message to be saved.
    /// Note that this is a soft limit and is only checked periodically.
    /// So the resulting amount of message can exceed this limit.
    ///
    /// This limit is created to prevent excessive creating of messages.
    diagnostics_message_limit: Option<usize>,

    /// Lock the message list, prevent new message from being added.
    lock_message_list: bool,
}

#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct DiagnosticsMessage {
    /// The diagnostics severity. Can be omitted. If omitted it is up to the client to
    /// interpret diagnostics as error, warning, info or hint.
    pub severity: Option<DiagnosticSeverity>,
    // An optional property to describe the error code.
    //pub code_description: Option<CodeDescription>, // Future versions
    /// The diagnostics message.
    pub message: String,
    /// Additional metadata about the diagnostic.
    pub tags: Option<Vec<DiagnosticTag>>,
    /// Documentation Link
    pub docs: Option<String>,
}

#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Debug)]
pub struct DMExtraInfo {
    /// The current range of the error
    pub range: Range,
    /// Extra data that will be used to replace the templates in the message
    pub message_template_data: HashMap<String, String>,
}

impl DMExtraInfo {
    pub fn new(range: Range) -> DMExtraInfo {
        DMExtraInfo {
            range,
            message_template_data: HashMap::new(),
        }
    }

    /// Print the `Range` as a String.
    ///
    /// Used for logging.
    pub fn range_as_string(&self) -> String {
        format!(
            "start: (line {}, char {}), end: (line {}, char {})",
            self.range.start.line + 1, // `line` is zero based, convert to one based.
            self.range.start.character,
            self.range.end.line + 1, // `line` is zero based, convert to one based.
            self.range.end.character
        )
    }
}

impl DiagnosticsInfo {
    pub fn new(
        diagnostics_messages: HashMap<String, DiagnosticsMessage>,
        source: Option<String>,
    ) -> Self {
        DiagnosticsInfo {
            diagnostics: vec![],
            diagnostics_messages,
            source,
            diagnostics_message_limit: Some(10000),
            lock_message_list: false,
        }
    }

    pub fn add_message(&mut self, extra_info: DMExtraInfo, code: &str) {
        if self.lock_message_list {
            log::debug!(
                "Diagnostic message list is locked, message `{}` was not added.",
                code
            );
            return;
        }
        log::error!("`{}` at: {}", code, extra_info.range_as_string());
        if std::env::var("RUST_BACKTRACE").unwrap_or_default() == "1" {
            // Add backtrace to errors when env variable is set.
            let backtrace = backtrace::Backtrace::new();
            log::error!("{:#?}", backtrace);
        }
        // Look up code
        let range = extra_info.range;
        let message = self.get_correct_message(extra_info, code);

        let new_message = Diagnostic {
            range,
            severity: message.severity,
            code: Some(NumberOrString::String(code.to_owned())),
            source: self.source.clone(),
            message: message.message.clone(),
            tags: message.tags,
            ..Default::default()
        };

        // Check if new message already exists (exact match)
        // For optimization reasons it only checks the last 10 messages.
        // This should not effect the output in any way in 99%-100% of cases.
        let is_duplicate = self
            .diagnostics
            .iter()
            .rev()
            .take(10)
            .any(|x| x == &new_message);
        if is_duplicate {
            log::warn!("Diagnostic message is duplicate");
        } else {
            self.diagnostics.push(new_message);
        }
    }

    /// Check if the message limit was reached.
    /// `true` if limit was reached.
    pub fn check_message_limit_reached(&self) -> bool {
        if let Some(message_limit) = self.diagnostics_message_limit {
            self.diagnostics.len() >= message_limit
        } else {
            false
        }
    }

    /// Lock the message list, not more diagnostic message can be added after this.
    pub fn lock_message_list(&mut self) {
        log::debug!("Diagnostic message list locked.");
        self.lock_message_list = true;
    }

    /// Get the current state of the message lock.
    pub fn get_message_lock_state(&self) -> bool {
        self.lock_message_list
    }

    /// Return a borrowed list of the diagnostic message list
    pub fn get_diagnostic_list(&self) -> &[Diagnostic] {
        &self.diagnostics
    }

    /// Extract the list of all diagnostic messages.
    /// This is destroy the `DiagnosticsInfo`.
    pub fn extract_diagnostic_list(self) -> Vec<Diagnostic> {
        self.diagnostics
    }

    fn get_correct_message(&self, extra_info: DMExtraInfo, code: &str) -> DiagnosticsMessage {
        // Get message from file
        let default_error = DiagnosticsMessage {
            message: "Language server error: Error code not found. \
            Please report this."
                .to_owned(),
            ..Default::default()
        };
        let mut diagnostics_message = match self.diagnostics_messages.get(code) {
            Some(message) => message.clone(),
            None => {
                log::error!("Could not find error code: {}", code);
                return default_error;
            }
        };
        // Convert placeholders in code
        diagnostics_message.message =
            Self::replace_template(diagnostics_message.message, extra_info);
        // Only for debugging/testing to see if all templates are filled in
        #[cfg(any(debug_assertions, test))]
        if diagnostics_message.message.contains("{{") {
            panic!(
                "Diagnostic message template not filled in: {}",
                diagnostics_message.message
            );
        }
        diagnostics_message
    }

    fn replace_template(mut message: String, extra_info: DMExtraInfo) -> String {
        for (key, value) in extra_info.message_template_data {
            // Create `{{key}}` because of format the `{` is escaped using `{{`.
            let template = format!("{{{{{}}}}}", key); //
            message = message.replace(&template, &value);
        }
        // Replace `{{start_line}}`
        message = message.replace("{{start_line}}", &extra_info.range.start.line.to_string());
        // Replace `{{start_char}}`
        message = message.replace(
            "{{start_char}}",
            &extra_info.range.start.character.to_string(),
        );
        // Replace `{{end_line}}`
        message = message.replace("{{end_line}}", &extra_info.range.end.line.to_string());
        // Replace `{{end_char}}`
        message = message.replace("{{end_char}}", &extra_info.range.end.character.to_string());
        message
    }

    pub fn load_from_file(set: DiagnosticMessageSet, source: Option<String>) -> DiagnosticsInfo {
        let file_bytes = match set {
            DiagnosticMessageSet::Lexer => crate::DiagnosticsMessages::get("lexer_dm.json"),
            DiagnosticMessageSet::Syntax => crate::DiagnosticsMessages::get("syntax_dm.json"),
            DiagnosticMessageSet::Semantic => crate::DiagnosticsMessages::get("semantic_dm.json"),
        }
        .expect("Error loading DiagnosticsInfo.");
        let diagnostics_messages = std::str::from_utf8(file_bytes.data.as_ref())
            .expect("Error loading DiagnosticsInfo, Non UTF-8 Found.");
        DiagnosticsInfo::new(parse_json_file(diagnostics_messages).unwrap(), source)
    }
}

fn parse_json_file<C: DeserializeOwned>(file: &str) -> Result<C, Error> {
    let parsed_result = &mut serde_json::de::Deserializer::from_str(file);
    let result: Result<C, _> = serde_path_to_error::deserialize(parsed_result);
    let parsed_object: C = match result {
        Ok(data) => data,
        Err(err) => {
            let path = err.path().to_string();
            log::error!("Error: {} \nIn: {}", err, path);
            return Err(Error::from(err));
        }
    };
    Ok(parsed_object)
}

/// Macro to crate a `HashMap` with a number of key-value pairs in it.
///
/// # Examples
///
/// ```rust
/// use std::collections::HashMap;
/// use df_ls_diagnostics::hash_map;
///
/// let my_hash_map = hash_map!{
///     "token_name" => "CREATURE",
///     "cat" => "",
/// };
///
/// let mut control = HashMap::new();
/// control.insert("token_name".to_owned(),"CREATURE");
/// control.insert("cat".to_owned(),"");
///
/// assert_eq!(my_hash_map, control);
/// ```
#[macro_export]
macro_rules! hash_map {
    ($($key:expr => $val:expr),* $(,)*) => ({
        #[allow(unused_mut)]
        let mut map = ::std::collections::HashMap::new();
        $( map.insert($key.to_owned(), $val); )*
        map
    });
}