1use std::ffi::OsString;
15use std::path::Path;
16
17use crate::check::{Builtin, Check, Fix, GitState, Outcome, Scope, Severity, Stage, Verdict};
18use crate::pushrefs::PushRefs;
19use crate::{dispatch, hooks};
20
21pub struct Ctx<'a> {
25 pub name: &'a str,
27 pub args: &'a [OsString],
29 pub hooks_dir: &'a Path,
32 pub push: &'a PushRefs,
35 pub manifest: &'a crate::manifest::Manifest,
39}
40
41pub type HookFn = fn(&Ctx) -> Verdict;
42
43pub const ENTRYPOINTS: &[(&str, HookFn)] = &[
46 ("pre-commit", dispatch::pre_commit),
47 ("pre-push", dispatch::pre_push),
48 ("commit-msg", |ctx| hooks::commit_msg::run(ctx.args)),
49 ("prepare-commit-msg", |ctx| {
50 hooks::prepare_commit_msg::run(ctx.args)
51 }),
52 ("post-commit", |ctx| hooks::post_commit::run(ctx)),
53];
54
55const MID_OPERATION: &[GitState] = &[
74 GitState::Merge,
75 GitState::Rebase,
76 GitState::CherryPick,
77 GitState::Revert,
78];
79
80pub const CHECKS: &[Builtin] = &[
81 Builtin {
83 name: "pre-commit-argo-lint",
84 stage: Stage::PreCommit,
85 scope: Scope::new(
86 hooks::k8s::EXTS,
87 &["kustomization.yaml", "kustomization.yml"],
88 )
89 .not_during(MID_OPERATION),
90 severity: Severity::Block,
91 fix: Fix::None,
92 run: |ctx| hooks::k8s::argo_lint(ctx.args),
93 },
94 Builtin {
95 name: "pre-commit-ban-terms",
96 stage: Stage::PreCommit,
97 scope: Scope::files(hooks::ban_terms::EXTS),
98 severity: Severity::Block,
99 fix: Fix::None,
100 run: |ctx| hooks::ban_terms::run(ctx.name, ctx.args),
101 },
102 Builtin {
107 name: "pre-commit-branch-pattern",
108 stage: Stage::PreCommit,
109 scope: Scope::ALWAYS,
110 severity: Severity::Warn,
111 fix: Fix::None,
112 run: |_ctx| hooks::branch_pattern::early(),
113 },
114 Builtin {
115 name: "pre-commit-cargo-fmt",
116 stage: Stage::PreCommit,
117 scope: Scope::new(hooks::rust_tools::EXTS, &["Cargo.toml"]).not_during(MID_OPERATION),
118 severity: Severity::Block,
119 fix: Fix::Rewrite,
120 run: |ctx| hooks::rust_tools::fmt(ctx.args),
121 },
122 Builtin {
123 name: "pre-commit-clippy",
124 stage: Stage::PreCommit,
125 scope: Scope::new(hooks::rust_tools::EXTS, &["Cargo.toml"]).not_during(MID_OPERATION),
126 severity: Severity::Block,
127 fix: Fix::None,
128 run: |ctx| hooks::rust_tools::clippy(ctx.args),
129 },
130 Builtin {
131 name: "pre-commit-go-vet",
132 stage: Stage::PreCommit,
133 scope: Scope::new(hooks::go_tools::EXTS, &["go.mod"]).not_during(MID_OPERATION),
134 severity: Severity::Block,
135 fix: Fix::None,
136 run: |ctx| hooks::go_tools::vet(ctx.args),
137 },
138 Builtin {
139 name: "pre-commit-gofmt",
140 stage: Stage::PreCommit,
141 scope: Scope::new(hooks::go_tools::EXTS, &["go.mod"]).not_during(MID_OPERATION),
142 severity: Severity::Block,
143 fix: Fix::Rewrite,
144 run: |ctx| hooks::go_tools::fmt(ctx.args),
145 },
146 Builtin {
147 name: "pre-commit-kube-linter",
148 stage: Stage::PreCommit,
149 scope: Scope::new(
150 hooks::k8s::EXTS,
151 &[".kube-linter*.yaml", ".kube-linter*.yml"],
152 )
153 .not_during(MID_OPERATION),
154 severity: Severity::Block,
155 fix: Fix::None,
156 run: |ctx| hooks::k8s::kube_linter(ctx.args),
157 },
158 Builtin {
159 name: "pre-commit-kubeconform",
160 stage: Stage::PreCommit,
161 scope: Scope::new(
162 hooks::k8s::EXTS,
163 &["kustomization.yaml", "kustomization.yml"],
164 )
165 .not_during(MID_OPERATION),
166 severity: Severity::Block,
167 fix: Fix::None,
168 run: |ctx| hooks::k8s::kubeconform(ctx.args),
169 },
170 Builtin {
171 name: "pre-commit-large-files",
172 stage: Stage::PreCommit,
173 scope: Scope::ALWAYS,
174 severity: Severity::Block,
175 fix: Fix::None,
176 run: |_ctx| hooks::large_files::staged(),
177 },
178 Builtin {
179 name: "pre-commit-lint-js",
180 stage: Stage::PreCommit,
181 scope: Scope::new(hooks::lint_js::EXTS, &["package.json"]).not_during(MID_OPERATION),
182 severity: Severity::Block,
183 fix: Fix::None,
184 run: |ctx| hooks::lint_js::run(ctx.args),
185 },
186 Builtin {
187 name: "pre-commit-lint-json-yaml",
188 stage: Stage::PreCommit,
189 scope: Scope::files(hooks::lint_json_yaml::EXTS).not_during(MID_OPERATION),
190 severity: Severity::Block,
191 fix: Fix::None,
192 run: |ctx| hooks::lint_json_yaml::run(ctx.args),
193 },
194 Builtin {
195 name: "pre-commit-merge-conflict",
196 stage: Stage::PreCommit,
197 scope: Scope::ALWAYS,
198 severity: Severity::Block,
199 fix: Fix::None,
200 run: |ctx| hooks::merge_conflict::run(ctx.name, ctx.args),
201 },
202 Builtin {
203 name: "pre-commit-package-lock",
204 stage: Stage::PreCommit,
205 scope: Scope::new(&[], &["package.json"]),
206 severity: Severity::Block,
207 fix: Fix::None,
208 run: |ctx| hooks::package_lock::run(ctx.args),
209 },
210 Builtin {
211 name: "pre-commit-prettier",
212 stage: Stage::PreCommit,
213 scope: Scope::new(
214 &[],
215 &[
216 ".prettierrc",
217 ".prettierrc.json",
218 ".prettierrc.yml",
219 ".prettierrc.yaml",
220 ".prettierrc.js",
221 "prettier.config.js",
222 ],
223 )
224 .not_during(MID_OPERATION),
225 severity: Severity::Block,
226 fix: Fix::Rewrite,
227 run: |ctx| hooks::prettier::run(ctx.args),
228 },
229 Builtin {
230 name: "pre-commit-pyright",
231 stage: Stage::PreCommit,
232 scope: Scope::new(
233 hooks::python_tools::EXTS,
234 &[
235 "pyrightconfig.json",
236 "pyrightconfig.jsonc",
237 "pyproject.toml",
238 ],
239 )
240 .not_during(MID_OPERATION),
241 severity: Severity::Block,
242 fix: Fix::None,
243 run: |ctx| hooks::python_tools::pyright(ctx.args),
244 },
245 Builtin {
246 name: "pre-commit-ruff",
247 stage: Stage::PreCommit,
248 scope: Scope::new(
249 hooks::python_tools::EXTS,
250 &["ruff.toml", ".ruff.toml", "pyproject.toml"],
251 )
252 .not_during(MID_OPERATION),
253 severity: Severity::Block,
254 fix: Fix::Rewrite,
255 run: |ctx| hooks::python_tools::ruff(ctx.args),
256 },
257 Builtin {
261 name: "pre-commit-secrets",
262 stage: Stage::PreCommit,
263 scope: Scope::ALWAYS,
264 severity: Severity::Block,
265 fix: Fix::None,
266 run: |_ctx| hooks::secrets::staged(),
267 },
268 Builtin {
269 name: "pre-commit-usual-name",
270 stage: Stage::PreCommit,
271 scope: Scope::ALWAYS,
272 severity: Severity::Block,
273 fix: Fix::None,
274 run: |ctx| hooks::usual_name::run(ctx.args),
275 },
276 Builtin {
277 name: "pre-commit-yamllint",
278 stage: Stage::PreCommit,
279 scope: Scope::new(
280 hooks::yamllint::EXTS,
281 &[".yamllint.yaml", ".yamllint.yml", ".yamllint"],
282 )
283 .not_during(MID_OPERATION),
284 severity: Severity::Block,
285 fix: Fix::None,
286 run: |ctx| hooks::yamllint::run(ctx.args),
287 },
288 Builtin {
290 name: "pre-push-branch-protect",
291 stage: Stage::PrePush,
292 scope: Scope::ALWAYS,
293 severity: Severity::Block,
294 fix: Fix::None,
295 run: |ctx| hooks::branch_protect::run(ctx.push.get()),
296 },
297 Builtin {
298 name: "pre-push-branch-pattern",
299 stage: Stage::PrePush,
300 scope: Scope::ALWAYS,
301 severity: Severity::Block,
302 fix: Fix::None,
303 run: |ctx| hooks::branch_pattern::run(ctx.push.get(), ctx.args),
304 },
305 Builtin {
306 name: "pre-push-secrets",
307 stage: Stage::PrePush,
308 scope: Scope::ALWAYS,
309 severity: Severity::Block,
310 fix: Fix::None,
311 run: |ctx| hooks::secrets::pushed(ctx.push.get()),
312 },
313 Builtin {
314 name: "pre-push-pull-rebase",
315 stage: Stage::PrePush,
316 scope: Scope::ALWAYS.not_during(&[GitState::Rebase, GitState::Merge]),
317 severity: Severity::Block,
318 fix: Fix::None,
319 run: |ctx| hooks::pull_rebase::run(ctx.args),
320 },
321 Builtin {
328 name: "pre-push-audit-go",
329 stage: Stage::PrePush,
330 scope: Scope::new(&[], &["go.sum"]),
331 severity: Severity::Block,
332 fix: Fix::None,
333 run: |ctx| hooks::audit::go(ctx.push.get()),
334 },
335 Builtin {
336 name: "pre-push-audit-js",
337 stage: Stage::PrePush,
338 scope: Scope::new(&[], &["package-lock.json"]),
339 severity: Severity::Block,
340 fix: Fix::None,
341 run: |ctx| hooks::audit::js(ctx.push.get()),
342 },
343 Builtin {
344 name: "pre-push-audit-python",
345 stage: Stage::PrePush,
346 scope: Scope::new(&[], &["requirements.txt"]),
347 severity: Severity::Block,
348 fix: Fix::None,
349 run: |ctx| hooks::audit::python(ctx.push.get()),
350 },
351 Builtin {
352 name: "pre-push-audit-rust",
353 stage: Stage::PrePush,
354 scope: Scope::new(&[], &["Cargo.lock"]),
355 severity: Severity::Block,
356 fix: Fix::None,
357 run: |ctx| hooks::audit::rust(ctx.push.get()),
358 },
359 Builtin {
360 name: "pre-push-run-tests-js",
361 stage: Stage::PrePush,
362 scope: Scope::new(hooks::run_tests::JS_EXTS, &["package.json"])
363 .not_during(&[GitState::Bisect, GitState::Rebase]),
364 severity: Severity::Block,
365 fix: Fix::None,
366 run: |ctx| hooks::run_tests::run(ctx.push.get(), &ctx.manifest.externals),
367 },
368 Builtin {
369 name: "pre-push-cargo-test",
370 stage: Stage::PrePush,
371 scope: Scope::new(hooks::rust_tools::EXTS, &["Cargo.toml"])
372 .not_during(&[GitState::Bisect, GitState::Rebase]),
373 severity: Severity::Block,
374 fix: Fix::None,
375 run: |ctx| hooks::rust_tools::test(ctx.push.get()),
376 },
377 Builtin {
378 name: "pre-push-go-test",
379 stage: Stage::PrePush,
380 scope: Scope::new(hooks::go_tools::EXTS, &["go.mod"])
381 .not_during(&[GitState::Bisect, GitState::Rebase]),
382 severity: Severity::Block,
383 fix: Fix::None,
384 run: |ctx| hooks::go_tools::test(ctx.push.get()),
385 },
386 Builtin {
387 name: "pre-push-pytest",
388 stage: Stage::PrePush,
389 scope: Scope::new(hooks::python_tools::EXTS, &["pytest.ini", "conftest.py"])
390 .not_during(&[GitState::Bisect, GitState::Rebase]),
391 severity: Severity::Block,
392 fix: Fix::None,
393 run: |ctx| hooks::python_tools::pytest(ctx.push.get()),
394 },
395];
396
397pub fn severity_of(check: &dyn Check) -> Severity {
403 effective_override(None, check.name()).unwrap_or_else(|| check.severity())
404}
405
406pub fn severity_key(check: &str) -> String {
408 format!("amont.severity.{check}")
409}
410
411#[derive(Debug, Default, Clone)]
422pub struct Overrides(std::collections::BTreeMap<String, Severity>);
423
424impl Overrides {
425 pub fn read() -> Overrides {
426 Overrides::from_config(crate::git::stdout(&[
427 "config",
428 "--get-regexp",
429 r"^amont\.severity\.",
430 ]))
431 }
432
433 pub fn from_config(out: Option<String>) -> Overrides {
441 let mut map = std::collections::BTreeMap::new();
442 for line in out.as_deref().unwrap_or_default().lines() {
443 let Some((key, value)) = line.split_once(' ') else {
444 continue;
445 };
446 let Some(check) = key.strip_prefix("amont.severity.") else {
447 continue;
448 };
449 match Severity::parse(value.trim()) {
452 Some(s) => {
453 map.insert(check.to_string(), s);
454 }
455 None => {
458 map.remove(check);
459 }
460 }
461 }
462 Overrides(map)
463 }
464
465 pub fn applied_to(&self, check: &str) -> Option<(&str, Severity)> {
472 self.0
473 .iter()
474 .filter_map(|(pattern, severity)| {
475 crate::names_check(check, pattern).map(|m| (m, pattern.as_str(), *severity))
476 })
477 .max_by_key(|(m, _, _)| *m)
478 .map(|(_, pattern, severity)| (pattern, severity))
479 }
480
481 pub fn of(&self, check: &dyn Check) -> Severity {
483 self.applied_to(check.name())
484 .map(|(_, severity)| severity)
485 .unwrap_or_else(|| check.severity())
486 }
487}
488
489#[cfg(test)]
490mod precedence {
491 use super::{Overrides, Severity};
492
493 fn overrides(lines: &[&str]) -> Overrides {
494 let text = lines
495 .iter()
496 .map(|l| format!("amont.severity.{l}\n"))
497 .collect::<String>();
498 Overrides::from_config(Some(text))
499 }
500
501 #[test]
505 fn the_more_specific_key_wins() {
506 let both = overrides(&["pre-commit warn", "pre-commit-clippy block"]);
507 assert_eq!(
508 both.applied_to("pre-commit-clippy"),
509 Some(("pre-commit-clippy", Severity::Block)),
510 "a full id beats its trigger"
511 );
512 assert_eq!(
513 both.applied_to("pre-commit-shellcheck"),
514 Some(("pre-commit", Severity::Warn)),
515 "and the trigger still governs every check it did not exempt"
516 );
517 }
518
519 #[test]
522 fn the_three_ways_to_name_a_check_are_ranked() {
523 let all = overrides(&["pre-commit warn", "clippy block", "pre-commit-clippy warn"]);
524 assert_eq!(
525 all.applied_to("pre-commit-clippy"),
526 Some(("pre-commit-clippy", Severity::Warn))
527 );
528
529 let no_full = overrides(&["pre-commit warn", "clippy block"]);
530 assert_eq!(
531 no_full.applied_to("pre-commit-clippy"),
532 Some(("clippy", Severity::Block)),
533 "a short name beats a trigger"
534 );
535 }
536
537 #[test]
540 fn a_key_that_names_no_check_applies_to_nothing() {
541 let typo = overrides(&["clipy warn", "e warn", " warn"]);
542 assert_eq!(typo.applied_to("pre-commit-clippy"), None);
543 }
544}
545
546pub fn effective_override(repo: Option<&Path>, check: &str) -> Option<Severity> {
556 overrides_in(repo).applied_to(check).map(|(_, s)| s)
557}
558
559pub fn effective_key(repo: Option<&Path>, check: &str) -> Option<String> {
565 overrides_in(repo)
566 .applied_to(check)
567 .map(|(pattern, _)| pattern.to_string())
568}
569
570fn overrides_in(repo: Option<&Path>) -> Overrides {
571 let args = ["config", "--get-regexp", r"^amont\.severity\."];
572 Overrides::from_config(match repo {
573 None => crate::git::stdout(&args),
574 Some(dir) => crate::git::stdout_in(dir, &args),
575 })
576}
577
578pub fn stage_checks(stage: Stage) -> impl Iterator<Item = &'static Builtin> {
580 CHECKS.iter().filter(move |check| check.stage == stage)
581}
582
583pub fn all_stage_checks<'a>(
590 stage: Stage,
591 manifest: &'a crate::manifest::Manifest,
592) -> Vec<&'a dyn Check> {
593 let mut out: Vec<&'a dyn Check> = stage_checks(stage)
594 .map(|check| check as &dyn Check)
595 .collect();
596 out.extend(
597 manifest
598 .externals
599 .iter()
600 .filter(|external| external.stage == stage)
601 .map(|external| external as &dyn Check),
602 );
603 out
604}
605
606pub fn lookup(name: &str, manifest: &crate::manifest::Manifest) -> Option<HookFn> {
607 if let Some((_, f)) = ENTRYPOINTS.iter().find(|(n, _)| *n == name) {
608 return Some(*f);
609 }
610 if CHECKS.iter().any(|check| check.name == name)
617 || manifest
618 .externals
619 .iter()
620 .any(|external| external.id == name)
621 {
622 return Some(|ctx: &Ctx| {
623 let check = one_named(ctx.name, ctx.manifest).expect("checked above");
624 Verdict::blocking(matches!(
625 (check.run(ctx), severity_of(check)),
626 (Outcome::Failed, Severity::Block)
627 ))
628 });
629 }
630 None
631}
632
633pub fn one_named<'a>(name: &str, manifest: &'a crate::manifest::Manifest) -> Option<&'a dyn Check> {
637 if let Some(builtin) = CHECKS.iter().find(|check| check.name == name) {
638 return Some(builtin);
639 }
640 manifest
641 .externals
642 .iter()
643 .find(|external| external.id == name)
644 .map(|external| external as &dyn Check)
645}
646
647#[cfg(test)]
648mod tests {
649 use super::{lookup, Overrides, Severity, Stage, CHECKS, ENTRYPOINTS};
650 use std::collections::BTreeSet;
651
652 #[test]
659 fn the_batch_agrees_with_the_authority() {
660 let d = std::env::temp_dir().join(format!("ov-{}", std::process::id()));
661 let _ = std::fs::remove_dir_all(&d);
662 std::fs::create_dir_all(&d).unwrap();
663 let git = |args: &[&str]| {
664 std::process::Command::new("git")
665 .args(args)
666 .current_dir(&d)
667 .output()
668 .expect("git");
669 };
670 git(&["init", "-q", "--template=", "."]);
671 let key = "amont.severity.pre-commit-merge-conflict";
672 git(&["config", "--add", key, "warn"]);
673 git(&["config", "--add", key, "block"]);
674
675 let raw = std::process::Command::new("git")
676 .args(["config", "--get-regexp", r"^amont\.severity\."])
677 .current_dir(&d)
678 .output()
679 .expect("git");
680 let batch = Overrides::from_config(Some(
681 String::from_utf8_lossy(&raw.stdout).trim().to_string(),
682 ));
683 let authority =
684 crate::git::stdout_in(&d, &["config", "--get", key]).and_then(|v| Severity::parse(&v));
685 let _ = std::fs::remove_dir_all(&d);
686
687 assert_eq!(
688 authority,
689 Some(Severity::Block),
690 "git applies the last entry"
691 );
692 assert_eq!(
693 batch.0.get("pre-commit-merge-conflict").copied(),
694 authority,
695 "the batch reader disagreed with `--get`"
696 );
697 }
698
699 #[test]
703 fn an_unrecognised_value_clears_rather_than_overrides() {
704 let o = Overrides::from_config(Some(
705 "amont.severity.a warn\namont.severity.a advisory\namont.severity.b warn".to_string(),
706 ));
707 assert_eq!(o.0.get("a"), None, "a typo must not leave `warn` standing");
708 assert_eq!(o.0.get("b").copied(), Some(Severity::Warn));
709 }
710
711 #[test]
712 fn names_are_unique_across_entrypoints_and_checks() {
713 let mut seen = BTreeSet::new();
714 for n in ENTRYPOINTS
715 .iter()
716 .map(|(n, _)| *n)
717 .chain(CHECKS.iter().map(|check| check.name))
718 {
719 assert!(seen.insert(n), "duplicate registration: {n}");
720 }
721 }
722
723 #[test]
725 fn the_shipped_shims_are_exactly_the_git_invoked_hooks() {
726 let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../../templates/hooks");
727 let mut shipped: Vec<String> = std::fs::read_dir(dir)
728 .expect("templates/hooks")
729 .flatten()
730 .map(|entry| entry.file_name().to_string_lossy().into_owned())
731 .collect();
732 shipped.sort();
733 assert_eq!(
734 shipped,
735 vec![
736 "commit-msg",
737 "post-commit",
738 "pre-commit",
739 "pre-push",
740 "prepare-commit-msg"
741 ]
742 );
743 let none = crate::manifest::Manifest::default();
744 for name in &shipped {
745 assert!(
746 lookup(name, &none).is_some(),
747 "shipped shim {name:?} has no handler"
748 );
749 }
750 }
751
752 #[test]
755 fn every_check_is_reachable_by_name() {
756 let none = crate::manifest::Manifest::default();
757 for check in CHECKS {
758 assert!(
759 lookup(check.name, &none).is_some(),
760 "{} not reachable",
761 check.name
762 );
763 }
764 assert!(lookup("pre-commit-not-a-check", &none).is_none());
765 }
766
767 #[test]
769 fn pre_push_runs_cheapest_first() {
770 let order: Vec<&str> = super::stage_checks(Stage::PrePush)
771 .map(|check| check.name)
772 .collect();
773 assert_eq!(
774 order,
775 vec![
776 "pre-push-branch-protect",
777 "pre-push-branch-pattern",
778 "pre-push-secrets",
779 "pre-push-pull-rebase",
780 "pre-push-audit-go",
781 "pre-push-audit-js",
782 "pre-push-audit-python",
783 "pre-push-audit-rust",
784 "pre-push-run-tests-js",
785 "pre-push-cargo-test",
786 "pre-push-go-test",
787 "pre-push-pytest",
788 ]
789 );
790 }
791
792 enum Consumes {
798 All,
799 Exts(&'static [&'static str]),
800 }
801
802 const CONSUMED: &[(&str, Consumes)] = &[
816 (
817 "pre-commit-argo-lint",
818 Consumes::Exts(crate::hooks::k8s::EXTS),
819 ),
820 (
824 "pre-commit-ban-terms",
825 Consumes::Exts(crate::hooks::ban_terms::EXTS),
826 ),
827 (
828 "pre-commit-cargo-fmt",
829 Consumes::Exts(crate::hooks::rust_tools::EXTS),
830 ),
831 (
832 "pre-commit-clippy",
833 Consumes::Exts(crate::hooks::rust_tools::RUST_PATHS),
834 ),
835 (
836 "pre-commit-go-vet",
837 Consumes::Exts(crate::hooks::go_tools::GO_PATHS),
838 ),
839 (
840 "pre-commit-gofmt",
841 Consumes::Exts(crate::hooks::go_tools::EXTS),
842 ),
843 (
844 "pre-commit-kube-linter",
845 Consumes::Exts(crate::hooks::k8s::EXTS),
846 ),
847 (
848 "pre-commit-kubeconform",
849 Consumes::Exts(crate::hooks::k8s::EXTS),
850 ),
851 (
852 "pre-commit-lint-js",
853 Consumes::Exts(crate::hooks::lint_js::EXTS),
854 ),
855 (
856 "pre-commit-lint-json-yaml",
857 Consumes::Exts(crate::hooks::lint_json_yaml::EXTS),
858 ),
859 ("pre-commit-merge-conflict", Consumes::All),
860 ("pre-commit-package-lock", Consumes::All),
861 (
865 "pre-commit-prettier",
866 Consumes::Exts(crate::hooks::prettier::EXTS),
867 ),
868 (
869 "pre-commit-pyright",
870 Consumes::Exts(crate::hooks::python_tools::EXTS),
871 ),
872 (
873 "pre-commit-ruff",
874 Consumes::Exts(crate::hooks::python_tools::EXTS),
875 ),
876 ("pre-commit-usual-name", Consumes::All),
877 (
878 "pre-commit-yamllint",
879 Consumes::Exts(crate::hooks::yamllint::EXTS),
880 ),
881 ("pre-commit-large-files", Consumes::All),
882 ("pre-commit-secrets", Consumes::All),
883 ("pre-push-secrets", Consumes::All),
884 ("pre-push-branch-protect", Consumes::All),
885 ("pre-push-branch-pattern", Consumes::All),
886 ("pre-push-pull-rebase", Consumes::All),
887 (
888 "pre-push-run-tests-js",
889 Consumes::Exts(crate::hooks::run_tests::JS_EXTS),
890 ),
891 (
892 "pre-push-pytest",
893 Consumes::Exts(crate::hooks::python_tools::EXTS),
894 ),
895 (
896 "pre-push-cargo-test",
897 Consumes::Exts(crate::hooks::rust_tools::RUST_PATHS),
898 ),
899 (
900 "pre-push-go-test",
901 Consumes::Exts(crate::hooks::go_tools::GO_PATHS),
902 ),
903 ];
904
905 #[test]
916 fn no_check_declares_a_file_type_it_does_not_consume() {
917 for (name, _) in CONSUMED {
918 assert!(
919 CHECKS.iter().any(|check| check.name == *name),
920 "CONSUMED names {name:?}, which is not a check"
921 );
922 }
923 for check in CHECKS {
924 let entry = CONSUMED.iter().find(|(name, _)| *name == check.name);
925 if check.scope.files.is_empty() && entry.is_none() {
926 continue;
927 }
928 let Some((_, consumes)) = entry else {
929 panic!(
930 "{} declares scope.files {:?} but is missing from CONSUMED — \
931 say what it actually reads",
932 check.name, check.scope.files
933 );
934 };
935 let Consumes::Exts(consumed) = consumes else {
936 continue; };
938 for ext in check.scope.files {
939 assert!(
940 consumed.contains(ext),
941 "{} declares {ext:?} in its scope but never asks for it — \
942 `amont list` would report a coverage the check does not have",
943 check.name
944 );
945 }
946 }
947 }
948
949 const HAS_FIXING_CODE: &[(&str, bool)] = &[
958 ("pre-commit-argo-lint", false),
959 ("pre-commit-ban-terms", false),
960 ("pre-commit-branch-pattern", false),
961 ("pre-commit-cargo-fmt", true),
962 ("pre-commit-clippy", false),
963 ("pre-commit-go-vet", false),
964 ("pre-commit-gofmt", true),
965 ("pre-commit-kube-linter", false),
966 ("pre-commit-kubeconform", false),
967 ("pre-commit-lint-js", false),
968 ("pre-commit-lint-json-yaml", false),
969 ("pre-commit-merge-conflict", false),
970 ("pre-commit-package-lock", false),
971 ("pre-commit-prettier", true),
972 ("pre-commit-pyright", false),
973 ("pre-commit-ruff", true),
974 ("pre-commit-usual-name", false),
975 ("pre-commit-yamllint", false),
976 ("pre-commit-large-files", false),
977 ("pre-commit-secrets", false),
978 ("pre-push-secrets", false),
979 ("pre-push-branch-protect", false),
980 ("pre-push-branch-pattern", false),
981 ("pre-push-pull-rebase", false),
982 ("pre-push-audit-go", false),
983 ("pre-push-audit-js", false),
984 ("pre-push-audit-python", false),
985 ("pre-push-audit-rust", false),
986 ("pre-push-run-tests-js", false),
987 ("pre-push-cargo-test", false),
988 ("pre-push-go-test", false),
989 ("pre-push-pytest", false),
990 ];
991
992 #[test]
995 fn every_rewrite_declaration_has_a_fixer() {
996 let declared: BTreeSet<&str> = CHECKS
997 .iter()
998 .filter(|check| check.fix == super::Fix::Rewrite)
999 .map(|check| check.name)
1000 .collect();
1001 let implemented: BTreeSet<&str> = HAS_FIXING_CODE
1002 .iter()
1003 .filter(|(_, has)| *has)
1004 .map(|(name, _)| *name)
1005 .collect();
1006 assert_eq!(
1007 declared, implemented,
1008 "a check declaring Fix::Rewrite with no fixer lies to `amont list --json`, \
1009 and a check with a fixer that does not declare it can never be reached"
1010 );
1011
1012 let listed: BTreeSet<&str> = HAS_FIXING_CODE.iter().map(|(name, _)| *name).collect();
1015 let all: BTreeSet<&str> = CHECKS.iter().map(|check| check.name).collect();
1016 assert_eq!(listed, all, "HAS_FIXING_CODE does not cover CHECKS");
1017 }
1018
1019 #[test]
1022 fn every_check_declares_a_stage_and_a_scope() {
1023 assert_eq!(CHECKS.len(), 32);
1024 let pre_commit = super::stage_checks(Stage::PreCommit).count();
1025 let pre_push = super::stage_checks(Stage::PrePush).count();
1026 assert_eq!(
1027 pre_commit + pre_push,
1028 CHECKS.len(),
1029 "every check has a stage"
1030 );
1031 }
1032}