Skip to main content

fallow_cli/
explain.rs

1//! CLI rendering for explainable rule output.
2//!
3//! The rule registry and JSON contract live in `fallow-api` so embedders and
4//! MCP do not depend on the CLI crate. This module keeps terminal rendering and
5//! compatibility re-exports for existing CLI call sites.
6
7use std::process::ExitCode;
8
9use colored::Colorize;
10use fallow_config::OutputFormat;
11
12use crate::report::sink::outln;
13
14pub use fallow_api::{
15    CHECK_RULES, DUPES_RULES, FLAGS_RULES, HEALTH_RULES, RuleDef, RuleGuide, SECURITY_RULES,
16    coverage_analyze_meta, coverage_setup_meta, rule_by_id, rule_by_token, rule_docs_url,
17    rule_guide, security_meta, serialize_explain_programmatic_json,
18};
19
20/// Run the standalone explain subcommand.
21#[must_use]
22pub fn run_explain(
23    issue_type: &str,
24    output: OutputFormat,
25    json_style: crate::json_style::JsonStyle,
26) -> ExitCode {
27    let Some(rule) = rule_by_token(issue_type) else {
28        return crate::error::emit_error(
29            &fallow_api::unknown_explain_error(issue_type).message,
30            2,
31            output,
32        );
33    };
34    let guide = rule_guide(rule);
35    match output {
36        OutputFormat::Json => match render_explain_json(issue_type, json_style) {
37            Ok(json) => {
38                outln!("{json}");
39                ExitCode::SUCCESS
40            }
41            Err(error) => crate::error::emit_error_with_style(
42                &error.message,
43                error.exit_code,
44                output,
45                json_style,
46            ),
47        },
48        OutputFormat::Human => print_explain_human(rule, &guide),
49        OutputFormat::Compact => print_explain_compact(rule),
50        OutputFormat::Markdown => print_explain_markdown(rule, &guide),
51        OutputFormat::Sarif
52        | OutputFormat::CodeClimate
53        | OutputFormat::PrCommentGithub
54        | OutputFormat::PrCommentGitlab
55        | OutputFormat::ReviewGithub
56        | OutputFormat::ReviewGitlab
57        | OutputFormat::Badge
58        | OutputFormat::GithubAnnotations
59        | OutputFormat::GithubSummary => crate::error::emit_error(
60            "explain supports human, compact, markdown, and json output",
61            2,
62            output,
63        ),
64    }
65}
66
67fn render_explain_json(
68    issue_type: &str,
69    json_style: crate::json_style::JsonStyle,
70) -> Result<String, fallow_api::ProgrammaticError> {
71    let value = serialize_explain_programmatic_json(
72        issue_type,
73        crate::output_runtime::current_root_envelope_mode(),
74        crate::output_runtime::telemetry_analysis_run_id().as_deref(),
75    )?;
76    json_style.serialize(&value).map_err(|error| {
77        fallow_api::ProgrammaticError::new(format!("JSON serialization error: {error}"), 2)
78            .with_code("json_serialization")
79    })
80}
81
82fn print_explain_human(rule: &RuleDef, guide: &RuleGuide) -> ExitCode {
83    println!("{}", rule.name.bold());
84    println!("{}", rule.id.dimmed());
85    println!();
86    println!("{}", rule.short);
87    println!();
88    println!("{}", "Why it matters".bold());
89    println!("{}", rule.full);
90    println!();
91    println!("{}", "Example".bold());
92    println!("{}", guide.example);
93    println!();
94    println!("{}", "How to fix".bold());
95    println!("{}", guide.how_to_fix);
96    println!();
97    println!("{} {}", "Docs:".dimmed(), rule_docs_url(rule).dimmed());
98    ExitCode::SUCCESS
99}
100
101fn print_explain_compact(rule: &RuleDef) -> ExitCode {
102    println!("explain:{}:{}:{}", rule.id, rule.short, rule_docs_url(rule));
103    ExitCode::SUCCESS
104}
105
106fn print_explain_markdown(rule: &RuleDef, guide: &RuleGuide) -> ExitCode {
107    println!("# {}", rule.name);
108    println!();
109    println!("`{}`", rule.id);
110    println!();
111    println!("{}", rule.short);
112    println!();
113    println!("## Why it matters");
114    println!();
115    println!("{}", rule.full);
116    println!();
117    println!("## Example");
118    println!();
119    println!("{}", guide.example);
120    println!();
121    println!("## How to fix");
122    println!();
123    println!("{}", guide.how_to_fix);
124    println!();
125    println!("[Docs]({})", rule_docs_url(rule));
126    ExitCode::SUCCESS
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn explain_json_respects_explicit_style() {
135        let compact = render_explain_json("unused-export", crate::json_style::JsonStyle::Compact)
136            .expect("compact explain JSON should serialize");
137        let pretty = render_explain_json("unused-export", crate::json_style::JsonStyle::Pretty)
138            .expect("pretty explain JSON should serialize");
139
140        assert!(
141            !compact.contains('\n'),
142            "compact JSON must stay on one line"
143        );
144        assert!(pretty.contains("\n  \""), "pretty JSON must be indented");
145        assert_eq!(
146            serde_json::from_str::<serde_json::Value>(&compact).unwrap(),
147            serde_json::from_str::<serde_json::Value>(&pretty).unwrap(),
148        );
149    }
150}