Skip to content

Commit 33dda62

Browse files
jdxclaudeautofix-ci[bot]
authored
feat(lint): add more lint checks (#446)
## Summary Adds new lint checks and improves error messages: ### New checks: | Code | Severity | Description | |------|----------|-------------| | `subcommand-required-no-subcommands` | error | Command has `subcommand_required=true` but no subcommands defined | | `variadic-arg-not-last` | warning | Variadic argument is not the last argument | | `count-flag-with-arg` | error | Count flag also has an argument (conflicting semantics) | ### Improvements: - `invalid-default-subcommand` error now shows the list of valid subcommands ### Example output: ``` error [invalid-default-subcommand]: default_subcommand 'nonexistent' does not exist (valid subcommands: install, update) ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds additional validation and clearer errors in CLI spec linting. > > - New checks: > - `subcommand-required-no-subcommands` (error): `subcommand_required=true` but no subcommands > - `variadic-arg-not-last` (warning): variadic arg appears before the last position > - `count-flag-with-arg` (error): count flag also has an argument > - Improves `invalid-default-subcommand` to include valid subcommands in the message > - Extends test coverage for all new behaviors > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ab08e6e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 5e030b1 commit 33dda62

File tree

1 file changed

+100
-0
lines changed

1 file changed

+100
-0
lines changed

cli/src/cli/lint.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,16 @@ fn lint_command(cmd: &SpecCommand, path: &[&str], issues: &mut Vec<LintIssue>) {
180180
});
181181
}
182182

183+
// Check for subcommand_required with no subcommands
184+
if cmd.subcommand_required && cmd.subcommands.is_empty() {
185+
issues.push(LintIssue {
186+
severity: Severity::Error,
187+
code: "subcommand-required-no-subcommands".to_string(),
188+
message: "Command has subcommand_required=true but no subcommands defined".to_string(),
189+
location: Some(format!("cmd {}", cmd_path)),
190+
});
191+
}
192+
183193
// Check for duplicate flag names
184194
let mut seen_flags: std::collections::HashMap<String, &SpecFlag> =
185195
std::collections::HashMap::new();
@@ -261,6 +271,18 @@ fn lint_command(cmd: &SpecCommand, path: &[&str], issues: &mut Vec<LintIssue>) {
261271
}
262272
}
263273

274+
// Check for variadic arg not at the end
275+
for (i, arg) in cmd.args.iter().enumerate() {
276+
if arg.var && i < cmd.args.len() - 1 {
277+
issues.push(LintIssue {
278+
severity: Severity::Warning,
279+
code: "variadic-arg-not-last".to_string(),
280+
message: format!("Variadic argument '{}' is not the last argument", arg.name),
281+
location: Some(format!("cmd {}", cmd_path)),
282+
});
283+
}
284+
}
285+
264286
// Recursively lint subcommands
265287
let new_path: Vec<&str> = path
266288
.iter()
@@ -314,6 +336,19 @@ fn lint_flag(flag: &SpecFlag, cmd_path: &str, issues: &mut Vec<LintIssue>) {
314336
});
315337
}
316338
}
339+
340+
// Check for count flag with arg (conflicting semantics)
341+
if flag.count && flag.arg.is_some() {
342+
issues.push(LintIssue {
343+
severity: Severity::Error,
344+
code: "count-flag-with-arg".to_string(),
345+
message: format!(
346+
"Flag '{}' is a count flag but also has an argument",
347+
flag.name
348+
),
349+
location: Some(format!("cmd {} flag {}", cmd_path, flag.name)),
350+
});
351+
}
317352
}
318353

319354
fn lint_arg(arg: &SpecArg, cmd_path: &str, issues: &mut Vec<LintIssue>) {
@@ -432,4 +467,69 @@ cmd "sub" help="A subcommand" {
432467
// Should only have info-level issues (missing-cmd-help for root)
433468
assert!(issues.iter().all(|i| i.severity == Severity::Info));
434469
}
470+
471+
#[test]
472+
fn test_lint_subcommand_required_no_subcommands() {
473+
let spec: Spec = r#"
474+
name "test"
475+
cmd "sub" subcommand_required=#true help="a subcommand with no subcommands"
476+
"#
477+
.parse()
478+
.unwrap();
479+
480+
let issues = lint_spec(&spec);
481+
assert!(issues
482+
.iter()
483+
.any(|i| i.code == "subcommand-required-no-subcommands"));
484+
}
485+
486+
#[test]
487+
fn test_lint_variadic_arg_not_last() {
488+
let spec: Spec = r#"
489+
name "test"
490+
arg "<files>…" help="files" var=#true
491+
arg "<output>" help="output"
492+
"#
493+
.parse()
494+
.unwrap();
495+
496+
let issues = lint_spec(&spec);
497+
assert!(issues.iter().any(|i| i.code == "variadic-arg-not-last"));
498+
}
499+
500+
#[test]
501+
fn test_lint_count_flag_with_arg() {
502+
let spec: Spec = r#"
503+
name "test"
504+
flag "-v --verbose" count=#true {
505+
arg "<level>"
506+
}
507+
"#
508+
.parse()
509+
.unwrap();
510+
511+
let issues = lint_spec(&spec);
512+
assert!(issues.iter().any(|i| i.code == "count-flag-with-arg"));
513+
}
514+
515+
#[test]
516+
fn test_lint_invalid_default_subcommand_shows_valid() {
517+
let spec: Spec = r#"
518+
name "test"
519+
default_subcommand "nonexistent"
520+
cmd "install" help="install"
521+
cmd "update" help="update"
522+
"#
523+
.parse()
524+
.unwrap();
525+
526+
let issues = lint_spec(&spec);
527+
let issue = issues
528+
.iter()
529+
.find(|i| i.code == "invalid-default-subcommand")
530+
.unwrap();
531+
assert!(issue.message.contains("valid subcommands:"));
532+
assert!(issue.message.contains("install"));
533+
assert!(issue.message.contains("update"));
534+
}
435535
}

0 commit comments

Comments
 (0)