mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 18:58:00 +00:00
fix(pipeline): classify native fetch() as HTTP_CALLS, not local shadows
Closes #856: native `fetch()` calls (Node 18+ built-in, and the identical browser/WHATWG API — no static way to tell them apart, and no need to: both make a real outbound HTTP request) were invisible to cross-repo intelligence. A bare `fetch(url)` has no import and typically no local definition, so registry resolution comes back empty and the call was silently dropped instead of becoming an HTTP_CALLS edge. The obvious fix — add "fetch" to the http_libraries substring table in service_patterns.c — is wrong: that table is consulted by an early, unconditional check in both pass_calls.c and pass_parallel.c that runs before/regardless of whether the callee resolves to a real local definition (by design, so `axios.get(url)` still classifies even when the registry mis-binds bare `get` to an unrelated local method). That's safe for "axios"/"requests" because nobody names their own function that; it is not safe for "fetch", which collides with a plausible local identifier (`function fetch(){}` or `const fetch = () => {}`). Instead, cbm_service_pattern_is_global_fetch() is a new, narrow, exact- match check consulted only in the *empty-resolution* fallback in both files — the existing #523 path for unindexed external libraries. By the time that branch runs, the registry has already had its chance to resolve "fetch" to a local/imported definition; only a genuine miss reaches the new check. This also means a member call like `repo.fetch()` is naturally excluded (its callee_name is "repo.fetch", not the bare "fetch" this checks for). pass_parallel.c's empty-resolution branch calls the low-level emit_http_async_service_edge() directly rather than going through emit_service_edge(), which re-derives its own classification from res->qualified_name via cbm_service_pattern_match() — a call with "fetch" would come back CBM_SVC_NONE there and silently fall through to a plain CALLS edge. Three new tests in test_pipeline.c: - bare fetch() -> HTTP_CALLS, sequential path (< 50 files) - bare fetch() -> HTTP_CALLS, forced through the parallel resolver (>= 50 files) - a local `function fetch(){}` shadowing the global -> plain CALLS to the local definition, zero HTTP_CALLS (the false-positive this PR must not introduce), bundled with a `repo.fetch()` member-call check in the first test Verified end-to-end, not just by inspection: prod build clean under -Wall -Wextra -Werror; test build clean under ASan+UBSan; full suite 5936 passed, 1 skipped (pre-existing, unrelated), 0 failed; clang-format clean on all touched files; lint-cppcheck's pre-existing failures (extract_defs.c, compat_regex.c) reproduced identically on unmodified main via stash/pop, confirming they predate this change. Refs #592. Signed-off-by: Alexandros Pappas <11921291+apappas1129@users.noreply.github.com>
This commit is contained in:
1 parent
ce23ce75e0
commit
d288a21ec2
5 files changed
+181
-1
No files matched your search
@@ -782,6 +782,10 @@ void cbm_service_patterns_init(void) {
|
||||
/* No-op — tables are static const */
|
||||
}
|
||||
|
||||
bool cbm_service_pattern_is_global_fetch(const char *callee_name) {
|
||||
return callee_name != NULL && strcmp(callee_name, "fetch") == 0;
|
||||
}
|
||||
|
||||
cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) {
|
||||
if (!resolved_qn || !resolved_qn[0]) {
|
||||
return CBM_SVC_NONE;
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#ifndef CBM_SERVICE_PATTERNS_H
|
||||
#define CBM_SERVICE_PATTERNS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* Edge type returned by pattern match. */
|
||||
typedef enum {
|
||||
CBM_SVC_NONE = 0, /* Not a service pattern — use normal CALLS */
|
||||
@@ -32,6 +34,16 @@ void cbm_service_patterns_init(void);
|
||||
* "project.venv.requests.api.get"). Import-alias transparent. */
|
||||
cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn);
|
||||
|
||||
/* True for a bare, unqualified call to the native `fetch()` API. Deliberately
|
||||
* NOT part of the substring tables above: those are matched unconditionally
|
||||
* against the raw callee name before/regardless of registry resolution
|
||||
* (the #523 external-library bypass), and "fetch" — unlike "axios" or
|
||||
* "requests" — collides with a plausible local identifier. Callers must
|
||||
* only consult this after registry resolution has come back empty, so a
|
||||
* locally resolvable `function fetch(){}` / `const fetch = () => {}` is
|
||||
* classified via its real resolved QN instead and never reaches this check. */
|
||||
bool cbm_service_pattern_is_global_fetch(const char *callee_name);
|
||||
|
||||
/* Per-worker TLS cache for cbm_service_pattern_match results. The
|
||||
* pattern matcher runs once per resolved CALL edge in emit_service_
|
||||
* edge — that's 6 pattern lists × ~30 patterns × strstr per call ≈
|
||||
|
||||
@@ -506,8 +506,15 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
* HTTP_CALLS/ASYNC_CALLS edge directly (target is a synthesized route
|
||||
* node, not the absent library). Without this the call is dropped and
|
||||
* cross-repo matching finds no edge to match (#523). The parallel path
|
||||
* has the equivalent empty-resolution fallback in resolve_file_calls. */
|
||||
* has the equivalent empty-resolution fallback in resolve_file_calls.
|
||||
*
|
||||
* Native `fetch()` (#856) belongs here too, not in the substring
|
||||
* tables above: it only counts as the global API once resolution has
|
||||
* already failed to find a local/imported `fetch` definition. */
|
||||
cbm_svc_kind_t esvc = cbm_service_pattern_match(call->callee_name);
|
||||
if (esvc == CBM_SVC_NONE && cbm_service_pattern_is_global_fetch(call->callee_name)) {
|
||||
esvc = CBM_SVC_HTTP;
|
||||
}
|
||||
if (esvc == CBM_SVC_HTTP || esvc == CBM_SVC_ASYNC) {
|
||||
const char *u = call->first_string_arg;
|
||||
bool has_url_or_topic = u && u[0] != '\0' &&
|
||||
|
||||
@@ -2299,6 +2299,20 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
emit_service_edge(ws->local_edge_buf, source_node, source_node, call, &fake_res,
|
||||
module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals,
|
||||
imp_count, false);
|
||||
} else if (cbm_service_pattern_is_global_fetch(call->callee_name)) {
|
||||
/* Native `fetch()` (#856): only the global API once resolution
|
||||
* has failed to find a local/imported `fetch`. Call the low-level
|
||||
* emitter directly — emit_service_edge re-derives its own kind
|
||||
* from res->qualified_name via cbm_service_pattern_match, which
|
||||
* "fetch" deliberately never matches (mirrors pass_calls.c). */
|
||||
const char *u = call->first_string_arg;
|
||||
if (u && u[0] != '\0' && (u[0] == '/' || strstr(u, "://") != NULL)) {
|
||||
cbm_resolution_t fake_res = {.qualified_name = call->callee_name,
|
||||
.confidence = PP_HALF_CONF,
|
||||
.strategy = "service_pattern"};
|
||||
emit_http_async_service_edge(ws->local_edge_buf, source_node, call, &fake_res,
|
||||
CBM_SVC_HTTP, u);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1129,6 +1129,146 @@ TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Native `fetch()` (#856), sequential path (< 50 files → pass_calls.c). A bare
|
||||
* unqualified call to the global fetch API has no import and no local
|
||||
* definition anywhere in this project, so registry resolution comes back
|
||||
* empty; classify it as HTTP_CALLS via the same #523-style empty-resolution
|
||||
* fallback used for axios/requests. A member call on an unrelated receiver
|
||||
* (`repo.fetch()`) must NOT be swept in — its callee_name is "repo.fetch",
|
||||
* not the bare "fetch" this check matches exactly. */
|
||||
TEST(pipeline_native_fetch_classified_as_http_calls) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_fetch_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
|
||||
write_temp_file(tmp, "src/api.ts",
|
||||
"export function loadData(): unknown {\n"
|
||||
" return fetch('/api/data');\n"
|
||||
"}\n");
|
||||
/* `repo.fetch()` — a method call, not the global. Must not over-match. */
|
||||
write_temp_file(tmp, "src/repo.ts",
|
||||
"export function useRepo(repo: { fetch: () => unknown }): unknown {\n"
|
||||
" return repo.fetch();\n"
|
||||
"}\n");
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/fetch.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1);
|
||||
/* Exactly the bare call, not the method call too. */
|
||||
ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Native `fetch()` (#856), parallel path (>= 50 files -> pass_parallel.c's
|
||||
* resolve_file_calls). Mirrors pipeline_native_fetch_classified_as_http_calls
|
||||
* but forces the parallel resolver, since the empty-resolution fallback is a
|
||||
* separate implementation there (resolve_file_calls calls
|
||||
* emit_http_async_service_edge directly rather than through emit_service_edge,
|
||||
* which would otherwise re-derive CBM_SVC_NONE for "fetch" and silently fall
|
||||
* through to a plain CALLS edge). */
|
||||
TEST(pipeline_native_fetch_parallel_classified_as_http_calls) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_fetch_par_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
|
||||
write_temp_file(tmp, "src/api.ts",
|
||||
"export function loadData(): unknown {\n"
|
||||
" return fetch('/api/data');\n"
|
||||
"}\n");
|
||||
for (int i = 0; i < 52; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "src/filler%d.ts", i);
|
||||
snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i,
|
||||
i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved = old_workers ? strdup(old_workers) : NULL;
|
||||
cbm_setenv("CBM_WORKERS", "4", 1); /* force parallel regardless of host cores */
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/fetch_par.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Native `fetch()` (#856): a LOCAL `fetch` definition must win over the
|
||||
* global-API guess. Registry resolution finds this project's own top-level
|
||||
* `fetch` function, so the call is a plain CALLS edge to it — never
|
||||
* HTTP_CALLS. Isolated in its own project: the registry's project-wide
|
||||
* unique-name fallback means a local `fetch` anywhere in a project can
|
||||
* legitimately capture bare `fetch()` calls project-wide, so this must not
|
||||
* share a project with the genuine-native-fetch test above. */
|
||||
TEST(pipeline_local_fetch_shadow_not_classified_as_http) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_fetch_shadow_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
|
||||
write_temp_file(tmp, "src/local_fetch.ts",
|
||||
"function fetch(url: string): string {\n"
|
||||
" return 'mock:' + url;\n"
|
||||
"}\n"
|
||||
"export function useLocalFetch(): string {\n"
|
||||
" return fetch('/api/data');\n"
|
||||
"}\n");
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/fetch_shadow.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 0);
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "useLocalFetch", "fetch"));
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Git history pass tests ─────────────────────────────────────── */
|
||||
|
||||
TEST(githistory_is_trackable) {
|
||||
@@ -6660,6 +6800,9 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_incremental_preserves_cross_file_calls);
|
||||
RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge);
|
||||
RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges);
|
||||
RUN_TEST(pipeline_native_fetch_classified_as_http_calls);
|
||||
RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls);
|
||||
RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http);
|
||||
/* Git history pass */
|
||||
RUN_TEST(githistory_is_trackable);
|
||||
RUN_TEST(githistory_compute_coupling);
|
||||
|
||||
Reference in new issue
Block a user