1mod diagnostic;
9mod enrich;
10pub mod render;
11mod source;
12
13use std::path::Path;
14use std::process::Command;
15
16use balise_core::error_codes::CMP0027;
17use balise_core::json::{self, Number, Value};
18
19use crate::{Failure, project};
20
21pub use diagnostic::{Diagnostic, Severity, is_rustc_code};
22
23pub struct Checked {
25 pub diagnostics: Vec<Diagnostic>,
26}
27
28impl Checked {
29 pub fn errors(&self) -> usize {
30 self.count(Severity::Error)
31 }
32
33 pub fn warnings(&self) -> usize {
34 self.count(Severity::Warning)
35 }
36
37 fn count(&self, severity: Severity) -> usize {
38 self.diagnostics
39 .iter()
40 .filter(|d| d.severity == severity)
41 .count()
42 }
43
44 pub fn failed(&self, ci: bool) -> bool {
46 self.errors() > 0 || (ci && self.warnings() > 0)
47 }
48
49 pub fn to_value(&self) -> Value {
50 let count = |n: usize| Value::Number(Number::Unsigned(n as u64));
51 Value::Object(vec![
52 (
53 "diagnostics".into(),
54 Value::Array(self.diagnostics.iter().map(Diagnostic::to_value).collect()),
55 ),
56 ("errors".into(), count(self.errors())),
57 ("warnings".into(), count(self.warnings())),
58 ])
59 }
60
61 pub fn from_value(value: &Value) -> Option<Checked> {
63 let diagnostics = diagnostic::array(value, "diagnostics")
64 .iter()
65 .map(Diagnostic::from_value)
66 .collect::<Option<_>>()?;
67 Some(Checked { diagnostics })
68 }
69}
70
71pub fn run(root: &Path, ci: bool) -> Result<Checked, Failure> {
74 let target = project::target_dir(root).join("check");
75 let mut diagnostics = cargo(
76 root,
77 &target,
78 &["clippy", "--all-targets", "--features", "dev"],
79 )?;
80 diagnostics.extend(source::undeclared_modules(root));
81 diagnostics.extend(source::static_muts(root));
82 if ci {
83 diagnostics.extend(formatting(root)?);
84 if diagnostics.iter().all(|d| d.severity != Severity::Error) {
86 diagnostics.extend(cargo(root, &target, &["build", "--release"])?);
87 }
88 }
89 let mut unique: Vec<Diagnostic> = Vec::new();
92 for d in diagnostics {
93 let same = |other: &Diagnostic| {
94 other.code == d.code
95 && other.message == d.message
96 && other.file == d.file
97 && other.line == d.line
98 && (other.column == d.column || d.code.starts_with("CMP"))
99 };
100 if !unique.iter().any(same) {
101 unique.push(d);
102 }
103 }
104 unique.sort_by(|a, b| {
106 (a.severity != Severity::Error, &a.file, a.line).cmp(&(
107 b.severity != Severity::Error,
108 &b.file,
109 b.line,
110 ))
111 });
112 Ok(Checked {
113 diagnostics: unique,
114 })
115}
116
117fn cargo(root: &Path, target: &Path, args: &[&str]) -> Result<Vec<Diagnostic>, Failure> {
119 let output = Command::new("cargo")
120 .args(args)
121 .args(["--quiet", "--message-format", "json"])
122 .env("CARGO_TARGET_DIR", target)
123 .current_dir(root)
124 .output()
125 .map_err(|error| cargo_failed(&error.to_string()))?;
126 let diagnostics: Vec<Diagnostic> = String::from_utf8_lossy(&output.stdout)
127 .lines()
128 .filter_map(|line| json::parse(line.as_bytes()).ok())
129 .filter(|message| diagnostic::text(message, "reason") == Some("compiler-message"))
130 .filter_map(|message| {
131 let message = diagnostic::get(&message, "message")?.clone();
132 Diagnostic::from_rustc(root, &message)
133 })
134 .collect();
135 let compiled = diagnostics.iter().any(|d| d.severity == Severity::Error);
136 if !output.status.success() && !compiled {
137 let stderr = String::from_utf8_lossy(&output.stderr);
138 let cause: Vec<&str> = stderr
139 .lines()
140 .map(str::trim)
141 .filter(|line| !line.is_empty())
142 .take(6)
143 .collect();
144 return Err(cargo_failed(&cause.join(" ")));
145 }
146 Ok(diagnostics)
147}
148
149fn cargo_failed(cause: &str) -> Failure {
150 Failure::problem(CMP0027, &[("cause", cause)], &[])
151}
152
153fn formatting(root: &Path) -> Result<Vec<Diagnostic>, Failure> {
155 let output = Command::new("cargo")
156 .args(["fmt", "--check", "--", "-l"])
157 .current_dir(root)
158 .output()
159 .map_err(|error| cargo_failed(&error.to_string()))?;
160 let prefix = format!("{}/", root.display());
161 Ok(String::from_utf8_lossy(&output.stdout)
162 .lines()
163 .filter(|line| !line.trim().is_empty())
164 .map(|line| {
165 let file = line
166 .strip_prefix(&prefix)
167 .unwrap_or(line)
168 .replace('\\', "/");
169 Diagnostic {
170 severity: Severity::Error,
171 code: "rustfmt".into(),
172 message: format!("`{file}` is not formatted"),
173 file: Some(file),
174 line: 1,
175 column: 1,
176 function: None,
177 snippet: Vec::new(),
178 label: None,
179 fix: Some("Run `cargo fmt`.".into()),
180 more: None,
181 }
182 })
183 .collect())
184}