feat(go): render a failure as something a person can act on - #977
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Central YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThe PR adds actionable Go parse-error rendering and carries exact flag spellings through parser and post-binding metadata.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the exact invoked alias is preserved on both missing-value parser paths, and generated post-binding metadata distinguishes short-only flags from one-character long flags. Important Files Changed
Reviews (9): Last reviewed commit: "fix(go): advise the form that was typed,..." | Re-trigger Greptile |
Instruction countsNothing was compared, and so nothing was gated. No series appears on both sides: either the base has no measurements recorded, or the two were measured on different runner classes, which are deliberately not comparable — counts shift between machine types by more than a real regression does. New, nothing to compare against: Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run. Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes. Shadow comparisonParsing
|
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
|
Real, and worse than cosmetic: a long-only The guess is gone. Two notes from getting there, both about tests rather than code:
This comment was generated by Claude Code. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 64779f4. Configure here.
`argv.Render` turns a binding failure into what a CLI prints to stderr: what went
wrong, the usage line for the command the user was actually in, and what to try
next.
error: unknown flag `--wat`
Usage: ex run [-f --force]
For more information, try `--help`.
This is the one part of the module with **no reference to match**, and saying so
is the point. usage-lib prints a one-line message inside miette's frame;
usage-argv renders through miette as well, underlining the offending token in the
command line. Neither travels — miette is a Rust library, and a Go CLI drawing
the same ASCII art would be imitating a diagnostic format rather than sharing
one. Every other renderer here is measured against usage-lib byte for byte, and
this one cannot be, so it is judged on a different standard: does the message say
what went wrong, where, and what to do about it.
Tested that way too. Every code renders something specific — a failure falling
through to "could not be parsed" tells a user nothing, so the test is that none
of them does — and the usage line names the command in scope rather than the
program, which is the part that makes the message actionable on a nested command.
`missing_flag_value` says the likeliest cause out loud, because the rule behind
it is not guessable: a flag-like token after a flag is refused as its value, and
the attached form is how to force it.
Help and version render nothing. They arrive as errors because a parse that stops
to print a page has produced no value, but a caller that renders one of them as a
failure has made two mistakes at once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…reach the terminal Two review findings on the error text, both about output a user cannot act on. A short-only flag was named `--f`. `primaryLong` fell back to the flag's *name* and everything prefixed `--`, so a flag reachable only as `-f` was reported as something nobody can enter — and the advice that comes with a missing value was worse than useless, telling someone with `-j` to write `--j=-1`, which is an unknown flag. Flags are spelled from their forms now: first long, else first short, and the attachment example follows the same spelling. The post-binding failures carry only a name, because they judge an entry rather than a token and no flag reaches them. A one-character name is taken to be a short, which is a guess and the right one nearly always: a flag's name is its long form wherever it has one. Printing `--f` for something typeable only as `-f` is the alternative. And a value quoted back into an error is escaped. What the user typed can contain control characters, and an escape sequence in an argument would otherwise reach the terminal through the message — where it can recolour the output, move the cursor, or forge lines that look like the program wrote them. Rendering a rejected value is not a reason to execute it. The escaping keeps the text legible rather than dropping it, so the user still sees what was refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The missing-value message advised attaching a dash-prefixed value and showed `--jobs=-1`. A negative number is the one dash-prefixed token the parser takes *detached*: `--jobs -1` binds, and `--jobs -x` is the failure being explained. So the example demonstrated the case exempt from the rule it was illustrating — advice that contradicts itself. `-x` now, which is a token that actually triggers it. Checked both ways round rather than reasoned about. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`spellName` read a one-character name as a short flag, reasoning that a name is a long form wherever there is one. A long-only `--a` is one character too, so it was rendered `-a` — a form that does not exist, and one that may belong to a *different* flag. Advice naming the wrong flag is worse than advice naming none. The guess is gone. `Meta` carries the spelling, worked out where the forms are visible — first long, else first short — by the table builder and the emitter alike, and the failures that judge an entry carry it through. Both sides of a conflict get their own, since they can be spelled differently. Where nothing is carried the bare name is printed rather than dressed as something the user cannot type. Two notes on the way here, both about tests rather than code. The emitter change silently missed the first time: the pattern was written at the wrong indentation and matched nothing, and the build still passed. The check that caught it was counting `Spelling` entries in mise's regenerated tables — 711 now, 0 then. And five emitter tests broke, all of them asserting a whole rendered row. A row grows a field every time the cold table learns something, so they now assert the part each test is about. That is what they meant all along. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Render escapes what the user typed before showing it back. `Error()` did not, and it is the string a caller gets from Go's own error interface — logged, wrapped, printed by code that never calls Render. An argument carrying an escape sequence reached the terminal through it, where it can recolour the output or forge a line that looks like the program's. Only the two fields that come from the command line. A flag's name and an argument's are the spec's, written by the author and held in the tables. `safe` returns its input untouched where there is nothing to escape, which is every real failure: a message that allocates once should not also allocate a copy of each token in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spelling was carried onto the metadata so that the rules judging an entry after binding could name it the way a user types it — they never see a `*Flag`. Three of the four codes did not pass it on, so an invalid choice, too few values or too many occurrences reported a short-only flag as `j`: not something anyone can type, which is the whole thing this was meant to stop printing. An argument keeps its bare name, because that is what it is called. The emitter's relationship tests are tightened while here. They asserted a name and a `Conflicts` slice as two substrings of the whole file, which passes when the relationship is attached to a different flag entirely — the regression those tests exist to catch. Both halves now come from the same row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A flag answers to several spellings, and the first is not always the one in front of the user. An inherited `--jobs --workers` whose `--jobs` a nearer command has taken still binds through `--workers` — and the advice for a missing value said `--jobs=-x`, which sets the other flag or fails again. The parser knows which form matched, so it carries it. For a long that costs nothing: with no attached value the token *is* the spelling, and a slice of it is free on a path that must not allocate. The short form is built in the failure branch rather than by the caller, for the same reason — the allocation test caught that on the first attempt. Where nothing was carried the flag's own first form is used, which at least exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Binding collects text on purpose — the grammar decides which token becomes which flag, not what it means — so `"8"` stays a string until something that knows the target type asks. `Int`, `Uint`, `Float`, `Bool`, `Duration` and `Each` are where the asking happens. Stacked on #977. ## Why separate functions rather than one generic `Convert` The set of types a CLI wants is small and closed, and each has its own idea of what it accepts: `1h30m` is a duration and not a number, `yes` is neither. Generated code calls the one matching its field. ## Every failure names the word and the type ``` error: `jobs` does not accept `lots` (expected a whole number) ``` "Invalid value" alone makes a user guess which of their words was wrong — and the whole reason the parser keeps the original bytes is so something downstream can show them back. `Each` exists so callers do not each write the same loop and get the early return wrong: it reports the **first** value that will not convert, because reporting the last sends the reader to the wrong word. That is a test rather than a comment. ## One deliberate inconsistency `Bool` takes Go's spellings (`1`, `t`, `T`, `true`, `TRUE`, `True`); `EnvTruth` takes only four. They answer different questions, and both are documented where they live: - `Bool` converts a value somebody **typed**. - `EnvTruth` decides whether an environment variable counts as **setting a value-less flag**, and there it matches usage-lib exactly — a spec's meaning should not change with the language reading it. A test asserts they stay different widths, so nobody "fixes" one into the other. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > New optional conversion helpers and error rendering paths; no changes to core binding grammar or security-sensitive flows beyond safer display of bad user input. > > **Overview** > Adds **post-binding converters** in `argv` (`Int`, `Uint`, `Float`, `Bool`, `Duration`, `Each`) so bound flag/arg strings become typed values, with failures surfaced as **`CodeInvalidValue`** carrying the offending text and expected type. > > **Float/uint edge cases** are aligned with the Rust port (e.g. `+8` for unsigned, rejecting Go-only float spellings, overflow/underflow and signed-NaN behavior) and **inputs are not trimmed**, matching cross-language spec behavior. **`Render`** / **`Error()`** quote rejected values through **`safe`**, and the README documents typed values while reframing the roadmap gap as a **generated typed struct** rather than missing converters. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 668ad12. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

argv.Renderturns a binding failure into what a CLI prints to stderr:Stacked on #975.
The one part with no reference to match
Every other renderer in this module is measured against usage-lib byte for byte. This one cannot be, and saying so plainly is the point: usage-lib prints a one-line message inside miette's frame, usage-argv renders through miette too with the offending token underlined, and neither travels — miette is a Rust library, and a Go CLI drawing the same ASCII art would be imitating a diagnostic format rather than sharing one.
So it is judged on a different standard — does the message say what went wrong, where, and what to try — and tested by asserting those three rather than by comparing bytes.
What the tests actually check
missing_flag_valuesays the likeliest cause out loud, because the rule behind it is not guessable: a flag-like token after a flag is refused as its value, and the attached form (--flag=-x) is how to force it.🤖 Generated with Claude Code
Note
Low Risk
User-facing CLI error text and terminal escaping only; parsing semantics unchanged aside from richer error metadata on missing flag values.
Overview
Adds
argv.Renderto format binding failures like clap: anerror:line, a Usage line for the command the user was in, and a pointer to--help. Help/version codes render nothing.Error quality and safety: Post-binding and parser failures now carry
Spelling/OtherSpellingonErrorandMeta(emitted from spec/Go gen) so messages quote flags as typed (--workersvs-j, conflict pairs) instead of guessing from names. Missing detached flag values record the token form that bound.safe()escapes control characters in user tokens for bothRenderandError().README documents the errors story;
render_test.goasserts content and spelling behavior rather than byte-matching Rust diagnostics.Reviewed by Cursor Bugbot for commit 2e3c1b5. Bugbot is set up for automated code reviews on this repo. Configure here.