Skip to main content

amont_runtime/hooks/
lint_json_yaml.rs

1//! pre-commit-lint-json-yaml — parse staged JSON/YAML so a syntax error never
2//! reaches the repo. Both linters soft-fail when absent: warn, don't block.
3
4use super::common::{fail, hl, ok, repo_root, staged_files, warn, which};
5use crate::check::Outcome;
6use std::path::Path;
7use std::process::{Command, Stdio};
8
9/// The extensions each half consumes, and their union.
10///
11/// EXPORTED so `registry.rs` can DECLARE the scope from the same constant the
12/// check FILTERS with. They drifted: the registry declared
13/// `[".json", ".yaml", ".yml"]` while this module asked for `[".yaml"]`, so
14/// `amont list` reported the check as covering `.yml` and a staged, broken
15/// `x.yml` returned `Outcome::Passed` with no output at all. A `const fn` over
16/// a `const` is legal in a const item, so the registry can reference these
17/// directly and the two cannot disagree again.
18pub const JSON: &[&str] = &[".json"];
19pub const YAML: &[&str] = &[".yaml", ".yml"];
20pub const EXTS: &[&str] = &[".json", ".yaml", ".yml"];
21
22/// Helm chart templates carry Go templating (`{{ }}`) and are not valid YAML
23/// until Helm renders them. A staged YAML under a chart's `templates/` — i.e.
24/// with a sibling `Chart.yaml` at the chart root — is skipped, or valid chart
25/// commits would need --no-verify just to get past this hook.
26pub fn is_helm_template(root: &str, file: &str) -> bool {
27    let Some(i) = file.find("/templates/") else {
28        return false;
29    };
30    Path::new(root)
31        .join(&file[..i])
32        .join("Chart.yaml")
33        .is_file()
34}
35
36fn parses(root: &str, tool: &str, args: &[&str]) -> bool {
37    Command::new(super::common::program(tool))
38        .args(args)
39        .current_dir(root)
40        .stdin(Stdio::null())
41        .stdout(Stdio::null())
42        .stderr(Stdio::null())
43        .status()
44        .map(|s| s.success())
45        .unwrap_or(false)
46}
47
48pub fn run(_args: &[std::ffi::OsString]) -> Outcome {
49    let json: Vec<String> = staged_files(JSON);
50    let yaml: Vec<String> = staged_files(YAML);
51    if json.is_empty() && yaml.is_empty() {
52        return Outcome::Passed;
53    }
54    let root = repo_root();
55    // Two independent halves. A missing parser silences one of them, and a
56    // half that never ran must not be reported as a half that passed — so
57    // both a failure anywhere and a gap anywhere outrank the clean case.
58    let mut failed = false;
59    let mut unavailable = false;
60
61    if !json.is_empty() {
62        if which("node").is_some() {
63            for f in &json {
64                let script = r#"JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"))"#;
65                // `--` before `f`: a staged file named e.g. `-e.json` would
66                // otherwise be read as another `-e` by node's own parser.
67                if !parses(&root, "node", &["-e", script, "--", f]) {
68                    fail(&format!("Invalid JSON: {}", hl(f)));
69                    failed = true;
70                }
71            }
72        } else {
73            warn(&format!(
74                "JSON files detected. To lint them, install {}",
75                hl("node")
76            ));
77            unavailable = true;
78        }
79    }
80
81    if !yaml.is_empty() {
82        if which("yq").is_some() {
83            for f in &yaml {
84                if is_helm_template(&root, f) {
85                    continue;
86                }
87                // `--` before `f`: a staged file starting with `-` would
88                // otherwise be read as a flag by yq's own parser.
89                if !parses(&root, "yq", &["e", "true", "--", f]) {
90                    fail(&format!("Invalid YAML: {}", hl(f)));
91                    failed = true;
92                }
93            }
94        } else {
95            warn(&format!(
96                "YAML files detected. To lint them, install {}",
97                hl("yq")
98            ));
99            unavailable = true;
100        }
101    }
102
103    match (failed, unavailable) {
104        (true, _) => Outcome::Failed,
105        (false, true) => Outcome::Unavailable,
106        (false, false) => {
107            ok("Json/Yaml Lint passed");
108            Outcome::Passed
109        }
110    }
111}