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    let mut cmd = Command::new(super::common::program(tool));
38    cmd.args(args)
39        .current_dir(root)
40        .stdin(Stdio::null())
41        .stdout(Stdio::null())
42        .stderr(Stdio::null());
43    super::common::bounded_success(&mut cmd, tool)
44}
45
46pub fn run(_args: &[std::ffi::OsString]) -> Outcome {
47    let json: Vec<String> = staged_files(JSON);
48    let yaml: Vec<String> = staged_files(YAML);
49    if json.is_empty() && yaml.is_empty() {
50        return Outcome::Passed;
51    }
52    let root = repo_root();
53    // Two independent halves. A missing parser silences one of them, and a
54    // half that never ran must not be reported as a half that passed — so
55    // both a failure anywhere and a gap anywhere outrank the clean case.
56    let mut failed = false;
57    let mut unavailable = false;
58
59    if !json.is_empty() {
60        if which("node").is_some() {
61            for f in &json {
62                let script = r#"JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"))"#;
63                // `--` before `f`: a staged file named e.g. `-e.json` would
64                // otherwise be read as another `-e` by node's own parser.
65                if !parses(&root, "node", &["-e", script, "--", f]) {
66                    fail(&format!("Invalid JSON: {}", hl(f)));
67                    failed = true;
68                }
69            }
70        } else {
71            warn(&format!(
72                "JSON files detected. To lint them, install {}",
73                hl("node")
74            ));
75            unavailable = true;
76        }
77    }
78
79    if !yaml.is_empty() {
80        if which("yq").is_some() {
81            for f in &yaml {
82                if is_helm_template(&root, f) {
83                    continue;
84                }
85                // `--` before `f`: a staged file starting with `-` would
86                // otherwise be read as a flag by yq's own parser.
87                if !parses(&root, "yq", &["e", "true", "--", f]) {
88                    fail(&format!("Invalid YAML: {}", hl(f)));
89                    failed = true;
90                }
91            }
92        } else {
93            warn(&format!(
94                "YAML files detected. To lint them, install {}",
95                hl("yq")
96            ));
97            unavailable = true;
98        }
99    }
100
101    match (failed, unavailable) {
102        (true, _) => Outcome::Failed,
103        (false, true) => Outcome::Unavailable,
104        (false, false) => {
105            ok("Json/Yaml Lint passed");
106            Outcome::Passed
107        }
108    }
109}