fix(search_code): error on invalid regex instead of silent empty (#283)

With regex=true, a syntactically invalid pattern (e.g. unclosed group) made
the underlying grep fail, which handle_search_code reported as an empty result
set — indistinguishable from a legitimate no-match. Validate the user pattern
with cbm_regcomp up front and return an explicit "invalid regex" error so
callers can tell a broken pattern from zero matches. regex=false is unaffected.
This commit is contained in:
Martin Vogel
2026-05-31 16:22:31 +02:00
parent 30dfa952d9
commit 3bc1d586d6
2 changed files with 58 additions and 0 deletions
+24
View File
@@ -3536,6 +3536,9 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
}
if (!validate_search_args(root_path, file_pattern)) {
if (has_path_filter) {
cbm_regfree(&path_regex);
}
free(root_path);
free(pattern);
free(project);
@@ -3543,6 +3546,27 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
return cbm_mcp_text_result("path or file_pattern contains invalid characters", true);
}
/* issue #283: when regex=true, a syntactically invalid pattern (e.g. an
* unclosed group) makes the underlying grep fail, which the handler would
* otherwise report as an empty result set — indistinguishable from a
* legitimate no-match. Validate the user's regex up front and return an
* explicit error so callers can tell "broken pattern" from "no matches". */
if (use_regex) {
cbm_regex_t probe;
if (cbm_regcomp(&probe, pattern, CBM_REG_EXTENDED | CBM_REG_NOSUB) != CBM_REG_OK) {
if (has_path_filter) {
cbm_regfree(&path_regex);
}
free(root_path);
free(pattern);
free(project);
free(file_pattern);
return cbm_mcp_text_result(
"invalid regex pattern (regex=true): check for unbalanced (), [], or {}", true);
}
cbm_regfree(&probe);
}
/* ── Phase 0.5: Multi-word → regex conversion ───────────── */
/* If pattern contains whitespace and is not already a regex, convert to a
* regex that matches all words in order: "foo bar baz" → "foo.*bar.*baz".
+34
View File
@@ -739,6 +739,39 @@ TEST(search_code_multi_word) {
PASS();
}
/* issue #283: search_code with regex=true and a syntactically invalid pattern
* must return an explicit error, not an empty result indistinguishable from a
* legitimate no-match. */
TEST(search_code_invalid_regex_errors_issue283) {
char tmp[512];
cbm_mcp_server_t *srv = setup_snippet_server(tmp, sizeof(tmp));
ASSERT_NOT_NULL(srv);
/* Unclosed group under regex=true → must be flagged as an error. */
char *resp =
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":91,\"method\":\"tools/call\","
"\"params\":{\"name\":\"search_code\","
"\"arguments\":{\"pattern\":\"func(\",\"regex\":true,"
"\"project\":\"test-project\"}}}");
ASSERT_NOT_NULL(resp);
ASSERT_NOT_NULL(strstr(resp, "\"isError\":true"));
ASSERT_NOT_NULL(strstr(resp, "invalid regex"));
free(resp);
/* Same pattern as a literal (regex=false) must NOT error. */
resp = cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":92,\"method\":\"tools/call\","
"\"params\":{\"name\":\"search_code\","
"\"arguments\":{\"pattern\":\"func(\",\"regex\":false,"
"\"project\":\"test-project\"}}}");
ASSERT_NOT_NULL(resp);
ASSERT_TRUE(strstr(resp, "invalid regex") == NULL);
free(resp);
cleanup_snippet_dir(tmp);
cbm_mcp_server_free(srv);
PASS();
}
TEST(tool_detect_changes_no_project) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
@@ -1937,6 +1970,7 @@ SUITE(mcp) {
RUN_TEST(tool_search_code_missing_pattern);
RUN_TEST(tool_search_code_no_project);
RUN_TEST(search_code_multi_word);
RUN_TEST(search_code_invalid_regex_errors_issue283);
RUN_TEST(tool_detect_changes_no_project);
RUN_TEST(tool_manage_adr_no_project);
RUN_TEST(tool_manage_adr_get_with_existing_adr);