{"id":"19df35c33fb4bbe6","repo":"vitest-dev/vitest","slug":"invalid-tags-expression-missing-closing-in","errorCode":null,"errorMessage":"Invalid tags expression: missing closing \")\" in \"${this.expr}\"","messagePattern":"Invalid tags expression: missing closing \"\\)\" in \"(.+?)\"","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/vitest/src/runtime/runner/utils/tags.ts","lineNumber":182,"sourceCode":"    | { type: 'or'; left: ASTNode; right: ASTNode }\n\nclass TokenStream {\n  private pos = 0\n  constructor(private tokens: Token[], public expr: string) {}\n\n  peek(): Token {\n    return this.tokens[this.pos]\n  }\n\n  next(): Token {\n    return this.tokens[this.pos++]\n  }\n\n  expect(type: Token['type']): Token {\n    const token = this.next()\n    if (token.type !== type) {\n      if (type === 'RPAREN' && token.type === 'EOF') {\n        throw new Error(`Invalid tags expression: missing closing \")\" in \"${this.expr}\"`)\n      }\n      throw new Error(`Invalid tags expression: expected \"${formatTokenType(type)}\" but got \"${formatToken(token)}\" in \"${this.expr}\"`)\n    }\n    return token\n  }\n\n  unexpectedToken(): never {\n    const token = this.peek()\n    if (token.type === 'EOF') {\n      throw new Error(`Invalid tags expression: unexpected end of expression in \"${this.expr}\"`)\n    }\n    throw new Error(`Invalid tags expression: unexpected \"${formatToken(token)}\" in \"${this.expr}\"`)\n  }\n}\n\nfunction formatTokenType(type: Token['type']): string {\n  switch (type) {\n    case 'TAG': return 'tag'","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/vitest-dev/vitest/blob/d568f8ce3739b532d5bf2c1ee1e45e8a8a473d09/packages/vitest/src/runtime/runner/utils/tags.ts#L164-L200","documentation":"Thrown by TokenStream.expect('RPAREN') when it is looking for a closing parenthesis but hits EOF instead. This happens inside parsePrimaryExpression after consuming a '(' and parsing a nested or-expression: the stream expected ')' to close the group but the expression ended first.","triggerScenarios":"A tag filter expression with an unbalanced opening parenthesis, e.g. `--tag '(unit && smoke'`. The parser enters parsePrimaryExpression, sees LPAREN, recurses, consumes all tokens, then expect(RPAREN) finds EOF.","commonSituations":"Manually editing a complex filter and deleting the closing paren. Copy-paste of a partial expression. Complex nested filters where it is easy to lose track of grouping.","solutions":["Add the missing ')' to balance every '('.","Count parentheses in the expression and ensure they are balanced.","Simplify the expression into separate --tag flags if nesting gets confusing.","For one-level grouping, drop the parentheses entirely: `unit && smoke` instead of `(unit && smoke)`."],"exampleFix":"// before\nvitest --tag '(unit && smoke'\n\n// after\nvitest --tag '(unit && smoke)'","handlingStrategy":"validation","validationCode":"function isBalancedParens(expr: string): boolean {\n  let depth = 0\n  for (const ch of expr) {\n    if (ch === '(') depth++\n    else if (ch === ')') depth--\n    if (depth < 0) return false\n  }\n  return depth === 0\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Count parens in every tag filter expression.","Prefer flat expressions over nested grouping where possible.","Add a preflight check that rejects unbalanced strings."],"tags":["tags","filter-expression","parser"],"analyzedSha":"d568f8ce3739b532d5bf2c1ee1e45e8a8a473d09","analyzedAt":"2026-08-03T20:23:56.861Z","schemaVersion":2}