Skip to main content

usage/go/
mod.rs

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