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(&[".js", ".jsx", ".ts", ".tsx", ".vue"]),
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-kube-linter",
132 stage: Stage::PreCommit,
133 scope: Scope::new(
134 hooks::k8s::EXTS,
135 &[".kube-linter*.yaml", ".kube-linter*.yml"],
136 )
137 .not_during(MID_OPERATION),
138 severity: Severity::Block,
139 fix: Fix::None,
140 run: |ctx| hooks::k8s::kube_linter(ctx.args),
141 },
142 Builtin {
143 name: "pre-commit-kubeconform",
144 stage: Stage::PreCommit,
145 scope: Scope::new(
146 hooks::k8s::EXTS,
147 &["kustomization.yaml", "kustomization.yml"],
148 )
149 .not_during(MID_OPERATION),
150 severity: Severity::Block,
151 fix: Fix::None,
152 run: |ctx| hooks::k8s::kubeconform(ctx.args),
153 },
154 Builtin {
155 name: "pre-commit-lint-js",
156 stage: Stage::PreCommit,
157 scope: Scope::new(hooks::lint_js::EXTS, &["package.json"]).not_during(MID_OPERATION),
158 severity: Severity::Block,
159 fix: Fix::None,
160 run: |ctx| hooks::lint_js::run(ctx.args),
161 },
162 Builtin {
163 name: "pre-commit-lint-json-yaml",
164 stage: Stage::PreCommit,
165 scope: Scope::files(hooks::lint_json_yaml::EXTS).not_during(MID_OPERATION),
166 severity: Severity::Block,
167 fix: Fix::None,
168 run: |ctx| hooks::lint_json_yaml::run(ctx.args),
169 },
170 Builtin {
171 name: "pre-commit-merge-conflict",
172 stage: Stage::PreCommit,
173 scope: Scope::ALWAYS,
174 severity: Severity::Block,
175 fix: Fix::None,
176 run: |ctx| hooks::merge_conflict::run(ctx.name, ctx.args),
177 },
178 Builtin {
179 name: "pre-commit-package-lock",
180 stage: Stage::PreCommit,
181 scope: Scope::new(&[], &["package.json"]),
182 severity: Severity::Block,
183 fix: Fix::None,
184 run: |ctx| hooks::package_lock::run(ctx.args),
185 },
186 Builtin {
187 name: "pre-commit-prettier",
188 stage: Stage::PreCommit,
189 scope: Scope::new(
190 &[],
191 &[
192 ".prettierrc",
193 ".prettierrc.json",
194 ".prettierrc.yml",
195 ".prettierrc.yaml",
196 ".prettierrc.js",
197 "prettier.config.js",
198 ],
199 )
200 .not_during(MID_OPERATION),
201 severity: Severity::Block,
202 fix: Fix::Rewrite,
203 run: |ctx| hooks::prettier::run(ctx.args),
204 },
205 Builtin {
206 name: "pre-commit-pyright",
207 stage: Stage::PreCommit,
208 scope: Scope::new(
209 hooks::python_tools::EXTS,
210 &[
211 "pyrightconfig.json",
212 "pyrightconfig.jsonc",
213 "pyproject.toml",
214 ],
215 )
216 .not_during(MID_OPERATION),
217 severity: Severity::Block,
218 fix: Fix::None,
219 run: |ctx| hooks::python_tools::pyright(ctx.args),
220 },
221 Builtin {
222 name: "pre-commit-ruff",
223 stage: Stage::PreCommit,
224 scope: Scope::new(
225 hooks::python_tools::EXTS,
226 &["ruff.toml", ".ruff.toml", "pyproject.toml"],
227 )
228 .not_during(MID_OPERATION),
229 severity: Severity::Block,
230 fix: Fix::Rewrite,
231 run: |ctx| hooks::python_tools::ruff(ctx.args),
232 },
233 Builtin {
237 name: "pre-commit-secrets",
238 stage: Stage::PreCommit,
239 scope: Scope::ALWAYS,
240 severity: Severity::Block,
241 fix: Fix::None,
242 run: |_ctx| hooks::secrets::staged(),
243 },
244 Builtin {
245 name: "pre-commit-usual-name",
246 stage: Stage::PreCommit,
247 scope: Scope::ALWAYS,
248 severity: Severity::Block,
249 fix: Fix::None,
250 run: |ctx| hooks::usual_name::run(ctx.args),
251 },
252 Builtin {
253 name: "pre-commit-yamllint",
254 stage: Stage::PreCommit,
255 scope: Scope::new(
256 hooks::yamllint::EXTS,
257 &[".yamllint.yaml", ".yamllint.yml", ".yamllint"],
258 )
259 .not_during(MID_OPERATION),
260 severity: Severity::Block,
261 fix: Fix::None,
262 run: |ctx| hooks::yamllint::run(ctx.args),
263 },
264 Builtin {
266 name: "pre-push-branch-protect",
267 stage: Stage::PrePush,
268 scope: Scope::ALWAYS,
269 severity: Severity::Block,
270 fix: Fix::None,
271 run: |ctx| hooks::branch_protect::run(ctx.push.get()),
272 },
273 Builtin {
274 name: "pre-push-branch-pattern",
275 stage: Stage::PrePush,
276 scope: Scope::ALWAYS,
277 severity: Severity::Block,
278 fix: Fix::None,
279 run: |ctx| hooks::branch_pattern::run(ctx.push.get(), ctx.args),
280 },
281 Builtin {
282 name: "pre-push-secrets",
283 stage: Stage::PrePush,
284 scope: Scope::ALWAYS,
285 severity: Severity::Block,
286 fix: Fix::None,
287 run: |ctx| hooks::secrets::pushed(ctx.push.get()),
288 },
289 Builtin {
290 name: "pre-push-pull-rebase",
291 stage: Stage::PrePush,
292 scope: Scope::ALWAYS.not_during(&[GitState::Rebase, GitState::Merge]),
293 severity: Severity::Block,
294 fix: Fix::None,
295 run: |ctx| hooks::pull_rebase::run(ctx.args),
296 },
297 Builtin {
304 name: "pre-push-audit-js",
305 stage: Stage::PrePush,
306 scope: Scope::new(&[], &["package-lock.json"]),
307 severity: Severity::Block,
308 fix: Fix::None,
309 run: |ctx| hooks::audit::js(ctx.push.get()),
310 },
311 Builtin {
312 name: "pre-push-audit-python",
313 stage: Stage::PrePush,
314 scope: Scope::new(&[], &["requirements.txt"]),
315 severity: Severity::Block,
316 fix: Fix::None,
317 run: |ctx| hooks::audit::python(ctx.push.get()),
318 },
319 Builtin {
320 name: "pre-push-audit-rust",
321 stage: Stage::PrePush,
322 scope: Scope::new(&[], &["Cargo.lock"]),
323 severity: Severity::Block,
324 fix: Fix::None,
325 run: |ctx| hooks::audit::rust(ctx.push.get()),
326 },
327 Builtin {
328 name: "pre-push-run-tests-js",
329 stage: Stage::PrePush,
330 scope: Scope::new(hooks::run_tests::JS_EXTS, &["package.json"])
331 .not_during(&[GitState::Bisect, GitState::Rebase]),
332 severity: Severity::Block,
333 fix: Fix::None,
334 run: |ctx| hooks::run_tests::run(ctx.push.get(), &ctx.manifest.externals),
335 },
336 Builtin {
337 name: "pre-push-cargo-test",
338 stage: Stage::PrePush,
339 scope: Scope::new(hooks::rust_tools::EXTS, &["Cargo.toml"])
340 .not_during(&[GitState::Bisect, GitState::Rebase]),
341 severity: Severity::Block,
342 fix: Fix::None,
343 run: |ctx| hooks::rust_tools::test(ctx.push.get()),
344 },
345];
346
347pub fn severity_of(check: &dyn Check) -> Severity {
353 effective_override(None, check.name()).unwrap_or_else(|| check.severity())
354}
355
356pub fn severity_key(check: &str) -> String {
358 format!("amont.severity.{check}")
359}
360
361#[derive(Debug, Default, Clone)]
372pub struct Overrides(std::collections::BTreeMap<String, Severity>);
373
374impl Overrides {
375 pub fn read() -> Overrides {
376 Overrides::from_config(crate::git::stdout(&[
377 "config",
378 "--get-regexp",
379 r"^amont\.severity\.",
380 ]))
381 }
382
383 pub fn from_config(out: Option<String>) -> Overrides {
391 let mut map = std::collections::BTreeMap::new();
392 for line in out.as_deref().unwrap_or_default().lines() {
393 let Some((key, value)) = line.split_once(' ') else {
394 continue;
395 };
396 let Some(check) = key.strip_prefix("amont.severity.") else {
397 continue;
398 };
399 match Severity::parse(value.trim()) {
402 Some(s) => {
403 map.insert(check.to_string(), s);
404 }
405 None => {
408 map.remove(check);
409 }
410 }
411 }
412 Overrides(map)
413 }
414
415 pub fn applied_to(&self, check: &str) -> Option<(&str, Severity)> {
422 self.0
423 .iter()
424 .filter_map(|(pattern, severity)| {
425 crate::names_check(check, pattern).map(|m| (m, pattern.as_str(), *severity))
426 })
427 .max_by_key(|(m, _, _)| *m)
428 .map(|(_, pattern, severity)| (pattern, severity))
429 }
430
431 pub fn of(&self, check: &dyn Check) -> Severity {
433 self.applied_to(check.name())
434 .map(|(_, severity)| severity)
435 .unwrap_or_else(|| check.severity())
436 }
437}
438
439#[cfg(test)]
440mod precedence {
441 use super::{Overrides, Severity};
442
443 fn overrides(lines: &[&str]) -> Overrides {
444 let text = lines
445 .iter()
446 .map(|l| format!("amont.severity.{l}\n"))
447 .collect::<String>();
448 Overrides::from_config(Some(text))
449 }
450
451 #[test]
455 fn the_more_specific_key_wins() {
456 let both = overrides(&["pre-commit warn", "pre-commit-clippy block"]);
457 assert_eq!(
458 both.applied_to("pre-commit-clippy"),
459 Some(("pre-commit-clippy", Severity::Block)),
460 "a full id beats its trigger"
461 );
462 assert_eq!(
463 both.applied_to("pre-commit-shellcheck"),
464 Some(("pre-commit", Severity::Warn)),
465 "and the trigger still governs every check it did not exempt"
466 );
467 }
468
469 #[test]
472 fn the_three_ways_to_name_a_check_are_ranked() {
473 let all = overrides(&["pre-commit warn", "clippy block", "pre-commit-clippy warn"]);
474 assert_eq!(
475 all.applied_to("pre-commit-clippy"),
476 Some(("pre-commit-clippy", Severity::Warn))
477 );
478
479 let no_full = overrides(&["pre-commit warn", "clippy block"]);
480 assert_eq!(
481 no_full.applied_to("pre-commit-clippy"),
482 Some(("clippy", Severity::Block)),
483 "a short name beats a trigger"
484 );
485 }
486
487 #[test]
490 fn a_key_that_names_no_check_applies_to_nothing() {
491 let typo = overrides(&["clipy warn", "e warn", " warn"]);
492 assert_eq!(typo.applied_to("pre-commit-clippy"), None);
493 }
494}
495
496pub fn effective_override(repo: Option<&Path>, check: &str) -> Option<Severity> {
506 overrides_in(repo).applied_to(check).map(|(_, s)| s)
507}
508
509pub fn effective_key(repo: Option<&Path>, check: &str) -> Option<String> {
515 overrides_in(repo)
516 .applied_to(check)
517 .map(|(pattern, _)| pattern.to_string())
518}
519
520fn overrides_in(repo: Option<&Path>) -> Overrides {
521 let args = ["config", "--get-regexp", r"^amont\.severity\."];
522 Overrides::from_config(match repo {
523 None => crate::git::stdout(&args),
524 Some(dir) => crate::git::stdout_in(dir, &args),
525 })
526}
527
528pub fn stage_checks(stage: Stage) -> impl Iterator<Item = &'static Builtin> {
530 CHECKS.iter().filter(move |check| check.stage == stage)
531}
532
533pub fn all_stage_checks<'a>(
540 stage: Stage,
541 manifest: &'a crate::manifest::Manifest,
542) -> Vec<&'a dyn Check> {
543 let mut out: Vec<&'a dyn Check> = stage_checks(stage)
544 .map(|check| check as &dyn Check)
545 .collect();
546 out.extend(
547 manifest
548 .externals
549 .iter()
550 .filter(|external| external.stage == stage)
551 .map(|external| external as &dyn Check),
552 );
553 out
554}
555
556pub fn lookup(name: &str, manifest: &crate::manifest::Manifest) -> Option<HookFn> {
557 if let Some((_, f)) = ENTRYPOINTS.iter().find(|(n, _)| *n == name) {
558 return Some(*f);
559 }
560 if CHECKS.iter().any(|check| check.name == name)
567 || manifest
568 .externals
569 .iter()
570 .any(|external| external.id == name)
571 {
572 return Some(|ctx: &Ctx| {
573 let check = one_named(ctx.name, ctx.manifest).expect("checked above");
574 Verdict::blocking(matches!(
575 (check.run(ctx), severity_of(check)),
576 (Outcome::Failed, Severity::Block)
577 ))
578 });
579 }
580 None
581}
582
583pub fn one_named<'a>(name: &str, manifest: &'a crate::manifest::Manifest) -> Option<&'a dyn Check> {
587 if let Some(builtin) = CHECKS.iter().find(|check| check.name == name) {
588 return Some(builtin);
589 }
590 manifest
591 .externals
592 .iter()
593 .find(|external| external.id == name)
594 .map(|external| external as &dyn Check)
595}
596
597#[cfg(test)]
598mod tests {
599 use super::{lookup, Overrides, Severity, Stage, CHECKS, ENTRYPOINTS};
600 use std::collections::BTreeSet;
601
602 #[test]
609 fn the_batch_agrees_with_the_authority() {
610 let d = std::env::temp_dir().join(format!("ov-{}", std::process::id()));
611 let _ = std::fs::remove_dir_all(&d);
612 std::fs::create_dir_all(&d).unwrap();
613 let git = |args: &[&str]| {
614 std::process::Command::new("git")
615 .args(args)
616 .current_dir(&d)
617 .output()
618 .expect("git");
619 };
620 git(&["init", "-q", "--template=", "."]);
621 let key = "amont.severity.pre-commit-merge-conflict";
622 git(&["config", "--add", key, "warn"]);
623 git(&["config", "--add", key, "block"]);
624
625 let raw = std::process::Command::new("git")
626 .args(["config", "--get-regexp", r"^amont\.severity\."])
627 .current_dir(&d)
628 .output()
629 .expect("git");
630 let batch = Overrides::from_config(Some(
631 String::from_utf8_lossy(&raw.stdout).trim().to_string(),
632 ));
633 let authority =
634 crate::git::stdout_in(&d, &["config", "--get", key]).and_then(|v| Severity::parse(&v));
635 let _ = std::fs::remove_dir_all(&d);
636
637 assert_eq!(
638 authority,
639 Some(Severity::Block),
640 "git applies the last entry"
641 );
642 assert_eq!(
643 batch.0.get("pre-commit-merge-conflict").copied(),
644 authority,
645 "the batch reader disagreed with `--get`"
646 );
647 }
648
649 #[test]
653 fn an_unrecognised_value_clears_rather_than_overrides() {
654 let o = Overrides::from_config(Some(
655 "amont.severity.a warn\namont.severity.a advisory\namont.severity.b warn".to_string(),
656 ));
657 assert_eq!(o.0.get("a"), None, "a typo must not leave `warn` standing");
658 assert_eq!(o.0.get("b").copied(), Some(Severity::Warn));
659 }
660
661 #[test]
662 fn names_are_unique_across_entrypoints_and_checks() {
663 let mut seen = BTreeSet::new();
664 for n in ENTRYPOINTS
665 .iter()
666 .map(|(n, _)| *n)
667 .chain(CHECKS.iter().map(|check| check.name))
668 {
669 assert!(seen.insert(n), "duplicate registration: {n}");
670 }
671 }
672
673 #[test]
675 fn the_shipped_shims_are_exactly_the_git_invoked_hooks() {
676 let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../../templates/hooks");
677 let mut shipped: Vec<String> = std::fs::read_dir(dir)
678 .expect("templates/hooks")
679 .flatten()
680 .map(|entry| entry.file_name().to_string_lossy().into_owned())
681 .collect();
682 shipped.sort();
683 assert_eq!(
684 shipped,
685 vec![
686 "commit-msg",
687 "post-commit",
688 "pre-commit",
689 "pre-push",
690 "prepare-commit-msg"
691 ]
692 );
693 let none = crate::manifest::Manifest::default();
694 for name in &shipped {
695 assert!(
696 lookup(name, &none).is_some(),
697 "shipped shim {name:?} has no handler"
698 );
699 }
700 }
701
702 #[test]
705 fn every_check_is_reachable_by_name() {
706 let none = crate::manifest::Manifest::default();
707 for check in CHECKS {
708 assert!(
709 lookup(check.name, &none).is_some(),
710 "{} not reachable",
711 check.name
712 );
713 }
714 assert!(lookup("pre-commit-not-a-check", &none).is_none());
715 }
716
717 #[test]
719 fn pre_push_runs_cheapest_first() {
720 let order: Vec<&str> = super::stage_checks(Stage::PrePush)
721 .map(|check| check.name)
722 .collect();
723 assert_eq!(
724 order,
725 vec![
726 "pre-push-branch-protect",
727 "pre-push-branch-pattern",
728 "pre-push-secrets",
729 "pre-push-pull-rebase",
730 "pre-push-audit-js",
731 "pre-push-audit-python",
732 "pre-push-audit-rust",
733 "pre-push-run-tests-js",
734 "pre-push-cargo-test",
735 ]
736 );
737 }
738
739 enum Consumes {
745 All,
746 Exts(&'static [&'static str]),
747 }
748
749 const CONSUMED: &[(&str, Consumes)] = &[
763 (
764 "pre-commit-argo-lint",
765 Consumes::Exts(crate::hooks::k8s::EXTS),
766 ),
767 ("pre-commit-ban-terms", Consumes::All),
771 (
772 "pre-commit-cargo-fmt",
773 Consumes::Exts(crate::hooks::rust_tools::EXTS),
774 ),
775 (
776 "pre-commit-clippy",
777 Consumes::Exts(crate::hooks::rust_tools::RUST_PATHS),
778 ),
779 (
780 "pre-commit-kube-linter",
781 Consumes::Exts(crate::hooks::k8s::EXTS),
782 ),
783 (
784 "pre-commit-kubeconform",
785 Consumes::Exts(crate::hooks::k8s::EXTS),
786 ),
787 (
788 "pre-commit-lint-js",
789 Consumes::Exts(crate::hooks::lint_js::EXTS),
790 ),
791 (
792 "pre-commit-lint-json-yaml",
793 Consumes::Exts(crate::hooks::lint_json_yaml::EXTS),
794 ),
795 ("pre-commit-merge-conflict", Consumes::All),
796 ("pre-commit-package-lock", Consumes::All),
797 (
801 "pre-commit-prettier",
802 Consumes::Exts(crate::hooks::prettier::EXTS),
803 ),
804 (
805 "pre-commit-pyright",
806 Consumes::Exts(crate::hooks::python_tools::EXTS),
807 ),
808 (
809 "pre-commit-ruff",
810 Consumes::Exts(crate::hooks::python_tools::EXTS),
811 ),
812 ("pre-commit-usual-name", Consumes::All),
813 (
814 "pre-commit-yamllint",
815 Consumes::Exts(crate::hooks::yamllint::EXTS),
816 ),
817 ("pre-commit-secrets", Consumes::All),
818 ("pre-push-secrets", Consumes::All),
819 ("pre-push-branch-protect", Consumes::All),
820 ("pre-push-branch-pattern", Consumes::All),
821 ("pre-push-pull-rebase", Consumes::All),
822 (
823 "pre-push-run-tests-js",
824 Consumes::Exts(crate::hooks::run_tests::JS_EXTS),
825 ),
826 (
827 "pre-push-cargo-test",
828 Consumes::Exts(crate::hooks::rust_tools::RUST_PATHS),
829 ),
830 ];
831
832 #[test]
843 fn no_check_declares_a_file_type_it_does_not_consume() {
844 for (name, _) in CONSUMED {
845 assert!(
846 CHECKS.iter().any(|check| check.name == *name),
847 "CONSUMED names {name:?}, which is not a check"
848 );
849 }
850 for check in CHECKS {
851 let entry = CONSUMED.iter().find(|(name, _)| *name == check.name);
852 if check.scope.files.is_empty() && entry.is_none() {
853 continue;
854 }
855 let Some((_, consumes)) = entry else {
856 panic!(
857 "{} declares scope.files {:?} but is missing from CONSUMED — \
858 say what it actually reads",
859 check.name, check.scope.files
860 );
861 };
862 let Consumes::Exts(consumed) = consumes else {
863 continue; };
865 for ext in check.scope.files {
866 assert!(
867 consumed.contains(ext),
868 "{} declares {ext:?} in its scope but never asks for it — \
869 `amont list` would report a coverage the check does not have",
870 check.name
871 );
872 }
873 }
874 }
875
876 const HAS_FIXING_CODE: &[(&str, bool)] = &[
885 ("pre-commit-argo-lint", false),
886 ("pre-commit-ban-terms", false),
887 ("pre-commit-branch-pattern", false),
888 ("pre-commit-cargo-fmt", true),
889 ("pre-commit-clippy", false),
890 ("pre-commit-kube-linter", false),
891 ("pre-commit-kubeconform", false),
892 ("pre-commit-lint-js", false),
893 ("pre-commit-lint-json-yaml", false),
894 ("pre-commit-merge-conflict", false),
895 ("pre-commit-package-lock", false),
896 ("pre-commit-prettier", true),
897 ("pre-commit-pyright", false),
898 ("pre-commit-ruff", true),
899 ("pre-commit-usual-name", false),
900 ("pre-commit-yamllint", false),
901 ("pre-commit-secrets", false),
902 ("pre-push-secrets", false),
903 ("pre-push-branch-protect", false),
904 ("pre-push-branch-pattern", false),
905 ("pre-push-pull-rebase", false),
906 ("pre-push-audit-js", false),
907 ("pre-push-audit-python", false),
908 ("pre-push-audit-rust", false),
909 ("pre-push-run-tests-js", false),
910 ("pre-push-cargo-test", false),
911 ];
912
913 #[test]
916 fn every_rewrite_declaration_has_a_fixer() {
917 let declared: BTreeSet<&str> = CHECKS
918 .iter()
919 .filter(|check| check.fix == super::Fix::Rewrite)
920 .map(|check| check.name)
921 .collect();
922 let implemented: BTreeSet<&str> = HAS_FIXING_CODE
923 .iter()
924 .filter(|(_, has)| *has)
925 .map(|(name, _)| *name)
926 .collect();
927 assert_eq!(
928 declared, implemented,
929 "a check declaring Fix::Rewrite with no fixer lies to `amont list --json`, \
930 and a check with a fixer that does not declare it can never be reached"
931 );
932
933 let listed: BTreeSet<&str> = HAS_FIXING_CODE.iter().map(|(name, _)| *name).collect();
936 let all: BTreeSet<&str> = CHECKS.iter().map(|check| check.name).collect();
937 assert_eq!(listed, all, "HAS_FIXING_CODE does not cover CHECKS");
938 }
939
940 #[test]
943 fn every_check_declares_a_stage_and_a_scope() {
944 assert_eq!(CHECKS.len(), 26);
945 let pre_commit = super::stage_checks(Stage::PreCommit).count();
946 let pre_push = super::stage_checks(Stage::PrePush).count();
947 assert_eq!(
948 pre_commit + pre_push,
949 CHECKS.len(),
950 "every check has a stage"
951 );
952 }
953}