neoclide/coc.nvim · error

Invalid regex: ${e instanceof Error ? e.message : String(e)}

Error message

Invalid regex: ${e instanceof Error ? e.message : String(e)}

What it means

searchWithJs in src/mcp/tools/workspace.ts builds a RegExp from user-supplied search input. If args.regex is true the pattern is used verbatim; otherwise it is escaped. When the pattern (verbatim mode) is not a valid regular expression, `new RegExp` throws and the library rethrows it wrapped as 'Invalid regex: <underlying message>'. This guards the MCP workspace search tool from crashing on malformed patterns.

Source

Thrown at src/mcp/tools/workspace.ts:130

    || /\(\?[=!<]/.test(pattern)
    || /\([^)]*[+*{][^)]*\)\s*(?:[+*?]|\{)/.test(pattern)
}

export async function searchWithJs(pattern: string, args: any, root: string, maxResults: number): Promise<SearchMatch[]> {
  if (args.regex === true && unsafeFallbackRegex(pattern)) {
    throw new Error('Regex is too complex for the JavaScript search fallback; install ripgrep to use it safely')
  }
  let include = new RelativePatternImpl(URI.file(root), typeof args.include === 'string' && args.include ? args.include : '**/*')
  let uris = await workspace.findFiles(include, args.exclude || null, 500)
  // One (first) match per line, searched from the start of every line: the
  // global flag would carry lastIndex across lines and skip matches.
  let flags = args.caseSensitive === true ? '' : 'i'
  let source = args.regex === true ? pattern : escapeRegExp(pattern)
  let re: RegExp
  try {
    re = new RegExp(source, flags)
  } catch (e) {
    throw new Error(`Invalid regex: ${e instanceof Error ? e.message : String(e)}`)
  }
  let results: SearchMatch[] = []
  for (let uri of uris) {
    if (results.length >= maxResults) break
    let filepath = uri.fsPath
    if (checkPath(filepath)) continue
    let content: string
    try {
      let stat = fs.statSync(filepath)
      if (stat.size > 2 * 1024 * 1024) continue
      content = fs.readFileSync(filepath, 'utf8')
    } catch (_e) {
      continue
    }
    if (content.includes('\0')) continue
    let lines = content.split('\n')
    for (let i = 0; i < lines.length; i++) {
      if (results.length >= maxResults) break

View on GitHub (pinned to 50e974d969)

Solutions

  1. Set args.regex=false (or omit it) so the pattern is escaped literally via escapeRegExp and always valid.
  2. Test the pattern in a JS runtime (new RegExp(pattern)) before sending it with regex=true.
  3. Fix the syntax error reported in the wrapped message (unmatched bracket, invalid quantifier, dangling escape).

Example fix

// before
await search({ pattern: 'src/(foo', regex: true })
// after
await search({ pattern: 'src/(foo', regex: false }) // or fix regex: 'src\/(foo
Defensive patterns

Strategy: validation

Validate before calling

function isValidRegex(p) { try { new RegExp(p); return true } catch { return false } }
if (args.regex === true && !isValidRegex(pattern)) throw new Error(`Invalid regex: ${pattern}`)

Type guard

function isSafePattern(p: unknown): p is string { return typeof p === 'string' && (() => { try { new RegExp(p); return true } catch { return false } })() }

Try / catch

try {
  await workspaceSearch({ pattern, regex: true })
} catch (e) {
  if (String(e.message).startsWith('Invalid regex:')) return searchLiteral(pattern)
  throw e
}

Prevention

When it happens

Trigger: Calling the workspace search tool with args.regex=true and a syntactically invalid pattern, e.g. '[abc', 'a{2,1}', a dangling backslash, or an invalid group like '(?P<name>x)'.

Common situations: LLM/MCP clients generating regex patterns dynamically with unbalanced brackets; users pasting globs or wildcard patterns (e.g. 'src/**/*.ts') into a regex-mode search; patterns containing unmatched '(' from copy-paste.


AI-assisted analysis of neoclide/coc.nvim@50e974d969 (2026-08-31). Data as JSON: /api/errors/90d053486160248e. Report an issue: GitHub.