Skip to main content

usage/go/
mod.rs

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