diff --git a/private/buf/buflsp/completion_cel.go b/private/buf/buflsp/completion_cel.go index 9a547eb082..02f5c75916 100644 --- a/private/buf/buflsp/completion_cel.go +++ b/private/buf/buflsp/completion_cel.go @@ -749,6 +749,12 @@ func celMacroCompletionItems(celEnv *cel.Env) []protocol.CompletionItem { continue } seen[name] = true + // Skip macros whose function name is an operator display symbol. + // The "in" membership operator is registered as a CEL macro but is + // an infix binary operator ("value in list"), not a callable function. + if _, ok := operators.Find(name); ok { + continue + } items = append(items, protocol.CompletionItem{ Label: name, @@ -813,9 +819,15 @@ func celKeywordCompletionItems(celEnv *cel.Env, expectedType *types.Type) []prot // celIsOperatorOrInternal returns true if name represents an operator or internal // function that should not appear as a user-visible completion item. func celIsOperatorOrInternal(name string) bool { + // Check internal operator names (e.g. "@in", "_&&_"). if _, ok := operators.FindReverse(name); ok { return true } + // Check operator display names (e.g. "in"). Some CEL environments register + // the in operator under its display name as well as its internal "@in" name. + if _, ok := operators.Find(name); ok { + return true + } return strings.HasPrefix(name, "@") || strings.HasPrefix(name, "_") } diff --git a/private/buf/buflsp/completion_cel_test.go b/private/buf/buflsp/completion_cel_test.go index efd1aa9170..63d1cca846 100644 --- a/private/buf/buflsp/completion_cel_test.go +++ b/private/buf/buflsp/completion_cel_test.go @@ -57,6 +57,7 @@ func TestCELCompletion(t *testing.T) { // 206: ` expression: "this.items[0]."` (IndexAccessHolder — indexed into list, yields element fields) // 210: ` expression: "this.locations[\"key\"]."` (IndexAccessHolder — indexed into map, yields value fields) // 222: ` expression: "this.items.filter(item, item.zip_code > 0).all(addr, addr."` (ChainedComprehensionHolder) + // 232: ` expression: "in"` (InOperatorHolder — "in" is an operator, not a function) tests := []struct { name string line uint32 @@ -450,6 +451,15 @@ func TestCELCompletion(t *testing.T) { }, expectedNotContains: []string{"size", "all", "true", "false", "null"}, }, + { + // Cursor at closing `"` of `"in"` — prefix "in". + // `in` is a CEL binary membership operator ("value in list"), not a + // callable function. It must NOT appear as a function completion "in()". + name: "in_operator_not_function_completion", + line: 232, + character: 19, // closing `"` after `in` + expectedNotContains: []string{"in"}, + }, } for _, tt := range tests { diff --git a/private/buf/buflsp/testdata/hover/cel_completion.proto b/private/buf/buflsp/testdata/hover/cel_completion.proto index 840f2d25dc..00d86df00c 100644 --- a/private/buf/buflsp/testdata/hover/cel_completion.proto +++ b/private/buf/buflsp/testdata/hover/cel_completion.proto @@ -223,3 +223,13 @@ message ChainedComprehensionHolder { expression: "this.items.filter(item, item.zip_code > 0).all(addr, addr." }; } + +// InOperatorHolder verifies that the "in" membership operator does not appear +// as a callable function completion ("in()") when typed as a prefix. +// "in" is a binary infix operator ("value in list"), not a function call. +message InOperatorHolder { + option (buf.validate.message).cel = { + id: "in.operator" + expression: "in" + }; +}