258 lines
8.6 KiB
Rust
258 lines
8.6 KiB
Rust
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(¯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<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
|
||
}
|
||
}
|
||
}
|