{"id":"683b68d1e649511f","repo":"vitest-dev/vitest","slug":"invalid-tags-expression-unexpected-formattoken","errorCode":null,"errorMessage":"Invalid tags expression: unexpected \"${formatToken(stream.peek())}\" in \"${expr}\"","messagePattern":"Invalid tags expression: unexpected \"(.+?)\" in \"(.+?)\"","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/vitest/src/runtime/runner/utils/tags.ts","lineNumber":60,"sourceCode":"    .map(t => `- ${t.name}${t.description ? `: ${t.description}` : ''}`)\n    .join('\\n')}`)\n}\n\nexport function createTagsFilter(tagsExpr: string[], availableTags: TestTagDefinition[]): (testTags: string[]) => boolean {\n  const matchers = tagsExpr.map(expr => parseTagsExpression(expr, availableTags))\n  return (testTags: string[]) => {\n    return matchers.every(matcher => matcher(testTags))\n  }\n}\n\ntype TagMatcher = (tags: string[]) => boolean\n\nfunction parseTagsExpression(expr: string, availableTags: TestTagDefinition[]): TagMatcher {\n  const tokens = tokenize(expr)\n  const stream = new TokenStream(tokens, expr)\n  const ast = parseOrExpression(stream, availableTags)\n  if (stream.peek().type !== 'EOF') {\n    throw new Error(`Invalid tags expression: unexpected \"${formatToken(stream.peek())}\" in \"${expr}\"`)\n  }\n  return (tags: string[]) => evaluateNode(ast, tags)\n}\n\nfunction formatToken(token: Token): string {\n  switch (token.type) {\n    case 'TAG': return token.value\n    default: return formatTokenType(token.type)\n  }\n}\n\ntype Token\n  = | { type: 'TAG'; value: string }\n    | { type: 'AND' }\n    | { type: 'OR' }\n    | { type: 'NOT' }\n    | { type: 'LPAREN' }\n    | { type: 'RPAREN' }","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/vitest-dev/vitest/blob/d568f8ce3739b532d5bf2c1ee1e45e8a8a473d09/packages/vitest/src/runtime/runner/utils/tags.ts#L42-L78","documentation":"Thrown by parseTagsExpression when a tag filter expression has already been fully parsed but the token stream still has a non-EOF token at the top level. This means the grammar parsed a complete or/and/not expression and then encountered something that cannot legally follow it (e.g. two tags in a row with no operator, or a stray ')'). It signals a malformed --tag / tagsFilter string.","triggerScenarios":"Passing a --tag filter with two bare tags separated only by whitespace, e.g. `--tag 'unit smoke'`. Using a closing parenthesis at the top level like `--tag 'unit)'`. Any string where, after a valid or/and/not expression, an unexpected token remains.","commonSituations":"Writing `--tag 'a b'` instead of `--tag 'a && b'`. Leftover characters after editing a complex filter expression. Misunderstanding that bare juxtaposition is not an implicit AND (you must use && / and).","solutions":["Join multiple tags with an explicit operator: use `a && b` or `a and b` instead of `a b`.","Remove the stray token the error points at (e.g. an extra closing paren or duplicate tag).","Re-read the supported grammar: TAG, !NOT, &&/and, ||/or, and (...) grouping.","Test the expression in isolation with the --tag CLI flag to iterate quickly."],"exampleFix":"// before\nvitest --tag 'unit smoke'\n\n// after\nvitest --tag 'unit && smoke'","handlingStrategy":"validation","validationCode":"// Validate a tag expression before passing it to --tag\nconst TOKEN = /^[!()]+|&&|\\|\\||\\b(?:and|or|not)\\b|[^\\s!()&|]+/gi\nfunction looksBalanced(expr: string): boolean {\n  let depth = 0\n  for (const ch of expr) {\n    if (ch === '(') depth++\n    if (ch === ')') depth--\n    if (depth < 0) return false\n  }\n  return depth === 0\n}\nif (!looksBalanced(process.env.TAG_FILTER!)) throw new Error('bad tag filter')","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always join tags with explicit && / || / !.","Never juxtapose two bare tags.","Validate parentheses are balanced before running."],"tags":["tags","filter-expression","parser"],"analyzedSha":"d568f8ce3739b532d5bf2c1ee1e45e8a8a473d09","analyzedAt":"2026-08-03T20:23:56.861Z","schemaVersion":2}