From e61e114dedfae417a72de86c047af6b37039a637 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 17 Feb 2026 21:40:04 +0900 Subject: [PATCH] convert missing context key refactoring --- filter/external.go | 2 +- filter/external_test.go | 9 +++------ filter/filter.go | 8 ++++++-- filter/fuzzy.go | 3 ++- filter/interface.go | 1 - filter/regexp.go | 3 ++- pipeline/pipeline.go | 15 ++++++++++++++- pipeline/pipeline_test.go | 17 +++++++++++++++++ 8 files changed, 45 insertions(+), 13 deletions(-) diff --git a/filter/external.go b/filter/external.go index c1c1b02..677b28a 100644 --- a/filter/external.go +++ b/filter/external.go @@ -66,7 +66,7 @@ func (ecf *ExternalCmd) Apply(ctx context.Context, buf []line.Line, out pipeline defer g.End() } - query := ctx.Value(queryKey{}).(string) + query := pipeline.QueryFromContext(ctx) args := append([]string(nil), ecf.args...) for i, v := range args { if v == "$QUERY" { diff --git a/filter/external_test.go b/filter/external_test.go index 49976ae..265c302 100644 --- a/filter/external_test.go +++ b/filter/external_test.go @@ -91,7 +91,7 @@ func TestExternalCmd_CancelCleansUpGoroutine(t *testing.T) { t.Errorf("goroutine leak: before=%d, after=%d", before, runtime.NumGoroutine()) } -func TestExternalCmd_ApplyPanicReturnsError(t *testing.T) { +func TestExternalCmd_ApplyWithoutQueryContext(t *testing.T) { idgen := &testIDGen{} lines := []line.Line{ @@ -102,12 +102,9 @@ func TestExternalCmd_ApplyPanicReturnsError(t *testing.T) { out := pipeline.ChanOutput(make(chan line.Line, 256)) // Call Apply with a context that does NOT have the query key set. - // This triggers a nil interface type assertion panic at the line: - // query := ctx.Value(queryKey).(string) - // The bug: this panic was silently swallowed, returning nil error. + // QueryFromContext returns "" safely (no panic). err := ecf.Apply(context.Background(), lines, out) - require.Error(t, err, "Apply should return an error when an internal panic occurs, not swallow it silently") - require.Contains(t, err.Error(), "panic") + require.NoError(t, err, "Apply with missing query context should not panic or error") } func TestExternalCmdFilter_NullSep(t *testing.T) { diff --git a/filter/filter.go b/filter/filter.go index d822a85..93f8a6f 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -1,11 +1,15 @@ package filter -import "context" +import ( + "context" + + "github.com/peco/peco/pipeline" +) // newContext initializes the context so that it is suitable // to be passed to `Run()` func newContext(ctx context.Context, query string) context.Context { - return context.WithValue(ctx, queryKey{}, query) + return pipeline.NewQueryContext(ctx, query) } // sort related stuff diff --git a/filter/fuzzy.go b/filter/fuzzy.go index 228f02a..1d27a6b 100644 --- a/filter/fuzzy.go +++ b/filter/fuzzy.go @@ -12,6 +12,7 @@ import ( "github.com/peco/peco/internal/util" "github.com/peco/peco/line" + "github.com/peco/peco/pipeline" ) // NewFuzzy builds a fuzzy-finder type of filter. @@ -40,7 +41,7 @@ func (ff Fuzzy) String() string { } func (ff *Fuzzy) applyInternal(ctx context.Context, lines []line.Line, emit func(line.Line)) error { - originalQuery := ctx.Value(queryKey{}).(string) + originalQuery := pipeline.QueryFromContext(ctx) // Parse negative terms and compile them as case-insensitive regexps posTerms, negTerms := SplitQueryTerms(originalQuery) diff --git a/filter/interface.go b/filter/interface.go index ee79c0b..ec3daa3 100644 --- a/filter/interface.go +++ b/filter/interface.go @@ -15,7 +15,6 @@ var ErrFilterNotFound = errors.New("specified filter was not found") var ignoreCaseFlags = regexpFlagList([]string{"i"}) var defaultFlags = regexpFlagList{} -type queryKey struct{} // DefaultCustomFilterBufferThreshold is the default value // for BufferThreshold setting on CustomFilters. diff --git a/filter/regexp.go b/filter/regexp.go index 3c03ed6..2060acb 100644 --- a/filter/regexp.go +++ b/filter/regexp.go @@ -10,6 +10,7 @@ import ( "github.com/peco/peco/internal/util" "github.com/peco/peco/line" + "github.com/peco/peco/pipeline" ) func (r regexpFlagList) flags(_ string) []string { @@ -153,7 +154,7 @@ func (f *regexpQueryFactory) Compile(s string, flags regexpFlags, quotemeta bool } func (rf *Regexp) applyInternal(ctx context.Context, lines []line.Line, emit func(line.Line)) error { - query := ctx.Value(queryKey{}).(string) + query := pipeline.QueryFromContext(ctx) posRegexps, negRegexps, err := rf.factory.Compile(query, rf.flags, rf.quotemeta) if err != nil { return fmt.Errorf("failed to compile queries as regular expression: %w", err) diff --git a/pipeline/pipeline.go b/pipeline/pipeline.go index 7319cbe..c6cfa29 100644 --- a/pipeline/pipeline.go +++ b/pipeline/pipeline.go @@ -9,6 +9,19 @@ import ( "github.com/peco/peco/line" ) +type queryContextKey struct{} + +// NewQueryContext returns a context with the query string stored under a typed key. +func NewQueryContext(ctx context.Context, query string) context.Context { + return context.WithValue(ctx, queryContextKey{}, query) +} + +// QueryFromContext retrieves the query string from the context. +func QueryFromContext(ctx context.Context) string { + v, _ := ctx.Value(queryContextKey{}).(string) + return v +} + func NilOutput(ctx context.Context) ChanOutput { ch := make(chan line.Line) go func() { @@ -86,7 +99,7 @@ func (p *Pipeline) SetDestination(d Destination) { // called while `Run` is running. func (p *Pipeline) Run(ctx context.Context) (err error) { if pdebug.Enabled { - g := pdebug.Marker("Pipeline.Run (%s)", ctx.Value("query")).BindError(&err) + g := pdebug.Marker("Pipeline.Run (%s)", QueryFromContext(ctx)).BindError(&err) defer g.End() } p.mutex.Lock() diff --git a/pipeline/pipeline_test.go b/pipeline/pipeline_test.go index 1aee402..058ab30 100644 --- a/pipeline/pipeline_test.go +++ b/pipeline/pipeline_test.go @@ -108,6 +108,23 @@ func (r *Receiver) Accept(ctx context.Context, in <-chan line.Line, out ChanOutp } } +func TestQueryContext(t *testing.T) { + t.Run("round-trip", func(t *testing.T) { + ctx := NewQueryContext(context.Background(), "hello") + got := QueryFromContext(ctx) + if got != "hello" { + t.Fatalf("expected %q, got %q", "hello", got) + } + }) + + t.Run("missing key returns empty", func(t *testing.T) { + got := QueryFromContext(context.Background()) + if got != "" { + t.Fatalf("expected empty string, got %q", got) + } + }) +} + func TestPipeline(t *testing.T) { src := NewLineFeeder(strings.NewReader(`foo bar