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::current_root_envelope_mode(),
69        crate::output_runtime::telemetry_analysis_run_id().as_deref(),
70    )?;
71    json_style.serialize(&value).map_err(|error| {
72        fallow_api::ProgrammaticError::new(format!("JSON serialization error: {error}"), 2)
73            .with_code("json_serialization")
74    })
75}
76
77fn print_explain_human(rule: &RuleDef, guide: &RuleGuide) -> ExitCode {
78    println!("{}", rule.name.bold());
79    println!("{}", rule.id.dimmed());
80    println!();
81    println!("{}", rule.short);
82    println!();
83    println!("{}", "Why it matters".bold());
84    println!("{}", rule.full);
85    println!();
86    println!("{}", "Example".bold());
87    println!("{}", guide.example);
88    println!();
89    println!("{}", "How to fix".bold());
90    println!("{}", guide.how_to_fix);
91    println!();
92    println!("{} {}", "Docs:".dimmed(), rule_docs_url(rule).dimmed());
93    ExitCode::SUCCESS
94}
95
96fn print_explain_compact(rule: &RuleDef) -> ExitCode {
97    println!("explain:{}:{}:{}", rule.id, rule.short, rule_docs_url(rule));
98    ExitCode::SUCCESS
99}
100
101fn print_explain_markdown(rule: &RuleDef, guide: &RuleGuide) -> ExitCode {
102    println!("# {}", rule.name);
103    println!();
104    println!("`{}`", rule.id);
105    println!();
106    println!("{}", rule.short);
107    println!();
108    println!("## Why it matters");
109    println!();
110    println!("{}", rule.full);
111    println!();
112    println!("## Example");
113    println!();
114    println!("{}", guide.example);
115    println!();
116    println!("## How to fix");
117    println!();
118    println!("{}", guide.how_to_fix);
119    println!();
120    println!("[Docs]({})", rule_docs_url(rule));
121    ExitCode::SUCCESS
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn explain_json_respects_explicit_style() {
130        let compact = render_explain_json("unused-export", crate::json_style::JsonStyle::Compact)
131            .expect("compact explain JSON should serialize");
132        let pretty = render_explain_json("unused-export", crate::json_style::JsonStyle::Pretty)
133            .expect("pretty explain JSON should serialize");
134
135        assert!(
136            !compact.contains('\n'),
137            "compact JSON must stay on one line"
138        );
139        assert!(pretty.contains("\n  \""), "pretty JSON must be indented");
140        assert_eq!(
141            serde_json::from_str::<serde_json::Value>(&compact).unwrap(),
142            serde_json::from_str::<serde_json::Value>(&pretty).unwrap(),
143        );
144    }
145}