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