Merge shared CI baseline fixes into dependency updates

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-21 22:18:02 +02:00
commit f309fd8425
11 files changed
+288 -23

No files matched your search

+30
View File
@@ -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);
+8
View File
@@ -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;
}
+2
View File
@@ -80,6 +80,8 @@ fi
printf '%s\n' "$changed" |
bash "$ROOT/scripts/ci/select-lanes.sh" --codeql-scope c-cpp |
while IFS= read -r path; do
# Native Windows Python writes CRLF; read removes only the LF.
path="${path%$'\r'}"
entrypoints=()
case "$path" in
internal/cbm/grammar_*.c | internal/cbm/ts_runtime.c)
+14 -3
View File
@@ -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;
}
+2 -2
View File
@@ -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
View File
@@ -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;
+111
View File
@@ -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);
+28
View File
@@ -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);
+3 -5
View File
@@ -46,14 +46,12 @@ if [[ "$manifest_count" != "$expected" ]]; then
fi
# --- 3. Every published surface must claim exactly that number. ---
# The #2729 messaging refresh dropped the count from docs/index.html,
# docs/llms.txt, pkg/npm/README.md, scripts/package-release.sh and server.json;
# section 4 below still fails if any of them states a count again unregistered.
SURFACES=(
README.md
docs/index.html
docs/llms.txt
pkg/npm/README.md
pkg/chocolatey/codebase-memory-mcp.nuspec
scripts/package-release.sh
server.json
)
# Files that legitimately say "<N> languages" about something OTHER than the
+25
View File
@@ -275,6 +275,31 @@ if proc.returncode != 0 or got != ["internal/cbm/grammar_go.c",
"vendored/sqlite3/sqlite3.c"]:
failures.append(f"codeql (c): the retrace helper must list the translation units for "
f"changed vendored code and wrappers, got {got} (exit {proc.returncode})")
# Native Windows Python emits CRLF even when the change list uses LF. Pin
# that producer/consumer boundary on every platform, including direct wrapper
# paths that must survive verbatim rather than being mapped to a family name.
crlf_root = work / "crlf-selector"
crlf_scripts = crlf_root / "scripts" / "ci"
crlf_scripts.mkdir(parents=True)
(crlf_scripts / retrace.name).write_bytes(retrace.read_bytes())
expected_retrace = ["internal/cbm/grammar_go.c", "internal/cbm/grammar_json.c",
"internal/cbm/ts_runtime.c", "vendored/sqlite3/sqlite3.c"]
for path in expected_retrace:
fixture = crlf_root / path
fixture.parent.mkdir(parents=True, exist_ok=True)
fixture.touch()
(crlf_scripts / "select-lanes.sh").write_bytes(
b"#!/usr/bin/env bash\nprintf '%s\\r\\n' "
b"internal/cbm/vendored/grammars/go/parser.c internal/cbm/grammar_json.c "
b"internal/cbm/ts_runtime.c vendored/sqlite3/sqlite3.c\n")
proc = subprocess.run([bash, str(crlf_scripts / retrace.name),
"--changed", str(changed), str(listed)],
capture_output=True, text=True, env=env)
got = listed.read_bytes() if listed.exists() else None
if proc.returncode != 0 or got != ("\n".join(expected_retrace) + "\n").encode("utf-8"):
failures.append(f"codeql CRLF: the retrace helper must preserve every changed "
f"wrapper and write LF entrypoints, got {got!r} (exit {proc.returncode})")
for label, args in (("an unreadable change list", ["--changed", str(work / "missing.txt")]),
("no pull request", [])):
listed.write_text("stale\n", encoding="utf-8")
+38 -4
View File
@@ -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);