1pub mod ai;
2pub mod android;
3pub mod containers;
4pub mod coreutils;
5pub mod dotnet;
6pub mod forges;
7pub mod go;
8pub mod jvm;
9pub mod magick;
10pub mod network;
11pub mod node;
12pub mod perl;
13pub mod php;
14pub mod python;
15pub mod r;
16pub mod ruby;
17pub mod rust;
18pub mod shell;
19pub mod swift;
20pub mod system;
21pub mod vcs;
22pub mod wrappers;
23pub mod xcode;
24
25use crate::parse::Token;
26use crate::verdict::Verdict;
27
28pub fn dispatch(tokens: &[Token]) -> Verdict {
29 let cmd = tokens[0].command_name();
30 None
31 .or_else(|| shell::dispatch(cmd, tokens))
32 .or_else(|| wrappers::dispatch(cmd, tokens))
33 .or_else(|| vcs::dispatch(cmd, tokens))
34 .or_else(|| forges::dispatch(cmd, tokens))
35 .or_else(|| node::dispatch(cmd, tokens))
36 .or_else(|| ruby::dispatch(cmd, tokens))
37 .or_else(|| python::dispatch(cmd, tokens))
38 .or_else(|| rust::dispatch(cmd, tokens))
39 .or_else(|| go::dispatch(cmd, tokens))
40 .or_else(|| jvm::dispatch(cmd, tokens))
41 .or_else(|| android::dispatch(cmd, tokens))
42 .or_else(|| php::dispatch(cmd, tokens))
43 .or_else(|| swift::dispatch(cmd, tokens))
44 .or_else(|| dotnet::dispatch(cmd, tokens))
45 .or_else(|| containers::dispatch(cmd, tokens))
46 .or_else(|| network::dispatch(cmd, tokens))
47 .or_else(|| ai::dispatch(cmd, tokens))
48 .or_else(|| system::dispatch(cmd, tokens))
49 .or_else(|| xcode::dispatch(cmd, tokens))
50 .or_else(|| perl::dispatch(cmd, tokens))
51 .or_else(|| r::dispatch(cmd, tokens))
52 .or_else(|| coreutils::dispatch(cmd, tokens))
53 .or_else(|| magick::dispatch(cmd, tokens))
54 .unwrap_or(Verdict::Denied)
55}
56
57#[cfg(test)]
58const HANDLED_CMDS: &[&str] = &[
59 "sh", "bash", "xargs", "timeout", "time", "env", "nice", "ionice", "hyperfine", "dotenv",
60 "git", "jj", "gh", "glab", "jjpr", "tea",
61 "npm", "yarn", "pnpm", "bun", "deno", "npx", "bunx", "nvm", "fnm", "volta",
62 "ruby", "ri", "bundle", "gem", "rbenv",
63 "pip", "uv", "poetry", "pyenv", "conda",
64 "cargo", "rustup",
65 "go",
66 "gradle", "mvn", "mvnw", "ktlint", "detekt",
67 "javap", "jar", "keytool", "jarsigner",
68 "adb", "apkanalyzer", "apksigner", "bundletool", "aapt2",
69 "emulator", "avdmanager", "sdkmanager", "zipalign", "lint",
70 "fastlane", "firebase",
71 "composer", "craft",
72 "swift",
73 "dotnet",
74 "curl",
75 "docker", "podman", "kubectl", "orbctl", "qemu-img",
76 "ollama", "llm", "hf", "claude", "aider", "codex", "opencode", "vibe",
77 "ddev", "dcli",
78 "brew", "mise", "asdf", "crontab", "defaults", "pmset", "sysctl", "cmake", "psql", "pg_isready",
79 "terraform", "heroku", "vercel", "flyctl",
80 "networksetup", "launchctl", "diskutil", "security", "csrutil", "log",
81 "xcodebuild", "plutil", "xcode-select", "xcrun", "pkgutil", "lipo", "codesign", "spctl",
82 "xcodegen", "tuist", "pod", "swiftlint", "swiftformat", "periphery", "xcbeautify", "agvtool", "simctl",
83 "perl",
84 "R", "Rscript",
85 "grep", "rg", "ag", "ack", "zgrep", "locate",
86 "cat", "head", "tail", "wc", "cut", "tr", "uniq", "less", "more", "zcat",
87 "diff", "comm", "paste", "tac", "rev", "nl",
88 "expand", "unexpand", "fold", "fmt", "col", "column", "iconv", "nroff",
89 "echo", "printf", "seq", "test", "expr", "bc", "factor", "bat",
90 "arch", "command", "hostname",
91 "find", "sed", "shuf", "sort", "yq", "xmllint", "awk", "gawk", "mawk", "nawk",
92 "magick",
93 "fd", "eza", "ls", "delta", "colordiff",
94 "dirname", "basename", "realpath", "readlink",
95 "file", "stat", "du", "df", "tree", "cmp", "zipinfo", "tar", "unzip", "gzip",
96 "true", "false",
97 "alias", "export", "printenv", "read", "type", "wait", "whereis", "which", "whoami", "date", "pwd", "cd", "unset",
98 "uname", "nproc", "uptime", "id", "groups", "tty", "locale", "cal", "sleep",
99 "who", "w", "last", "lastlog",
100 "ps", "top", "htop", "iotop", "procs", "dust", "lsof", "pgrep", "lsblk", "free",
101 "jq", "base64", "xxd", "getconf", "uuidgen",
102 "md5sum", "md5", "sha256sum", "shasum", "sha1sum", "sha512sum",
103 "cksum", "b2sum", "sum", "strings", "hexdump", "od", "size",
104 "sw_vers", "mdls", "otool", "nm", "system_profiler", "ioreg", "vm_stat", "mdfind", "man",
105 "dig", "nslookup", "host", "whois", "netstat", "ss", "ifconfig", "route", "ping",
106 "identify", "shellcheck", "cloc", "tokei", "cucumber", "branchdiff", "workon", "safe-chains",
107];
108
109pub fn handler_docs() -> Vec<crate::docs::CommandDoc> {
110 let mut docs = Vec::new();
111 docs.extend(vcs::command_docs());
112 docs.extend(forges::command_docs());
113 docs.extend(node::command_docs());
114 docs.extend(ruby::command_docs());
115 docs.extend(python::command_docs());
116 docs.extend(rust::command_docs());
117 docs.extend(go::command_docs());
118 docs.extend(jvm::command_docs());
119 docs.extend(android::command_docs());
120 docs.extend(php::command_docs());
121 docs.extend(swift::command_docs());
122 docs.extend(dotnet::command_docs());
123 docs.extend(containers::command_docs());
124 docs.extend(ai::command_docs());
125 docs.extend(network::command_docs());
126 docs.extend(system::command_docs());
127 docs.extend(xcode::command_docs());
128 docs.extend(perl::command_docs());
129 docs.extend(r::command_docs());
130 docs.extend(coreutils::command_docs());
131 docs.extend(shell::command_docs());
132 docs.extend(wrappers::command_docs());
133 docs.extend(magick::command_docs());
134 docs
135}
136
137#[cfg(test)]
138#[derive(Debug)]
139pub(crate) enum CommandEntry {
140 Positional { cmd: &'static str },
141 Custom { cmd: &'static str, valid_prefix: Option<&'static str> },
142 Subcommand { cmd: &'static str, subs: &'static [SubEntry], bare_ok: bool },
143 Delegation { cmd: &'static str },
144}
145
146#[cfg(test)]
147#[derive(Debug)]
148pub(crate) enum SubEntry {
149 Policy { name: &'static str },
150 Nested { name: &'static str, subs: &'static [SubEntry] },
151 Custom { name: &'static str, valid_suffix: Option<&'static str> },
152 Positional,
153 Guarded { name: &'static str, valid_suffix: &'static str },
154}
155
156use crate::command::CommandDef;
157
158const COMMAND_DEFS: &[&CommandDef] = &[
159 &ai::CODEX, &ai::OLLAMA, &ai::OPENCODE, &ai::LLM, &ai::HF,
160 &containers::DOCKER, &containers::PODMAN, &containers::KUBECTL, &containers::ORBCTL, &containers::QEMU_IMG,
161 &dotnet::DOTNET,
162 &go::GO,
163 &android::APKANALYZER, &android::APKSIGNER, &android::BUNDLETOOL, &android::AAPT2,
164 &android::AVDMANAGER,
165 &jvm::GRADLE, &jvm::KEYTOOL,
166 &magick::MAGICK,
167 &node::NPM, &node::PNPM, &node::BUN, &node::DENO,
168 &node::NVM, &node::FNM, &node::VOLTA,
169 &php::COMPOSER, &php::CRAFT,
170 &python::PIP, &python::UV, &python::POETRY,
171 &python::PYENV, &python::CONDA,
172 &ruby::BUNDLE, &ruby::GEM, &ruby::RBENV,
173 &rust::CARGO, &rust::RUSTUP,
174 &vcs::GIT,
175 &swift::SWIFT,
176 &system::BREW, &system::MISE, &system::ASDF, &system::DDEV, &system::DCLI, &system::CMAKE,
177 &system::DEFAULTS, &system::TERRAFORM, &system::HEROKU, &system::VERCEL,
178 &system::FLYCTL, &system::FASTLANE, &system::FIREBASE,
179 &system::SECURITY, &system::CSRUTIL, &system::DISKUTIL,
180 &system::LAUNCHCTL, &system::LOG,
181 &xcode::XCODEBUILD, &xcode::PLUTIL, &xcode::XCODE_SELECT,
182 &xcode::XCODEGEN, &xcode::TUIST, &xcode::POD, &xcode::SWIFTLINT,
183 &xcode::PERIPHERY, &xcode::AGVTOOL, &xcode::SIMCTL,
184];
185
186pub fn all_opencode_patterns() -> Vec<String> {
187 let mut patterns = Vec::new();
188 for def in COMMAND_DEFS {
189 patterns.extend(def.opencode_patterns());
190 }
191 for def in coreutils::all_flat_defs() {
192 patterns.extend(def.opencode_patterns());
193 }
194 for def in jvm::jvm_flat_defs() {
195 patterns.extend(def.opencode_patterns());
196 }
197 for def in android::android_flat_defs() {
198 patterns.extend(def.opencode_patterns());
199 }
200 for def in ai::ai_flat_defs() {
201 patterns.extend(def.opencode_patterns());
202 }
203 for def in ruby::ruby_flat_defs() {
204 patterns.extend(def.opencode_patterns());
205 }
206 for def in system::system_flat_defs() {
207 patterns.extend(def.opencode_patterns());
208 }
209 for def in xcode::xcbeautify_flat_defs() {
210 patterns.extend(def.opencode_patterns());
211 }
212 patterns.sort();
213 patterns.dedup();
214 patterns
215}
216
217#[cfg(test)]
218fn full_registry() -> Vec<&'static CommandEntry> {
219 let mut entries = Vec::new();
220 entries.extend(shell::REGISTRY);
221 entries.extend(wrappers::REGISTRY);
222 entries.extend(vcs::full_registry());
223 entries.extend(forges::full_registry());
224 entries.extend(node::full_registry());
225 entries.extend(jvm::full_registry());
226 entries.extend(android::full_registry());
227 entries.extend(network::REGISTRY);
228 entries.extend(system::full_registry());
229 entries.extend(xcode::full_registry());
230 entries.extend(perl::REGISTRY);
231 entries.extend(r::REGISTRY);
232 entries.extend(coreutils::full_registry());
233 entries
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use std::collections::HashSet;
240
241 const UNKNOWN_FLAG: &str = "--xyzzy-unknown-42";
242 const UNKNOWN_SUB: &str = "xyzzy-unknown-42";
243
244 fn check_entry(entry: &CommandEntry, failures: &mut Vec<String>) {
245 match entry {
246 CommandEntry::Positional { .. } | CommandEntry::Delegation { .. } => {}
247 CommandEntry::Custom { cmd, valid_prefix } => {
248 let base = valid_prefix.unwrap_or(cmd);
249 let test = format!("{base} {UNKNOWN_FLAG}");
250 if crate::is_safe_command(&test) {
251 failures.push(format!("{cmd}: accepted unknown flag: {test}"));
252 }
253 }
254 CommandEntry::Subcommand { cmd, subs, bare_ok } => {
255 if !bare_ok && crate::is_safe_command(cmd) {
256 failures.push(format!("{cmd}: accepted bare invocation"));
257 }
258 let test = format!("{cmd} {UNKNOWN_SUB}");
259 if crate::is_safe_command(&test) {
260 failures.push(format!("{cmd}: accepted unknown subcommand: {test}"));
261 }
262 for sub in *subs {
263 check_sub(cmd, sub, failures);
264 }
265 }
266 }
267 }
268
269 fn check_sub(prefix: &str, entry: &SubEntry, failures: &mut Vec<String>) {
270 match entry {
271 SubEntry::Policy { name } => {
272 let test = format!("{prefix} {name} {UNKNOWN_FLAG}");
273 if crate::is_safe_command(&test) {
274 failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
275 }
276 }
277 SubEntry::Nested { name, subs } => {
278 let path = format!("{prefix} {name}");
279 let test = format!("{path} {UNKNOWN_SUB}");
280 if crate::is_safe_command(&test) {
281 failures.push(format!("{path}: accepted unknown subcommand: {test}"));
282 }
283 for sub in *subs {
284 check_sub(&path, sub, failures);
285 }
286 }
287 SubEntry::Custom { name, valid_suffix } => {
288 let base = match valid_suffix {
289 Some(s) => format!("{prefix} {name} {s}"),
290 None => format!("{prefix} {name}"),
291 };
292 let test = format!("{base} {UNKNOWN_FLAG}");
293 if crate::is_safe_command(&test) {
294 failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
295 }
296 }
297 SubEntry::Positional => {}
298 SubEntry::Guarded { name, valid_suffix } => {
299 let test = format!("{prefix} {name} {valid_suffix} {UNKNOWN_FLAG}");
300 if crate::is_safe_command(&test) {
301 failures.push(format!("{prefix} {name}: accepted unknown flag: {test}"));
302 }
303 }
304 }
305 }
306
307 #[test]
308 fn all_commands_reject_unknown() {
309 let registry = full_registry();
310 let mut failures = Vec::new();
311 for entry in ®istry {
312 check_entry(entry, &mut failures);
313 }
314 assert!(
315 failures.is_empty(),
316 "unknown flags/subcommands accepted:\n{}",
317 failures.join("\n")
318 );
319 }
320
321 #[test]
322 fn command_defs_reject_unknown() {
323 for def in COMMAND_DEFS {
324 def.auto_test_reject_unknown();
325 }
326 }
327
328 #[test]
329 fn flat_defs_reject_unknown() {
330 for def in coreutils::all_flat_defs() {
331 def.auto_test_reject_unknown();
332 }
333 for def in xcode::xcbeautify_flat_defs() {
334 def.auto_test_reject_unknown();
335 }
336 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
337 def.auto_test_reject_unknown();
338 }
339 }
340
341 #[test]
342 fn help_eligible_command_defs() {
343 for def in COMMAND_DEFS {
344 let names: Vec<&str> = std::iter::once(def.name).chain(def.aliases.iter().copied()).collect();
345 for name in &names {
346 if def.help_eligible {
347 for flag in &["--help", "-h", "--version", "-V"] {
348 let cmd = format!("{name} {flag}");
349 assert!(
350 crate::is_safe_command(&cmd),
351 "{name}: help_eligible=true but rejected {flag}",
352 );
353 }
354 } else {
355 assert!(
356 !crate::is_safe_command(&format!("{name} --help")),
357 "{name}: help_eligible=false but accepted --help",
358 );
359 }
360 }
361 }
362 }
363
364 #[test]
365 fn help_eligible_flat_defs() {
366 use crate::policy::FlagStyle;
367 let check_def = |def: &crate::command::FlatDef| {
368 if def.help_eligible {
369 for flag in &["--help", "-h", "--version", "-V"] {
370 let cmd = format!("{} {flag}", def.name);
371 assert!(
372 crate::is_safe_command(&cmd),
373 "{}: help_eligible=true but rejected {flag}",
374 def.name,
375 );
376 }
377 } else if def.policy.flag_style != FlagStyle::Positional {
378 assert!(
379 !crate::is_safe_command(&format!("{} --help", def.name)),
380 "{}: help_eligible=false but accepted --help",
381 def.name,
382 );
383 }
384 };
385 for def in coreutils::all_flat_defs()
386 .into_iter()
387 .chain(xcode::xcbeautify_flat_defs())
388 {
389 check_def(def);
390 }
391 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
392 check_def(def);
393 }
394 }
395
396 #[test]
397 fn bare_false_rejects_bare_invocation() {
398 let check_def = |def: &crate::command::FlatDef| {
399 if !def.policy.bare {
400 assert!(
401 !crate::is_safe_command(def.name),
402 "{}: bare=false but bare invocation accepted",
403 def.name,
404 );
405 }
406 };
407 for def in coreutils::all_flat_defs()
408 .into_iter()
409 .chain(xcode::xcbeautify_flat_defs())
410 {
411 check_def(def);
412 }
413 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
414 check_def(def);
415 }
416 }
417
418 fn visit_subs(prefix: &str, subs: &[crate::command::SubDef], visitor: &mut dyn FnMut(&str, &crate::command::SubDef)) {
419 for sub in subs {
420 visitor(prefix, sub);
421 if let crate::command::SubDef::Nested { name, subs: inner } = sub {
422 visit_subs(&format!("{prefix} {name}"), inner, visitor);
423 }
424 }
425 }
426
427 #[test]
428 fn guarded_subs_require_guard() {
429 let mut failures = Vec::new();
430 for def in COMMAND_DEFS {
431 visit_subs(def.name, def.subs, &mut |prefix, sub| {
432 if let crate::command::SubDef::Guarded { name, guard_long, .. } = sub {
433 let without = format!("{prefix} {name}");
434 if crate::is_safe_command(&without) {
435 failures.push(format!("{without}: accepted without guard {guard_long}"));
436 }
437 let with = format!("{prefix} {name} {guard_long}");
438 if !crate::is_safe_command(&with) {
439 failures.push(format!("{with}: rejected with guard {guard_long}"));
440 }
441 }
442 });
443 }
444 assert!(failures.is_empty(), "guarded sub issues:\n{}", failures.join("\n"));
445 }
446
447 #[test]
448 fn guarded_subs_accept_guard_short() {
449 let mut failures = Vec::new();
450 for def in COMMAND_DEFS {
451 visit_subs(def.name, def.subs, &mut |prefix, sub| {
452 if let crate::command::SubDef::Guarded { name, guard_short: Some(short), .. } = sub {
453 let with_short = format!("{prefix} {name} {short}");
454 if !crate::is_safe_command(&with_short) {
455 failures.push(format!("{with_short}: rejected with guard_short"));
456 }
457 }
458 });
459 }
460 assert!(failures.is_empty(), "guard_short issues:\n{}", failures.join("\n"));
461 }
462
463 #[test]
464 fn nested_subs_reject_bare() {
465 let mut failures = Vec::new();
466 for def in COMMAND_DEFS {
467 visit_subs(def.name, def.subs, &mut |prefix, sub| {
468 if let crate::command::SubDef::Nested { name, .. } = sub {
469 let bare = format!("{prefix} {name}");
470 if crate::is_safe_command(&bare) {
471 failures.push(format!("{bare}: nested sub accepted bare invocation"));
472 }
473 }
474 });
475 }
476 assert!(failures.is_empty(), "nested bare issues:\n{}", failures.join("\n"));
477 }
478
479 #[test]
480 fn process_substitution_blocked() {
481 let cmds = ["echo <(cat /etc/passwd)", "echo >(rm -rf /)", "grep pattern <(ls)"];
482 for cmd in &cmds {
483 assert!(
484 !crate::is_safe_command(cmd),
485 "process substitution not blocked: {cmd}",
486 );
487 }
488 }
489
490 #[test]
491 fn positional_style_accepts_unknown_args() {
492 use crate::policy::FlagStyle;
493 for def in coreutils::all_flat_defs() {
494 if def.policy.flag_style == FlagStyle::Positional {
495 let test = format!("{} --unknown-xyz", def.name);
496 assert!(
497 crate::is_safe_command(&test),
498 "{}: FlagStyle::Positional but rejected unknown arg",
499 def.name,
500 );
501 }
502 }
503 }
504
505 fn visit_policies(prefix: &str, subs: &[crate::command::SubDef], visitor: &mut dyn FnMut(&str, &crate::policy::FlagPolicy)) {
506 for sub in subs {
507 match sub {
508 crate::command::SubDef::Policy { name, policy, .. } => {
509 visitor(&format!("{prefix} {name}"), policy);
510 }
511 crate::command::SubDef::Guarded { name, guard_long, policy, .. } => {
512 visitor(&format!("{prefix} {name} {guard_long}"), policy);
513 }
514 crate::command::SubDef::Nested { name, subs: inner } => {
515 visit_policies(&format!("{prefix} {name}"), inner, visitor);
516 }
517 _ => {}
518 }
519 }
520 }
521
522 #[test]
523 fn valued_flags_accept_eq_syntax() {
524 let mut failures = Vec::new();
525
526 let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
527 for flag in def.policy.valued.iter() {
528 let cmd = format!("{} {flag}=test_value", def.name);
529 if !crate::is_safe_command(&cmd) {
530 failures.push(format!("{cmd}: valued flag rejected with = syntax"));
531 }
532 }
533 };
534 for def in coreutils::all_flat_defs()
535 .into_iter()
536 .chain(xcode::xcbeautify_flat_defs())
537 {
538 check_flat(def, &mut failures);
539 }
540 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
541 check_flat(def, &mut failures);
542 }
543
544 for def in COMMAND_DEFS {
545 visit_policies(def.name, def.subs, &mut |prefix, policy| {
546 for flag in policy.valued.iter() {
547 let cmd = format!("{prefix} {flag}=test_value");
548 if !crate::is_safe_command(&cmd) {
549 failures.push(format!("{cmd}: valued flag rejected with = syntax"));
550 }
551 }
552 });
553 }
554
555 assert!(failures.is_empty(), "valued = syntax issues:\n{}", failures.join("\n"));
556 }
557
558 #[test]
559 fn max_positional_enforced() {
560 let mut failures = Vec::new();
561
562 let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
563 if let Some(max) = def.policy.max_positional {
564 let args: Vec<&str> = (0..=max).map(|_| "testarg").collect();
565 let cmd = format!("{} {}", def.name, args.join(" "));
566 if crate::is_safe_command(&cmd) {
567 failures.push(format!(
568 "{}: max_positional={max} but accepted {} positional args",
569 def.name,
570 max + 1,
571 ));
572 }
573 }
574 };
575 for def in coreutils::all_flat_defs()
576 .into_iter()
577 .chain(xcode::xcbeautify_flat_defs())
578 {
579 check_flat(def, &mut failures);
580 }
581 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
582 check_flat(def, &mut failures);
583 }
584
585 for def in COMMAND_DEFS {
586 visit_policies(def.name, def.subs, &mut |prefix, policy| {
587 if let Some(max) = policy.max_positional {
588 let args: Vec<&str> = (0..=max).map(|_| "testarg").collect();
589 let cmd = format!("{prefix} {}", args.join(" "));
590 if crate::is_safe_command(&cmd) {
591 failures.push(format!(
592 "{prefix}: max_positional={max} but accepted {} positional args",
593 max + 1,
594 ));
595 }
596 }
597 });
598 }
599
600 assert!(failures.is_empty(), "max_positional issues:\n{}", failures.join("\n"));
601 }
602
603 #[test]
604 fn doc_generation_non_empty() {
605 let mut failures = Vec::new();
606
607 for def in COMMAND_DEFS {
608 let doc = def.to_doc();
609 if doc.description.trim().is_empty() {
610 failures.push(format!("{}: CommandDef produced empty doc", def.name));
611 }
612 if doc.url.is_empty() {
613 failures.push(format!("{}: CommandDef has empty URL", def.name));
614 }
615 }
616
617 let check_flat = |def: &crate::command::FlatDef, failures: &mut Vec<String>| {
618 let doc = def.to_doc();
619 if doc.description.trim().is_empty() && !def.policy.bare {
620 failures.push(format!("{}: FlatDef produced empty doc", def.name));
621 }
622 if doc.url.is_empty() {
623 failures.push(format!("{}: FlatDef has empty URL", def.name));
624 }
625 };
626 for def in coreutils::all_flat_defs()
627 .into_iter()
628 .chain(xcode::xcbeautify_flat_defs())
629 {
630 check_flat(def, &mut failures);
631 }
632 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
633 check_flat(def, &mut failures);
634 }
635
636 assert!(failures.is_empty(), "doc generation issues:\n{}", failures.join("\n"));
637 }
638
639 #[test]
640 fn registry_covers_handled_commands() {
641 let registry = full_registry();
642 let mut all_cmds: HashSet<&str> = registry
643 .iter()
644 .map(|e| match e {
645 CommandEntry::Positional { cmd }
646 | CommandEntry::Custom { cmd, .. }
647 | CommandEntry::Subcommand { cmd, .. }
648 | CommandEntry::Delegation { cmd } => *cmd,
649 })
650 .collect();
651 for def in COMMAND_DEFS {
652 all_cmds.insert(def.name);
653 }
654 for def in coreutils::all_flat_defs() {
655 all_cmds.insert(def.name);
656 }
657 for def in xcode::xcbeautify_flat_defs() {
658 all_cmds.insert(def.name);
659 }
660 for def in jvm::jvm_flat_defs().into_iter().chain(android::android_flat_defs()).chain(ai::ai_flat_defs()).chain(ruby::ruby_flat_defs()).chain(system::system_flat_defs()) {
661 all_cmds.insert(def.name);
662 }
663 let handled: HashSet<&str> = HANDLED_CMDS.iter().copied().collect();
664
665 let missing: Vec<_> = handled.difference(&all_cmds).collect();
666 assert!(missing.is_empty(), "not in registry or COMMAND_DEFS: {missing:?}");
667
668 let extra: Vec<_> = all_cmds.difference(&handled).collect();
669 assert!(extra.is_empty(), "not in HANDLED_CMDS: {extra:?}");
670 }
671
672}