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