1use std::io::{self, Write};
30use std::path::PathBuf;
31
32use crate::discovery::Language;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum ComponentStatus {
37 Available,
39 NotFound,
41 Unusable,
43 NotImplemented,
45}
46
47impl ComponentStatus {
48 #[must_use]
50 pub const fn label(self) -> &'static str {
51 match self {
52 Self::Available => "available",
53 Self::NotFound => "not found",
54 Self::Unusable => "unusable",
55 Self::NotImplemented => "not implemented",
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum Requirement {
63 Required,
65 Optional,
67}
68
69impl Requirement {
70 #[must_use]
72 pub const fn label(self) -> &'static str {
73 match self {
74 Self::Required => "required",
75 Self::Optional => "optional",
76 }
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct ComponentReport {
83 pub name: &'static str,
85 pub requirement: Requirement,
87 pub status: ComponentStatus,
89 pub detail: String,
91 pub notes: Vec<String>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct HelperFacts {
102 pub path: PathBuf,
104 pub state: HelperState,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum HelperState {
111 Answered(Greeting),
113 Silent(String),
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct Greeting {
124 pub version: String,
126 pub protocol: u32,
128 pub toolchains: Vec<String>,
130 pub capabilities: Vec<String>,
132 pub executes: Vec<String>,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub struct HelperComponent {
144 pub name: &'static str,
146 pub binary: &'static str,
148 pub analyses: &'static [Language],
155 pub enables: &'static str,
157 pub advice: &'static str,
159}
160
161pub const RUST_HELPER: HelperComponent = HelperComponent {
167 name: "rust-compiler-helper",
168 binary: "codehelion-backend-rust",
169 analyses: &[Language::Rust],
170 enables: "semantic analysis of Rust",
171 advice: "install codehelion-backend-rust beside this binary or on PATH",
172};
173
174pub const CLANG_HELPER: HelperComponent = HelperComponent {
176 name: "clang-helper",
177 binary: "codehelion-backend-clang",
178 analyses: &[Language::C, Language::Cpp],
179 enables: "semantic analysis of C and C++",
180 advice: "install codehelion-backend-clang beside this binary or on PATH",
181};
182
183pub const OPTIONAL_HELPERS: [HelperComponent; 2] = [RUST_HELPER, CLANG_HELPER];
185
186fn inspect_self() -> ComponentReport {
187 ComponentReport {
188 name: "codehelion",
189 requirement: Requirement::Required,
190 status: ComponentStatus::Available,
191 detail: format!("codehelion {}", env!("CARGO_PKG_VERSION")),
192 notes: Vec::new(),
193 }
194}
195
196fn inspect_helper(helper: HelperComponent, found: Option<HelperFacts>) -> ComponentReport {
197 let (status, detail, notes) = match found {
198 None => (
201 ComponentStatus::NotFound,
202 format!(
203 "not needed for fast or structural analysis; enables {}. To add it, {}.",
204 helper.enables, helper.advice
205 ),
206 Vec::new(),
207 ),
208 Some(facts) => {
209 let path = facts.path.display().to_string();
210 match facts.state {
211 HelperState::Answered(greeting) => {
212 (ComponentStatus::Available, path, describe(&greeting))
213 }
214 HelperState::Silent(reason) => (
217 ComponentStatus::Unusable,
218 path,
219 vec![format!("this build could not talk to it: {reason}")],
220 ),
221 }
222 }
223 };
224 ComponentReport {
225 name: helper.name,
226 requirement: Requirement::Optional,
227 status,
228 detail,
229 notes,
230 }
231}
232
233fn describe(greeting: &Greeting) -> Vec<String> {
240 let mut notes = vec![format!(
241 "version {}, protocol {}",
242 greeting.version, greeting.protocol
243 )];
244 if !greeting.toolchains.is_empty() {
245 notes.push(format!("analyses with: {}", greeting.toolchains.join(", ")));
246 }
247 if greeting.capabilities.is_empty() {
250 notes.push("supplies: nothing this build asked about".to_string());
251 } else {
252 notes.push(format!("supplies: {}", greeting.capabilities.join(", ")));
253 }
254 if greeting.executes.is_empty() {
257 notes.push("runs nothing out of a project, whatever is permitted".to_string());
258 } else {
259 notes.push(format!(
260 "runs when permitted: {}",
261 greeting.executes.join(", ")
262 ));
263 }
264 notes
265}
266
267#[must_use]
277pub fn diagnose_with(find: &dyn Fn(&str) -> Option<HelperFacts>) -> Vec<ComponentReport> {
278 let mut reports = vec![inspect_self()];
279 for helper in OPTIONAL_HELPERS {
280 reports.push(inspect_helper(helper, find(helper.binary)));
281 }
282 reports
283}
284
285#[must_use]
292pub fn diagnose() -> Vec<ComponentReport> {
293 diagnose_with(&|_| None)
294}
295
296pub fn render(reports: &[ComponentReport], out: &mut impl Write) -> io::Result<()> {
302 writeln!(out, "codehelion environment diagnostics")?;
303 writeln!(out)?;
304 let name_width = reports.iter().map(|r| r.name.len()).max().unwrap_or(0);
305 for report in reports {
306 writeln!(
307 out,
308 " {name:<name_width$} {req:<8} {status:<15} {detail}",
309 name = report.name,
310 req = report.requirement.label(),
311 status = report.status.label(),
312 detail = report.detail,
313 )?;
314 for note in &report.notes {
315 writeln!(out, " {:<name_width$} {note}", "")?;
316 }
317 }
318 Ok(())
319}
320
321#[cfg(test)]
322#[allow(clippy::unwrap_used, clippy::expect_used)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn diagnose_reports_codehelion_first_and_available() {
328 let reports = diagnose();
329 let first = reports.first().expect("at least one report");
330 assert_eq!(first.name, "codehelion");
331 assert_eq!(first.requirement, Requirement::Required);
332 assert_eq!(first.status, ComponentStatus::Available);
333 assert!(first.detail.contains(env!("CARGO_PKG_VERSION")));
334 }
335
336 #[test]
340 fn a_machine_without_helpers_is_told_what_it_still_has() {
341 let reports = diagnose();
342 let helpers: Vec<_> = reports.iter().filter(|r| r.name != "codehelion").collect();
343 assert_eq!(helpers.len(), OPTIONAL_HELPERS.len());
344 for helper in helpers {
345 assert_eq!(helper.requirement, Requirement::Optional);
346 assert_eq!(helper.status, ComponentStatus::NotFound);
347 assert!(
348 helper.detail.contains("not needed for fast or structural"),
349 "{}",
350 helper.detail
351 );
352 }
353 }
354
355 #[test]
358 fn the_advice_names_the_program_that_was_looked_for() {
359 for helper in OPTIONAL_HELPERS {
360 let report = inspect_helper(helper, None);
361 assert!(report.detail.contains(helper.binary), "{}", report.detail);
362 }
363 }
364
365 fn greeting() -> Greeting {
366 Greeting {
367 version: "0.1.0".to_string(),
368 protocol: 2,
369 toolchains: vec!["rust-analyzer 0.0.344".to_string()],
370 capabilities: vec!["types".to_string(), "name_resolution".to_string()],
371 executes: vec!["build-script".to_string()],
372 }
373 }
374
375 fn answered(name: &str) -> HelperFacts {
376 HelperFacts {
377 path: PathBuf::from("/opt/bin").join(name),
378 state: HelperState::Answered(greeting()),
379 }
380 }
381
382 #[test]
383 fn a_helper_that_is_there_is_reported_with_where_it_is() {
384 let reports =
385 diagnose_with(&|name| (name == OPTIONAL_HELPERS[0].binary).then(|| answered(name)));
386 let found = &reports[1];
387 assert_eq!(found.name, OPTIONAL_HELPERS[0].name);
388 assert_eq!(found.status, ComponentStatus::Available);
389 assert!(found.detail.contains("/opt/bin"), "{}", found.detail);
390 assert_eq!(reports[2].status, ComponentStatus::NotFound);
393 }
394
395 #[test]
400 fn a_helper_that_answered_says_what_it_is_and_what_it_supplies() {
401 let reports =
402 diagnose_with(&|name| (name == OPTIONAL_HELPERS[0].binary).then(|| answered(name)));
403 let notes = reports[1].notes.join("\n");
404 assert!(notes.contains("version 0.1.0"), "{notes}");
405 assert!(notes.contains("protocol 2"), "{notes}");
406 assert!(notes.contains("rust-analyzer 0.0.344"), "{notes}");
407 assert!(notes.contains("types, name_resolution"), "{notes}");
408 assert!(
411 notes.contains("runs when permitted: build-script"),
412 "{notes}"
413 );
414 }
415
416 #[test]
419 fn a_helper_that_offers_nothing_says_so_rather_than_saying_less() {
420 let report = inspect_helper(
421 OPTIONAL_HELPERS[0],
422 Some(HelperFacts {
423 path: PathBuf::from("/opt/bin/helper"),
424 state: HelperState::Answered(Greeting {
425 capabilities: Vec::new(),
426 executes: Vec::new(),
427 ..greeting()
428 }),
429 }),
430 );
431 assert!(
432 report
433 .notes
434 .iter()
435 .any(|note| note.starts_with("supplies:")),
436 "{:?}",
437 report.notes
438 );
439 assert!(
442 report
443 .notes
444 .iter()
445 .any(|note| note.contains("runs nothing")),
446 "{:?}",
447 report.notes
448 );
449 }
450
451 #[test]
455 fn a_helper_that_would_not_answer_is_neither_available_nor_missing() {
456 let report = inspect_helper(
457 OPTIONAL_HELPERS[0],
458 Some(HelperFacts {
459 path: PathBuf::from("/opt/bin/helper"),
460 state: HelperState::Silent("speaks protocol 3, this build speaks 2".to_string()),
461 }),
462 );
463 assert_eq!(report.status, ComponentStatus::Unusable);
464 assert!(
465 report.detail.contains("/opt/bin/helper"),
466 "{}",
467 report.detail
468 );
469 assert!(
470 report.notes.iter().any(|note| note.contains("protocol 3")),
471 "{:?}",
472 report.notes
473 );
474 }
475
476 #[test]
477 fn what_a_helper_said_is_printed_under_it() {
478 let mut buffer = Vec::new();
479 let reports =
480 diagnose_with(&|name| (name == OPTIONAL_HELPERS[0].binary).then(|| answered(name)));
481 render(&reports, &mut buffer).expect("render should succeed");
482 let text = String::from_utf8(buffer).expect("output is utf-8");
483 let lines: Vec<&str> = text.lines().collect();
484 let at = lines
485 .iter()
486 .position(|line| line.contains(OPTIONAL_HELPERS[0].name))
487 .expect("the helper is listed");
488 assert!(lines[at + 1].contains("version 0.1.0"), "{text}");
489 }
490
491 #[test]
492 fn render_lists_every_component_and_the_version() {
493 let mut buffer = Vec::new();
494 render(&diagnose(), &mut buffer).expect("render should succeed");
495 let text = String::from_utf8(buffer).expect("output is utf-8");
496 assert!(text.contains("codehelion"));
497 assert!(text.contains(env!("CARGO_PKG_VERSION")));
498 assert!(text.contains("rust-compiler-helper"));
499 assert!(text.contains("not found"));
500 }
501}