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, rule_severity_key, security_meta, serialize_explain_programmatic_json,
18};
19
20/// Run the standalone explain subcommand.
21#[must_use]
22pub(crate) 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_programmatic_error(
29            &fallow_api::unknown_explain_error(issue_type),
30            output,
31            json_style,
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_programmatic_error(&error, output, json_style),
42        },
43        OutputFormat::Human => print_explain_human(rule, &guide),
44        OutputFormat::Compact => print_explain_compact(rule),
45        OutputFormat::Markdown => print_explain_markdown(rule, &guide),
46        OutputFormat::Sarif
47        | OutputFormat::CodeClimate
48        | OutputFormat::PrCommentGithub
49        | OutputFormat::PrCommentGitlab
50        | OutputFormat::ReviewGithub
51        | OutputFormat::ReviewGitlab
52        | OutputFormat::Badge
53        | OutputFormat::GithubAnnotations
54        | OutputFormat::GithubSummary => crate::error::emit_error(
55            "explain supports human, compact, markdown, and json output",
56            2,
57            output,
58        ),
59    }
60}
61
62fn render_explain_json(
63    issue_type: &str,
64    json_style: crate::json_style::JsonStyle,
65) -> Result<String, fallow_api::ProgrammaticError> {
66    let value = serialize_explain_programmatic_json(
67        issue_type,
68        crate::output_runtime::telemetry_analysis_run_id().as_deref(),
69    )?;
70    json_style.serialize(&value).map_err(|error| {
71        fallow_api::ProgrammaticError::new(format!("JSON serialization error: {error}"), 2)
72            .with_code("json_serialization")
73    })
74}
75
76fn print_explain_human(rule: &RuleDef, guide: &RuleGuide) -> ExitCode {
77    println!("{}", rule.name.bold());
78    println!("{}", rule.id.dimmed());
79    println!();
80    println!("{}", rule.short);
81    println!();
82    println!("{}", "Why it matters".bold());
83    println!("{}", rule.full);
84    println!();
85    println!("{}", "Example".bold());
86    println!("{}", guide.example);
87    println!();
88    println!("{}", "How to fix".bold());
89    println!("{}", guide.how_to_fix);
90    println!();
91    println!("{} {}", "Docs:".dimmed(), rule_docs_url(rule).dimmed());
92    ExitCode::SUCCESS
93}
94
95fn print_explain_compact(rule: &RuleDef) -> ExitCode {
96    println!("explain:{}:{}:{}", rule.id, rule.short, rule_docs_url(rule));
97    ExitCode::SUCCESS
98}
99
100fn print_explain_markdown(rule: &RuleDef, guide: &RuleGuide) -> ExitCode {
101    println!("# {}", rule.name);
102    println!();
103    println!("`{}`", rule.id);
104    println!();
105    println!("{}", rule.short);
106    println!();
107    println!("## Why it matters");
108    println!();
109    println!("{}", rule.full);
110    println!();
111    println!("## Example");
112    println!();
113    println!("{}", guide.example);
114    println!();
115    println!("## How to fix");
116    println!();
117    println!("{}", guide.how_to_fix);
118    println!();
119    println!("[Docs]({})", rule_docs_url(rule));
120    ExitCode::SUCCESS
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn explain_json_respects_explicit_style() {
129        let compact = render_explain_json("unused-export", crate::json_style::JsonStyle::Compact)
130            .expect("compact explain JSON should serialize");
131        let pretty = render_explain_json("unused-export", crate::json_style::JsonStyle::Pretty)
132            .expect("pretty explain JSON should serialize");
133
134        assert!(
135            !compact.contains('\n'),
136            "compact JSON must stay on one line"
137        );
138        assert!(pretty.contains("\n  \""), "pretty JSON must be indented");
139        assert_eq!(
140            serde_json::from_str::<serde_json::Value>(&compact).unwrap(),
141            serde_json::from_str::<serde_json::Value>(&pretty).unwrap(),
142        );
143    }
144}