249 lines
8.9 KiB
Rust

use log::info;
use serde::{Deserialize, Serialize};
use regex::Regex;
use ropey::Rope;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use tower_lsp::lsp_types::*;
use crate::server::LspServer;
use crate::utils::command::is_command_valid;
use super::{AbstractLinterConfiguration, LinterStatus};
#[derive(Debug, Serialize, Deserialize)]
pub struct VerilatorConfiguration {
pub language_id: String,
pub linter: VerilatorLinter,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VerilatorLinter {
pub name: String,
pub exe_name: String,
/// 目前是否启动
pub enabled: bool,
pub path: String,
pub args: Vec<String>,
}
impl VerilatorLinter {
fn new(invoker: &str, args: Vec<String>) -> Self {
Self {
name: "verilator".to_string(),
exe_name: invoker.to_string(),
enabled: false,
path: invoker.to_string(),
args,
}
}
}
/// convert captured severity string to DiagnosticSeverity
fn verilator_severity(severity: &str) -> Option<DiagnosticSeverity> {
match severity {
"Error" => Some(DiagnosticSeverity::ERROR),
s if s.starts_with("Warning") => Some(DiagnosticSeverity::WARNING),
// NOTE: afaik, verilator doesn't have an info or hint severity
_ => Some(DiagnosticSeverity::INFORMATION),
}
}
/// syntax checking using verilator --lint-only
fn verilator_syntax(
rope: &Rope,
file_path: PathBuf,
verilator_syntax_path: &str,
verilator_syntax_args: &[String],
) -> Option<Vec<Diagnostic>> {
let mut child = Command::new(verilator_syntax_path)
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.args(verilator_syntax_args)
.arg(file_path.to_str()?)
.spawn()
.ok()?;
static RE: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
let re = RE.get_or_init(|| {
Regex::new(
r"%(?P<severity>Error|Warning)(-(?P<warning_type>[A-Z0-9_]+))?: (?P<filepath>[^:]+):(?P<line>\d+):((?P<col>\d+):)? ?(?P<message>.*)",
)
.unwrap()
});
// write file to stdin, read output from stdout
rope.write_to(child.stdin.as_mut()?).ok()?;
let output = child.wait_with_output().ok()?;
if !output.status.success() {
let mut diags: Vec<Diagnostic> = Vec::new();
let raw_output = String::from_utf8(output.stderr).ok()?;
let filtered_output = raw_output
.lines()
.filter(|line| line.starts_with('%'))
.collect::<Vec<&str>>();
for error in filtered_output {
let caps = match re.captures(error) {
Some(caps) => caps,
None => continue,
};
// check if diagnostic is for this file, since verilator can provide diagnostics for
// included files
if caps.name("filepath")?.as_str() != file_path.to_str().unwrap_or("") {
continue;
}
let severity = verilator_severity(caps.name("severity")?.as_str());
let line: u32 = caps.name("line")?.as_str().to_string().parse().ok()?;
let col: u32 = caps.name("col").map_or("1", |m| m.as_str()).parse().ok()?;
let pos = Position::new(line - 1, col - 1);
let msg = match severity {
Some(DiagnosticSeverity::ERROR) => caps.name("message")?.as_str().to_string(),
Some(DiagnosticSeverity::WARNING) => format!(
"{}: {}",
caps.name("warning_type")?.as_str(),
caps.name("message")?.as_str()
),
_ => "".to_string(),
};
diags.push(Diagnostic::new(
Range::new(pos, pos),
severity,
None,
Some("verilator".to_string()),
msg,
None,
None,
));
}
Some(diags)
} else {
None
}
}
impl AbstractLinterConfiguration for VerilatorConfiguration {
fn new(language_id: &str, invoker: &str, args: Vec<String>) -> Self {
VerilatorConfiguration {
language_id: language_id.to_string(),
linter: VerilatorLinter::new(invoker, args)
}
}
fn provide_diagnostics(
&self,
uri: &Url,
#[allow(unused)]
rope: &Rope,
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 child = Command::new(&invoke_name)
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.args(&self.linter.args)
.arg(path_string)
.spawn()
.ok()?;
let output = child.wait_with_output().ok()?;
let output_string = String::from_utf8(output.stderr).ok()?;
info!("verilator 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"%(?P<severity>Error|Warning)(-(?P<warning_type>[A-Z0-9_]+))?: (?P<filepath>[^:]+):(?P<line>\d+):((?P<col>\d+):)? ?(?P<message>.*)").unwrap() });
// verilator 错误输出样例
// %Warning-IMPLICIT: library/Apply/Comm/FDE/AGC/AGC.v:23:12: Signal definition not found, creating implicitly: 'r_rms'
// : ... Suggested alternative: 'ref_rms'
// 23 | assign r_rms = (reference * reference);
// | ^~~~~
// ... Use "/* verilator lint_off IMPLICIT */" and lint_on around source to disable this message.
// %Error: Exiting due to 5 warning(s)
// 先将上面的输出处理为一整块一整块的数组,每块分解为两个部分:第一行(用于解析行号和列号)和后面几行(具体错误地址)
let mut error_tuples = Vec::<(String, String)>::new();
let mut current_error_tuple: Option<(&str, Vec<&str>)> = None;
for error_line in output_string.lines() {
if error_line.starts_with("%") {
if let Some((first, ref description)) = current_error_tuple {
error_tuples.push((first.to_string(), description.join("\n")));
}
current_error_tuple = Some((error_line, Vec::<&str>::new()));
} else {
if let Some((_, ref mut description)) = current_error_tuple {
description.push(error_line);
} else {
continue;
}
}
}
for error_tuple in error_tuples {
let captures = match regex.captures(error_tuple.0.as_str()) {
Some(caps) => caps,
None => continue
};
// 因为 verilator 会递归检查,因此报错信息中可能会出现非本文件内的信息
if captures.name("filepath")?.as_str().replace("\\", "/") != path_string {
continue;
}
let line: u32 = captures.name("line")?.as_str().to_string().parse().ok()?;
let col: u32 = captures.name("col").map_or("1", |m| m.as_str()).parse().ok()?;
// verilator 的诊断索引都是 one index 的
let pos = Position::new(line - 1, col - 1);
let severity = verilator_severity(captures.name("severity")?.as_str());
let message = match severity {
Some(DiagnosticSeverity::ERROR) => captures.name("message")?.as_str().to_string(),
Some(DiagnosticSeverity::WARNING) => format!(
"{}: {}",
captures.name("warning_type")?.as_str(),
captures.name("message")?.as_str()
),
_ => "".to_string(),
};
let message = format!("{}\n\n---\n\n{}", message, error_tuple.1);
let diagnostic = Diagnostic {
range: Range::new(pos, pos),
code: None,
severity,
source: Some("Digital IDE: verilator".to_string()),
message,
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!("{}.exe", self.linter.exe_name)
} else {
self.linter.exe_name.to_string()
}
}
}