use std::{collections::HashSet, process::{Command, Stdio}}; #[allow(unused)] use log::info; use regex::{escape, Regex}; use serde::{Deserialize, Serialize}; use crate::{diagnostics::find_non_whitespace_indices, server::LspServer, utils::from_uri_to_escape_path_string}; use ropey::Rope; use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range, Url}; use super::AbstractLinterConfiguration; #[derive(Debug, Serialize, Deserialize)] pub struct VivadoConfiguration { pub language_id: String, pub linter: VivadoLinter, } #[derive(Debug, Serialize, Deserialize)] pub struct VivadoLinter { pub name: String, pub exe_name: String, /// 目前是否启动 pub enabled: bool, pub path: String, pub args: Vec, } impl VivadoLinter { fn new(invoker: &str, args: Vec) -> Self { Self { name: "vivado".to_string(), exe_name: invoker.to_string(), enabled: false, path: invoker.to_string(), args, } } } impl AbstractLinterConfiguration for VivadoConfiguration { fn new(language_id: &str, invoker: &str, args: Vec) -> Self { VivadoConfiguration { language_id: language_id.to_string(), linter: VivadoLinter::new(invoker, args) } } fn provide_diagnostics( &self, uri: &Url, #[allow(unused)] rope: &Rope, #[allow(unused)] server: &LspServer ) -> Option> { let mut diagnostics = Vec::::new(); let invoke_name = self.get_invoke_name(); let pathbuf = uri.to_file_path().unwrap(); let path_string = pathbuf.to_str().unwrap(); let cache_info = server.cache.cache_info.read().unwrap(); let cwd = match &cache_info.linter_cache { Some(pc) => pc, None => { info!("缓存系统尚未完成初始化,本次诊断取消"); return None; } }; // vivado 比较特殊,需要先分析出当前文件用了哪些其他文件的宏,然后把那部分宏所在的文件加入编译参数中 let dependence_files = get_all_dependence_files(uri, server); let child = Command::new(&invoke_name) .current_dir(cwd) .stdin(Stdio::piped()) .stderr(Stdio::piped()) .stdout(Stdio::piped()) .args(&self.linter.args) .args(dependence_files) .arg(path_string) .spawn() .ok()?; let output = child.wait_with_output().ok()?; let output_string = String::from_utf8(output.stdout).ok()?; info!("vivado linter: {:?}, output:\n{}", path_string, output_string); static REGEX_STORE: std::sync::OnceLock = std::sync::OnceLock::new(); let regex = REGEX_STORE.get_or_init(|| { Regex::new(r"ERROR: \[VRFC (?P\S+)] (?P.+) \[(?P.+):(?P\d+)\]").unwrap() }); for error_line in output_string.lines() { let caps = match regex.captures(error_line) { Some(caps) => caps, None => continue }; let error_code = caps.name("error_code").unwrap().as_str(); let error_description = caps.name("description").unwrap().as_str(); let error_no = caps.name("line").unwrap().as_str(); let error_no = match error_no.parse::() { // Xilinx Vivado xvlog 在报告错误和警告时,行数是从 1 开始计数的。 Ok(no) => no - 1, Err(_) => 0 }; if let Some((start_char, end_char)) = find_vivado_suitable_range(rope, error_no, error_description) { let range = Range { start: Position { line: error_no as u32, character: start_char as u32 }, end: Position { line: error_no as u32, character: end_char as u32 } }; if error_description.contains("due to previous errors") { continue; } let diagnostic = Diagnostic { range, code: Some(NumberOrString::String(error_code.to_string())), severity: Some(DiagnosticSeverity::ERROR), source: Some("Digital IDE: vivado".to_string()), message: error_description.to_string(), related_information: None, tags: None, code_description: None, data: None }; diagnostics.push(diagnostic); } } Some(diagnostics) } fn get_linter_path(&self) -> &str { &self.linter.path } fn get_exe_name(&self) -> String { if std::env::consts::OS == "windows" { format!("{}.bat", self.linter.exe_name) } else { self.linter.exe_name.to_string() } } } /// 计算出当前文件所有用到的别的文件(比如使用了其他文件的宏) /// 必须把这些文件也编入诊断中,才能基于 vivado 得到合理的结果 fn get_all_dependence_files( uri: &Url, server: &LspServer ) -> Vec { let mut files = HashSet::::new(); let path_string = from_uri_to_escape_path_string(uri).unwrap(); let mut used_macro_names = HashSet::::new(); let hdl_param = server.db.hdl_param.clone(); let path_to_hdl_file = hdl_param.path_to_hdl_file.read().unwrap(); if let Some(hdl_file) = path_to_hdl_file.get(&path_string) { for macro_symbol in &hdl_file.parse_result.symbol_table.macro_usages { used_macro_names.insert(macro_symbol.name.to_string()); } } for (file_path, hdl_file) in path_to_hdl_file.iter() { if file_path == path_string.as_str() { // 只看其他文件 continue; } for define in hdl_file.fast.fast_macro.defines.iter() { let macro_name = define.name.to_string(); if used_macro_names.contains(¯o_name) { used_macro_names.remove(¯o_name); files.insert(file_path.to_string()); } } // 如果 unused_macro_names 都找到了对应的 path,直接 break 即可 if used_macro_names.is_empty() { break; } } // 释放锁 drop(path_to_hdl_file); files.into_iter().collect() } /// 根据 vivado 返回的诊断信息,返回适合的错误在那一行的起始位置 /// 默认是返回该行的第一个非空字符到最后一个非空字符中间的位置,即 find_non_whitespace_indices fn find_vivado_suitable_range( rope: &Rope, error_no: usize, error_description: &str ) -> Option<(usize, usize)> { // 一般 vivado 会把出错的关键词用单引号包起来 // 只需要提取这个单词,并在行中匹配它,如果任何一步失败,都采用 find_non_whitespace_indices 即可 static REGEX_STORE: std::sync::OnceLock = std::sync::OnceLock::new(); let regex = REGEX_STORE.get_or_init(|| { Regex::new(r"'([^']+)'").unwrap() }); let error_keyword = match regex.captures(error_description) { Some(caps) => { caps.get(1).unwrap().as_str() } None => { return find_non_whitespace_indices(rope, error_no) } }; let error_keyword = escape(error_keyword); let pattern = format!(r"\b(?i){}\b", error_keyword); let regex = Regex::new(&pattern).unwrap(); if let Some(line_text) = rope.line(error_no).as_str() { // 处理特殊情况: error_keyword 为 ; if error_keyword == ";" { if let Some(index) = line_text.find(";") { return Some((index, index)); } } if let Some(mat) = regex.find(line_text) { // info!("mat {} {}", mat.start(), mat.end()); return Some((mat.start(), mat.end())); } } find_non_whitespace_indices(rope, error_no) } /// 判断是否为类似于 xxx ignored 的错误 /// ERROR: [VRFC 10-8530] module 'main' is ignored due to previous errors [/home/dide/project/Digital-Test/Digital-macro/user/src/main.v:1] #[allow(unused)] fn is_ignore_type(diag: &Diagnostic) -> bool { // 获取 vrfc 编码 let vrfc_code = if let Some(NumberOrString::String(code)) = &diag.code { code } else { return false; }; match vrfc_code.as_str() { "10-8530" => { true } _ => { false } } }