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 typed conversion helpers for bound Go argv values and actionable invalid-value errors.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (9): Last reviewed commit: "fix(go): keep the value where Go complai..." | 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
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fc3f88a. Configure here.
Binding collects text on purpose: the grammar decides which token becomes which flag or argument, 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. Separate functions rather than one generic `Convert`, because 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 carries the text that would not convert *and* the type it was going to. "Invalid value" alone makes a user guess which of their words was wrong, and the whole reason the parser keeps the original bytes around is so that something downstream can show them back. `Each` exists so that 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. One deliberate inconsistency, documented where both live: `Bool` takes Go's spellings — `1`, `t`, `T`, `true`, `TRUE`, `True` — while `EnvTruth` takes only four. They answer different questions. `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 because a spec's meaning should not change with the language reading it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The converters trimmed surrounding whitespace, which is leniency neither the
grammar nor the sibling has. Checked rather than argued:
" 8 ".parse::<i64>() -> false
" 1.5 ".parse::<f64>() -> false
" true ".parse::<bool>() -> false
The parser goes out of its way to hand over the bytes the operating system gave
it — it does not re-split a token, and it keeps a value that is not valid UTF-8
rather than mangling it — so a converter quietly tidying them would mean a quoted
argument means one thing in Go and another in Rust from the same spec. `--jobs
" 8 "` is now refused on both sides.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`CodeInvalidValue` embedded the user's text raw while the two messages beside it — unknown flag and unexpected argument — already went through `safe`. It is the likeliest of the three to carry something strange, since it exists precisely because the text was not what the target type expected, so a crafted value could push escape sequences to stderr through `Render`. Escaped now, and tested the same way as the others: a value carrying an escape, a carriage return and a newline comes out legible with none of them intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same rule the tokens follow, for the field this commit's parent added: a value the converters refused is text off the command line, and `Error()` handed it to whatever prints or logs the error without escaping it. Of the three fields quoted back, this is the likeliest to hold something strange — it is here precisely because the text was not what the type expected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two places where Go's standard library and Rust's disagree, and the same spec would have accepted different values depending on which side compiled it. `+8` is a non-negative whole number, and `"+8".parse::<u64>()` says 8. `strconv.ParseUint` refuses a sign outright, so the Go binding rejected it — and said it was not a non-negative whole number, about a value that is plainly both. `strconv.ParseFloat` takes digit separators and hexadecimal floats; `f64` takes neither, so `1_5` and `0x1.8p0` converted in Go and failed in Rust. Everything else agrees — `inf`, `NaN`, a bare `.5` — which is why this is a check on two characters rather than a grammar of its own. Both were confirmed by running the two standard libraries side by side, as the trimming rule above was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two more edges where the same spec would have converted differently depending on which language compiled it, both confirmed by running the two standard libraries side by side. A number too large to hold is `inf` in Rust. Go returns the same ±Inf and calls it a range error, and treating every error as a refusal threw the value away. An underflow is zero on both sides, and Go does not complain about that one at all. A signed NaN — `+nan`, `-NaN` — parses in Rust and not in Go. The sign means nothing either way. The spellings that already agreed are pinned in the same test, so the special cases cannot quietly grow into a grammar of their own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`argv.Walk` reads the words before the cursor and reports what it is standing in; `argv.Candidates` says what could go there — subcommands and their aliases, the flags in scope, and the values a `choices` list allows. Stacked on #978. ## Asking the parser rather than re-deriving it That is the whole design. A completion advertising a flag the parser would refuse is worse than no completion, so the scope a candidate comes from is the scope a token would be resolved in: - a global is offered inside a subcommand - a subcommand redeclaring an inherited name offers **its own** - a hidden flag binds without being advertised - past a `--`, nothing is offered — there is no flag of this CLI to type there ## Errors are not failures here A line being completed is unfinished by definition, so a parse error means "the grammar runs out here" — which is the position being asked about. `missing_flag_value` says the cursor is standing in a flag's value, and that flag's choices take the position entirely. `help` says the cursor is naming a command to *read about*, where nothing else belongs. ## Not included Turning candidates into the text each shell expects, and running the `complete` scripts a spec can declare. The first is per-shell formatting, the second runs subprocesses, and neither belongs in a package whose claim is that a parse does not allocate. Both are noted in the README. ## One note from writing the tests The first fixture numbered its keys 10–14 in an eight-entry table. Both cold tables are indexed by key, so every lookup missed and the assertions about hiding and choices passed **for the wrong reason** — they only started failing once the keys were dense. The invariant is documented on `Metadata`; worth knowing that breaking it fails quietly rather than loudly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > New completion surface area mirrors subtle parser/help scope rules; behavior is heavily tested but mistakes would mislead users without breaking parses. > > **Overview** > Adds **parser-driven tab completion** in `argv`: **`Walk`** parses words before the cursor into a **`Position`** (command chain, whether flags/subcommands still apply, awaiting flag value, variadic collection, pending arg, `help` topic mode), and **`Candidates`** returns subcommands/aliases, in-scope flag spellings, and **`choices`** values filtered by a partial prefix. > > Completion reuses the same scope rules as help via **`flagsInScope`** (per-spelling shadowing of inherited globals, negation vs long-form precedence) and refactors **`everyFormInScope`** / **`negationSurvives`** in `scope.go` so help and completions stay aligned. **`Parser.SubcommandsPossible`** exposes when descent into subcommands has stopped. > > README documents the completion API and notes that per-shell formatting and `complete` scripts are still out of scope. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 84b303e. 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>
#990) The front door. An author calls `Parse` and gets a value with fields rather than a loop over events — binding, the post-binding rules and the three tables are unchanged underneath. ```go cli, err := mycli.Parse(os.Args[1:]) if cli.Run != nil { fmt.Println(cli.Run.Task, cli.Run.Args) } ``` Stacked on #989. ## Fields are strings and bools, on purpose That is what a usage spec knows: it says what a value is *called* and never what type it is. Turning `"8"` into an `int` stays the caller's business — the conversions in #978 exist for exactly that, and inferring a type from an argument's name would be guessing. ## Two things mise found that a small fixture could not **Field names collide within a struct.** A command can declare a `--shell` flag beside a `shell` subcommand, and mise does it with `shell`, `version`, `command`, `env` and `tool`. The *kind* disambiguates — `Shell` and `ShellCmd` — because that says which one it is where `Shell2` would say only that there were two. The assignment is worked out once and shared by the declarations and by `Parse`, so the two cannot disagree about where a value goes. **A subcommand's defaults were dropped.** The fallback assignment started as a function taking the root struct, which cannot reach a subcommand's — that lives in a local of `Parse`. mise's `bootstrap packages import --manager` defaults to `brew` and came back empty. It is inline now, where the variables are; only the keys of commands the words selected are in scope, so the variable is never nil when its key is. Both are tested against mise's real command lines, including the `[ARGS]… [-- ARGS_LAST]…` split through the generated structs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Changes are documentation, a thin env helper, and shadow integration tests; generated `Parse` behavior is exercised on real mise argv but does not alter the core parser’s hot path. > > **Overview** > **Go CLI authors are steered toward generated `Parse` instead of hand-rolling `argv.New` event loops.** `go/README.md` now shows `mycli.Parse(os.Args[1:])`, nested command structs with nil pointers for unselected branches, and notes that binding, post-rules, env/default fill, and validation all happen inside `Parse` with `string` / `bool` / `[]string` fields. > > **`argv.LookupEnv` is added** as the process-environment hook that generated `Parse` passes into `Fill`, keeping tests injectable while real CLIs read `os.LookupEnv`. > > **The mise shadow package gains integration tests** for the generated front door: struct fill (`use -g node@20`), `--` arg splitting on `tasks run`, subcommand defaults (`bootstrap packages import` → `brew`), and choice validation (`--log-level chatty` → `invalid_choice`). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2fedce2. 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>

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,DurationandEachare where the asking happens.Stacked on #977.
Why separate functions rather than one generic
ConvertThe set of types a CLI wants is small and closed, and each has its own idea of what it accepts:
1h30mis a duration and not a number,yesis neither. Generated code calls the one matching its field.Every failure names the word and the type
"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.
Eachexists 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
Booltakes Go's spellings (1,t,T,true,TRUE,True);EnvTruthtakes only four. They answer different questions, and both are documented where they live:Boolconverts a value somebody typed.EnvTruthdecides 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
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 asCodeInvalidValuecarrying the offending text and expected type.Float/uint edge cases are aligned with the Rust port (e.g.
+8for 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 throughsafe, and the README documents typed values while reframing the roadmap gap as a generated typed struct rather than missing converters.Reviewed by Cursor Bugbot for commit 668ad12. Bugbot is set up for automated code reviews on this repo. Configure here.