Skip to content

Commit da26b90

Browse files
brendan-kellamclaudegithub-actions[bot]
authored
fix(queryLanguage): allow parenthesized regex alternation in filter values (#946)
* 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 <[email protected]> * changelog Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * chore: move fix entry to Unreleased section in CHANGELOG Co-authored-by: Brendan Kellam <[email protected]> * chore(ci): consolidate test-backend and test-web into a single test workflow Replaces the two separate workflows with a single `test.yml` that runs `yarn test` at the repo root, which executes all workspace tests topologically via `yarn workspaces foreach`. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Brendan Kellam <[email protected]>
1 parent 3e3e0a6 commit da26b90

File tree

6 files changed

+110
-41
lines changed

6 files changed

+110
-41
lines changed

.github/workflows/test-backend.yml

Lines changed: 0 additions & 28 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
name: Test Web
1+
name: Test
22

33
on:
44
pull_request:
55
branches: ["main"]
66

77

88
jobs:
9-
build:
9+
test:
1010
runs-on: ubuntu-latest
1111
permissions:
1212
contents: read
@@ -15,14 +15,13 @@ jobs:
1515
uses: actions/checkout@v4
1616
with:
1717
submodules: "true"
18-
- name: Use Node.Js
18+
- name: Use Node.js
1919
uses: actions/setup-node@v4
2020
with:
21-
node-version: '20.x'
22-
21+
node-version: '20.x'
22+
2323
- name: Install
2424
run: yarn install --frozen-lockfile
25-
25+
2626
- name: Test
27-
run: yarn workspace @sourcebot/web test
28-
27+
run: yarn test

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- Fixed search query parser rejecting parenthesized regex alternation in filter values (e.g. `file:(test|spec)`, `-file:(test|spec)`). [#946](https://github.com/sourcebot-dev/sourcebot/pull/946)
12+
1013
## [4.12.0] - 2026-02-26
1114

1215
### Added

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)