Skip to content

Commit 50020bf

Browse files
committed
fix(skilldoc): remove false positional-as-flag validation rule
The validator's positional-as-flag check asserted that a flag folded into a required positional always fails the binary's Args check when passed alone, and flagged any doc example using such a flag. That premise doesn't hold: every generated command with this kind of fold uses requireBodyFieldOrExactArg or requireBodyFieldOrArgs (internal/cli/args.go), both of which explicitly accept the flag as a standalone alternative to the positional. cligen never emits the bare requireExactArg/requireArgs form that would make a flag-only call fail, so the failure mode this rule targeted cannot occur for any currently generated command — a truthful version of the check would never fire. Removed it rather than keep an inert rule; a folded flag now validates through the same path as any other registered flag, and an actually unregistered flag is still caught as unknown-flag. Adjusted the corresponding test to assert the real behavior: a folded flag used alone or alongside its positional validates clean, while a genuinely unregistered flag on the same command still trips unknown-flag.
1 parent 45f3c28 commit 50020bf

2 files changed

Lines changed: 31 additions & 37 deletions

File tree

internal/skilldoc/validate.go

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type Doc struct {
1616
type Issue struct {
1717
Doc string
1818
Line int
19-
Kind string // "unknown-command" | "unknown-flag" | "positional-as-flag" | "stale-fence"
19+
Kind string // "unknown-command" | "unknown-flag" | "stale-fence"
2020
Detail string
2121
}
2222

@@ -104,27 +104,23 @@ func lineOf(body string, off int) int {
104104
return strings.Count(body[:off], "\n") + 1
105105
}
106106

107-
// commandIndex maps a command path to its set of declared flag names and to the
108-
// set of flags cligen folded into required positionals, and carries the sorted
109-
// list of paths for longest-prefix resolution.
107+
// commandIndex maps a command path to its set of declared flag names, and
108+
// carries the sorted list of paths for longest-prefix resolution.
110109
type commandIndex struct {
111-
flags map[string]map[string]bool
112-
folded map[string]map[string]bool
113-
paths []string
110+
flags map[string]map[string]bool
111+
paths []string
114112
}
115113

116114
func indexDump(d Dump) commandIndex {
117115
idx := commandIndex{
118-
flags: make(map[string]map[string]bool),
119-
folded: make(map[string]map[string]bool),
116+
flags: make(map[string]map[string]bool),
120117
}
121118
for _, c := range d.Commands {
122119
set := make(map[string]bool, len(c.Flags))
123120
for _, f := range c.Flags {
124121
set[f.Name] = true
125122
}
126123
idx.flags[c.Path] = set
127-
idx.folded[c.Path] = foldedFlagNames(positionalsOf(c.Use))
128124
idx.paths = append(idx.paths, c.Path)
129125
}
130126
// Longest paths first so resolveCommand prefers the most specific match.
@@ -156,26 +152,19 @@ func validateExample(idx commandIndex, docPath string, ex Example) []Issue {
156152
}}
157153
}
158154

159-
folded := idx.folded[path]
160155
var issues []Issue
161156
for _, tok := range ex.Tokens {
162157
name, isFlag := flagName(tok)
163158
if !isFlag || HasPlaceholder(name) {
164159
continue
165160
}
166-
// cligen folded this field into a required positional: the flag is still
167-
// registered (so it is in flagSet) but passing it as a flag fails the
168-
// binary's Args check. Catch it before the flagSet pass would wave it
169-
// through — this is the exact misuse only a live run surfaced before.
170-
if folded[name] {
171-
issues = append(issues, Issue{
172-
Doc: docPath,
173-
Line: ex.Line,
174-
Kind: "positional-as-flag",
175-
Detail: "--" + name + " is folded into a required positional of `" + path + "` — pass it as a bare argument, not a flag",
176-
})
177-
continue
178-
}
161+
// A field cligen folds into a required positional keeps a same-named flag
162+
// registered as a genuine alternative source: every generated command
163+
// with such a fold uses requireBodyFieldOrExactArg/requireBodyFieldOrArgs
164+
// (internal/cli/args.go), which explicitly accepts the flag alone — cligen
165+
// never emits the bare requireExactArg/requireArgs form that would make
166+
// passing the flag fail. So a folded name is just a flag like any other
167+
// here: fall through to the flagSet check below.
179168
if globalFlags[name] || flagSet[name] {
180169
continue
181170
}

internal/skilldoc/validate_test.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,14 @@ func TestValidate_UnknownCommandAndFlag(t *testing.T) {
4545
}
4646
}
4747

48-
// A field cligen folded into a required positional is still a registered flag,
49-
// but passing it as `--flag` fails the binary's Args check. The validator must
50-
// catch this misuse (kind "positional-as-flag") — the exact error that only a
51-
// live run surfaced before Use was threaded into the oracle. Passing the field
52-
// positionally must stay clean, and the same flag name on a command where it is
53-
// NOT folded (two required ids) must remain valid.
54-
func TestValidate_FoldedPositionalAsFlag(t *testing.T) {
48+
// A field cligen folds into a required positional keeps a same-named flag
49+
// registered as a genuine alternative source: every generated command with
50+
// such a fold uses requireBodyFieldOrExactArg/requireBodyFieldOrArgs
51+
// (internal/cli/args.go), which explicitly accepts the flag alone. So passing
52+
// that flag — with or without the positional also present — must validate
53+
// clean; only a flag that is not registered on the command at all is an
54+
// actual defect.
55+
func TestValidate_FoldedFlagIsValidAlternative(t *testing.T) {
5556
d := Dump{Commands: []Command{
5657
{ // single required id → cligen folds page-id into a positional
5758
Path: "status-page change-active-list",
@@ -67,23 +68,27 @@ func TestValidate_FoldedPositionalAsFlag(t *testing.T) {
6768
},
6869
}}
6970
docs := []Doc{
70-
{Path: "bad", Body: "```bash\nfduty status-page change-active-list --page-id 5\n```\n"},
71-
{Path: "good", Body: "```bash\nfduty status-page change-active-list 5 --type incident\n```\n"},
71+
{Path: "flag-alone", Body: "```bash\nfduty status-page change-active-list --page-id 5 --type incident\n```\n"},
72+
{Path: "positional", Body: "```bash\nfduty status-page change-active-list 5 --type incident\n```\n"},
7273
{Path: "twoid", Body: "```bash\nfduty status-page change-timeline-create --page-id 5 --change-id 9\n```\n"},
74+
{Path: "unknown-on-folder", Body: "```bash\nfduty status-page change-active-list --page-id 5 --bogus x\n```\n"},
7375
}
7476
byDoc := map[string][]Issue{}
7577
for _, is := range Validate(d, docs) {
7678
byDoc[is.Doc] = append(byDoc[is.Doc], is)
7779
}
78-
if n := len(byDoc["bad"]); n != 1 || byDoc["bad"][0].Kind != "positional-as-flag" {
79-
t.Errorf("bad: want 1 positional-as-flag, got %+v", byDoc["bad"])
80+
if n := len(byDoc["flag-alone"]); n != 0 {
81+
t.Errorf("flag-alone: folded flag used without the positional want 0 issues, got %+v", byDoc["flag-alone"])
8082
}
81-
if n := len(byDoc["good"]); n != 0 {
82-
t.Errorf("good: positional usage want 0 issues, got %+v", byDoc["good"])
83+
if n := len(byDoc["positional"]); n != 0 {
84+
t.Errorf("positional: positional usage want 0 issues, got %+v", byDoc["positional"])
8385
}
8486
if n := len(byDoc["twoid"]); n != 0 {
8587
t.Errorf("twoid: --page-id on non-folding command want 0 issues, got %+v", byDoc["twoid"])
8688
}
89+
if n := len(byDoc["unknown-on-folder"]); n != 1 || byDoc["unknown-on-folder"][0].Kind != "unknown-flag" {
90+
t.Errorf("unknown-on-folder: want 1 unknown-flag, got %+v", byDoc["unknown-on-folder"])
91+
}
8792
}
8893

8994
func TestValidate_GlobalFlagsAllowed(t *testing.T) {

0 commit comments

Comments
 (0)