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    AUDIT_RULES, CHECK_RULES, DUPES_RULES, FLAGS_RULES, HEALTH_RULES, RuleDef, RuleGuide,
16    SECURITY_RULES, coverage_analyze_meta, coverage_setup_meta, rule_by_id, rule_by_token,
17    rule_docs_url, rule_guide, rule_severity_key, security_meta,
18    serialize_explain_programmatic_json,
19};
20
21/// Run the standalone explain subcommand.
22#[must_use]
23pub(crate) fn run_explain(
24    issue_type: &str,
25    output: OutputFormat,
26    json_style: crate::json_style::JsonStyle,
27) -> ExitCode {
28    let Some(rule) = rule_by_token(issue_type) else {
29        return crate::error::emit_programmatic_error(
30            &fallow_api::unknown_explain_error(issue_type),
31            output,
32            json_style,
33        );
34    };
35    let guide = rule_guide(rule);
36    match output {
37        OutputFormat::Json => match render_explain_json(issue_type, json_style) {
38            Ok(json) => {
39                outln!("{json}");
40                ExitCode::SUCCESS
41            }
42            Err(error) => crate::error::emit_programmatic_error(&error, output, json_style),
43        },
44        OutputFormat::Human => print_explain_human(rule, &guide),
45        OutputFormat::Compact => print_explain_compact(rule),
46        OutputFormat::Markdown => print_explain_markdown(rule, &guide),
47        OutputFormat::Sarif
48        | OutputFormat::CodeClimate
49        | OutputFormat::PrCommentGithub
50        | OutputFormat::PrCommentGitlab
51        | OutputFormat::ReviewGithub
52        | OutputFormat::ReviewGitlab
53        | OutputFormat::Badge
54        | OutputFormat::GithubAnnotations
55        | OutputFormat::GithubSummary => crate::error::emit_error(
56            "explain supports human, compact, markdown, and json output",
57            2,
58            output,
59        ),
60    }
61}
62
63fn render_explain_json(
64    issue_type: &str,
65    json_style: crate::json_style::JsonStyle,
66) -> Result<String, fallow_api::ProgrammaticError> {
67    let value = serialize_explain_programmatic_json(
68        issue_type,
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}