diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml deleted file mode 100644 index 5d4b5fde1..000000000 --- a/.github/workflows/test-backend.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Test Backend - -on: - pull_request: - branches: ["main"] - - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: "true" - - name: Use Node.Js - uses: actions/setup-node@v4 - with: - node-version: '20.x' - - - name: Install - run: yarn install --frozen-lockfile - - - name: Test - run: yarn workspace @sourcebot/backend test - diff --git a/.github/workflows/test-web.yml b/.github/workflows/test.yml similarity index 71% rename from .github/workflows/test-web.yml rename to .github/workflows/test.yml index 108cc2778..d52e3147a 100644 --- a/.github/workflows/test-web.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Test Web +name: Test on: pull_request: @@ -6,7 +6,7 @@ on: jobs: - build: + test: runs-on: ubuntu-latest permissions: contents: read @@ -15,14 +15,13 @@ jobs: uses: actions/checkout@v4 with: submodules: "true" - - name: Use Node.Js + - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' - + node-version: '20.x' + - name: Install run: yarn install --frozen-lockfile - + - name: Test - run: yarn workspace @sourcebot/web test - + run: yarn test diff --git a/CHANGELOG.md b/CHANGELOG.md index b2e77a908..84beae193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- 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) + ## [4.12.0] - 2026-02-26 ### Added diff --git a/packages/queryLanguage/src/tokens.ts b/packages/queryLanguage/src/tokens.ts index 96c66ae59..4fbd656c1 100644 --- a/packages/queryLanguage/src/tokens.ts +++ b/packages/queryLanguage/src/tokens.ts @@ -313,14 +313,44 @@ export const wordToken = new ExternalTokenizer((input, stack) => { return; } - // If starts with '(' and has balanced parens, don't consume as word - // (let parenToken handle it) + // If starts with '(' and has balanced parens, determine whether this is a + // regex alternation value (e.g. file:(test|spec)) or a ParenExpr grouping. + // We're in a value context when the immediately preceding non-whitespace char + // is ':', meaning we're right after a prefix keyword. In that case consume the + // entire '(...)' as a word using depth-tracking so the consuming loop doesn't + // stop early at ')'. Otherwise defer to parenToken for grouping. + let inValueParenContext = false; if (input.next === OPEN_PAREN && hasBalancedParensAt(input, 0)) { - return; + let backOffset = -1; + while (isWhitespace(input.peek(backOffset))) { + backOffset--; + } + if (input.peek(backOffset) === COLON) { + inValueParenContext = true; + } else { + return; // Not a value context — defer to parenToken for grouping + } } - + const startPos = input.pos; + if (inValueParenContext) { + // Consume the parenthesized pattern with depth tracking so we consume + // the matching ')' without stopping early. A ')' at depth 0 means we've + // hit an outer ParenExpr closing paren — stop without consuming it. + let depth = 0; + while (input.next !== EOF) { + const ch = input.next; + if (isWhitespace(ch)) break; + if (ch === OPEN_PAREN) { + depth++; + } else if (ch === CLOSE_PAREN) { + if (depth === 0) break; // outer ParenExpr closing — don't consume + depth--; + } + input.advance(); + } + } else { // Consume characters while (input.next !== EOF) { const ch = input.next; @@ -339,7 +369,8 @@ export const wordToken = new ExternalTokenizer((input, stack) => { input.advance(); } - + } + if (input.pos > startPos) { input.acceptToken(word); } diff --git a/packages/queryLanguage/test/negation.txt b/packages/queryLanguage/test/negation.txt index c229324c0..105c96347 100644 --- a/packages/queryLanguage/test/negation.txt +++ b/packages/queryLanguage/test/negation.txt @@ -253,3 +253,27 @@ Program(NegateExpr(ParenExpr)) ==> Program(NegateExpr(PrefixExpr(FileExpr))) + +# Negate file with regex alternation in value + +-file:(test|spec) + +==> + +Program(NegateExpr(PrefixExpr(FileExpr))) + +# Negate repo with regex alternation in value + +-repo:(org1|org2) + +==> + +Program(NegateExpr(PrefixExpr(RepoExpr))) + +# Complex query with negated file alternation + +chat lang:TypeScript -file:(test|spec) + +==> + +Program(AndExpr(Term,PrefixExpr(LangExpr),NegateExpr(PrefixExpr(FileExpr)))) diff --git a/packages/queryLanguage/test/prefixes.txt b/packages/queryLanguage/test/prefixes.txt index 00533ec03..017939042 100644 --- a/packages/queryLanguage/test/prefixes.txt +++ b/packages/queryLanguage/test/prefixes.txt @@ -334,3 +334,43 @@ Program(ParenExpr(PrefixExpr(FileExpr))) Program(ParenExpr(AndExpr(PrefixExpr(FileExpr),PrefixExpr(LangExpr)))) +# File with regex alternation in value + +file:(test|spec) + +==> + +Program(PrefixExpr(FileExpr)) + +# Repo with regex alternation in value + +repo:(org1|org2) + +==> + +Program(PrefixExpr(RepoExpr)) + +# Sym with regex alternation in value + +sym:(Foo|Bar) + +==> + +Program(PrefixExpr(SymExpr)) + +# Content with regex alternation in value + +content:(error|warning) + +==> + +Program(PrefixExpr(ContentExpr)) + +# File alternation combined with other filters + +file:(test|spec) lang:TypeScript + +==> + +Program(AndExpr(PrefixExpr(FileExpr),PrefixExpr(LangExpr))) +