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) breakView on GitHub (pinned to 50e974d969)
Solutions
- Set args.regex=false (or omit it) so the pattern is escaped literally via escapeRegExp and always valid.
- Test the pattern in a JS runtime (new RegExp(pattern)) before sending it with regex=true.
- 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
- Only set regex=true for patterns you have validated with new RegExp in the client.
- Default to regex=false (literal/escaped) search unless regex semantics are required.
- Test patterns in regex101 or a REPL before sending them from tooling.
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.