Skip to content

feat: warn unknown options#202

Open
kricsleo wants to merge 4 commits intounjs:mainfrom
kricsleo:feat/warn-unknown-options
Open

feat: warn unknown options#202
kricsleo wants to merge 4 commits intounjs:mainfrom
kricsleo:feat/warn-unknown-options

Conversation

@kricsleo
Copy link
Copy Markdown
Member

@kricsleo kricsleo commented Apr 21, 2025

resolves #201

This PR adds support for warning about unknown options.
This is enabled by default but can be disabled by setting meta.allowUnknownOptions: true.

Summary by CodeRabbit

  • Bug Fixes

    • Unknown top-level options are only reported when appropriate; subcommand options are no longer misreported.
  • New Features

    • Commands can opt into allowing unknown options via a new configuration flag.
    • Robust unknown-option validation added to surface clear error messages.
  • Tests

    • Tests updated to cover unknown-option behavior, subcommand handling, and the new configuration flag.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 21, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@9cb0edc). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #202   +/-   ##
=======================================
  Coverage        ?   97.75%           
=======================================
  Files           ?        9           
  Lines           ?      401           
  Branches        ?      139           
=======================================
  Hits            ?      392           
  Misses          ?        8           
  Partials        ?        1           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Use Object.keys instead of for...in, Set instead of array for lookups,
and skip parent validation when a subcommand handles the args.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 92fe6b47-fe53-4a88-a3a8-d97915606a15

📥 Commits

Reviewing files that changed from the base of the PR and between e69f0db and 921b195.

📒 Files selected for processing (4)
  • src/command.ts
  • src/types.ts
  • src/validate.ts
  • test/main.test.ts
✅ Files skipped from review due to trivial changes (2)
  • src/types.ts
  • src/validate.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/command.ts
  • test/main.test.ts

📝 Walkthrough

Walkthrough

Adds unknown-option validation and a new allowUnknownOptions meta flag; runCommand now tracks subcommand handling and only runs validation when no subcommand handled and allowUnknownOptions is falsy.

Changes

Cohort / File(s) Summary
Type Definitions
src/types.ts
Added optional allowUnknownOptions?: boolean to CommandMeta.
Validation Logic
src/validate.ts
Added validateUnknownOptions(argsDef, args) which computes allowed option names (including aliases) and throws CLIError with code E_UNKNOWN_OPTION for unknown options.
Command Dispatch
src/command.ts
Resolve cmd.meta earlier, add handledBySubCommand flag, call runCommand recursively for resolved subcommands, and conditionally invoke validateUnknownOptions only when no subcommand handled and allowUnknownOptions is falsy.
Tests
test/main.test.ts
Changed test teardown to afterEach; added tests asserting unknown-option warning behavior, subcommand option passthrough, and meta.allowUnknownOptions: true behavior.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI as "CLI Parser"
  participant Runner as "runCommand"
  participant Validator as "validateUnknownOptions"
  participant Sub as "Subcommand (runCommand)"

  User->>CLI: supply raw args
  CLI->>Runner: runCommand(cmd, rawArgs)
  Runner->>Runner: resolve cmd.meta (cmdMeta)
  alt subcommand selected
    Runner->>Runner: set handledBySubCommand = true
    Runner->>Sub: runCommand(subCmd, subArgs)
    Sub-->>Runner: subcommand result
  else no subcommand
    Runner->>Validator: validateUnknownOptions(argsDef, parsedArgs)
    Validator-->>Runner: ok or throws E_UNKNOWN_OPTION
    Runner->>Runner: execute command handler
  end
  Runner-->>CLI: result / exit
  CLI-->>User: output / errors
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐰 I hop through flags both near and far,
I sniff for typos like a tiny star.
Subcommands scamper through my gate,
While unknown flags meet my gentle fate.
A soft little warning, quick as a jig—hop! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: warn unknown options' clearly and concisely summarizes the main change: adding functionality to warn about unknown CLI options.
Linked Issues check ✅ Passed The PR implements the core requirement from issue #201: detecting and warning about unknown CLI options, with opt-out support via allowUnknownOptions flag.
Out of Scope Changes check ✅ Passed All changes directly support the unknown-options warning feature: new validation function, CommandMeta extension, command execution logic modifications, and comprehensive tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/command.ts (1)

40-70: ⚠️ Potential issue | 🟠 Major

handledBySubCommand suppresses parent validation too aggressively.

Once a subcommand is found, the root command stops validating unknown options for the entire argv. That means prog --bogus foo never reports --bogus, and a subcommand-only container still throws E_NO_COMMAND for prog --bogus before the new validation runs. Validate only the slice owned by the current command (tokens before the subcommand) before delegating or raising E_NO_COMMAND.

Based on learnings, subcommand lookup taking priority over positionals is a deliberate design convention.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command.ts` around lines 40 - 70, The current flow sets
handledBySubCommand true and skips parent option validation entirely when a
subcommand is found, which hides unknown options before the subcommand token;
change this so the parent only validates the argv slice it owns (tokens before
the subcommand). Concretely: compute subCommandArgIndex via
opts.rawArgs.findIndex as you already do, then before delegating to
runCommand(resolveValue(subCommands[subCommandName])), call
validateUnknownOptions on the parent slice (e.g., build cmdArgs/parsedArgs for
opts.rawArgs.slice(0, subCommandArgIndex)) unless cmdMeta?.allowUnknownOptions
is true; keep the existing unknown-subcommand CLIError path and only skip
validation for the delegated slice passed to runCommand. Update usage of
handledBySubCommand (or remove it) so it no longer suppresses validation for the
parent command.
🧹 Nitpick comments (2)
src/command.ts (1)

4-4: Use an explicit .ts extension for this import.

./validate should follow the same ESM import convention as the rest of the changed src/**/*.ts code.

As per coding guidelines, src/**/*.ts: Use explicit .ts extensions in imports following ESM conventions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command.ts` at line 4, Update the ESM import in src/command.ts to use an
explicit .ts extension: replace the current import statement that references
"./validate" with one referencing "./validate.ts" so the module loader follows
the project's ESM convention (symbol: validateUnknownOptions in src/command.ts).
src/validate.ts (1)

1-2: Use explicit .ts extensions in these new imports.

./_utils and ./types should be imported as ./_utils.ts and ./types.ts to match the repo’s ESM import rule.

As per coding guidelines, src/**/*.ts: Use explicit .ts extensions in imports following ESM conventions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/validate.ts` around lines 1 - 2, Update the ESM imports to use explicit
.ts extensions: change the import of CLIError and toArray (currently from
"./_utils") to "./_utils.ts" and change the type imports ArgsDef and ParsedArgs
(currently from "./types") to "./types.ts"; ensure the import specifiers
referencing CLIError, toArray, ArgsDef, and ParsedArgs are the only changes so
the module resolution follows the repo's ESM rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/main.test.ts`:
- Around line 155-156: The test currently asserts
expect(consolaErrorMock).not.toHaveBeenCalledWith("Unknown option `--foo`")
which only forbids that specific message; change it to
expect(consolaErrorMock).not.toHaveBeenCalled() so the test verifies no error
logs were emitted at all when unknown options are allowed—update the assertion
near the mockRunCommand check (referencing mockRunCommand and consolaErrorMock)
to use not.toHaveBeenCalled().

---

Outside diff comments:
In `@src/command.ts`:
- Around line 40-70: The current flow sets handledBySubCommand true and skips
parent option validation entirely when a subcommand is found, which hides
unknown options before the subcommand token; change this so the parent only
validates the argv slice it owns (tokens before the subcommand). Concretely:
compute subCommandArgIndex via opts.rawArgs.findIndex as you already do, then
before delegating to runCommand(resolveValue(subCommands[subCommandName])), call
validateUnknownOptions on the parent slice (e.g., build cmdArgs/parsedArgs for
opts.rawArgs.slice(0, subCommandArgIndex)) unless cmdMeta?.allowUnknownOptions
is true; keep the existing unknown-subcommand CLIError path and only skip
validation for the delegated slice passed to runCommand. Update usage of
handledBySubCommand (or remove it) so it no longer suppresses validation for the
parent command.

---

Nitpick comments:
In `@src/command.ts`:
- Line 4: Update the ESM import in src/command.ts to use an explicit .ts
extension: replace the current import statement that references "./validate"
with one referencing "./validate.ts" so the module loader follows the project's
ESM convention (symbol: validateUnknownOptions in src/command.ts).

In `@src/validate.ts`:
- Around line 1-2: Update the ESM imports to use explicit .ts extensions: change
the import of CLIError and toArray (currently from "./_utils") to "./_utils.ts"
and change the type imports ArgsDef and ParsedArgs (currently from "./types") to
"./types.ts"; ensure the import specifiers referencing CLIError, toArray,
ArgsDef, and ParsedArgs are the only changes so the module resolution follows
the repo's ESM rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ca5af49f-ad74-406e-896d-af350970c0fd

📥 Commits

Reviewing files that changed from the base of the PR and between 6341dc3 and e69f0db.

📒 Files selected for processing (4)
  • src/command.ts
  • src/types.ts
  • src/validate.ts
  • test/main.test.ts

Comment on lines +155 to +156
expect(mockRunCommand).toHaveBeenCalledWith(command, { rawArgs });
expect(consolaErrorMock).not.toHaveBeenCalledWith("Unknown option `--foo`");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the test file to understand the context
head -n 200 test/main.test.ts | tail -n 100

Repository: unjs/citty

Length of output: 2762


🏁 Script executed:

# Get the specific lines mentioned in the review
sed -n '150,160p' test/main.test.ts

Repository: unjs/citty

Length of output: 330


🏁 Script executed:

# Look for the test setup to understand consolaErrorMock
sed -n '1,50p' test/main.test.ts

Repository: unjs/citty

Length of output: 1344


🏁 Script executed:

# Find the test block containing lines 155-156
sed -n '140,165p' test/main.test.ts

Repository: unjs/citty

Length of output: 773


Strengthen this assertion to fully verify no error is logged when unknown options are allowed.

not.toHaveBeenCalledWith("Unknown option --foo") only rejects that specific message; if consola.error is called with any other message, the test still passes. Use not.toHaveBeenCalled() to ensure no errors are logged at all, which better matches the test's intent.

💡 Suggested change
-    expect(consolaErrorMock).not.toHaveBeenCalledWith("Unknown option `--foo`");
+    expect(consolaErrorMock).not.toHaveBeenCalled();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main.test.ts` around lines 155 - 156, The test currently asserts
expect(consolaErrorMock).not.toHaveBeenCalledWith("Unknown option `--foo`")
which only forbids that specific message; change it to
expect(consolaErrorMock).not.toHaveBeenCalled() so the test verifies no error
logs were emitted at all when unknown options are allowed—update the assertion
near the mockRunCommand check (referencing mockRunCommand and consolaErrorMock)
to use not.toHaveBeenCalled().

@kricsleo
Copy link
Copy Markdown
Member Author

kricsleo commented Apr 2, 2026

I've resolved the conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Warn for unknown options

2 participants