mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge commit '3457b21b0a3d75960c07ed3ec666a9e684985ef9' into HEAD
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
commit
72e8be9089
8 files changed
+258
-18
No files matched your search
@@ -416,6 +416,33 @@ static TSNode resolve_lean_func_name(TSNode node, TSNode name) {
|
||||
return cbm_find_child_by_kind(node, "ident");
|
||||
}
|
||||
|
||||
// Haskell infix-form definition head (`a <+> b`, ``x `plus` y``): the defined
|
||||
// name is the operator, not the left operand that is the head's first child. A
|
||||
// backticked `infix_id` yields its inner variable (`plus`); a symbolic `operator`
|
||||
// is returned as-is and parenthesised by cbm_func_name_node_text, matching the
|
||||
// prefix form `(<+>) a b = ...` (#2440).
|
||||
static TSNode resolve_haskell_infix_head_name(TSNode head) {
|
||||
enum { INFIX_MIN_CHILDREN = 3, INFIX_OP_IDX = 1 };
|
||||
TSNode op = ts_node_child_by_field_name(head, TS_FIELD("operator"));
|
||||
if (ts_node_is_null(op) && ts_node_child_count(head) >= INFIX_MIN_CHILDREN) {
|
||||
op = ts_node_child(head, INFIX_OP_IDX);
|
||||
}
|
||||
if (!ts_node_is_null(op)) {
|
||||
const char *ok = ts_node_type(op);
|
||||
if (strcmp(ok, "operator") == 0) {
|
||||
return op;
|
||||
}
|
||||
if (strcmp(ok, "infix_id") == 0) {
|
||||
TSNode v = cbm_find_child_by_kind(op, "variable");
|
||||
if (!ts_node_is_null(v)) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
TSNode null_node = {0};
|
||||
return null_node;
|
||||
}
|
||||
|
||||
// Haskell: resolve function name from first named child (variable/name).
|
||||
static TSNode resolve_haskell_func_name(TSNode node) {
|
||||
if (ts_node_named_child_count(node) > 0) {
|
||||
@@ -424,6 +451,9 @@ static TSNode resolve_haskell_func_name(TSNode node) {
|
||||
if (strcmp(hk, "variable") == 0 || strcmp(hk, "name") == 0) {
|
||||
return head;
|
||||
}
|
||||
if (strcmp(hk, "infix") == 0) {
|
||||
return resolve_haskell_infix_head_name(head);
|
||||
}
|
||||
if (ts_node_named_child_count(head) > 0) {
|
||||
TSNode v = ts_node_named_child(head, 0);
|
||||
const char *vk = ts_node_type(v);
|
||||
|
||||
@@ -1213,6 +1213,14 @@ char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source,
|
||||
if (text && lang == CBM_LANG_NIX) {
|
||||
cbm_nix_strip_attr_quotes(text);
|
||||
}
|
||||
/* Haskell infix-form operator definition: `a <+> b = …` resolves to the bare
|
||||
* `operator` node `<+>`, while the prefix form `(<+>) a b = …` is named by its
|
||||
* `prefix_id` text `(<+>)`. Parenthesise here, not in the resolver, so both
|
||||
* forms mint the same name and the def name and the call-scope QN, which both
|
||||
* route through this function, cannot disagree (#2440). */
|
||||
if (text && lang == CBM_LANG_HASKELL && strcmp(ts_node_type(name_node), "operator") == 0) {
|
||||
return cbm_arena_sprintf(a, "(%s)", text);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
+14
-3
@@ -2899,12 +2899,23 @@ static cyp_partial_t partial_or(cyp_partial_t l, cyp_partial_t r) {
|
||||
: CYP_PARTIAL_UNKNOWN;
|
||||
}
|
||||
|
||||
/* A condition whose alias is not bound yet is UNKNOWN; a literal-only LHS
|
||||
* (func condition with no variable arg) has nothing to wait for. */
|
||||
static bool partial_alias_unbound(binding_t *b, const char *alias) {
|
||||
return alias && !binding_get(b, alias) && !binding_get_edge(b, alias);
|
||||
}
|
||||
|
||||
/* A condition is UNKNOWN while any alias it reads is not bound yet: its own
|
||||
* variable, and every function argument's -- a func condition is filed under
|
||||
* its first argument's alias, so the b in coalesce(a.x, b.name) is only seen
|
||||
* here. A literal-only LHS has nothing to wait for. */
|
||||
static cyp_partial_t eval_condition_partial(const cbm_condition_t *c, binding_t *b) {
|
||||
if (c->variable && !binding_get(b, c->variable) && !binding_get_edge(b, c->variable)) {
|
||||
if (partial_alias_unbound(b, c->variable)) {
|
||||
return CYP_PARTIAL_UNKNOWN;
|
||||
}
|
||||
for (int i = 0; i < c->arg_count; i++) {
|
||||
if (partial_alias_unbound(b, c->args[i].variable)) {
|
||||
return CYP_PARTIAL_UNKNOWN;
|
||||
}
|
||||
}
|
||||
return eval_condition(c, b) ? CYP_PARTIAL_TRUE : CYP_PARTIAL_FALSE;
|
||||
}
|
||||
|
||||
|
||||
@@ -449,8 +449,8 @@ static bool application_regular_db_exists(const char *project) {
|
||||
if (written <= 0 || (size_t)written >= sizeof(path)) {
|
||||
return false;
|
||||
}
|
||||
struct stat status;
|
||||
return stat(path, &status) == 0 && S_ISREG(status.st_mode);
|
||||
cbm_path_info_t status;
|
||||
return cbm_path_info_utf8(path, &status) == CBM_PATH_INFO_OK && status.is_regular;
|
||||
}
|
||||
|
||||
static bool application_canonical_directory_exists(const char *path) {
|
||||
|
||||
+27
-9
@@ -522,14 +522,28 @@ static watcher_git_status_t git_repo_status(cbm_watcher_t *w, project_state_t *s
|
||||
* a `.git` file (the gitlink form used by linked worktrees and initialized
|
||||
* submodules). Deliberately a filesystem check, not a git invocation — the whole
|
||||
* point is to learn something `rev-parse` cannot tell us, because it walks up. */
|
||||
/* Preserve stat's link-following behavior while inspecting UTF-8 paths. */
|
||||
static int watcher_path_info(const char *path, cbm_path_info_t *info) {
|
||||
int status = cbm_path_info_utf8(path, info);
|
||||
if (status == CBM_PATH_INFO_OK && info->is_symlink) {
|
||||
char canonical[CBM_SZ_4K];
|
||||
if (!cbm_canonical_path(path, canonical, sizeof(canonical))) {
|
||||
return CBM_PATH_INFO_UNAVAILABLE;
|
||||
}
|
||||
status = cbm_path_info_utf8(canonical, info);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
static bool git_has_own_dot_git(const char *root_path) {
|
||||
char path[CBM_SZ_4K];
|
||||
int written = snprintf(path, sizeof(path), "%s/.git", root_path);
|
||||
if (written <= 0 || (size_t)written >= sizeof(path)) {
|
||||
return false;
|
||||
}
|
||||
struct stat st;
|
||||
return stat(path, &st) == 0 && (S_ISDIR(st.st_mode) || S_ISREG(st.st_mode));
|
||||
cbm_path_info_t info;
|
||||
return watcher_path_info(path, &info) == CBM_PATH_INFO_OK &&
|
||||
(info.is_directory || info.is_regular);
|
||||
}
|
||||
|
||||
/* Does the ancestor repository actually track anything inside this directory?
|
||||
@@ -990,13 +1004,17 @@ static root_status_t root_status(const char *root_path, int *out_errno) {
|
||||
if (!root_path) {
|
||||
return ROOT_UNCERTAIN;
|
||||
}
|
||||
struct stat st;
|
||||
if (stat(root_path, &st) == 0) {
|
||||
/* Exists but is no longer a directory → the root directory is gone. */
|
||||
return S_ISDIR(st.st_mode) ? ROOT_PRESENT : ROOT_MISSING;
|
||||
cbm_path_info_t info;
|
||||
int status = watcher_path_info(root_path, &info);
|
||||
if (status == CBM_PATH_INFO_OK) {
|
||||
return info.is_directory ? ROOT_PRESENT : ROOT_MISSING;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
*out_errno = status == CBM_PATH_INFO_ABSENT ? ENOENT : EIO;
|
||||
#else
|
||||
*out_errno = errno;
|
||||
return cbm_watcher_root_missing_errno(errno) ? ROOT_MISSING : ROOT_UNCERTAIN;
|
||||
#endif
|
||||
return status == CBM_PATH_INFO_ABSENT ? ROOT_MISSING : ROOT_UNCERTAIN;
|
||||
}
|
||||
|
||||
/* Sustained-absence window (seconds) before a missing root may be pruned.
|
||||
@@ -1338,8 +1356,8 @@ static void publish_status(project_state_t *s, bool scanned) {
|
||||
|
||||
/* Init baseline for a project: check if git, get HEAD, count files */
|
||||
static bool init_baseline(cbm_watcher_t *w, project_state_t *s) {
|
||||
struct stat st;
|
||||
if (stat(s->root_path, &st) != 0) {
|
||||
cbm_path_info_t info;
|
||||
if (watcher_path_info(s->root_path, &info) != CBM_PATH_INFO_OK || !info.is_directory) {
|
||||
cbm_log_warn("watcher.root_gone", "project", s->project_name, "path", s->root_path);
|
||||
s->baseline_done = true;
|
||||
s->is_git = false;
|
||||
|
||||
@@ -1917,6 +1917,32 @@ TEST(cypher_exists_has_outgoing_calls) {
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 2);
|
||||
cbm_cypher_result_free(&r);
|
||||
|
||||
rc = cbm_cypher_execute(s,
|
||||
"MATCH (a)-[:CALLS]->(b) WHERE b.name <> \"SubmitOrder\" "
|
||||
"RETURN a.name, b.name",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 2);
|
||||
cbm_cypher_result_free(&r);
|
||||
|
||||
rc = cbm_cypher_execute(s,
|
||||
"MATCH (a)-[:CALLS]->(b) WHERE b.name = \"SubmitOrder\" OR "
|
||||
"NOT b.name = \"SubmitOrder\" RETURN count(*)",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
ASSERT_STR_EQ(r.rows[0][0], "3");
|
||||
cbm_cypher_result_free(&r);
|
||||
|
||||
rc = cbm_cypher_execute(s,
|
||||
"MATCH (a)-[:CALLS]->(b) WITH a, b WHERE NOT b.name = "
|
||||
"\"SubmitOrder\" RETURN a.name, b.name",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 2);
|
||||
cbm_cypher_result_free(&r);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
@@ -3378,6 +3404,86 @@ TEST(cypher_exec_where_mixed_alias_xor) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cypher_exec_where_not_target_node) {
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(s,
|
||||
"MATCH (a)-[:CALLS]->(b) WHERE NOT b.name = \"SubmitOrder\" "
|
||||
"RETURN a.name, b.name",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 2);
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cypher_exec_where_not_target_relationship) {
|
||||
cbm_store_t *s = setup_cypher_multi_edge_store();
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(s,
|
||||
"MATCH (a)-[r:HTTP_CALLS]->(b) WHERE NOT r.url_path = "
|
||||
"\"/health\" RETURN r.url_path",
|
||||
"testproj", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
ASSERT_STR_EQ(r.rows[0][0], "/api/orders");
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cypher_exec_where_not_source_node) {
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(s,
|
||||
"MATCH (a)-[:CALLS]->(b) WHERE NOT a.name = "
|
||||
"\"HandleOrder\" RETURN a.name, b.name",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
ASSERT_STR_EQ(r.rows[0][0], "ValidateOrder");
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cypher_exec_where_not_target_node_count) {
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(s,
|
||||
"MATCH (a)-[:CALLS]->(b) WHERE NOT b.name = "
|
||||
"\"SubmitOrder\" RETURN count(*)",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
ASSERT_STR_EQ(r.rows[0][0], "2");
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The early WHERE pass must wait for every alias a condition reads. A function
|
||||
* condition is filed under its first argument's alias, so in
|
||||
* coalesce(a.no_such_prop, b.name) the target b hid behind the bound a: the
|
||||
* seed row read coalesce(NULL, <unbound b>) as a definite mismatch and pruned
|
||||
* every row before CALLS expansion, while the same predicate after WITH was
|
||||
* right. */
|
||||
TEST(cypher_exec_where_func_arg_on_unbound_target) {
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(s,
|
||||
"MATCH (a)-[:CALLS]->(b) WHERE coalesce(a.no_such_prop, b.name) = "
|
||||
"\"SubmitOrder\" RETURN a.name",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
ASSERT_STR_EQ(r.rows[0][0], "ValidateOrder");
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cypher_exec_where_in) {
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
cbm_cypher_result_t r = {0};
|
||||
@@ -6243,6 +6349,11 @@ SUITE(cypher) {
|
||||
RUN_TEST(cypher_exec_where_mixed_alias_and);
|
||||
RUN_TEST(cypher_exec_where_mixed_alias_or);
|
||||
RUN_TEST(cypher_exec_where_mixed_alias_xor);
|
||||
RUN_TEST(cypher_exec_where_not_target_node);
|
||||
RUN_TEST(cypher_exec_where_not_target_relationship);
|
||||
RUN_TEST(cypher_exec_where_not_source_node);
|
||||
RUN_TEST(cypher_exec_where_not_target_node_count);
|
||||
RUN_TEST(cypher_exec_where_func_arg_on_unbound_target);
|
||||
RUN_TEST(cypher_exec_where_in);
|
||||
RUN_TEST(cypher_exec_where_not_in);
|
||||
RUN_TEST(cypher_exec_where_is_null);
|
||||
|
||||
@@ -1777,6 +1777,33 @@ TEST(haskell_function) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2440: an infix-form definition (`a <+> b = ...`, ``x `plus` y = ...``) has
|
||||
* no `name` field; its first named child is an `infix` node whose first child
|
||||
* is the LEFT OPERAND, so the def was named `a` / `x`. It must be named after
|
||||
* the operator, consistent with the prefix form `(<+>) a b = ...`. */
|
||||
TEST(haskell_infix_definition_name) {
|
||||
CBMFileResult *r = extract("module M where\n"
|
||||
"\n"
|
||||
"(<+>) :: Int -> Int -> Int\n"
|
||||
"a <+> b = a + b\n"
|
||||
"\n"
|
||||
"plus :: Int -> Int -> Int\n"
|
||||
"x `plus` y = x + y\n"
|
||||
"\n"
|
||||
"f :: Int\n"
|
||||
"f = 1 <+> 2\n",
|
||||
CBM_LANG_HASKELL, "t", "M.hs");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT(has_def(r, "Function", "f"));
|
||||
ASSERT(has_def(r, "Function", "plus"));
|
||||
ASSERT(has_def(r, "Function", "(<+>)"));
|
||||
ASSERT_FALSE(has_def(r, "Function", "a"));
|
||||
ASSERT_FALSE(has_def(r, "Function", "x"));
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* --- OCaml --- */
|
||||
TEST(ocaml_function) {
|
||||
CBMFileResult *r =
|
||||
@@ -13177,6 +13204,7 @@ SUITE(extraction) {
|
||||
RUN_TEST(elixir_function);
|
||||
RUN_TEST(elixir_call_string_argument);
|
||||
RUN_TEST(haskell_function);
|
||||
RUN_TEST(haskell_infix_definition_name);
|
||||
RUN_TEST(ocaml_function);
|
||||
RUN_TEST(erlang_function);
|
||||
|
||||
|
||||
+38
-4
@@ -1103,8 +1103,8 @@ TEST(watcher_stop_and_unwatch_cancel_blocked_git_without_backstop) {
|
||||
watcher_windows_image_identity_t expected_git;
|
||||
ASSERT_TRUE(watcher_windows_capture_image_identity(fake_git, &expected_git));
|
||||
|
||||
const char *old_path_raw = getenv("PATH");
|
||||
const char *old_marker_raw = getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV);
|
||||
const char *old_path_raw = cbm_runtime_getenv("PATH");
|
||||
const char *old_marker_raw = cbm_runtime_getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV);
|
||||
char *old_path = old_path_raw ? cbm_strdup(old_path_raw) : NULL;
|
||||
char *old_marker = old_marker_raw ? cbm_strdup(old_marker_raw) : NULL;
|
||||
size_t path_size = strlen(bin) + 2 + (old_path ? strlen(old_path) : 0);
|
||||
@@ -1238,8 +1238,8 @@ TEST(watcher_stop_and_unwatch_cancel_blocked_git_without_backstop) {
|
||||
ASSERT_TRUE(watcher_test_self_image(self));
|
||||
ASSERT_EQ(symlink(self, fake_git), 0);
|
||||
|
||||
const char *old_path_raw = getenv("PATH");
|
||||
const char *old_marker_raw = getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV);
|
||||
const char *old_path_raw = cbm_runtime_getenv("PATH");
|
||||
const char *old_marker_raw = cbm_runtime_getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV);
|
||||
char *old_path = old_path_raw ? strdup(old_path_raw) : NULL;
|
||||
char *old_marker = old_marker_raw ? strdup(old_marker_raw) : NULL;
|
||||
size_t path_size = strlen(bin) + 2 + (old_path ? strlen(old_path) : 0);
|
||||
@@ -3995,7 +3995,41 @@ TEST(watcher_null_watch_count) {
|
||||
* SUITE
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(watcher_unicode_root_is_not_pruned_as_missing) {
|
||||
char *parent = th_mktempdir("cbm_watch_utf8");
|
||||
ASSERT_NOT_NULL(parent);
|
||||
char root[CBM_SZ_4K];
|
||||
snprintf(root, sizeof(root), "%s/\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E", parent);
|
||||
ASSERT_EQ(th_mkdir_p(root), 0);
|
||||
const char *saved = cbm_runtime_getenv("CBM_WATCHER_PRUNE_GRACE_S");
|
||||
char *saved_copy = saved ? strdup(saved) : NULL;
|
||||
cbm_setenv("CBM_WATCHER_PRUNE_GRACE_S", "0", 1);
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(store, "unicode-root", root);
|
||||
cbm_watcher_t *watcher = cbm_watcher_new(store, NULL, NULL);
|
||||
cbm_watcher_watch(watcher, "unicode-root", root);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
cbm_watcher_poll_once(watcher);
|
||||
}
|
||||
cbm_project_t info = {0};
|
||||
bool kept = cbm_watcher_watch_count(watcher) == 1 &&
|
||||
cbm_store_get_project(store, "unicode-root", &info) == CBM_STORE_OK;
|
||||
cbm_project_free_fields(&info);
|
||||
cbm_watcher_free(watcher);
|
||||
cbm_store_close(store);
|
||||
if (saved_copy) {
|
||||
cbm_setenv("CBM_WATCHER_PRUNE_GRACE_S", saved_copy, 1);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WATCHER_PRUNE_GRACE_S");
|
||||
}
|
||||
free(saved_copy);
|
||||
ASSERT_EQ(th_rmtree(parent), 0);
|
||||
ASSERT_TRUE(kept);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(watcher) {
|
||||
RUN_TEST(watcher_unicode_root_is_not_pruned_as_missing);
|
||||
/* Adaptive interval */
|
||||
RUN_TEST(poll_interval_base);
|
||||
RUN_TEST(poll_interval_scaling);
|
||||
|
||||
Reference in new issue
Block a user