Skip to main content

usage/go/
mod.rs

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