Skip to content

Commit 7681d37

Browse files
fix(queryLanguage): allow parenthesized regex alternation in filter values
Queries like `file:(test|spec)` or `-file:(test|spec)` previously failed with "No parse at N" because the word tokenizer unconditionally deferred to parenToken whenever a token started with balanced parentheses, even in value contexts (right after a prefix keyword colon like `file:`, `repo:`, `sym:`, etc.). The fix detects value context by looking backward for a preceding ':' and, when found, uses depth-tracking to consume the entire '(...)' as a word instead of deferring. This correctly handles nested parens, stops at an outer ParenExpr closing paren, and leaves all existing parse behaviour unchanged for non-value contexts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3e3e0a6 commit 7681d37

File tree

3 files changed

+100
-5
lines changed

3 files changed

+100
-5
lines changed

packages/queryLanguage/src/tokens.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -313,14 +313,44 @@ export const wordToken = new ExternalTokenizer((input, stack) => {
313313
return;
314314
}
315315

316-
// If starts with '(' and has balanced parens, don't consume as word
317-
// (let parenToken handle it)
316+
// If starts with '(' and has balanced parens, determine whether this is a
317+
// regex alternation value (e.g. file:(test|spec)) or a ParenExpr grouping.
318+
// We're in a value context when the immediately preceding non-whitespace char
319+
// is ':', meaning we're right after a prefix keyword. In that case consume the
320+
// entire '(...)' as a word using depth-tracking so the consuming loop doesn't
321+
// stop early at ')'. Otherwise defer to parenToken for grouping.
322+
let inValueParenContext = false;
318323
if (input.next === OPEN_PAREN && hasBalancedParensAt(input, 0)) {
319-
return;
324+
let backOffset = -1;
325+
while (isWhitespace(input.peek(backOffset))) {
326+
backOffset--;
327+
}
328+
if (input.peek(backOffset) === COLON) {
329+
inValueParenContext = true;
330+
} else {
331+
return; // Not a value context — defer to parenToken for grouping
332+
}
320333
}
321-
334+
322335
const startPos = input.pos;
323336

337+
if (inValueParenContext) {
338+
// Consume the parenthesized pattern with depth tracking so we consume
339+
// the matching ')' without stopping early. A ')' at depth 0 means we've
340+
// hit an outer ParenExpr closing paren — stop without consuming it.
341+
let depth = 0;
342+
while (input.next !== EOF) {
343+
const ch = input.next;
344+
if (isWhitespace(ch)) break;
345+
if (ch === OPEN_PAREN) {
346+
depth++;
347+
} else if (ch === CLOSE_PAREN) {
348+
if (depth === 0) break; // outer ParenExpr closing — don't consume
349+
depth--;
350+
}
351+
input.advance();
352+
}
353+
} else {
324354
// Consume characters
325355
while (input.next !== EOF) {
326356
const ch = input.next;
@@ -339,7 +369,8 @@ export const wordToken = new ExternalTokenizer((input, stack) => {
339369

340370
input.advance();
341371
}
342-
372+
}
373+
343374
if (input.pos > startPos) {
344375
input.acceptToken(word);
345376
}

packages/queryLanguage/test/negation.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,27 @@ Program(NegateExpr(ParenExpr))
253253
==>
254254

255255
Program(NegateExpr(PrefixExpr(FileExpr)))
256+
257+
# Negate file with regex alternation in value
258+
259+
-file:(test|spec)
260+
261+
==>
262+
263+
Program(NegateExpr(PrefixExpr(FileExpr)))
264+
265+
# Negate repo with regex alternation in value
266+
267+
-repo:(org1|org2)
268+
269+
==>
270+
271+
Program(NegateExpr(PrefixExpr(RepoExpr)))
272+
273+
# Complex query with negated file alternation
274+
275+
chat lang:TypeScript -file:(test|spec)
276+
277+
==>
278+
279+
Program(AndExpr(Term,PrefixExpr(LangExpr),NegateExpr(PrefixExpr(FileExpr))))

packages/queryLanguage/test/prefixes.txt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,43 @@ Program(ParenExpr(PrefixExpr(FileExpr)))
334334

335335
Program(ParenExpr(AndExpr(PrefixExpr(FileExpr),PrefixExpr(LangExpr))))
336336

337+
# File with regex alternation in value
338+
339+
file:(test|spec)
340+
341+
==>
342+
343+
Program(PrefixExpr(FileExpr))
344+
345+
# Repo with regex alternation in value
346+
347+
repo:(org1|org2)
348+
349+
==>
350+
351+
Program(PrefixExpr(RepoExpr))
352+
353+
# Sym with regex alternation in value
354+
355+
sym:(Foo|Bar)
356+
357+
==>
358+
359+
Program(PrefixExpr(SymExpr))
360+
361+
# Content with regex alternation in value
362+
363+
content:(error|warning)
364+
365+
==>
366+
367+
Program(PrefixExpr(ContentExpr))
368+
369+
# File alternation combined with other filters
370+
371+
file:(test|spec) lang:TypeScript
372+
373+
==>
374+
375+
Program(AndExpr(PrefixExpr(FileExpr),PrefixExpr(LangExpr)))
376+

0 commit comments

Comments
 (0)