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