Skip to main content

usage/go/
mod.rs

1//! Emitting Go parse tables from a spec.
2//!
3//! The Go side of usage has no derive macro to emit its tables, because Go has no
4//! macros: what a Rust CLI gets from `#[derive(Cli)]` at compile time, a Go CLI
5//! gets from this, at build time, through `go:generate`. The output is a plain Go
6//! file an author checks in and a reviewer can read.
7//!
8//! # What it emits, and what it does not
9//!
10//! Two tables, kept apart on purpose. The hot one is what binding reads: which
11//! token becomes which flag or argument, and nothing else. The cold one — `Meta` —
12//! carries what the rules decided after the last token need: `required`,
13//! `choices`, `default`, `env`, the var bounds, and the four that compare one
14//! entry against another. A parse never touches the second.
15//!
16//! Help text is in neither. mise's runs to several hundred kilobytes, and a table
17//! carrying it would put all of that in front of the parser; rendering help is its
18//! own cold table and its own piece of work.
19//!
20//! # Why package-level `var` and not `const`
21//!
22//! Go has no `const` for composite data. What it does have is a linker that
23//! statically initializes package-level variables holding plain data, which is
24//! the property the whole design rests on: `go tool nm` reports these symbols as
25//! type `D`, and the generated package has no `init` function. So a 211-command
26//! table costs bytes in the binary and no instructions at startup — the thing
27//! cobra and kong each pay a million or more for.
28//!
29//! Commands are emitted as separate variables rather than one nested literal
30//! because `default_subcommand` has to point at a node inside the tree, and a
31//! composite literal cannot refer to its own interior.
32
33mod structs;
34mod types;
35
36pub use types::ValueType;
37
38use std::collections::{BTreeMap, HashMap};
39use std::fmt::Write as _;
40
41use crate::case::AsPascalCase;
42
43use crate::spec::unknown_flags::UnknownFlags;
44use crate::{
45    Spec, SpecArg, SpecChoices, SpecCommand, SpecComplete, SpecDoubleDashChoices, SpecFlag,
46    SpecFlagAction,
47};
48
49/// How to emit.
50#[derive(Debug, Clone, Default)]
51pub struct GoOptions {
52    /// The Go package clause. Defaults to the spec's `bin`, made into an
53    /// identifier.
54    ///
55    /// Must satisfy [`is_valid_package`]. A caller taking this from a user should
56    /// check it and say so; one that does not gets it sanitized, because emitting a
57    /// file that cannot compile helps nobody.
58    pub package: Option<String>,
59}
60
61/// Turn a spec into a Go source file declaring its parse tables.
62///
63/// This does not check the spec: it cannot fail, so a spec with both `unknown_env` and a
64/// `mount` is emitted with a parser that refuses the mounted commands' variables.
65/// [`generate_with_types`] refuses that combination and is what `usage generate go` calls.
66/// Returning a `Result` here waits for v8; see `V8.md`.
67pub fn generate(spec: &Spec, opts: &GoOptions) -> String {
68    Emitter::new(spec, opts).run()
69}
70
71fn has_mount(cmd: &SpecCommand) -> bool {
72    !cmd.mounts.is_empty() || cmd.subcommands.values().any(has_mount)
73}
74
75/// Generate explicitly typed fields, keyed by the generated flag/argument constants.
76///
77/// Unlike [`generate`], this rejects unknown keys and entries without text values.
78/// Types are Go-specific configuration, not inferred from usage placeholder names.
79pub fn generate_with_types(
80    spec: &Spec,
81    opts: &GoOptions,
82    field_types: &BTreeMap<String, ValueType>,
83) -> Result<String, String> {
84    // A mount's commands arrive at run time and this generator does not emit them, so
85    // `UnknownEnvKnown` could not hold their variables and the generated parser would
86    // refuse what usage-lib, which resolves mounts, accepts.
87    if spec.unknown_env.is_some() && has_mount(&spec.cmd) {
88        return Err(
89            "unknown_env cannot be combined with mount in the Go generator: the variables a \
90             mounted command declares are not known at generation time"
91                .to_string(),
92        );
93    }
94    if field_types.is_empty() {
95        return Ok(generate(spec, opts));
96    }
97    let mut probe = Emitter::new(spec, opts);
98    let mut commands = Vec::new();
99    probe.collect(&spec.cmd, &[], true, &mut commands);
100    types::validate(&commands, field_types)?;
101    let mut emitter = Emitter::new(spec, opts);
102    emitter.field_types = field_types.clone();
103    Ok(emitter.run())
104}
105
106/// One entry's identifiers: the exported key constant, and for a command the
107/// variable holding it.
108struct Named {
109    key: String,
110    var: String,
111    number: u64,
112}
113
114struct Emitter<'a> {
115    spec: &'a Spec,
116    field_types: BTreeMap<String, ValueType>,
117    package: String,
118    /// Every identifier handed out, so a second entry wanting the same spelling
119    /// gets a suffix instead of silently colliding.
120    taken: HashMap<String, u32>,
121    /// Assigned in emission order, so a key is stable as long as the spec is.
122    next_key: u64,
123    out: String,
124}
125
126impl<'a> Emitter<'a> {
127    /// Create an emitter with a sanitized package name and empty allocation state.
128    fn new(spec: &'a Spec, opts: &GoOptions) -> Self {
129        // An explicit package that is not an identifier is sanitized rather than
130        // emitted: a caller that wants to reject it should ask `is_valid_package`
131        // first, which the CLI does.
132        let package = match opts.package.as_deref() {
133            Some(name) if is_valid_package(name) => name.to_string(),
134            Some(name) => package_ident(name),
135            None => package_ident(&spec.bin),
136        };
137        Emitter {
138            spec,
139            field_types: BTreeMap::new(),
140            package,
141            taken: HashMap::new(),
142            next_key: 0,
143            out: String::new(),
144        }
145    }
146
147    /// Reserve an identifier, adding a numeric suffix if the spelling is taken.
148    ///
149    /// Collisions are ordinary rather than exotic: mise has both a `macos-defaults`
150    /// command and a `macos defaults` path, and both want to be spelled
151    /// `CmdMacosDefaults`.
152    ///
153    /// The suffixed spelling is reserved too, and the loop is what makes that
154    /// safe. Counting alone was not enough: `macos-defaults` and `macos defaults`
155    /// produce `CmdMacosDefaults` and `CmdMacosDefaults2`, and a third command
156    /// named `macos-defaults2` asks for `CmdMacosDefaults2` directly — which was
157    /// unclaimed, so the file declared it twice and did not compile.
158    fn unique(&mut self, base: &str) -> String {
159        let mut n = self.taken.get(base).copied().unwrap_or(0);
160        loop {
161            n += 1;
162            let candidate = if n == 1 {
163                base.to_string()
164            } else {
165                format!("{base}{n}")
166            };
167            if !self.taken.contains_key(&candidate) {
168                self.taken.insert(base.to_string(), n);
169                // The spelling itself, so a later entry that asks for it by name is
170                // suffixed rather than handed a duplicate.
171                self.taken.entry(candidate.clone()).or_insert(0);
172                return candidate;
173            }
174        }
175    }
176
177    fn name(&mut self, prefix: &str, path: &[&str], own: &str) -> Named {
178        let mut base = String::from(prefix);
179        for segment in path {
180            let _ = write!(base, "{}", AsPascalCase(segment));
181        }
182        let _ = write!(base, "{}", AsPascalCase(own));
183        let key = self.unique(&base);
184        self.next_key += 1;
185        Named {
186            var: format!("cmd{}", &key[prefix.len()..]),
187            key,
188            number: self.next_key,
189        }
190    }
191
192    /// Collect the specification and render constants, tables, metadata, help, and structs.
193    fn run(mut self) -> String {
194        // Collected first so the constants can be emitted in one block before any
195        // table refers to them, which is also the order a reader wants: the names
196        // they will switch on, then the data.
197        let mut commands = Vec::new();
198        self.collect(&self.spec.cmd.clone(), &[], true, &mut commands);
199
200        self.header();
201        self.constants(&commands);
202        self.tables(&commands);
203        self.metadata(&commands);
204        self.help_table(&commands);
205        self.unknown_env();
206        structs::emit(
207            &mut self.out,
208            &commands,
209            &self.field_types,
210            self.spec.unknown_env.is_some(),
211        );
212
213        // Each command is followed by a blank line, which leaves one at the end of
214        // the file. gofmt strips it, and a generated file that is not gofmt-clean
215        // is one every adopter has to run a formatter over before committing.
216        let trimmed = self.out.trim_end().len();
217        self.out.truncate(trimmed);
218        self.out.push('\n');
219        self.out
220    }
221
222    /// The spec's `unknown_env`, as the two values `Parse` hands `argv.RefuseUnknownEnv`.
223    ///
224    /// The known names are worked out here rather than at run time because a generated
225    /// package carries tables, not a spec: the `config` properties that also read
226    /// variables are not in them.
227    fn unknown_env(&mut self) {
228        let Some(decl) = &self.spec.unknown_env else {
229            return;
230        };
231        let mut known: Vec<String> = crate::spec::unknown_env::declared_env_names(self.spec)
232            .into_iter()
233            .map(str::to_string)
234            .chain(decl.allow.iter().cloned())
235            .collect();
236        known.sort();
237        known.dedup();
238        let _ = writeln!(
239            self.out,
240            "// UnknownEnvPrefix scopes the spec's unknown_env check: a variable under it that\n\
241             // nothing declares fails the parse.\n\
242             const UnknownEnvPrefix = {}\n\n\
243             // UnknownEnvKnown is every variable the spec declares, anywhere in the command\n\
244             // tree or its config, and every one it allows.\n\
245             var UnknownEnvKnown = {}\n",
246            go_string(&decl.prefix),
247            string_slice(&known)
248        );
249    }
250
251    /// Walk the tree, naming everything, so that emission is a second pass with no
252    /// lookaheads.
253    fn collect(&mut self, cmd: &SpecCommand, path: &[&str], root: bool, out: &mut Vec<Emitted>) {
254        let named = if root {
255            self.next_key += 1;
256            Named {
257                // Claimed through the same counter as everything else, not just
258                // spelled: a subcommand named `root` would otherwise be handed
259                // `CmdRoot` too, and the file would declare the constant twice and
260                // fail to compile.
261                key: self.unique("CmdRoot"),
262                var: "Root".to_string(),
263                number: self.next_key,
264            }
265        } else {
266            self.name("Cmd", &path[..path.len() - 1], path[path.len() - 1])
267        };
268
269        let flags = cmd
270            .flags
271            .iter()
272            .map(|f| (f.clone(), self.name("Flag", path, &f.name)))
273            .collect::<Vec<_>>();
274        let args = cmd
275            .args
276            .iter()
277            .map(|a| (a.clone(), self.name("Arg", path, &a.name)))
278            .collect::<Vec<_>>();
279        let clause_args = cmd
280            .clause
281            .as_ref()
282            .map(|clause| {
283                clause
284                    .args
285                    .iter()
286                    .map(|a| {
287                        (
288                            a.clone(),
289                            self.name("Arg", path, &format!("{}-{}", clause.name, a.name)),
290                        )
291                    })
292                    .collect::<Vec<_>>()
293            })
294            .unwrap_or_default();
295        let clause_flags = cmd
296            .clause
297            .as_ref()
298            .map(|clause| {
299                clause
300                    .flags
301                    .iter()
302                    .map(|flag| {
303                        (
304                            flag.clone(),
305                            self.name("Flag", path, &format!("{}-{}", clause.name, flag.name)),
306                        )
307                    })
308                    .collect::<Vec<_>>()
309            })
310            .unwrap_or_default();
311
312        let index = out.len();
313        out.push(Emitted {
314            named,
315            cmd: cmd.clone(),
316            flags,
317            args,
318            clause_flags,
319            clause_args,
320            subcommands: Vec::new(),
321            root,
322        });
323
324        // Declaration order, not sorted: a recent change made the spec hold the
325        // order a CLI declares its commands in, and a generated file that reordered
326        // them would lose it for no gain — lookup is by name either way.
327        let mut children = Vec::new();
328        for (name, sub) in &cmd.subcommands {
329            // An alias appears in `subcommands` under its own key as well as the
330            // canonical name; emitting it twice would declare two commands where the
331            // spec has one.
332            if name != &sub.name {
333                continue;
334            }
335            let mut child_path = path.to_vec();
336            child_path.push(name);
337            let at = out.len();
338            self.collect(sub, &child_path, false, out);
339            children.push(at);
340        }
341        out[index].subcommands = children;
342    }
343
344    /// Emit the generated-file header and imports required by configured field types.
345    fn header(&mut self) {
346        let imports = if self
347            .field_types
348            .values()
349            .any(|ty| *ty == ValueType::Duration)
350        {
351            "import (\n\t\"time\"\n\n\t\"github.com/jdx/usage/go/argv\"\n)"
352        } else {
353            "import \"github.com/jdx/usage/go/argv\""
354        };
355        let _ = writeln!(
356            self.out,
357            "// Code generated by `usage generate go`. DO NOT EDIT.\n\
358             //\n\
359             // Binding tables for `{}`, read by\n\
360             // [github.com/jdx/usage/go/argv]. Regenerate rather than editing: the spec is\n\
361             // the definition, and a hand-edit here is a difference no reviewer can see.\n\
362             //\n\
363             // These are package-level variables holding plain data, so the linker lays them\n\
364             // out and nothing runs before main.\n\
365             \n\
366             package {}\n\
367             \n\
368             {imports}\n",
369            self.spec.bin, self.package
370        );
371
372        if let Some(version) = self
373            .spec
374            .version
375            .as_ref()
376            .or(self.spec.long_version.as_ref())
377        {
378            let _ = writeln!(
379                self.out,
380                "// Version is what the spec declares, so a caller answering `--version` has it\n\
381                 // without the parse tables carrying a string binding never reads.\n\
382                 const Version = {}\n",
383                go_string(version)
384            );
385        }
386        if let Some(version) = &self.spec.long_version {
387            let _ = writeln!(
388                self.out,
389                "// LongVersion is the extended text printed for `--version`; `-V` uses Version.\n\
390                 const LongVersion = {}\n",
391                go_string(version)
392            );
393        }
394    }
395
396    fn constants(&mut self, commands: &[Emitted]) {
397        let _ = writeln!(
398            self.out,
399            "// Keys identify a table entry without a string comparison: switch on the Key an\n\
400             // event carries rather than on its Name, which is there for diagnostics.\n\
401             const ("
402        );
403        let mut entries: Vec<(&str, u64)> = Vec::new();
404        for e in commands {
405            entries.push((&e.named.key, e.named.number));
406            entries.extend(e.flags.iter().map(|(_, n)| (n.key.as_str(), n.number)));
407            entries.extend(
408                e.clause_flags
409                    .iter()
410                    .map(|(_, n)| (n.key.as_str(), n.number)),
411            );
412            entries.extend(e.args.iter().map(|(_, n)| (n.key.as_str(), n.number)));
413            entries.extend(
414                e.clause_args
415                    .iter()
416                    .map(|(_, n)| (n.key.as_str(), n.number)),
417            );
418        }
419        // One run, so every name pads to the longest — which is what gofmt does to
420        // a const block with no blank line in it.
421        let width = entries.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
422        for (key, number) in entries {
423            let _ = writeln!(
424                self.out,
425                "\t{key}{:pad$} uint64 = {number}",
426                "",
427                pad = width - key.len()
428            );
429        }
430        let _ = writeln!(self.out, ")\n");
431    }
432
433    fn tables(&mut self, commands: &[Emitted]) {
434        // Resolved once, against the root's *direct* subcommands, because the spec
435        // declares it once at the top and it names one of them. Searching the whole
436        // tree instead is what wired mise's `default_subcommand run` to `oci run`,
437        // which comes first in a depth-first walk — the parser would then have
438        // descended into a command that is not the root's child at all. A name
439        // nothing answers to is left unset rather than guessed at.
440        let default_subcommand = self.spec.default_subcommand.as_ref().and_then(|name| {
441            let direct = || commands[0].subcommands.iter().map(|at| &commands[*at]);
442            // Names before aliases, as the grammar says: a command's own name outranks
443            // another command's alias, so which one this resolves to does not depend on
444            // the order the spec declares them in.
445            direct()
446                .find(|e| &e.cmd.name == name)
447                .or_else(|| {
448                    direct().find(|e| {
449                        e.cmd.aliases.contains(name) || e.cmd.hidden_aliases.contains(name)
450                    })
451                })
452                .map(|e| e.named.var.clone())
453        });
454
455        for (i, e) in commands.iter().enumerate() {
456            let doc = if e.root {
457                format!(
458                    "// Root is the command tree for `{}`. Pass it to argv.New.",
459                    self.spec.bin
460                )
461            } else {
462                format!("// {}", e.cmd.full_cmd.join(" "))
463            };
464            let mut lines = vec![
465                Line::Field("Name".into(), go_string(&e.cmd.name)),
466                Line::Field("Key".into(), e.named.key.clone()),
467            ];
468
469            let aliases: Vec<&String> = e
470                .cmd
471                .aliases
472                .iter()
473                .chain(e.cmd.hidden_aliases.iter())
474                .collect();
475            if !aliases.is_empty() {
476                // A hidden alias selects a command exactly as a visible one does:
477                // hiding is about help output, which binding never reads.
478                let list = aliases
479                    .iter()
480                    .map(|a| go_string(a))
481                    .collect::<Vec<_>>()
482                    .join(", ");
483                lines.push(Line::Field("Aliases".into(), format!("[]string{{{list}}}")));
484            }
485
486            if !e.flags.is_empty() || !e.clause_flags.is_empty() {
487                let mut block = vec!["Flags: []*argv.Flag{".to_string()];
488                for (flag, named) in e.flags.iter().chain(&e.clause_flags) {
489                    block.push(format!("\t{},", flag_literal(flag, named)));
490                }
491                block.push("},".to_string());
492                lines.push(Line::Block(block));
493            }
494
495            if !e.args.is_empty() {
496                let mut block = vec!["Args: []*argv.Arg{".to_string()];
497                for (arg, named) in &e.args {
498                    block.push(format!("\t{},", arg_literal(arg, named)));
499                }
500                block.push("},".to_string());
501                lines.push(Line::Block(block));
502            }
503            if let Some(clause) = &e.cmd.clause {
504                let mut block = vec!["Clause: &argv.Clause{".to_string()];
505                block.push(format!("\tKey: {},", e.named.key));
506                block.push(format!("\tName: {},", go_string(&clause.name)));
507                block.push(format!(
508                    "\tSeparator: {},",
509                    go_string(clause.separator.as_deref().unwrap_or_default())
510                ));
511                block.push("\tFlags: []*argv.Flag{".to_string());
512                for (flag, named) in &e.clause_flags {
513                    block.push(format!("\t\t{},", flag_literal(flag, named)));
514                }
515                block.push("\t},".to_string());
516                block.push("\tArgs: []*argv.Arg{".to_string());
517                for (arg, named) in &e.clause_args {
518                    block.push(format!("\t\t{},", arg_literal(arg, named)));
519                }
520                block.push("\t},".to_string());
521                block.push("},".to_string());
522                lines.push(Line::Block(block));
523            }
524
525            if !e.subcommands.is_empty() {
526                let list = e
527                    .subcommands
528                    .iter()
529                    .map(|at| commands[*at].named.var.clone())
530                    .collect::<Vec<_>>()
531                    .join(", ");
532                lines.push(Line::Field(
533                    "Subcommands".into(),
534                    format!("[]*argv.Command{{{list}}}"),
535                ));
536            }
537
538            // Already resolved: inheritance is the generator's job, so that the
539            // parser reads one field rather than walking ancestors per token.
540            if effective_unknown_flags(self.spec, commands, i) == UnknownFlags::Error {
541                lines.push(Line::Field(
542                    "UnknownFlags".into(),
543                    "argv.UnknownFlagsError".into(),
544                ));
545            }
546
547            if e.cmd.external_subcommand {
548                lines.push(Line::Field("ExternalSubcommand".into(), "true".into()));
549            }
550            if e.cmd.arg_required_else_help {
551                lines.push(Line::Field("ArgRequiredElseHelp".into(), "true".into()));
552            }
553            if e.cmd.disable_help_flag {
554                lines.push(Line::Field("DisableHelpFlag".into(), "true".into()));
555            }
556            if e.cmd.disable_help_subcommand {
557                lines.push(Line::Field("DisableHelpSubcommand".into(), "true".into()));
558            }
559            if e.cmd.disable_version_flag {
560                lines.push(Line::Field("DisableVersionFlag".into(), "true".into()));
561            }
562            if e.cmd.subcommand_negates_reqs {
563                lines.push(Line::Field("SubcommandNegatesReqs".into(), "true".into()));
564            }
565            if e.cmd.args_conflicts_with_subcommands {
566                lines.push(Line::Field(
567                    "ArgsConflictWithSubcommands".into(),
568                    "true".into(),
569                ));
570            }
571            if e.cmd.subcommand_precedence_over_arg {
572                lines.push(Line::Field(
573                    "SubcommandPrecedenceOverArg".into(),
574                    "true".into(),
575                ));
576            }
577            if e.cmd.allow_missing_positional {
578                lines.push(Line::Field("AllowMissingPositional".into(), "true".into()));
579            }
580            if e.cmd.dont_delimit_trailing_values {
581                lines.push(Line::Field(
582                    "DontDelimitTrailingValues".into(),
583                    "true".into(),
584                ));
585            }
586            if e.root {
587                if self.spec.default_subcommand_flags {
588                    lines.push(Line::Field("DefaultSubcommandFlags".into(), "true".into()));
589                }
590                if self.spec.default_subcommand_on_empty {
591                    lines.push(Line::Field(
592                        "DefaultSubcommandOnEmpty".into(),
593                        "true".into(),
594                    ));
595                }
596                if let Some(var) = &default_subcommand {
597                    lines.push(Line::Field("DefaultSubcommand".into(), var.clone()));
598                }
599                if self.spec.version.is_some() || self.spec.long_version.is_some() {
600                    // Only where the CLI declares a version: a `--version` that answers
601                    // with nothing is worse than one that is not there.
602                    lines.push(Line::Field("Version".into(), "true".into()));
603                }
604            }
605
606            let _ = writeln!(self.out, "{doc}");
607            let _ = writeln!(self.out, "var {} = &argv.Command{{", e.named.var);
608            render(&mut self.out, "\t", &lines);
609            let _ = writeln!(self.out, "}}\n");
610        }
611    }
612}
613
614impl Emitter<'_> {
615    /// Emit the cold table: everything binding deliberately does not know.
616    ///
617    /// Indexed by key, which is what makes a lookup an index rather than a map —
618    /// and a Go map would have to be built at init, which is the one thing these
619    /// tables are for avoiding. Keys are handed out to commands as well as to
620    /// flags and arguments, and a command has no cold half, so its slot is an
621    /// empty entry rather than a gap: `Metadata.Lookup` checks the key it finds
622    /// and reports nothing when it does not match, so an empty slot answers
623    /// correctly and the index stays dense.
624    fn metadata(&mut self, commands: &[Emitted]) {
625        // By key, so the slice can be written in one pass in index order.
626        let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
627        for e in commands {
628            for (flag, named) in e.flags.iter().chain(&e.clause_flags) {
629                by_key.insert(named.number, self.flag_meta(flag, named, e, commands));
630            }
631            for (arg, named) in &e.args {
632                by_key.insert(named.number, arg_meta(self.spec, arg, named, e, commands));
633            }
634            for (arg, named) in &e.clause_args {
635                by_key.insert(named.number, arg_meta(self.spec, arg, named, e, commands));
636            }
637        }
638
639        let total = commands
640            .iter()
641            .map(|e| 1 + e.flags.len() + e.clause_flags.len() + e.args.len() + e.clause_args.len())
642            .sum::<usize>() as u64;
643
644        let _ = writeln!(
645            self.out,
646            "// Meta is the cold table, read only by the rules that are decided once the\n\
647             // last token has been read: required, choices, the env-then-default fallback,\n\
648             // the var bounds, and the four that compare one entry against another. A parse\n\
649             // never touches it.\n\
650             //\n\
651             // Indexed by key, so entry Key sits at Meta[Key-1]. A command's slot is empty:\n\
652             // commands take keys too, and have no cold half.\n\
653             var Meta = argv.Metadata{{"
654        );
655        for key in 1..=total {
656            match by_key.get(&key) {
657                Some(entry) => {
658                    let _ = writeln!(self.out, "\t{entry},");
659                }
660                None => {
661                    let _ = writeln!(self.out, "\t{{}},");
662                }
663            }
664        }
665        let _ = writeln!(self.out, "}}\n");
666    }
667
668    /// The cold half of a flag.
669    fn flag_meta(
670        &self,
671        flag: &SpecFlag,
672        named: &Named,
673        owner: &Emitted,
674        commands: &[Emitted],
675    ) -> String {
676        let mut fields = vec![
677            format!("Key: {}", named.key),
678            format!("Name: {}", go_string(&flag.name)),
679            "Flag: true".to_string(),
680        ];
681        let scoped = owner
682            .clause_flags
683            .iter()
684            .any(|(_, candidate)| candidate.key == named.key);
685        if (!owner.cmd.args_override_self || scoped)
686            && !flag.var
687            && !flag.count
688            && !flag.arg.as_ref().is_some_and(|arg| arg.var)
689        {
690            fields.push("RejectDuplicate: true".to_string());
691        }
692        if flag.required {
693            fields.push("Required: true".to_string());
694        }
695        if flag.arg.is_none() {
696            fields.push("RequiresIfBoolean: true".to_string());
697        }
698        // How a user types it, worked out where the forms are visible: the rules
699        // that judge an entry never see a flag, and guessing from the name gets a
700        // one-letter long form and a short the wrong way round.
701        if let Some(long) = flag.long.first() {
702            fields.push(format!("Spelling: {}", go_string(&format!("--{long}"))));
703        } else if let Some(short) = flag.short.first() {
704            fields.push(format!("Spelling: {}", go_string(&format!("-{short}"))));
705        }
706        // What the value is called, which is what says whether a path belongs
707        // there — `--into <DIR>` completes directories because of the name.
708        if let Some(value) = flag.arg.as_ref() {
709            fields.push(format!("ValueName: {}", go_string(&value.name)));
710        }
711        let named_value = flag
712            .arg
713            .as_ref()
714            .map(|a| a.name.as_str())
715            .unwrap_or(flag.name.as_str());
716        let inline = flag.arg.as_ref().and_then(|a| a.complete.as_ref());
717        if let Some(kind) = complete_type(self.spec, inline, named_value) {
718            fields.push(format!("CompleteType: {}", go_string(kind)));
719        }
720        // Written on the value a flag takes, never on the flag.
721        if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
722            fields.push(format!(
723                "Choices: {}",
724                string_slice(&visible_choices(choices))
725            ));
726            fields.push(format!(
727                "AcceptedChoices: {}",
728                string_slice(&accepted_choices(choices))
729            ));
730            if choices.ignore_case {
731                fields.push("IgnoreCase: true".to_string());
732            }
733            if accepts_unlisted(choices) {
734                fields.push("AllowUnknownChoices: true".to_string());
735            }
736        }
737        // A default can be written in either place, and usage-lib falls back to
738        // the one on the value. `env` deliberately does not follow the same
739        // nesting, because usage-lib does not read it there either.
740        let default = if !flag.default.is_empty() {
741            &flag.default
742        } else {
743            flag.arg
744                .as_ref()
745                .map(|a| &a.default)
746                .unwrap_or(&flag.default)
747        };
748        if !default.is_empty() {
749            fields.push(format!("Default: {}", string_slice(default)));
750        }
751        if let Some(env) = &flag.env {
752            fields.push(format!("Env: {}", go_string(env)));
753        }
754        if !flag.env_fallback.is_empty() {
755            fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
756        }
757        if !flag.deprecated_env.is_empty() {
758            fields.push(format!(
759                "DeprecatedEnv: {}",
760                string_slice(&flag.deprecated_env)
761            ));
762        }
763        let minimum = flag
764            .arg
765            .as_ref()
766            .filter(|arg| arg.var)
767            .and_then(|arg| arg.var_min)
768            .or(flag.var_min);
769        if let Some(min) = minimum {
770            fields.push(format!("VarMin: {}", clamp_var_max(min)));
771        }
772        // Occurrences. The per-occurrence value bound is a limit binding applies
773        // and lives on the parse table.
774        if let Some(max) = flag.var_max {
775            fields.push(format!("VarMax: {}", clamp_var_max(max)));
776        }
777
778        for (label, names) in [
779            ("Conflicts", &flag.conflicts),
780            ("Overrides", &flag.overrides),
781            ("RequiredUnless", &flag.required_unless),
782            ("RequiredUnlessAll", &flag.required_unless_all),
783            ("RequiredIf", &flag.required_if),
784            ("Requires", &flag.requires),
785        ] {
786            let keys = resolve_relationship(names, owner, commands);
787            if !keys.is_empty() {
788                fields.push(format!("{label}: {}", key_slice(&keys)));
789            }
790        }
791        for (label, conditions) in [
792            ("RequiredIfEq", &flag.required_if_eq),
793            ("RequiredIfEqAll", &flag.required_if_eq_all),
794        ] {
795            let values = conditions
796                .iter()
797                .filter_map(|condition| {
798                    resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
799                        .into_iter()
800                        .next()
801                        .map(|key| {
802                            format!("{{Key: {key}, Value: {}}}", go_string(&condition.value))
803                        })
804                })
805                .collect::<Vec<_>>();
806            if !values.is_empty() {
807                fields.push(format!(
808                    "{label}: []argv.ValueCondition{{{}}}",
809                    values.join(", ")
810                ));
811            }
812        }
813        let requires_if = flag
814            .requires_if
815            .iter()
816            .filter_map(|condition| {
817                resolve_relationship(std::slice::from_ref(&condition.requires), owner, commands)
818                    .into_iter()
819                    .next()
820                    .map(|key| format!("{{Value: {}, Key: {key}}}", go_string(&condition.value)))
821            })
822            .collect::<Vec<_>>();
823        if !requires_if.is_empty() {
824            fields.push(format!(
825                "RequiresIf: []argv.ValueRequirement{{{}}}",
826                requires_if.join(", ")
827            ));
828        }
829        let default_if = flag
830            .default_if
831            .iter()
832            .filter_map(|condition| {
833                resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
834                    .into_iter()
835                    .next()
836                    .map(|key| match &condition.when {
837                        None => format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)),
838                        Some(when) => format!(
839                            "{{Key: {key}, When: {}, Value: {}}}",
840                            go_string(when),
841                            go_string(&condition.value)
842                        ),
843                    })
844            })
845            .collect::<Vec<_>>();
846        if !default_if.is_empty() {
847            fields.push(format!(
848                "DefaultIf: []argv.DefaultIf{{{}}}",
849                default_if.join(", ")
850            ));
851        }
852
853        format!("{{{}}}", fields.join(", "))
854    }
855}
856
857/// The cold half of a positional argument.
858/// The type a spec's `complete` block names for an entry, if it names one.
859///
860/// One written inside the entry's own `arg` wins, as it does in the reference.
861/// Otherwise by lowercased name, which is how usage-lib files them:
862/// `complete "FILE"` and an argument written `<file>` are the same position as far
863/// as the reference is concerned.
864fn complete_type<'a>(
865    spec: &'a Spec,
866    inline: Option<&'a SpecComplete>,
867    name: &str,
868) -> Option<&'a str> {
869    inline
870        .or_else(|| spec.complete.get(&name.to_lowercase()))
871        .and_then(|c| c.type_.as_deref())
872}
873
874fn arg_meta(
875    spec: &Spec,
876    arg: &SpecArg,
877    named: &Named,
878    owner: &Emitted,
879    commands: &[Emitted],
880) -> String {
881    let mut fields = vec![
882        format!("Key: {}", named.key),
883        format!("Name: {}", go_string(&arg.name)),
884    ];
885    if arg.required {
886        fields.push("Required: true".to_string());
887    }
888    // What the position takes, where the spec said so. Read by completion rather
889    // than by any post-binding rule: an author who wrote `complete "input"
890    // type="file"` named what belongs there, and the alternative is inferring it
891    // from a name they did not choose.
892    if let Some(kind) = complete_type(spec, arg.complete.as_ref(), &arg.name) {
893        fields.push(format!("CompleteType: {}", go_string(kind)));
894    }
895    if let Some(choices) = &arg.choices {
896        fields.push(format!(
897            "Choices: {}",
898            string_slice(&visible_choices(choices))
899        ));
900        fields.push(format!(
901            "AcceptedChoices: {}",
902            string_slice(&accepted_choices(choices))
903        ));
904        if choices.ignore_case {
905            fields.push("IgnoreCase: true".to_string());
906        }
907        if accepts_unlisted(choices) {
908            fields.push("AllowUnknownChoices: true".to_string());
909        }
910    }
911    if !arg.default.is_empty() {
912        fields.push(format!("Default: {}", string_slice(&arg.default)));
913    }
914    if let Some(env) = &arg.env {
915        fields.push(format!("Env: {}", go_string(env)));
916    }
917    if !arg.env_fallback.is_empty() {
918        fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
919    }
920    if !arg.deprecated_env.is_empty() {
921        fields.push(format!(
922            "DeprecatedEnv: {}",
923            string_slice(&arg.deprecated_env)
924        ));
925    }
926    if let Some(min) = arg.var_min {
927        fields.push(format!("VarMin: {}", clamp_var_max(min)));
928    }
929    let conflicts = resolve_relationship(&arg.conflicts, owner, commands);
930    if !conflicts.is_empty() {
931        fields.push(format!("Conflicts: {}", key_slice(&conflicts)));
932    }
933    for (label, names) in [
934        ("Requires", &arg.requires),
935        ("RequiredIf", &arg.required_if),
936        ("RequiredUnless", &arg.required_unless),
937        ("RequiredUnlessAll", &arg.required_unless_all),
938    ] {
939        let keys = resolve_relationship(names, owner, commands);
940        if !keys.is_empty() {
941            fields.push(format!("{label}: {}", key_slice(&keys)));
942        }
943    }
944    for (label, conditions) in [
945        ("RequiredIfEq", &arg.required_if_eq),
946        ("RequiredIfEqAll", &arg.required_if_eq_all),
947    ] {
948        let values = conditions
949            .iter()
950            .filter_map(|condition| {
951                resolve_relationship(std::slice::from_ref(&condition.selector), owner, commands)
952                    .into_iter()
953                    .next()
954                    .map(|key| format!("{{Key: {key}, Value: {}}}", go_string(&condition.value)))
955            })
956            .collect::<Vec<_>>();
957        if !values.is_empty() {
958            fields.push(format!(
959                "{label}: []argv.ValueCondition{{{}}}",
960                values.join(", ")
961            ));
962        }
963    }
964    // No VarMax: for an argument the bound is a limit binding applies, which is
965    // what makes `[a]… [b]` fillable at all, so judging it again would fail an
966    // invocation that never broke it.
967    format!("{{{}}}", fields.join(", "))
968}
969
970/// Turn the names in a relationship into the keys they refer to.
971///
972/// Resolved here, where the whole command is visible, so that nothing downstream
973/// searches by name on a path it would repeat per parse. The names arrive as
974/// written — `--stdin`, dashes and all — so they are matched against a flag's long
975/// forms, its shorts, and the name the spec gives it.
976///
977/// A name nothing answers to is dropped. That is a spec bug worth reporting, but
978/// this function has no way to; the check belongs beside the duplicate-form and
979/// duplicate-key checks that already run where the whole tree is visible.
980fn resolve_relationship(names: &[String], owner: &Emitted, commands: &[Emitted]) -> Vec<String> {
981    let mut out = Vec::new();
982    for name in names {
983        // The declaring command's own flags first, then any ancestor's globals —
984        // the scope a token has, in the order a token gets it, so a subcommand
985        // redeclaring an inherited name shadows it here as it does at parse time.
986        let mut found = match_flag(owner, name, false);
987        if found.is_none() && !name.starts_with('-') {
988            found = owner
989                .args
990                .iter()
991                .chain(owner.clause_args.iter())
992                .find(|(arg, _)| arg.name == *name)
993                .map(|(_, named)| named.key.clone());
994        }
995        if found.is_none() {
996            let path = &owner.cmd.full_cmd;
997            for depth in (0..path.len()).rev() {
998                let ancestor = commands
999                    .iter()
1000                    .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
1001                if let Some(key) = ancestor.and_then(|a| match_flag(a, name, true)) {
1002                    found = Some(key);
1003                    break;
1004                }
1005            }
1006        }
1007        if let Some(key) = found {
1008            out.push(key);
1009        }
1010    }
1011    out
1012}
1013
1014/// Find a flag by any spelling a declaration may use for it.
1015///
1016/// The negation counts, and resolves to the same entry: usage-lib treats
1017/// `conflicts = "--no-color"` as naming the `color` flag and reports the conflict
1018/// whichever of the two spellings was typed. The relationship is between entries
1019/// rather than between tokens, which is what the key model already assumes.
1020fn match_flag(cmd: &Emitted, name: &str, globals_only: bool) -> Option<String> {
1021    // Two passes, in the order the parser itself looks: every ordinary form
1022    // first, then negations.
1023    //
1024    // That order is not a nicety. The parser tries every long form before it
1025    // tries any negation, so with `--a` declaring `negate = "--zap"` and a
1026    // separate `--zap`, typing `--zap` binds *zap*. A per-candidate search hands
1027    // the relationship to `a`, and the table then enforces a rule against a flag
1028    // the command line never binds. The table has to agree with the binder it
1029    // feeds.
1030    let eligible = |flag: &SpecFlag| !globals_only || flag.global;
1031
1032    // The form is part of the name: `--q` does not reach the short `-q`, and
1033    // `-color` does not reach the long `--color`. usage-lib resolves neither.
1034    let (long, short, bare) = if let Some(rest) = name.strip_prefix("--") {
1035        (Some(rest), None, None)
1036    } else if let Some(rest) = name.strip_prefix('-') {
1037        let mut chars = rest.chars();
1038        match (chars.next(), chars.next()) {
1039            (Some(c), None) => (None, Some(c), None),
1040            _ => (None, None, None),
1041        }
1042    } else {
1043        (None, None, Some(name))
1044    };
1045
1046    let ordinary = cmd.flags.iter().chain(&cmd.clause_flags).find(|(flag, _)| {
1047        if !eligible(flag) {
1048            return false;
1049        }
1050        if let Some(bare) = bare {
1051            return flag.name == bare;
1052        }
1053        if let Some(long) = long {
1054            return flag.long.iter().any(|l| l == long);
1055        }
1056        short.is_some_and(|c| flag.short.contains(&c))
1057    });
1058    if let Some((_, named)) = ordinary {
1059        return Some(named.key.clone());
1060    }
1061
1062    // Negations, compared exactly as both sides were written — dashes included.
1063    // `negate = "-no-tint"` is named by `-no-tint` and not by `--no-tint`, and
1064    // usage-lib resolves it that way round too.
1065    cmd.flags
1066        .iter()
1067        .chain(&cmd.clause_flags)
1068        .find(|(flag, _)| eligible(flag) && flag.negate.as_deref() == Some(name))
1069        .map(|(_, named)| named.key.clone())
1070}
1071fn string_slice(values: &[String]) -> String {
1072    let list = values
1073        .iter()
1074        .map(|v| go_string(v))
1075        .collect::<Vec<_>>()
1076        .join(", ");
1077    format!("[]string{{{list}}}")
1078}
1079
1080/// Whether the table should let a value outside its lists through.
1081///
1082/// A generated table cannot know what `choices run=` would print — its command runs where
1083/// the program runs, not where the table is generated — so those values are left unchecked
1084/// rather than every one of them refused.
1085fn accepts_unlisted(choices: &SpecChoices) -> bool {
1086    !choices.strict || choices.run().is_some()
1087}
1088
1089fn accepted_choices(choices: &SpecChoices) -> Vec<String> {
1090    choices
1091        .choices
1092        .iter()
1093        .chain(
1094            choices
1095                .details
1096                .iter()
1097                .flat_map(|choice| choice.aliases.iter().map(|alias| &alias.value)),
1098        )
1099        .cloned()
1100        .collect()
1101}
1102
1103fn visible_choices(choices: &SpecChoices) -> Vec<String> {
1104    choices
1105        .choices
1106        .iter()
1107        .filter(|value| {
1108            !choices
1109                .details
1110                .iter()
1111                .any(|choice| choice.value == value.as_str() && choice.hide)
1112        })
1113        .chain(choices.details.iter().flat_map(|choice| {
1114            choice
1115                .aliases
1116                .iter()
1117                .filter(|alias| !alias.hide)
1118                .map(|alias| &alias.value)
1119        }))
1120        .cloned()
1121        .collect()
1122}
1123
1124fn key_slice(keys: &[String]) -> String {
1125    format!("[]uint64{{{}}}", keys.join(", "))
1126}
1127
1128impl Emitter<'_> {
1129    /// Emit the help table: what a page prints.
1130    ///
1131    /// A third table rather than more fields on `Meta`, because Go's linker drops
1132    /// an unreferenced package-level symbol whole — folding help text into the
1133    /// post-binding table would make every CLI that applies a rule carry mise's
1134    /// several hundred kilobytes of help strings too.
1135    fn help_table(&mut self, commands: &[Emitted]) {
1136        let mut by_key: BTreeMap<u64, String> = BTreeMap::new();
1137        for e in commands {
1138            by_key.insert(e.named.number, command_help(e));
1139            for (flag, named) in e.flags.iter().chain(&e.clause_flags) {
1140                by_key.insert(named.number, flag_help(flag, named));
1141            }
1142            for (arg, named) in &e.args {
1143                by_key.insert(named.number, arg_help(arg, named));
1144            }
1145            for (arg, named) in &e.clause_args {
1146                by_key.insert(named.number, arg_help(arg, named));
1147            }
1148        }
1149
1150        let total = commands
1151            .iter()
1152            .map(|e| 1 + e.flags.len() + e.clause_flags.len() + e.args.len() + e.clause_args.len())
1153            .sum::<usize>() as u64;
1154
1155        let _ = writeln!(
1156            self.out,
1157            "// HelpText is the third table, read only when a page is rendered. Neither the\n\
1158             // parser nor the post-binding rules touch it, and a CLI that never prints help\n\
1159             // does not carry it: Go's linker drops an unreferenced table whole.\n\
1160             //\n\
1161             // Indexed by key, like the others.\n\
1162             var HelpText = argv.HelpTable{{"
1163        );
1164        for key in 1..=total {
1165            match by_key.get(&key) {
1166                Some(entry) => {
1167                    let _ = writeln!(self.out, "\t{entry},");
1168                }
1169                None => {
1170                    let _ = writeln!(self.out, "\t{{}},");
1171                }
1172            }
1173        }
1174        let _ = writeln!(self.out, "}}\n");
1175
1176        let mut fields = vec![
1177            format!("Name: {}", go_string(&self.spec.name)),
1178            format!("Bin: {}", go_string(&self.spec.bin)),
1179        ];
1180        if let Some(version) = self
1181            .spec
1182            .version
1183            .as_ref()
1184            .or(self.spec.long_version.as_ref())
1185        {
1186            fields.push(format!("Version: {}", go_string(version)));
1187        }
1188        if let Some(version) = &self.spec.long_version {
1189            fields.push(format!("LongVersion: {}", go_string(version)));
1190        }
1191        // `about` alone, with no fall back to the long one: usage-lib's short page
1192        // prints nothing where a spec wrote only `about_long`, and the long page
1193        // is what reads LongAbout.
1194        if let Some(about) = &self.spec.about {
1195            fields.push(format!("About: {}", go_string(about)));
1196        }
1197        if let Some(long) = &self.spec.about_long {
1198            fields.push(format!("LongAbout: {}", go_string(long)));
1199        }
1200        if let Some(author) = &self.spec.author {
1201            fields.push(format!("Author: {}", go_string(author)));
1202        }
1203        if let Some(license) = &self.spec.license {
1204            fields.push(format!("License: {}", go_string(license)));
1205        }
1206        // The style a spec gives its logo is not carried across: the Go renderer
1207        // produces the portable plain page and colours nothing on it.
1208        if let Some(logo) = &self.spec.logo {
1209            fields.push(format!("Logo: {}", go_string(logo)));
1210        }
1211        if let Some(before) = &self.spec.before_help {
1212            fields.push(format!("BeforeHelp: {}", go_string(before)));
1213        }
1214        if let Some(after) = &self.spec.after_help {
1215            fields.push(format!("AfterHelp: {}", go_string(after)));
1216        }
1217        if let Some(before) = &self.spec.before_help_long {
1218            fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1219        }
1220        if let Some(after) = &self.spec.after_help_long {
1221            fields.push(format!("AfterLongHelp: {}", go_string(after)));
1222        }
1223        // One template for the whole tree, naming the sections a page is assembled from.
1224        if let Some(template) = &self.spec.help_template {
1225            fields.push(format!("HelpTemplate: {}", go_string(template)));
1226        }
1227        let _ = writeln!(
1228            self.out,
1229            "// HelpMeta is what a page needs from the spec's root rather than from any one\n\
1230             // command: root presentation metadata and CLI-wide layout.\n\
1231             var HelpMeta = argv.HelpSpec{{{}}}\n",
1232            fields.join(", ")
1233        );
1234    }
1235}
1236
1237/// The help entry for a command: its about text.
1238fn command_help(e: &Emitted) -> String {
1239    let mut fields = vec![format!("Key: {}", e.named.key)];
1240    if e.cmd.hide {
1241        fields.push("Hide: true".to_string());
1242    }
1243    if let Some(heading) = &e.cmd.help_heading {
1244        fields.push(format!("Heading: {}", go_string(heading)));
1245    }
1246    if let Some(order) = e.cmd.display_order {
1247        fields.push(format!("DisplayOrder: {order}"));
1248        fields.push("DisplayOrderSet: true".to_string());
1249    }
1250    if let Some(help) = e.cmd.help.as_deref().or(e.cmd.help_long.as_deref()) {
1251        fields.push(format!("Short: {}", go_string(help)));
1252    }
1253    if let Some(long) = &e.cmd.help_long {
1254        fields.push(format!("Long: {}", go_string(long)));
1255    }
1256    if let Some(message) = &e.cmd.deprecated {
1257        fields.push(format!("Deprecated: {}", go_string(message)));
1258    }
1259    if let Some(at) = &e.cmd.deprecated_warn_at {
1260        fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1261    }
1262    if let Some(at) = &e.cmd.deprecated_remove_at {
1263        fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1264    }
1265    if let Some(heading) = &e.cmd.subcommand_help_heading {
1266        fields.push(format!("SubcommandHelpHeading: {}", go_string(heading)));
1267    }
1268    if let Some(name) = &e.cmd.subcommand_value_name {
1269        fields.push(format!("SubcommandValueName: {}", go_string(name)));
1270    }
1271    if e.cmd.next_line_help {
1272        fields.push("NextLineHelp: true".to_string());
1273    }
1274    if e.cmd.flatten_help {
1275        fields.push("FlattenHelp: true".to_string());
1276    }
1277    if e.cmd.subcommand_required {
1278        fields.push("SubcommandRequired: true".to_string());
1279    }
1280    // Visible only: the parse table merges hidden aliases in beside these,
1281    // because binding does not care which is which. A page does.
1282    let visible: Vec<String> = e
1283        .cmd
1284        .aliases
1285        .iter()
1286        .filter(|a| !e.cmd.hidden_aliases.contains(a))
1287        .cloned()
1288        .collect();
1289    if !visible.is_empty() {
1290        fields.push(format!("VisibleAliases: {}", string_slice(&visible)));
1291    }
1292    if let Some(before) = &e.cmd.before_help {
1293        fields.push(format!("BeforeHelp: {}", go_string(before)));
1294    }
1295    if let Some(after) = &e.cmd.after_help {
1296        fields.push(format!("AfterHelp: {}", go_string(after)));
1297    }
1298    // The long page's own brackets, which most of mise's commands use: their
1299    // examples are written as `after_long_help`, and a generated CLI that dropped
1300    // them printed a page with the examples missing.
1301    if let Some(before) = &e.cmd.before_help_long {
1302        fields.push(format!("BeforeLongHelp: {}", go_string(before)));
1303    }
1304    if let Some(after) = &e.cmd.after_help_long {
1305        fields.push(format!("AfterLongHelp: {}", go_string(after)));
1306    }
1307    if !e.cmd.examples.is_empty() {
1308        let items = e
1309            .cmd
1310            .examples
1311            .iter()
1312            .map(|x| {
1313                let mut parts = Vec::new();
1314                if let Some(header) = &x.header {
1315                    parts.push(format!("Header: {}", go_string(header)));
1316                }
1317                parts.push(format!("Code: {}", go_string(&x.code)));
1318                // The line the long page prints above the command. It introduces
1319                // the invocation rather than commenting on it, and a generated CLI
1320                // that dropped it printed the command with nothing to say why.
1321                if let Some(help) = &x.help {
1322                    parts.push(format!("Help: {}", go_string(help)));
1323                }
1324                format!("{{{}}}", parts.join(", "))
1325            })
1326            .collect::<Vec<_>>()
1327            .join(", ");
1328        fields.push(format!("Examples: []argv.Example{{{items}}}"));
1329    }
1330    // The prose introducing each help section. Lowered from a spec it travels with the
1331    // rest of the help metadata, and a generated CLI that dropped it printed the heading
1332    // with nothing under it while every other reader of the same spec showed the text.
1333    if !e.cmd.headings.is_empty() {
1334        let items = e
1335            .cmd
1336            .headings
1337            .iter()
1338            .map(|heading| {
1339                format!(
1340                    "{{Title: {}, Help: {}}}",
1341                    go_string(&heading.title),
1342                    go_string(&heading.help)
1343                )
1344            })
1345            .collect::<Vec<_>>()
1346            .join(", ");
1347        fields.push(format!("Headings: []argv.Heading{{{items}}}"));
1348    }
1349    format!("{{{}}}", fields.join(", "))
1350}
1351
1352fn flag_help(flag: &SpecFlag, named: &Named) -> String {
1353    let mut fields = vec![format!("Key: {}", named.key)];
1354    if let Some(message) = &flag.deprecated {
1355        fields.push(format!("Deprecated: {}", go_string(message)));
1356    }
1357    if let Some(at) = &flag.deprecated_warn_at {
1358        fields.push(format!("DeprecatedWarnAt: {}", go_string(at)));
1359    }
1360    if let Some(at) = &flag.deprecated_remove_at {
1361        fields.push(format!("DeprecatedRemoveAt: {}", go_string(at)));
1362    }
1363    if flag.hide {
1364        fields.push("Hide: true".to_string());
1365    }
1366    if let Some(order) = flag.display_order {
1367        fields.push(format!("DisplayOrder: {order}"));
1368        fields.push("DisplayOrderSet: true".to_string());
1369    }
1370    for (name, hidden) in [
1371        ("HideDefaultValue", flag.hide_default_value),
1372        ("HideEnv", flag.hide_env),
1373        ("HideEnvValues", flag.hide_env_values),
1374        ("HidePossibleValues", flag.hide_possible_values),
1375        ("HideShortHelp", flag.hide_short_help),
1376        ("HideLongHelp", flag.hide_long_help),
1377    ] {
1378        if hidden {
1379            fields.push(format!("{name}: true"));
1380        }
1381    }
1382    // Required *and* undefaulted, which is what decides the brackets: a required
1383    // flag with a default is one the user never has to type.
1384    if flag.required && flag.default.is_empty() {
1385        fields.push("Demanded: true".to_string());
1386    }
1387    if flag.var {
1388        fields.push("Repeatable: true".to_string());
1389    }
1390    if let Some(arg) = &flag.arg {
1391        if arg.name != flag.name {
1392            fields.push(format!("ValueName: {}", go_string(&arg.name)));
1393        }
1394        // The value's own requiredness, which is independent of the flag's:
1395        // `<--v <n>>` is a required flag whose value must be given, and
1396        // `<--jobs [n]>` a required flag whose value has a default.
1397        if arg.required && arg.default.is_empty() {
1398            fields.push("ValueDemanded: true".to_string());
1399        }
1400        if !arg.value_names.is_empty() {
1401            fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1402        }
1403        if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1404            fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1405        }
1406    }
1407    // The whole `help`, not its first line: usage-lib's short page prints the
1408    // text as declared, and mise has flags whose help is two lines.
1409    if let Some(help) = flag.help.as_deref().or(flag.help_first_line.as_deref()) {
1410        fields.push(format!("Short: {}", go_string(help)));
1411    }
1412    if let Some(long) = flag.help_long.as_deref().or(flag.help.as_deref()) {
1413        fields.push(format!("Long: {}", go_string(long)));
1414    }
1415    if let Some(heading) = &flag.help_heading {
1416        fields.push(format!("Heading: {}", go_string(heading)));
1417    }
1418    // Annotations. A flag's choices are declared on the value it takes.
1419    if let Some(choices) = flag.arg.as_ref().and_then(|a| a.choices.as_ref()) {
1420        fields.push(format!(
1421            "Choices: {}",
1422            string_slice(&visible_choices(choices))
1423        ));
1424    }
1425    if let Some(env) = &flag.env {
1426        fields.push(format!("Env: {}", go_string(env)));
1427    }
1428    if !flag.env_fallback.is_empty() {
1429        fields.push(format!("EnvFallback: {}", string_slice(&flag.env_fallback)));
1430    }
1431    if !flag.deprecated_env.is_empty() {
1432        fields.push(format!(
1433            "DeprecatedEnv: {}",
1434            string_slice(&flag.deprecated_env)
1435        ));
1436    }
1437    let default = if !flag.default.is_empty() {
1438        &flag.default
1439    } else {
1440        flag.arg
1441            .as_ref()
1442            .map(|a| &a.default)
1443            .unwrap_or(&flag.default)
1444    };
1445    if !default.is_empty() {
1446        fields.push(format!("Default: {}", string_slice(default)));
1447    }
1448    format!("{{{}}}", fields.join(", "))
1449}
1450
1451fn arg_help(arg: &SpecArg, named: &Named) -> String {
1452    let mut fields = vec![format!("Key: {}", named.key)];
1453    if let Some(order) = arg.display_order {
1454        fields.push(format!("DisplayOrder: {order}"));
1455        fields.push("DisplayOrderSet: true".to_string());
1456    }
1457    if arg.hide {
1458        fields.push("Hide: true".to_string());
1459    }
1460    for (name, hidden) in [
1461        ("HideDefaultValue", arg.hide_default_value),
1462        ("HideEnv", arg.hide_env),
1463        ("HideEnvValues", arg.hide_env_values),
1464        ("HidePossibleValues", arg.hide_possible_values),
1465        ("HideShortHelp", arg.hide_short_help),
1466        ("HideLongHelp", arg.hide_long_help),
1467    ] {
1468        if hidden {
1469            fields.push(format!("{name}: true"));
1470        }
1471    }
1472    if arg.required && arg.default.is_empty() {
1473        fields.push("Demanded: true".to_string());
1474    }
1475    if !arg.value_names.is_empty() {
1476        fields.push(format!("ValueNames: {}", string_slice(&arg.value_names)));
1477    }
1478    if arg.var && arg.var_min == arg.var_max && arg.var_min.is_some_and(|n| n > 1) {
1479        fields.push(format!("ValueArity: {}", arg.var_min.unwrap()));
1480    }
1481    if let Some(help) = arg.help.as_deref().or(arg.help_first_line.as_deref()) {
1482        fields.push(format!("Short: {}", go_string(help)));
1483    }
1484    if let Some(long) = arg.help_long.as_deref().or(arg.help.as_deref()) {
1485        fields.push(format!("Long: {}", go_string(long)));
1486    }
1487    if let Some(heading) = &arg.help_heading {
1488        fields.push(format!("Heading: {}", go_string(heading)));
1489    }
1490    if let Some(choices) = &arg.choices {
1491        fields.push(format!(
1492            "Choices: {}",
1493            string_slice(&visible_choices(choices))
1494        ));
1495    }
1496    if let Some(env) = &arg.env {
1497        fields.push(format!("Env: {}", go_string(env)));
1498    }
1499    if !arg.env_fallback.is_empty() {
1500        fields.push(format!("EnvFallback: {}", string_slice(&arg.env_fallback)));
1501    }
1502    if !arg.deprecated_env.is_empty() {
1503        fields.push(format!(
1504            "DeprecatedEnv: {}",
1505            string_slice(&arg.deprecated_env)
1506        ));
1507    }
1508    if !arg.default.is_empty() {
1509        fields.push(format!("Default: {}", string_slice(&arg.default)));
1510    }
1511    format!("{{{}}}", fields.join(", "))
1512}
1513
1514/// A line inside a `const` block or a composite literal.
1515///
1516/// The distinction exists only to reproduce gofmt's alignment, which pads within
1517/// *runs* of consecutive single-line entries and starts a new run after anything
1518/// that spans lines. Emitting gofmt-clean output rather than close-enough output
1519/// is what lets a generated file be committed as it comes out: the alternative is
1520/// every adopter needing a formatting step, and this repo's own CI failing
1521/// `gofmt -l` on the table it checks in.
1522enum Line {
1523    /// `Key: value,` — aligned against its neighbours.
1524    Field(String, String),
1525    /// Verbatim, and it breaks the run either side of it.
1526    Block(Vec<String>),
1527}
1528
1529/// Render lines with gofmt's column alignment.
1530fn render(out: &mut String, indent: &str, lines: &[Line]) {
1531    let mut run: Vec<(&String, &String)> = Vec::new();
1532
1533    fn flush(out: &mut String, indent: &str, run: &mut Vec<(&String, &String)>) {
1534        let width = run.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
1535        for (key, value) in run.iter() {
1536            let _ = writeln!(
1537                out,
1538                "{indent}{key}:{:width$} {value},",
1539                "",
1540                width = width - key.len()
1541            );
1542        }
1543        run.clear();
1544    }
1545
1546    for line in lines {
1547        match line {
1548            Line::Field(key, value) => run.push((key, value)),
1549            Line::Block(block) => {
1550                flush(out, indent, &mut run);
1551                for l in block {
1552                    let _ = writeln!(out, "{indent}{l}");
1553                }
1554            }
1555        }
1556    }
1557    flush(out, indent, &mut run);
1558}
1559
1560/// One command, named and ready to emit.
1561struct Emitted {
1562    named: Named,
1563    cmd: SpecCommand,
1564    flags: Vec<(SpecFlag, Named)>,
1565    args: Vec<(SpecArg, Named)>,
1566    clause_flags: Vec<(SpecFlag, Named)>,
1567    clause_args: Vec<(SpecArg, Named)>,
1568    /// Indices into the flat list, in declaration order.
1569    subcommands: Vec<usize>,
1570    root: bool,
1571}
1572
1573/// What an unrecognized flag-like token means at a command, with inheritance
1574/// applied.
1575///
1576/// The nearest enclosing command that states a preference wins, then the spec,
1577/// then `value`. Walked over `full_cmd` rather than threaded through the collect
1578/// pass, so that emission does not depend on the order commands happen to sit in.
1579fn effective_unknown_flags(spec: &Spec, commands: &[Emitted], at: usize) -> UnknownFlags {
1580    let path = &commands[at].cmd.full_cmd;
1581    for depth in (0..=path.len()).rev() {
1582        let ancestor = commands
1583            .iter()
1584            .find(|e| e.cmd.full_cmd.len() == depth && e.cmd.full_cmd[..] == path[..depth]);
1585        if let Some(mode) = ancestor.and_then(|e| e.cmd.unknown_flags) {
1586            return mode;
1587        }
1588    }
1589    spec.unknown_flags.unwrap_or_default()
1590}
1591
1592fn flag_literal(flag: &SpecFlag, named: &Named) -> String {
1593    let mut fields = vec![
1594        format!("Key: {}", named.key),
1595        format!("Name: {}", go_string(&flag.name)),
1596    ];
1597    if !flag.long.is_empty() {
1598        let longs = flag
1599            .long
1600            .iter()
1601            .map(|l| go_string(l))
1602            .collect::<Vec<_>>()
1603            .join(", ");
1604        fields.push(format!("Longs: []string{{{longs}}}"));
1605    }
1606    if !flag.hidden_aliases.is_empty() {
1607        fields.push(format!(
1608            "HiddenLongs: {}",
1609            string_slice(&flag.hidden_aliases)
1610        ));
1611    }
1612    if !flag.short.is_empty() {
1613        let shorts = flag
1614            .short
1615            .iter()
1616            .map(|c| go_byte(*c))
1617            .collect::<Vec<_>>()
1618            .join(", ");
1619        fields.push(format!("Shorts: []byte{{{shorts}}}"));
1620    }
1621    if !flag.hidden_short_aliases.is_empty() {
1622        let shorts = flag
1623            .hidden_short_aliases
1624            .iter()
1625            .map(|c| go_byte(*c))
1626            .collect::<Vec<_>>()
1627            .join(", ");
1628        fields.push(format!("HiddenShorts: []byte{{{shorts}}}"));
1629    }
1630    if let Some(negate) = &flag.negate {
1631        // The spec stores the negation with its dashes; the table wants the bare
1632        // name, since that is what the parser has after stripping the `--`.
1633        fields.push(format!(
1634            "Negate: {}",
1635            go_string(negate.trim_start_matches('-'))
1636        ));
1637    }
1638    if flag.arg.is_some() {
1639        fields.push("TakesValue: true".to_string());
1640    }
1641    if flag.value_optional {
1642        fields.push("ValueOptional: true".to_string());
1643    }
1644    if flag.bool_value {
1645        fields.push("BoolValue: true".to_string());
1646    }
1647    let action = match flag.action {
1648        SpecFlagAction::Set => None,
1649        SpecFlagAction::Help => Some("argv.ActionHelp"),
1650        SpecFlagAction::HelpShort => Some("argv.ActionHelpShort"),
1651        SpecFlagAction::HelpLong => Some("argv.ActionHelpLong"),
1652        SpecFlagAction::HelpAll => Some("argv.ActionHelpAll"),
1653        SpecFlagAction::Version => Some("argv.ActionVersion"),
1654    };
1655    if let Some(action) = action {
1656        fields.push(format!("Action: {action}"));
1657    }
1658    // Only a variadic *argument* is greedy. The spec's flag-level `var` means the
1659    // flag may be repeated and takes one value each time, which needs nothing from
1660    // the parser: it reports every occurrence separately either way. Conflating the
1661    // two makes a merely repeatable flag greedy enough to eat a positional.
1662    if let Some(arg) = flag.arg.as_ref().filter(|a| a.var) {
1663        fields.push("Variadic: true".to_string());
1664        if let Some(max) = arg.var_max {
1665            fields.push(format!("VarMax: {}", clamp_var_max(max)));
1666        }
1667    }
1668    if flag.allow_hyphen_values() {
1669        fields.push("AllowHyphenValues: true".to_string());
1670    }
1671    if let Some(arg) = &flag.arg {
1672        if arg.allow_negative_numbers {
1673            fields.push("AllowNegativeNumbers: true".to_string());
1674        }
1675        if let Some(terminator) = &arg.value_terminator {
1676            fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1677        }
1678        if let Some(delimiter) = arg.delimiter {
1679            fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1680        }
1681    }
1682    if flag.require_equals {
1683        fields.push("RequireEquals: true".to_string());
1684    }
1685    if let Some(missing) = &flag.default_missing {
1686        fields.push(format!("DefaultMissing: {}", go_string(missing)));
1687    }
1688    if flag.global {
1689        fields.push("Global: true".to_string());
1690    }
1691    format!("{{{}}}", fields.join(", "))
1692}
1693
1694fn arg_literal(arg: &SpecArg, named: &Named) -> String {
1695    let mut fields = vec![
1696        format!("Key: {}", named.key),
1697        format!("Name: {}", go_string(&arg.name)),
1698    ];
1699    if let Some(sigil) = &arg.sigil {
1700        fields.push(format!("Sigil: {}", go_string(sigil)));
1701    }
1702    if arg.required {
1703        fields.push("Required: true".to_string());
1704    }
1705    if arg.var {
1706        fields.push("Var: true".to_string());
1707        if let Some(max) = arg.var_max {
1708            fields.push(format!("VarMax: {}", clamp_var_max(max)));
1709        }
1710    }
1711    if arg.allow_negative_numbers {
1712        fields.push("AllowNegativeNumbers: true".to_string());
1713    }
1714    if let Some(terminator) = &arg.value_terminator {
1715        fields.push(format!("ValueTerminator: {}", go_string(terminator)));
1716    }
1717    if let Some(delimiter) = arg.delimiter {
1718        fields.push(format!("Delimiter: {}", go_byte(delimiter)));
1719    }
1720    let double_dash = match arg.double_dash {
1721        SpecDoubleDashChoices::Required => Some("argv.DoubleDashRequired"),
1722        SpecDoubleDashChoices::Preserve => Some("argv.DoubleDashPreserve"),
1723        SpecDoubleDashChoices::Automatic => Some("argv.DoubleDashAutomatic"),
1724        _ => None,
1725    };
1726    if let Some(dd) = double_dash {
1727        fields.push(format!("DoubleDash: {dd}"));
1728    }
1729    format!("{{{}}}", fields.join(", "))
1730}
1731
1732/// Zero means unbounded in the table, which is also what an absent `var_max`
1733/// lowers to, so the two agree. A bound past a `uint32` saturates rather than
1734/// wrapping: truncating four billion and one to one would read as "stop at once"
1735/// rather than "no real limit".
1736fn clamp_var_max(max: usize) -> u32 {
1737    u32::try_from(max).unwrap_or(u32::MAX)
1738}
1739
1740/// Go's reserved words, which cannot be a package name.
1741///
1742/// Not hypothetical: `go`, `range`, `select`, `import` and `package` are all
1743/// plausible names for a CLI, and `package go` does not compile.
1744const GO_KEYWORDS: &[&str] = &[
1745    "break",
1746    "case",
1747    "chan",
1748    "const",
1749    "continue",
1750    "default",
1751    "defer",
1752    "else",
1753    "fallthrough",
1754    "for",
1755    "func",
1756    "go",
1757    "goto",
1758    "if",
1759    "import",
1760    "interface",
1761    "map",
1762    "package",
1763    "range",
1764    "return",
1765    "select",
1766    "struct",
1767    "switch",
1768    "type",
1769    "var",
1770];
1771
1772/// Two more names a table package cannot have, for two different reasons.
1773///
1774/// `_` is refused where it is written: `invalid package name _`. `init` declares
1775/// perfectly well and cannot be *imported* — an import binds the package name as
1776/// an identifier in file scope, and `init` may only be a func, so an importer gets
1777/// `cannot import package as init - init must be a func`. A table package exists
1778/// to be imported, so it is out either way.
1779///
1780/// Both checked against the compiler rather than taken from a citation. The issue
1781/// usually cited for `init` is about the import, and `package init` on its own
1782/// does build — so a validator written from the citation would have rejected it
1783/// for a reason that is not true.
1784const UNUSABLE_PACKAGE_NAMES: &[&str] = &["_", "init"];
1785
1786/// Whether a string can be written after `package` and then imported.
1787///
1788/// Deliberately ASCII-only. Go itself allows a Unicode letter, but a package name
1789/// that needs one is a worse problem for an adopter than the restriction is.
1790pub fn is_valid_package(name: &str) -> bool {
1791    !name.is_empty()
1792        && !GO_KEYWORDS.contains(&name)
1793        && !UNUSABLE_PACKAGE_NAMES.contains(&name)
1794        && !name.starts_with(|c: char| c.is_ascii_digit())
1795        && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1796}
1797
1798/// A Go field name from a spec name: exported, and an identifier.
1799fn field_name(name: &str) -> String {
1800    let ident = format!("{}", AsPascalCase(name));
1801    if ident.is_empty() || ident.starts_with(|c: char| c.is_ascii_digit()) {
1802        format!("X{ident}")
1803    } else {
1804        ident
1805    }
1806}
1807
1808/// A Go package identifier from a binary name: `my-cli` is not one, `mycli` is.
1809///
1810/// Only ever applied to a name derived from the spec, which the author did not
1811/// choose for this purpose and cannot be asked to fix. A `--package` given
1812/// explicitly is checked rather than mangled — see [`is_valid_package`] — because
1813/// silently emitting `mypkg` for someone who asked for `my-pkg` is a surprise
1814/// waiting in a build script.
1815fn package_ident(bin: &str) -> String {
1816    let lowered: String = bin
1817        .chars()
1818        .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
1819        .collect::<String>()
1820        .to_ascii_lowercase();
1821    if is_valid_package(&lowered) {
1822        lowered
1823    } else {
1824        // One rule rather than a second copy of the conditions, so the sanitizer
1825        // cannot come to disagree with the validator about what is acceptable.
1826        // `cli` in front keeps it recognizable: `cligo`, `cli7zip`, `cliinit`.
1827        format!("cli{lowered}")
1828    }
1829}
1830
1831/// A Go string literal.
1832///
1833/// Written out rather than borrowed from Rust's `{:?}`, which escapes to Rust's
1834/// rules: it spells a delete character `\u{7f}`, which Go does not accept.
1835fn go_string(s: &str) -> String {
1836    let mut out = String::with_capacity(s.len() + 2);
1837    out.push('"');
1838    for c in s.chars() {
1839        match c {
1840            '"' => out.push_str("\\\""),
1841            '\\' => out.push_str("\\\\"),
1842            '\n' => out.push_str("\\n"),
1843            '\r' => out.push_str("\\r"),
1844            '\t' => out.push_str("\\t"),
1845            c if (c as u32) < 0x20 || c as u32 == 0x7f => {
1846                let _ = write!(out, "\\x{:02x}", c as u32);
1847            }
1848            c => out.push(c),
1849        }
1850    }
1851    out.push('"');
1852    out
1853}
1854
1855/// A Go byte literal for a short flag.
1856///
1857/// Non-ASCII shorts are emitted as their low byte, which can never match: a
1858/// cluster is walked one byte at a time. The spec is what should refuse them, and
1859/// silently dropping one here would be a flag that vanished.
1860fn go_byte(c: char) -> String {
1861    match c {
1862        '\'' => "'\\''".to_string(),
1863        '\\' => "'\\\\'".to_string(),
1864        c if c.is_ascii_graphic() => format!("'{c}'"),
1865        c => format!("0x{:02x}", (c as u32) & 0xff),
1866    }
1867}
1868
1869#[cfg(test)]
1870mod tests {
1871    use super::*;
1872
1873    /// The emitted `Meta` line for an entry, so a test can assert about the part
1874    /// it cares about rather than the whole rendered row — which grows a field
1875    /// every time the cold table learns something.
1876    ///
1877    /// Used for what a row *does* say as well as for what it does not. Two
1878    /// substring checks over the whole file — one for the name, one for the
1879    /// relationship — pass when the relationship is attached to a different flag
1880    /// entirely, which is the regression these tests exist to catch.
1881    fn entry_of(out: &str, name: &str) -> String {
1882        out.lines()
1883            .find(|l| l.contains(&format!("Name: \"{name}\", Flag: true")))
1884            .unwrap_or_default()
1885            .to_string()
1886    }
1887
1888    fn go(kdl: &str) -> String {
1889        let spec: Spec = kdl.parse().expect("the fixture spec should parse");
1890        generate(&spec, &GoOptions::default())
1891    }
1892
1893    #[test]
1894    fn a_whole_cli() {
1895        let out = go(r#"
1896name "ex"
1897bin "ex"
1898version "1.2.3"
1899long_version "1.2.3\ncommit abc123"
1900flag "-v --verbose" global=#true help="be loud"
1901flag "--color" negate="--no-color"
1902flag "-j --jobs <n>"
1903flag "--include <pattern>..." var_max=3
1904arg "<file>"
1905arg "[rest]..." var=#true
1906cmd "install" {
1907    alias "i"
1908    flag "-f --force"
1909    arg "<pkg>"
1910}
1911cmd "config" {
1912    cmd "ls" {
1913        flag "--no-header"
1914    }
1915}
1916"#);
1917        insta::assert_snapshot!(out);
1918    }
1919
1920    #[test]
1921    fn rich_choices_keep_acceptance_visibility_and_strictness_separate() {
1922        let out = go(r#"
1923name "ex"
1924bin "ex"
1925flag "--color <when>" {
1926    choices ignore_case=#true strict=#false {
1927        choice "always" {
1928            alias "yes"
1929            alias "on" hide=#true
1930        }
1931        choice "never" hide=#true
1932    }
1933}
1934"#);
1935        let entry = entry_of(&out, "color");
1936        assert!(
1937            entry.contains(r#"Choices: []string{"always", "yes"}"#),
1938            "{entry}"
1939        );
1940        assert!(
1941            entry.contains(r#"AcceptedChoices: []string{"always", "never", "yes", "on"}"#),
1942            "{entry}"
1943        );
1944        assert!(entry.contains("IgnoreCase: true"), "{entry}");
1945        assert!(entry.contains("AllowUnknownChoices: true"), "{entry}");
1946    }
1947
1948    #[test]
1949    fn choices_from_a_command_are_left_unchecked_rather_than_run() {
1950        // `exit 1` would fail if it ran: generating the table must not run it.
1951        let out = go(r#"
1952name "ex"
1953bin "ex"
1954flag "--service <name>" {
1955    choices "local" run="exit 1"
1956}
1957"#);
1958        let entry = entry_of(&out, "service");
1959        assert!(entry.contains(r#"Choices: []string{"local"}"#), "{entry}");
1960        assert!(entry.contains("AllowUnknownChoices: true"), "{entry}");
1961    }
1962
1963    /// Inheritance is resolved here so the parser reads one field per command.
1964    #[test]
1965    fn unknown_flags_are_inherited_and_overridable() {
1966        let out = go(r#"
1967name "ex"
1968bin "ex"
1969unknown_flags "error"
1970cmd "strict" {
1971    cmd "deep" {}
1972}
1973cmd "exec" unknown_flags="value" {
1974    cmd "nested" {}
1975}
1976"#);
1977        insta::assert_snapshot!(out);
1978    }
1979
1980    /// The known names cover every way a spec reads a variable, from every command and
1981    /// clause and the config, because generated Go has only the tables to go on.
1982    #[test]
1983    fn unknown_env_reaches_generated_go_with_every_declared_name() {
1984        let out = go(r#"
1985name "ex"
1986bin "ex"
1987unknown_env prefix="EX_" {
1988    allow "EX_LIBRARY"
1989}
1990flag "--port <port>" env="EX_PORT" env_fallback="EX_OLD_PORT"
1991cmd "serve" {
1992    flag "--tls" deprecated_env="EX_SSL"
1993    clause "route" {
1994        flag "--path <path>" env="EX_ROUTE_PATH"
1995        arg "<route>"
1996    }
1997}
1998cmd "publish" {
1999    arg "[dir]" env="EX_DIR"
2000}
2001config {
2002    prop "jobs" env="EX_JOBS"
2003}
2004"#);
2005        assert!(out.contains("const UnknownEnvPrefix = \"EX_\""), "{out}");
2006        assert!(
2007            out.contains(
2008                "var UnknownEnvKnown = []string{\"EX_DIR\", \"EX_JOBS\", \"EX_LIBRARY\", \"EX_OLD_PORT\", \"EX_PORT\", \"EX_ROUTE_PATH\", \"EX_SSL\"}"
2009            ),
2010            "{out}"
2011        );
2012        assert!(
2013            out.contains(
2014                "argv.RefuseUnknownEnv(UnknownEnvPrefix, UnknownEnvKnown, argv.EnvNames())"
2015            ),
2016            "{out}"
2017        );
2018
2019        let without = go("name \"ex\"\nbin \"ex\"\nflag \"--port <port>\" env=\"EX_PORT\"\n");
2020        assert!(!without.contains("UnknownEnv"), "{without}");
2021    }
2022
2023    /// A mounted command's variables are not known when the tables are generated, so
2024    /// the combination is refused rather than emitted as a parser that rejects them.
2025    #[test]
2026    fn unknown_env_with_a_mount_is_refused() {
2027        let spec: Spec = "name \"ex\"\nbin \"ex\"\nunknown_env prefix=\"EX_\"\ncmd \"plugin\" {\n    mount run=\"ex-plugin --usage\"\n}\n"
2028            .parse()
2029            .unwrap();
2030        let err = generate_with_types(&spec, &GoOptions::default(), &BTreeMap::new()).unwrap_err();
2031        assert!(err.contains("mount"), "{err}");
2032
2033        let spec: Spec =
2034            "name \"ex\"\nbin \"ex\"\ncmd \"plugin\" {\n    mount run=\"ex-plugin --usage\"\n}\n"
2035                .parse()
2036                .unwrap();
2037        assert!(generate_with_types(&spec, &GoOptions::default(), &BTreeMap::new()).is_ok());
2038    }
2039
2040    /// mise declares both a `macos-defaults` command and a `macos defaults` path,
2041    /// and both want the same Go identifier.
2042    #[test]
2043    fn colliding_names_get_distinct_identifiers() {
2044        let out = go(r#"
2045name "ex"
2046bin "ex"
2047cmd "macos-defaults" {
2048    flag "--apply"
2049}
2050cmd "macos" {
2051    cmd "defaults" {
2052        flag "--apply"
2053    }
2054}
2055"#);
2056        insta::assert_snapshot!(out);
2057    }
2058
2059    #[test]
2060    fn default_flag_routing_reaches_generated_go_tables() {
2061        let out = go(
2062            "name ex\nbin ex\ndefault_subcommand run\ndefault_subcommand_flags #true\ncmd run {}\n",
2063        );
2064        assert!(out.contains("DefaultSubcommandFlags: true"), "{out}");
2065    }
2066
2067    #[test]
2068    fn a_default_subcommand_points_into_the_tree() {
2069        let out = go(r#"
2070name "ex"
2071bin "ex"
2072default_subcommand "run"
2073arg "[task]"
2074cmd "run" {
2075    arg "[args]..." var=#true
2076}
2077"#);
2078        insta::assert_snapshot!(out);
2079    }
2080
2081    #[test]
2082    fn a_bin_name_that_is_not_an_identifier_still_gives_a_package() {
2083        assert_eq!(package_ident("my-cli"), "mycli");
2084        assert_eq!(package_ident("7zip"), "cli7zip");
2085        assert_eq!(package_ident(""), "cli");
2086        // `package go` does not compile, and `go` is a plausible name for a CLI.
2087        assert_eq!(package_ident("go"), "cligo");
2088        assert_eq!(package_ident("type"), "clitype");
2089        // `package _` is refused outright; `package init` declares fine and cannot
2090        // be imported, which for a table package is the same thing.
2091        assert_eq!(package_ident("_"), "cli_");
2092        assert_eq!(package_ident("init"), "cliinit");
2093        // Two underscores is fine, and only the exact name is reserved.
2094        assert_eq!(package_ident("__"), "__");
2095        assert_eq!(package_ident("initialize"), "initialize");
2096
2097        // Whatever it produces must be something the validator accepts, for every
2098        // one of these — the sanitizer disagreeing with the check is how a file
2099        // that does not compile gets emitted.
2100        for bin in [
2101            "my-cli", "7zip", "", "go", "type", "_", "init", "__", "MiSe",
2102        ] {
2103            let out = package_ident(bin);
2104            assert!(is_valid_package(&out), "{bin:?} sanitized to {out:?}");
2105        }
2106    }
2107
2108    /// Counting alone let a third command collide with a generated suffix.
2109    #[test]
2110    fn a_name_matching_a_generated_suffix_still_gets_its_own() {
2111        let out = go(r#"
2112name "ex"
2113bin "ex"
2114cmd "macos-defaults" {}
2115cmd "macos" {
2116    cmd "defaults" {}
2117}
2118cmd "macos-defaults2" {}
2119"#);
2120        // The invariant, not a guess at the spelling. The third command lands on
2121        // `CmdMacosDefaults22` rather than `...3`, which is unlovely and correct;
2122        // asserting the exact name would pin the suffix scheme instead of the
2123        // property that matters, which is that nothing is declared twice.
2124        assert_declares_each_constant_once(&out);
2125    }
2126
2127    /// Every constant in the emitted `const` block, in declaration order.
2128    fn constant_names(out: &str) -> Vec<&str> {
2129        out.lines()
2130            .skip_while(|l| !l.starts_with("const ("))
2131            .skip(1)
2132            .take_while(|l| !l.starts_with(')'))
2133            .filter_map(|l| l.split_whitespace().next())
2134            .collect()
2135    }
2136
2137    /// Two entries sharing a constant is a file that does not compile.
2138    fn assert_declares_each_constant_once(out: &str) {
2139        let names = constant_names(out);
2140        assert!(!names.is_empty(), "no constants at all:\n{out}");
2141        let mut seen = std::collections::HashSet::new();
2142        for name in &names {
2143            assert!(seen.insert(*name), "{name} is declared twice:\n{out}");
2144        }
2145    }
2146
2147    #[test]
2148    fn a_package_that_would_not_compile_is_refused_rather_than_emitted() {
2149        assert!(is_valid_package("mycli"));
2150        assert!(is_valid_package("mise_tables"));
2151        assert!(!is_valid_package("my-pkg"));
2152        assert!(!is_valid_package("7zip"));
2153        assert!(!is_valid_package(""));
2154        assert!(!is_valid_package("range"));
2155        assert!(!is_valid_package("_"));
2156        assert!(!is_valid_package("init"));
2157        assert!(is_valid_package("__"));
2158
2159        // A library caller that skips the check still gets a file that compiles.
2160        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
2161        let out = generate(
2162            &spec,
2163            &GoOptions {
2164                package: Some("my-pkg".into()),
2165            },
2166        );
2167        assert!(out.contains("package mypkg"), "{out}");
2168    }
2169
2170    /// The bug the checked-in mise tables caught: `default_subcommand run` was
2171    /// wired to `oci run`, which a depth-first walk reaches first.
2172    ///
2173    /// It names a subcommand *of the root*, so nothing deeper is a candidate — and
2174    /// the parser would otherwise descend into a command that is not the root's
2175    /// child at all.
2176    /// The long page's text is a table entry too.
2177    ///
2178    /// mise writes its examples as `after_long_help`, on 115 of its commands, so a
2179    /// generator that dropped them emitted a `--help` with every example missing —
2180    /// while the page tests, which build their tables by lowering rather than by
2181    /// generating, saw nothing wrong. The two producers are compared against each
2182    /// other now; this is the same rule from the emitter's side.
2183    #[test]
2184    fn the_long_pages_text_reaches_the_tables() {
2185        let out = go(r#"
2186name "ex"
2187bin "ex"
2188about "Short."
2189about_long "Long."
2190before_long_help "ROOT-BEFORE"
2191after_long_help "ROOT-AFTER"
2192cmd "run" help="Run it" {
2193    before_long_help "RUN-BEFORE"
2194    after_long_help "RUN-AFTER"
2195}
2196"#);
2197        let meta = out
2198            .lines()
2199            .find(|l| l.contains("var HelpMeta"))
2200            .expect("a root header is emitted");
2201        assert!(
2202            meta.contains(r#"About: "Short.""#) && meta.contains(r#"LongAbout: "Long.""#),
2203            "the two abouts are separate fields: {meta}"
2204        );
2205        assert!(
2206            meta.contains(r#"BeforeLongHelp: "ROOT-BEFORE""#)
2207                && meta.contains(r#"AfterLongHelp: "ROOT-AFTER""#),
2208            "the root's long brackets are emitted: {meta}"
2209        );
2210
2211        let run = out
2212            .lines()
2213            .find(|l| l.contains("Short: \"Run it\""))
2214            .expect("the command has a help entry");
2215        assert!(
2216            run.contains(r#"BeforeLongHelp: "RUN-BEFORE""#)
2217                && run.contains(r#"AfterLongHelp: "RUN-AFTER""#),
2218            "a command's long brackets are emitted: {run}"
2219        );
2220    }
2221
2222    /// An example's help line reaches the tables.
2223    ///
2224    /// The long page prints it above the command, where it introduces the
2225    /// invocation; a generated CLI that dropped it printed the command with
2226    /// nothing to say why. mise cannot show this — it writes its examples as
2227    /// `after_long_help` text rather than as `example` nodes — so the producer
2228    /// comparison over mise's spec cannot see it either.
2229    #[test]
2230    fn an_examples_help_line_reaches_the_tables() {
2231        let out = go(r#"
2232name "ex"
2233bin "ex"
2234cmd "run" help="Run it" {
2235    example "ex run --fast" header="Speed" help="When you are in a hurry"
2236    example "ex run"
2237}
2238"#);
2239        let run = out
2240            .lines()
2241            .find(|l| l.contains("Examples: []argv.Example"))
2242            .expect("the command's examples are emitted");
2243        assert!(
2244            run.contains(
2245                r#"{Header: "Speed", Code: "ex run --fast", Help: "When you are in a hurry"}"#
2246            ),
2247            "all three fields are emitted: {run}"
2248        );
2249        // And a bare example says only what it has, rather than an empty header.
2250        assert!(
2251            run.contains(r#"{Code: "ex run"}"#),
2252            "an example with no header emits no header: {run}"
2253        );
2254    }
2255
2256    /// A section's prose reaches the tables.
2257    ///
2258    /// It is lowered from a spec the same way, so the two producers only agree
2259    /// if the emitter writes it too — and mise declares no `heading`, so the
2260    /// producer comparison over its spec cannot see this either.
2261    #[test]
2262    fn heading_prose_reaches_the_tables() {
2263        let out = go(r#"
2264name "ex"
2265bin "ex"
2266cmd "run" help="Run it" {
2267    heading "Filters" help="Filters accumulate from left to right."
2268    flag "--allow <NAME>" help="Allow it" help_heading="Filters"
2269}
2270"#);
2271        let run = out
2272            .lines()
2273            .find(|l| l.contains("Headings: []argv.Heading"))
2274            .expect("the command's headings are emitted");
2275        assert!(
2276            run.contains(r#"{Title: "Filters", Help: "Filters accumulate from left to right."}"#),
2277            "both fields are emitted: {run}"
2278        );
2279    }
2280
2281    /// `about_long` alone leaves the short page's About unset, because usage-lib
2282    /// prints nothing there: the long text belongs to the long page.
2283    #[test]
2284    fn a_long_about_alone_does_not_become_the_short_one() {
2285        let out = go(r#"
2286name "ex"
2287bin "ex"
2288about_long "Long only."
2289"#);
2290        let meta = out
2291            .lines()
2292            .find(|l| l.contains("var HelpMeta"))
2293            .expect("a root header is emitted");
2294        assert!(
2295            !meta.contains(", About: ") && meta.contains(r#"LongAbout: "Long only.""#),
2296            "only the long one is set: {meta}"
2297        );
2298    }
2299
2300    #[test]
2301    fn a_default_subcommand_ignores_a_deeper_command_of_the_same_name() {
2302        let out = go(r#"
2303name "ex"
2304bin "ex"
2305default_subcommand "run"
2306cmd "oci" {
2307    cmd "run" {}
2308}
2309cmd "run" {
2310    arg "[args]..." var=#true
2311}
2312"#);
2313        assert!(
2314            out.contains("DefaultSubcommand: cmdRun,"),
2315            "should point at the root's own `run`, got:\n{out}"
2316        );
2317    }
2318
2319    #[test]
2320    fn command_builtin_controls_reach_generated_go_tables() {
2321        let out = go(r#"
2322name "ex"
2323bin "ex"
2324disable_help_flag #true
2325disable_help_subcommand #true
2326disable_version_flag #true
2327"#);
2328        let root = out
2329            .split("var Root = &argv.Command{")
2330            .nth(1)
2331            .expect("the root command should be emitted")
2332            .split("}\n")
2333            .next()
2334            .unwrap();
2335        assert!(root.contains("DisableHelpFlag:"), "{root}");
2336        assert!(root.contains("DisableHelpSubcommand:"), "{root}");
2337        assert!(root.contains("DisableVersionFlag:"), "{root}");
2338    }
2339
2340    /// A command's own name outranks another command's alias, so which command the
2341    /// emitted `DefaultSubcommand` points at does not depend on declaration order.
2342    #[test]
2343    fn a_default_subcommand_prefers_a_name_to_another_commands_alias() {
2344        let ordered = |first: &str, second: &str| {
2345            go(&format!(
2346                r#"
2347name "ex"
2348bin "ex"
2349default_subcommand "run"
2350{first}
2351{second}
2352"#
2353            ))
2354        };
2355        let alpha = "cmd \"alpha\" {\n    alias \"run\"\n}";
2356        let run = "cmd \"run\" {\n    arg \"[args]...\" var=#true\n}";
2357        for out in [ordered(alpha, run), ordered(run, alpha)] {
2358            assert!(
2359                out.contains("DefaultSubcommand: cmdRun,"),
2360                "should point at the command named `run`, got:\n{out}"
2361            );
2362        }
2363    }
2364
2365    #[test]
2366    fn an_external_subcommand_is_emitted_on_the_command_that_declares_it() {
2367        let out = go(r#"
2368name "ex"
2369bin "ex"
2370external_subcommand #true
2371cmd "install"
2372cmd "exec" external_subcommand=#true
2373"#);
2374        let block = |var: &str| {
2375            let start = out
2376                .find(&format!("var {var} ="))
2377                .unwrap_or_else(|| panic!("{var} should be emitted, got:\n{out}"));
2378            let rest = &out[start..];
2379            let end = rest[1..]
2380                .find("\nvar ")
2381                .map(|i| i + 1)
2382                .unwrap_or(rest.len());
2383            &rest[..end]
2384        };
2385        assert!(
2386            block("Root").contains("ExternalSubcommand: true"),
2387            "the root should forward unmatched words:\n{}",
2388            block("Root")
2389        );
2390        assert!(
2391            block("cmdExec").contains("ExternalSubcommand: true"),
2392            "a nested command can forward too:\n{}",
2393            block("cmdExec")
2394        );
2395        assert!(
2396            !block("cmdInstall").contains("ExternalSubcommand"),
2397            "a command that does not declare it should not carry it:\n{}",
2398            block("cmdInstall")
2399        );
2400    }
2401
2402    #[test]
2403    fn arg_required_else_help_reaches_the_table_and_typed_front_door() {
2404        let out = go(r#"
2405name "ex"
2406bin "ex"
2407cmd "run" arg_required_else_help=#true {
2408    flag "--all"
2409}
2410"#);
2411        assert!(
2412            out.contains("ArgRequiredElseHelp: true"),
2413            "the command table should carry the policy:\n{out}"
2414        );
2415        assert!(
2416            out.contains("p.Command().ArgRequiredElseHelp && p.CommandStart() == len(args)"),
2417            "the typed parser should enforce it before fallbacks:\n{out}"
2418        );
2419    }
2420
2421    #[test]
2422    fn subcommand_negates_requirements_reaches_generated_go() {
2423        let out = go(
2424            "name \"ex\"\nbin \"ex\"\nsubcommand_negates_reqs #true\nflag \"--config\" required=#true\ncmd \"run\"\n",
2425        );
2426        assert!(out.contains("SubcommandNegatesReqs: true"), "{out}");
2427        assert!(
2428            out.contains("checkRequirements := i == len(chain)-1 || !cmd.SubcommandNegatesReqs"),
2429            "{out}"
2430        );
2431        assert!(
2432            out.contains("CheckRelationshipsWithValuesAndRequirements"),
2433            "{out}"
2434        );
2435    }
2436
2437    #[test]
2438    fn argument_subcommand_conflicts_reach_generated_go() {
2439        let out = go(
2440            "name \"ex\"\nbin \"ex\"\nargs_conflicts_with_subcommands #true\nflag \"--verbose\"\ncmd \"run\"\n",
2441        );
2442        assert!(out.contains("ArgsConflictWithSubcommands: true"), "{out}");
2443    }
2444
2445    #[test]
2446    fn allow_missing_positional_reaches_generated_go() {
2447        let out = go(
2448            "name \"ex\"\nbin \"ex\"\nallow_missing_positional #true\narg \"[optional]\"\narg \"<required>\"\n",
2449        );
2450        assert!(out.contains("AllowMissingPositional: true"), "{out}");
2451        assert!(out.contains("Name: \"optional\""), "{out}");
2452        assert!(out.contains("Name: \"required\", Required: true"), "{out}");
2453    }
2454
2455    #[test]
2456    fn optional_flag_values_reach_generated_go() {
2457        let out = go("name \"ex\"\nbin \"ex\"\nflag \"--color [WHEN]\" value_optional=#true\n");
2458        assert!(
2459            out.contains("TakesValue: true, ValueOptional: true"),
2460            "{out}"
2461        );
2462    }
2463
2464    #[test]
2465    fn explicit_boolean_values_reach_generated_go() {
2466        let out = go(
2467            "name \"ex\"\nbin \"ex\"\nflag \"--color\" negate=\"--no-color\" bool_value=#true\n",
2468        );
2469        assert!(out.contains("BoolValue: true"), "{out}");
2470        assert!(out.contains("if ev.Flag.BoolValue"), "{out}");
2471        assert!(
2472            out.contains("given[ev.Flag.Key] = []string{ev.Value}"),
2473            "{out}"
2474        );
2475        assert!(
2476            out.contains("(ev.Value == \"true\") != ev.Negated"),
2477            "{out}"
2478        );
2479    }
2480
2481    #[test]
2482    fn flag_actions_reach_generated_go() {
2483        let out = go(
2484            "name \"ex\"\nbin \"ex\"\nflag \"--help-all\" action=\"help_all\"\nflag \"--version\" action=\"version\"\n",
2485        );
2486        assert!(out.contains("Action: argv.ActionHelpAll"), "{out}");
2487        assert!(out.contains("Action: argv.ActionVersion"), "{out}");
2488    }
2489
2490    #[test]
2491    fn granular_help_hides_reach_generated_go() {
2492        let out = go(
2493            "name \"ex\"\nbin \"ex\"\nflag \"--mode <mode>\" hide_default_value=#true hide_env=#true hide_env_values=#true hide_possible_values=#true hide_short_help=#true hide_long_help=#true\n",
2494        );
2495        for field in [
2496            "HideDefaultValue: true",
2497            "HideEnv: true",
2498            "HideEnvValues: true",
2499            "HidePossibleValues: true",
2500            "HideShortHelp: true",
2501            "HideLongHelp: true",
2502        ] {
2503            assert!(out.contains(field), "missing {field}:\n{out}");
2504        }
2505    }
2506
2507    #[test]
2508    fn strict_duplicate_policy_reaches_metadata() {
2509        let permissive = go("name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\n");
2510        assert!(!permissive.contains("RejectDuplicate"), "{permissive}");
2511
2512        let strict =
2513            go("name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\n");
2514        assert!(strict.contains("RejectDuplicate: true"), "{strict}");
2515    }
2516
2517    #[test]
2518    fn clause_args_and_later_entries_reach_go_cold_tables() {
2519        let out = go(r#"
2520name "ex"
2521bin "ex"
2522cmd "run" {
2523    flag "--needs-task" requires="task"
2524    clause "items" separator=":::" {
2525        flag "--postinstall <command>"
2526        arg "<task>" help="Task to run"
2527    }
2528}
2529cmd "later" {
2530    flag "--mode <mode>" help="Later flag"
2531}
2532"#);
2533        let meta = out
2534            .split_once("var Meta = argv.Metadata{")
2535            .and_then(|(_, rest)| rest.split_once("var HelpText = argv.HelpTable{"))
2536            .map(|(meta, _)| meta)
2537            .expect("generated metadata and help tables");
2538        let help = out
2539            .split_once("var HelpText = argv.HelpTable{")
2540            .and_then(|(_, rest)| rest.split_once("var HelpMeta = argv.HelpSpec{"))
2541            .map(|(help, _)| help)
2542            .expect("generated help table and metadata");
2543
2544        assert!(
2545            meta.contains("Key: ArgRunItemsTask, Name: \"task\""),
2546            "{out}"
2547        );
2548        assert!(
2549            meta.lines().any(|line| {
2550                line.contains("Key: FlagRunItemsPostinstall")
2551                    && line.contains("RejectDuplicate: true")
2552            }),
2553            "{out}"
2554        );
2555        assert!(
2556            meta.lines().any(|line| {
2557                line.contains("Key: FlagRunNeedsTask")
2558                    && line.contains("Requires: []uint64{ArgRunItemsTask}")
2559            }),
2560            "{out}"
2561        );
2562        assert!(meta.contains("Key: FlagLaterMode, Name: \"mode\""), "{out}");
2563        assert!(
2564            help.contains("Key: ArgRunItemsTask, Demanded: true"),
2565            "{out}"
2566        );
2567        assert!(
2568            help.lines().any(|line| {
2569                line.contains("Key: FlagLaterMode") && line.contains("Short: \"Later flag\"")
2570            }),
2571            "{out}"
2572        );
2573        for generated in [
2574            "type RunCmdItemsClause struct {",
2575            "Postinstall string",
2576            "Flags: []*argv.Flag{",
2577            "clauseFlag := map[uint64]bool{FlagRunItemsPostinstall: true}",
2578            "case argv.KindClauseSeparator:",
2579            "for instanceIndex, instance := range clauseInstances[CmdRun] {",
2580            "if err := argv.Check(Meta.Lookup(key), values, instanceOccurrences[key]); err != nil {",
2581            "clauseSources := map[uint64]argv.Source{FlagRunItemsPostinstall: argv.Unset, ArgRunItemsTask: argv.Unset}",
2582            "cmdRunV.Items = append(cmdRunV.Items, item)",
2583        ] {
2584            assert!(out.contains(generated), "missing {generated:?}:\n{out}");
2585        }
2586        assert!(
2587            out.lines().any(|line| {
2588                line.contains("Items")
2589                    && line.contains("[]RunCmdItemsClause")
2590                    && line.contains("// clause items")
2591            }),
2592            "{out}"
2593        );
2594    }
2595
2596    #[test]
2597    fn strict_negated_flags_track_each_spelling_separately() {
2598        let out = go(
2599            "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n",
2600        );
2601        assert!(out.contains("polaritySeen := map[uint64]uint8{}"), "{out}");
2602        assert!(
2603            out.contains("polaritySeen[ev.Flag.Key]&polarity != 0"),
2604            "{out}"
2605        );
2606        assert!(out.contains("if duplicateSeen[key]"), "{out}");
2607    }
2608
2609    #[test]
2610    fn strict_global_duplicate_tracking_resets_at_subcommands() {
2611        let out = go(
2612            "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\" global=#true\ncmd \"run\" {\n  args_override_self #false\n}\n",
2613        );
2614        assert!(
2615            out.contains("levelSeen = map[uint64]int{}"),
2616            "a subcommand should start a new duplicate scope:\n{out}"
2617        );
2618        assert!(out.contains("strictSeen[ev.Flag.Key] = true"), "{out}");
2619        assert!(out.contains("if strictSeen[key]"), "{out}");
2620    }
2621
2622    /// A subcommand actually named `root` wants the constant the root has.
2623    #[test]
2624    fn a_subcommand_named_root_does_not_collide_with_the_root() {
2625        let out = go(r#"
2626name "ex"
2627bin "ex"
2628cmd "root" {
2629    flag "--wat"
2630}
2631"#);
2632        // By first token, because the const block is column-aligned: matching
2633        // "CmdRoot uint64" would find nothing and pass for the wrong reason.
2634        let declared = |name: &str| {
2635            out.lines()
2636                .filter(|l| l.split_whitespace().next() == Some(name))
2637                .count()
2638        };
2639        assert_eq!(declared("CmdRoot"), 1, "CmdRoot declared twice:\n{out}");
2640        assert_eq!(declared("CmdRoot2"), 1, "no distinct key for it:\n{out}");
2641        assert_declares_each_constant_once(&out);
2642    }
2643
2644    /// The two `var_max` are different questions, and the corpus pins them apart:
2645    /// on a flag's *argument* it bounds one occurrence's values and belongs in the
2646    /// binding table, while on the flag it counts occurrences and is checked after
2647    /// the parse.
2648    #[test]
2649    fn only_the_per_occurrence_bound_reaches_the_table() {
2650        let out = go(r#"
2651name "ex"
2652bin "ex"
2653flag "--include <pattern>..." {
2654    arg "<pattern>..." var=#true var_min=2 var_max=2
2655}
2656flag "--tag <t>" var=#true var_max=1
2657"#);
2658        assert!(
2659            out.contains("Name: \"include\", Longs: []string{\"include\"}, TakesValue: true, Variadic: true, VarMax: 2"),
2660            "{out}"
2661        );
2662        assert!(
2663            out.contains("Name: \"include\", Flag: true") && out.contains("VarMin: 2"),
2664            "the nested value minimum must reach post-binding metadata:\n{out}"
2665        );
2666        let tag = out.lines().find(|l| l.contains("\"tag\"")).unwrap();
2667        assert!(!tag.contains("VarMax"), "occurrence bound leaked: {tag}");
2668    }
2669
2670    #[test]
2671    fn exact_arity_with_one_label_reaches_go_help() {
2672        let out = go(r#"
2673name "ex"
2674bin "ex"
2675flag "--pair <ITEM>..." {
2676    arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2677        value_names "ITEM"
2678        }
2679    }
2680arg "<ITEM>..." var=#true var_min=2 var_max=2 {
2681    value_names "ITEM"
2682}
2683"#);
2684        assert_eq!(out.matches("ValueArity: 2").count(), 2, "{out}");
2685        assert_eq!(
2686            out.matches("ValueNames: []string{\"ITEM\"}").count(),
2687            2,
2688            "{out}"
2689        );
2690    }
2691
2692    #[test]
2693    fn scoped_flag_binding_preserves_counts_and_boolean_polarity() {
2694        let out = go(r#"
2695name "ex"
2696bin "ex"
2697clause "tools" {
2698    flag "--required" required=#true
2699    flag "-v --verbose" count=#true
2700    flag "--color" negate="--no-color" bool_value=#true
2701    arg "<tool>"
2702}
2703"#);
2704
2705        for generated in [
2706            "if clauseFlag[f.Key] {",
2707            "instanceOccurrences := clauseInstanceOccurrences[CmdRoot][instanceIndex]",
2708            "argv.Check(Meta.Lookup(key), values, instanceOccurrences[key])",
2709            "item.Required = !instanceNegated[FlagToolsRequired]",
2710            "item.Verbose = instanceOccurrences[FlagToolsVerbose]",
2711            "item.Color = (values[len(values)-1] == \"true\") != instanceNegated[FlagToolsColor]",
2712            "item.Color = !instanceNegated[FlagToolsColor]",
2713        ] {
2714            assert!(out.contains(generated), "missing {generated:?}:\n{out}");
2715        }
2716    }
2717
2718    #[test]
2719    fn a_bare_optional_scoped_flag_does_not_index_an_empty_value() {
2720        let out = go(r#"
2721name "ex"
2722bin "ex"
2723clause "tools" {
2724    flag "--label [LABEL]" value_optional=#true
2725    arg "<tool>"
2726}
2727"#);
2728
2729        assert!(
2730            out.contains(
2731                "if values := instance[FlagToolsLabel]; len(values) > 0 {\n\t\t\t\titem.Label = values[len(values)-1]"
2732            ),
2733            "{out}"
2734        );
2735        assert!(
2736            !out.contains("if values, ok := instance[FlagToolsLabel]; ok"),
2737            "{out}"
2738        );
2739    }
2740
2741    #[test]
2742    fn allow_hyphen_values_reaches_the_table() {
2743        let out = go(r#"
2744name "ex"
2745bin "ex"
2746flag "--args <ARGS>" allow_hyphen_values=#true
2747"#);
2748        assert!(
2749            out.contains("Name: \"args\", Longs: []string{\"args\"}, TakesValue: true, AllowHyphenValues: true"),
2750            "{out}"
2751        );
2752    }
2753
2754    #[test]
2755    fn require_equals_reaches_the_table() {
2756        let out = go(r#"
2757name "ex"
2758bin "ex"
2759flag "--inspect <PORT>" require_equals=#true
2760"#);
2761        assert!(
2762            out.contains("Name: \"inspect\", Longs: []string{\"inspect\"}, TakesValue: true, RequireEquals: true"),
2763            "{out}"
2764        );
2765    }
2766
2767    #[test]
2768    fn default_missing_reaches_the_table() {
2769        let out = go(r#"
2770name "ex"
2771bin "ex"
2772flag "--color <WHEN>" default_missing="always"
2773"#);
2774        assert!(
2775            out.contains(
2776                "Name: \"color\", Longs: []string{\"color\"}, TakesValue: true, DefaultMissing: \"always\""
2777            ),
2778            "{out}"
2779        );
2780    }
2781
2782    /// A relationship names a flag by any spelling that reaches it, and from
2783    /// anywhere the flag is in scope.
2784    ///
2785    /// Both halves were silently resolving to nothing, which is worse than an
2786    /// error: the rule simply never fired, while usage-lib enforced it.
2787    #[test]
2788    fn a_relationship_resolves_through_scope_and_negation() {
2789        let out = go(r#"
2790name "ex"
2791bin "ex"
2792flag "--quiet" global=#true
2793flag "--color" negate="--no-color"
2794flag "--plain" conflicts="--no-color"
2795cmd "run" {
2796    flag "--loud" conflicts="--quiet"
2797    flag "--solo" conflicts="--plain"
2798}
2799"#);
2800        // A negation names the flag it belongs to.
2801        assert!(out.contains("Conflicts: []uint64{FlagColor}"), "{out}");
2802        // An inherited global is in scope from below.
2803        assert!(out.contains("Conflicts: []uint64{FlagQuiet}"), "{out}");
2804        // `--plain` is not global, so from a subcommand it names nothing — the
2805        // other half, and the one a looser search would get wrong.
2806        assert!(
2807            !entry_of(&out, "solo").contains("Conflicts"),
2808            "a non-global should not resolve from below:\n{out}"
2809        );
2810    }
2811
2812    #[test]
2813    fn positional_conflicts_reach_go_metadata_in_both_directions() {
2814        let out = go(r#"
2815name "ex"
2816bin "ex"
2817flag "--from-file <file>" conflicts="value"
2818arg "[value]" conflicts="--from-file"
2819"#);
2820
2821        assert!(
2822            entry_of(&out, "from-file").contains("Conflicts: []uint64{ArgValue}"),
2823            "{out}"
2824        );
2825        assert!(
2826            out.lines().any(|line| {
2827                line.contains("{Key: ArgValue, Name: \"value\"")
2828                    && line.contains("Conflicts: []uint64{FlagFromFile}")
2829            }),
2830            "{out}"
2831        );
2832    }
2833
2834    #[test]
2835    fn a_value_conditional_requirement_reaches_go_metadata() {
2836        let out = go(r#"
2837name "ex"
2838bin "ex"
2839flag "--format <format>" {
2840    requires_if "json" "--schema"
2841}
2842flag "--schema <file>"
2843"#);
2844        assert!(
2845            entry_of(&out, "format").contains(
2846                "RequiresIf: []argv.ValueRequirement{{Value: \"json\", Key: FlagSchema}}"
2847            ),
2848            "{out}"
2849        );
2850        assert!(
2851            out.contains("argv.CheckRelationshipsWithValues"),
2852            "the emitted parser must enforce the metadata:\n{out}"
2853        );
2854    }
2855
2856    #[test]
2857    fn required_if_eq_makes_generated_go_supply_values() {
2858        let out = go(r#"
2859name "ex"
2860bin "ex"
2861flag "--token <token>" {
2862    required_if_eq "--mode" "remote"
2863}
2864flag "--mode <mode>"
2865"#);
2866        assert!(
2867            entry_of(&out, "token").contains(
2868                "RequiredIfEq: []argv.ValueCondition{{Key: FlagMode, Value: \"remote\"}}"
2869            ),
2870            "{out}"
2871        );
2872        assert!(out.contains("resolved := map[uint64][]string{}"), "{out}");
2873        assert!(out.contains("argv.CheckRelationshipsWithValues"), "{out}");
2874    }
2875
2876    #[test]
2877    fn boolean_sources_are_normalized_for_value_relationships() {
2878        let out = go(r#"
2879name "ex"
2880bin "ex"
2881flag "--token <token>" {
2882    required_if_eq "--mode" "true"
2883}
2884flag "--mode" negate="--no-mode" bool_value=#true
2885"#);
2886        assert!(
2887            entry_of(&out, "mode").contains("RequiresIfBoolean: true"),
2888            "{out}"
2889        );
2890    }
2891
2892    #[test]
2893    fn a_conditional_default_reaches_go_metadata() {
2894        let out = go(r#"
2895name "ex"
2896bin "ex"
2897flag "--bin-names" {
2898    default_if "--json" "true"
2899    default_if "--output" "json" "pretty"
2900}
2901flag "--json"
2902flag "--output <fmt>"
2903"#);
2904        assert!(
2905            entry_of(&out, "bin-names")
2906                .contains("DefaultIf: []argv.DefaultIf{{Key: FlagJson, Value: \"true\"}"),
2907            "{out}"
2908        );
2909        assert!(
2910            entry_of(&out, "bin-names").contains("When: \"json\""),
2911            "{out}"
2912        );
2913        assert!(
2914            out.contains("argv.ApplyDefaultIf"),
2915            "the emitted parser must apply the metadata:\n{out}"
2916        );
2917        assert!(
2918            out.contains("negated[ev.Flag.Key] = ev.Negated"),
2919            "Equals default_if needs the negate form:\n{out}"
2920        );
2921    }
2922
2923    /// The form is part of the name, and usage-lib resolves neither of the
2924    /// mismatched ones — so resolving them would have a generated CLI enforcing a
2925    /// rule the reference does not.
2926    #[test]
2927    fn a_relationship_needs_the_right_form() {
2928        let out = go(r#"
2929name "ex"
2930bin "ex"
2931flag "-q --quiet"
2932flag "--color"
2933flag "--a" conflicts="--q"
2934flag "--b" conflicts="-color"
2935flag "--c" conflicts="-q"
2936flag "--d" conflicts="--color"
2937"#);
2938        // `--q` is not a long form of anything, and `-color` is not a short.
2939        assert!(!entry_of(&out, "a").contains("Conflicts"), "{out}");
2940        assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
2941        // The forms the flags actually have.
2942        assert!(
2943            entry_of(&out, "c").contains("Conflicts: []uint64{FlagQuiet}"),
2944            "{out}"
2945        );
2946        assert!(
2947            entry_of(&out, "d").contains("Conflicts: []uint64{FlagColor}"),
2948            "{out}"
2949        );
2950    }
2951
2952    /// The table has to agree with the binder it feeds.
2953    ///
2954    /// The parser tries every long form before any negation, so with `--a`
2955    /// declaring `negate="--zap"` and a separate `--zap`, typing `--zap` binds
2956    /// *zap*. A per-candidate search handed the relationship to `a`, which would
2957    /// have enforced the rule against a flag the command line never binds.
2958    #[test]
2959    fn an_ordinary_form_beats_another_flags_negation() {
2960        let out = go(r#"
2961name "ex"
2962bin "ex"
2963flag "--a" negate="--zap"
2964flag "--zap"
2965flag "--p" conflicts="--zap"
2966"#);
2967        assert!(
2968            entry_of(&out, "p").contains("Conflicts: []uint64{FlagZap}"),
2969            "should name the flag `--zap` binds, not the one negating to it:\n{out}"
2970        );
2971    }
2972
2973    /// A negation is named by the form it was written as, whatever the dashes.
2974    #[test]
2975    fn a_single_dash_negation_is_named_by_its_own_form() {
2976        let out = go(r#"
2977name "ex"
2978bin "ex"
2979flag "--tint" negate="-no-tint"
2980flag "--plain" conflicts="-no-tint"
2981flag "--other" conflicts="--no-tint"
2982"#);
2983        assert!(
2984            entry_of(&out, "plain").contains("Conflicts: []uint64{FlagTint}"),
2985            "the exact form should resolve:\n{out}"
2986        );
2987        // And the form it was not written as does not.
2988        assert!(
2989            !entry_of(&out, "other").contains("Conflicts"),
2990            "`--no-tint` is not how it was declared:\n{out}"
2991        );
2992    }
2993
2994    /// A negation is matched as the spec wrote it, dashes and all.
2995    #[test]
2996    fn a_negation_is_matched_as_written() {
2997        let out = go(r#"
2998name "ex"
2999bin "ex"
3000flag "--color" negate="--no-color"
3001flag "--tint" negate="-no-tint"
3002flag "--a" conflicts="--no-color"
3003flag "--b" conflicts="--no-tint"
3004"#);
3005        assert!(
3006            entry_of(&out, "a").contains("Conflicts: []uint64{FlagColor}"),
3007            "{out}"
3008        );
3009        // `--no-tint` is not the form `-no-tint`, so it names nothing — as in
3010        // usage-lib, which does not resolve it either.
3011        assert!(!entry_of(&out, "b").contains("Conflicts"), "{out}");
3012    }
3013
3014    #[test]
3015    fn a_complete_inside_an_arg_is_its_complete_type() {
3016        let out = go(r#"
3017name "ex"
3018bin "ex"
3019complete "target" type="file"
3020arg "<target>" {
3021    complete type="dir"
3022}
3023flag "--into <where>" {
3024    complete type="dir"
3025}
3026"#);
3027        let target = out
3028            .lines()
3029            .find(|l| l.contains("Name: \"target\"") && l.contains("CompleteType"))
3030            .unwrap_or_default();
3031        assert!(target.contains("CompleteType: \"dir\""), "{out}");
3032        assert!(
3033            entry_of(&out, "into").contains("CompleteType: \"dir\""),
3034            "{out}"
3035        );
3036    }
3037
3038    #[test]
3039    fn strings_are_escaped_to_go_rules() {
3040        assert_eq!(go_string(r#"a"b\c"#), r#""a\"b\\c""#);
3041        assert_eq!(go_string("tab\there"), r#""tab\there""#);
3042        // Rust would spell this `\u{7f}`, which Go rejects.
3043        assert_eq!(go_string("\u{7f}"), r#""\x7f""#);
3044    }
3045}