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