mod diagnostic;
mod enrich;
pub mod render;
mod source;
use std::path::Path;
use std::process::Command;
use balise_core::error_codes::CMP0027;
use balise_core::json::{self, Number, Value};
use crate::{Failure, project};
pub use diagnostic::{Diagnostic, Severity, is_rustc_code};
pub struct Checked {
pub diagnostics: Vec<Diagnostic>,
}
impl Checked {
pub fn errors(&self) -> usize {
self.count(Severity::Error)
}
pub fn warnings(&self) -> usize {
self.count(Severity::Warning)
}
fn count(&self, severity: Severity) -> usize {
self.diagnostics
.iter()
.filter(|d| d.severity == severity)
.count()
}
pub fn failed(&self, ci: bool) -> bool {
self.errors() > 0 || (ci && self.warnings() > 0)
}
pub fn to_value(&self) -> Value {
let count = |n: usize| Value::Number(Number::Unsigned(n as u64));
Value::Object(vec![
(
"diagnostics".into(),
Value::Array(self.diagnostics.iter().map(Diagnostic::to_value).collect()),
),
("errors".into(), count(self.errors())),
("warnings".into(), count(self.warnings())),
])
}
pub fn from_value(value: &Value) -> Option<Checked> {
let diagnostics = diagnostic::array(value, "diagnostics")
.iter()
.map(Diagnostic::from_value)
.collect::<Option<_>>()?;
Some(Checked { diagnostics })
}
}
pub fn run(root: &Path, ci: bool) -> Result<Checked, Failure> {
let target = project::target_dir(root).join("check");
let mut diagnostics = cargo(
root,
&target,
&["clippy", "--all-targets", "--features", "dev"],
)?;
diagnostics.extend(source::undeclared_modules(root));
diagnostics.extend(source::static_muts(root));
if ci {
diagnostics.extend(formatting(root)?);
if diagnostics.iter().all(|d| d.severity != Severity::Error) {
diagnostics.extend(cargo(root, &target, &["build", "--release"])?);
}
}
let mut unique: Vec<Diagnostic> = Vec::new();
for d in diagnostics {
let same = |other: &Diagnostic| {
other.code == d.code
&& other.message == d.message
&& other.file == d.file
&& other.line == d.line
&& (other.column == d.column || d.code.starts_with("CMP"))
};
if !unique.iter().any(same) {
unique.push(d);
}
}
unique.sort_by(|a, b| {
(a.severity != Severity::Error, &a.file, a.line).cmp(&(
b.severity != Severity::Error,
&b.file,
b.line,
))
});
Ok(Checked {
diagnostics: unique,
})
}
fn cargo(root: &Path, target: &Path, args: &[&str]) -> Result<Vec<Diagnostic>, Failure> {
let output = Command::new("cargo")
.args(args)
.args(["--quiet", "--message-format", "json"])
.env("CARGO_TARGET_DIR", target)
.current_dir(root)
.output()
.map_err(|error| cargo_failed(&error.to_string()))?;
let diagnostics: Vec<Diagnostic> = String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| json::parse(line.as_bytes()).ok())
.filter(|message| diagnostic::text(message, "reason") == Some("compiler-message"))
.filter_map(|message| {
let message = diagnostic::get(&message, "message")?.clone();
Diagnostic::from_rustc(root, &message)
})
.collect();
let compiled = diagnostics.iter().any(|d| d.severity == Severity::Error);
if !output.status.success() && !compiled {
let stderr = String::from_utf8_lossy(&output.stderr);
let cause: Vec<&str> = stderr
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.take(6)
.collect();
return Err(cargo_failed(&cause.join(" ")));
}
Ok(diagnostics)
}
fn cargo_failed(cause: &str) -> Failure {
Failure::problem(CMP0027, &[("cause", cause)], &[])
}
fn formatting(root: &Path) -> Result<Vec<Diagnostic>, Failure> {
let output = Command::new("cargo")
.args(["fmt", "--check", "--", "-l"])
.current_dir(root)
.output()
.map_err(|error| cargo_failed(&error.to_string()))?;
let prefix = format!("{}/", root.display());
Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| {
let file = line
.strip_prefix(&prefix)
.unwrap_or(line)
.replace('\\', "/");
Diagnostic {
severity: Severity::Error,
code: "rustfmt".into(),
message: format!("`{file}` is not formatted"),
file: Some(file),
line: 1,
column: 1,
function: None,
snippet: Vec::new(),
label: None,
fix: Some("Run `cargo fmt`.".into()),
more: None,
}
})
.collect())
}