[object Object]

← back to Cli Printing Press

fix(cli): coerce number-typed --limit param to int (#1370)

80f5bc6ca5d1c5aae2ca0880d19a5cd2396046ab · 2026-05-13 21:38:39 -0700 · Trevin Chow

LLM-derived specs in --docs mode emit `limit: number` for endpoints
whose `--limit` flag is semantically a count. The generator then
declared `var flagLimit float64` and bound it via `Float64Var`, which
breaks the `truncateJSONArray(data, flagLimit)` call site because the
helper expects `int`. OpenAPI specs declare `integer` and never hit this
path, so no golden output changes.

Mirrors the existing per-name override convention in `goTypeForParam`,
`cobraFlagFuncForParam`, and `defaultValForParam`: a new
`isFlagLimitParam` predicate coerces `number`-typed `limit` flags to
`int`/`IntVar` regardless of the spec's numeric type declaration. Keyed
on the same case-insensitive match `endpointNeedsClientLimit` uses, so
the override fires exactly where the truncate caller lives.

Closes #1082

Files touched

Diff

commit 80f5bc6ca5d1c5aae2ca0880d19a5cd2396046ab
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Wed May 13 21:38:39 2026 -0700

    fix(cli): coerce number-typed --limit param to int (#1370)
    
    LLM-derived specs in --docs mode emit `limit: number` for endpoints
    whose `--limit` flag is semantically a count. The generator then
    declared `var flagLimit float64` and bound it via `Float64Var`, which
    breaks the `truncateJSONArray(data, flagLimit)` call site because the
    helper expects `int`. OpenAPI specs declare `integer` and never hit this
    path, so no golden output changes.
    
    Mirrors the existing per-name override convention in `goTypeForParam`,
    `cobraFlagFuncForParam`, and `defaultValForParam`: a new
    `isFlagLimitParam` predicate coerces `number`-typed `limit` flags to
    `int`/`IntVar` regardless of the spec's numeric type declaration. Keyed
    on the same case-insensitive match `endpointNeedsClientLimit` uses, so
    the override fires exactly where the truncate caller lives.
    
    Closes #1082
---
 internal/generator/generator.go             | 39 +++++++++++++++++++++----
 internal/generator/limit_truncation_test.go | 45 +++++++++++++++++++++++++++++
 2 files changed, 79 insertions(+), 5 deletions(-)

diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index d91dd090..c21e4575 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -2890,6 +2890,19 @@ func isCursorParam(name string) bool {
 	return false
 }
 
+// isFlagLimitParam returns true when a parameter is the canonical
+// pagination/truncation `--limit` flag, which is always count-shaped
+// (integer) regardless of whether the spec declares it as `number`.
+// LLM-derived specs in `--docs` mode have produced `limit: number`,
+// which previously emitted `Float64Var`/`float64 flagLimit` and broke
+// `truncateJSONArray(data, flagLimit)` at compile time because the
+// helper expects an `int`. Keyed on the same case-insensitive match
+// `endpointNeedsClientLimit` uses, so the override fires exactly where
+// the truncate caller lives.
+func isFlagLimitParam(name string) bool {
+	return strings.EqualFold(strings.TrimSpace(name), "limit")
+}
+
 func primitiveKind(t string) string {
 	switch strings.ToLower(strings.TrimSpace(t)) {
 	case "string":
@@ -3033,9 +3046,11 @@ func cobraFlagFunc(t string) string {
 }
 
 // goTypeForParam returns the Go type for a parameter, overriding int→string
-// for ID-like parameters to avoid overflow and zero-value confusion, and
+// for ID-like parameters to avoid overflow and zero-value confusion,
 // numeric→string for pagination cursors so they survive scientific-notation
-// rendering of large Unix timestamps and millisecond cursors.
+// rendering of large Unix timestamps and millisecond cursors, and
+// float→int for the canonical `--limit` flag whose semantics are always
+// a count.
 func goTypeForParam(name, t string) string {
 	kind := primitiveKind(t)
 	if isIDParam(name) && kind == "int" {
@@ -3044,11 +3059,15 @@ func goTypeForParam(name, t string) string {
 	if isCursorParam(name) && (kind == "int" || kind == "float") {
 		return "string"
 	}
+	if isFlagLimitParam(name) && kind == "float" {
+		return "int"
+	}
 	return goType(t)
 }
 
 // cobraFlagFuncForParam returns the cobra flag function, overriding IntVar→StringVar
-// for ID-like parameters and Float64Var/IntVar→StringVar for pagination cursors.
+// for ID-like parameters, Float64Var/IntVar→StringVar for pagination cursors,
+// and Float64Var→IntVar for the canonical `--limit` flag.
 func cobraFlagFuncForParam(name, t string) string {
 	kind := primitiveKind(t)
 	if isIDParam(name) && kind == "int" {
@@ -3057,12 +3076,17 @@ func cobraFlagFuncForParam(name, t string) string {
 	if isCursorParam(name) && (kind == "int" || kind == "float") {
 		return "StringVar"
 	}
+	if isFlagLimitParam(name) && kind == "float" {
+		return "IntVar"
+	}
 	return cobraFlagFunc(t)
 }
 
 // defaultValForParam returns the default value for a flag parameter,
-// overriding int→string for ID-like parameters and numeric→string for
-// pagination cursors so the StringVar default matches the StringVar field type.
+// overriding int→string for ID-like parameters, numeric→string for
+// pagination cursors so the StringVar default matches the StringVar field type,
+// and float→int for the canonical `--limit` flag so the IntVar default
+// matches its coerced int type.
 func defaultValForParam(p spec.Param) string {
 	kind := primitiveKind(p.Type)
 	if isIDParam(p.Name) && kind == "int" {
@@ -3077,6 +3101,11 @@ func defaultValForParam(p spec.Param) string {
 		}
 		return `""`
 	}
+	if isFlagLimitParam(p.Name) && kind == "float" {
+		coerced := p
+		coerced.Type = "integer"
+		return defaultVal(coerced)
+	}
 	return defaultVal(p)
 }
 
diff --git a/internal/generator/limit_truncation_test.go b/internal/generator/limit_truncation_test.go
index 1e994c80..84e5e5a2 100644
--- a/internal/generator/limit_truncation_test.go
+++ b/internal/generator/limit_truncation_test.go
@@ -136,3 +136,48 @@ func TestClientLimitGenerationEmitsHelperAndBuilds(t *testing.T) {
 
 	runGoCommand(t, outputDir, "build", "./internal/cli")
 }
+
+// TestNumberTypedLimitParamCoercesToInt verifies that a spec declaring
+// `limit` as `number` (the LLM-derived `--docs`-mode shape from #1082)
+// still emits `var flagLimit int` and `IntVar`, so the generated
+// `truncateJSONArray(data, flagLimit)` call compiles. OpenAPI specs
+// declare `integer` and exercise the same path; this test pins the
+// override that prevents `Float64Var`/`float64 flagLimit` regression.
+func TestNumberTypedLimitParamCoercesToInt(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("limit-number")
+	apiSpec.Resources = map[string]spec.Resource{
+		"webhooks": {
+			Description: "Manage webhooks",
+			Endpoints: map[string]spec.Endpoint{
+				"list-payloads": {
+					Method:      "GET",
+					Path:        "/webhooks/payloads",
+					Description: "List webhook payloads",
+					Params: []spec.Param{{
+						Name:    "limit",
+						Type:    "number",
+						Default: 0.0,
+					}},
+					Response: spec.ResponseDef{Type: "array", Item: "Payload"},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "limit-number-pp-cli")
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	commandSrc := readGeneratedFile(t, outputDir, "internal", "cli", "promoted_webhooks.go")
+	assert.Contains(t, commandSrc, "var flagLimit int",
+		"number-typed limit param must coerce to int Go type")
+	assert.Contains(t, commandSrc, "IntVar(&flagLimit",
+		"number-typed limit param must bind via IntVar")
+	assert.NotContains(t, commandSrc, "Float64Var(&flagLimit",
+		"number-typed limit param must not emit Float64Var")
+	assert.NotContains(t, commandSrc, "var flagLimit float64",
+		"number-typed limit param must not declare flagLimit as float64")
+
+	runGoCommand(t, outputDir, "build", "./internal/cli")
+}

← a6010384 fix(cli): detect cross-cutting novel features in dogfood sco  ·  back to Cli Printing Press  ·  test(cli): pin args[] index for flag-exposed path param shap ca1de8fa →