From 120df1b137551e636bf168697df06b81e14ca3ba Mon Sep 17 00:00:00 2001 From: StressTestor <212606152+StressTestor@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:16:46 -0600 Subject: [PATCH] fix(complete): exit cleanly when words before the cursor fail to parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `complete-word` ran `parse_partial` with `?`, so any parse error on the words before the cursor (e.g. an unexpected word earlier on the line) propagated out as a hard error printed to stderr. A completion helper runs inline as you type, so that error garbled the shell prompt instead of just yielding no candidates. Catch the parse error in the completion path and return no candidates (exit 0) instead, logging the error at debug. The library parser stays strict for every other caller — only the interactive completion helper swallows the error, which is the one place a parse failure should be silent rather than surfaced. Fixes #596 --- cli/src/cli/complete_word.rs | 14 +++++++++++++- cli/tests/complete_word.rs | 24 +++++++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/cli/src/cli/complete_word.rs b/cli/src/cli/complete_word.rs index 168e96ff..41454a7f 100644 --- a/cli/src/cli/complete_word.rs +++ b/cli/src/cli/complete_word.rs @@ -98,7 +98,19 @@ impl CompleteWord { ctx.insert("PREV", &(cword - 1)); } - let parsed = usage::parse::parse_partial(spec, &words)?; + // The words before the cursor may not parse (e.g. an unexpected word + // earlier on the line). A completion helper must stay silent rather than + // surface a hard parse error to stderr, which garbles the shell prompt + // mid-completion — emit no candidates instead. (#596) + let parsed = match usage::parse::parse_partial(spec, &words) { + Ok(parsed) => parsed, + Err(e) => { + debug!( + "parse_partial failed during completion (words: {words:?}), no candidates: {e}" + ); + return Ok(vec![]); + } + }; debug!("parsed cmd: {}", parsed.cmd.full_cmd.join(" ")); // Check if previous token was a restart_token - if so, complete from first arg diff --git a/cli/tests/complete_word.rs b/cli/tests/complete_word.rs index 370f7260..5d26c468 100644 --- a/cli/tests/complete_word.rs +++ b/cli/tests/complete_word.rs @@ -15,6 +15,21 @@ fn complete_word_completer() { .stdout("plugin-1\tdesc\nplugin-2\tdesc\nplugin-3\tdesc\n"); } +#[test] +fn complete_word_after_unexpected_word_is_silent() { + // #596: completion after an unexpected word must NOT print a hard parse error + // to stderr (which garbles the shell prompt) — the helper should exit 0 with + // no candidates instead. + let spec = "name \"mycli\"\ncmd \"build\"\ncmd \"test\"\n"; + Command::new(cargo::cargo_bin!("usage")) + .args([ + "cw", "--shell", "bash", "--spec", spec, "--", "mycli", "help", "", + ]) + .assert() + .success() + .stdout(""); +} + #[test] fn complete_word_subcommands() { assert_cmd("basic.usage.kdl", &["plugins", "install"]).stdout(contains("install")); @@ -258,11 +273,14 @@ fn complete_word_non_global_flags_stop_search() { path.insert(0, env::current_dir().unwrap().join("..").join("examples")); env::set_var("PATH", env::join_paths(path).unwrap()); - // Non-global flag --local should stop subcommand search - // The parser will fail to recognize 'run' as a subcommand and error + // Non-global flag --local stops the subcommand search, so 'run' is not + // recognized as a subcommand. Completion must still exit cleanly with no + // candidates — NOT surface a hard parse error to stderr, which would garble + // the shell prompt (#596). (Previously this asserted .failure() with + // "unexpected word", which was the bug.) let mut cmd = cmd("test-boolean-flags.sh", Some("fish")); cmd.args(["--", "--local", "run", ""]); - cmd.assert().failure().stderr(contains("unexpected word")); + cmd.assert().success().stdout(""); } #[test]