Skip to content

Commit 19f6714

Browse files
authored
Merge pull request #116 from reeflective/dev
fix(completion): recover from panics in the application completer
2 parents be2926f + 7ee6a10 commit 19f6714

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

completion.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,24 @@ func (rl *Shell) startMenuComplete(completer completion.Completer) {
227227
}
228228

229229
// commandCompletion generates the completions for commands/args/flags.
230-
func (rl *Shell) commandCompletion() completion.Values {
230+
//
231+
// The application-provided completer is user code that readline calls on nearly
232+
// every keystroke (menu completion and as-you-type autocomplete both funnel
233+
// through here). A panic in it — a bad completer or transient command-tree
234+
// state — is recovered and surfaced as a completion message, so a single faulty
235+
// completion degrades into a visible error instead of crashing the shell.
236+
func (rl *Shell) commandCompletion() (values completion.Values) {
231237
if rl.Completer == nil {
232238
return completion.Values{}
233239
}
234240

241+
defer func() {
242+
if r := recover(); r != nil {
243+
msg := CompleteMessage("completion error: %v", r)
244+
values = msg.convert()
245+
}
246+
}()
247+
235248
line, cursor := rl.completer.Line()
236249
comps := rl.Completer(*line, cursor.Pos())
237250

completion_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package readline
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
// TestCommandCompletionRecoversFromPanic ensures a panic in the
9+
// application-provided completer is recovered and surfaced as a completion
10+
// message instead of propagating and crashing the shell. readline calls the
11+
// completer on nearly every keystroke, so a single bad completer or transient
12+
// state must not take down the process.
13+
func TestCommandCompletionRecoversFromPanic(t *testing.T) {
14+
rl := NewShell()
15+
rl.Completer = func([]rune, int) Completions {
16+
panic("boom")
17+
}
18+
19+
defer func() {
20+
if r := recover(); r != nil {
21+
t.Fatalf("commandCompletion did not recover, panicked with: %v", r)
22+
}
23+
}()
24+
25+
values := rl.commandCompletion()
26+
27+
if values.Messages.IsEmpty() {
28+
t.Fatal("recovered completion produced no message, want a completion error message")
29+
}
30+
31+
var found bool
32+
for _, msg := range values.Messages.Get() {
33+
if strings.Contains(msg, "completion error") && strings.Contains(msg, "boom") {
34+
found = true
35+
}
36+
}
37+
38+
if !found {
39+
t.Fatalf("recovered messages = %v, want one containing the panic value", values.Messages.Get())
40+
}
41+
}
42+
43+
// TestCommandCompletionNilCompleter verifies the no-completer case stays a clean
44+
// no-op (empty values, no message).
45+
func TestCommandCompletionNilCompleter(t *testing.T) {
46+
rl := NewShell()
47+
rl.Completer = nil
48+
49+
values := rl.commandCompletion()
50+
51+
if !values.Messages.IsEmpty() {
52+
t.Fatalf("nil completer produced messages %v, want none", values.Messages.Get())
53+
}
54+
}

0 commit comments

Comments
 (0)