258 lines
8.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String>,
}
impl VivadoLinter {
fn new(invoker: &str, args: Vec<String>) -> 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<String>) -> 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<Vec<Diagnostic>> {
let mut diagnostics = Vec::<Diagnostic>::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<Regex> = std::sync::OnceLock::new();
let regex = REGEX_STORE.get_or_init(|| { Regex::new(r"ERROR: \[VRFC (?P<error_code>\S+)] (?P<description>.+) \[(?P<file>.+):(?P<line>\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::<usize>() {
// 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<String> {
let mut files = HashSet::<String>::new();
let path_string = from_uri_to_escape_path_string(uri).unwrap();
let mut used_macro_names = HashSet::<String>::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(&macro_name) {
used_macro_names.remove(&macro_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<Regex> = 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
}
}
}