fix(pipeline): compose axios.create baseURL into HTTP_CALLS (#1916)

The Vue-admin wrapper pattern -- `const api = axios.create({ baseURL:
'/api' }); export default api` in one module, `api.get('/orders')` in
another -- produced no HTTP_CALLS edge, so cross-repo-intelligence found
nothing to link to the backend's GET /api/orders.

Root cause: nothing recorded that `api` is an axios instance. The call
`api.get('/orders')` fell through to the route-registration suffix
fallback (`.get` + a '/'-leading argument), which treats it as an Express
`app.get` REGISTRATION: a CALLS edge to a phantom server-side Route
`/orders` (via=route_registration), and never an HTTP_CALLS edge. The
base path was unknowable anyway.

Fix, reusing HTTP_CALLS/Route (no new labels):
- extraction (JS/TS/TSX/ArkTS): a module-level binding initialised by
  `axios.create(...)` records http_client="axios" and the literal
  `baseURL` (NULL when absent or non-literal, never guessed); the Module
  def carries the same pair when its default export is such a client
  (`export default api` / `export default axios.create(...)`). ES default
  imports are flagged (CBMImport.is_default).
- both call resolvers (pass_calls.c, pass_parallel.c), before the #523
  callee-spelling check and the route-registration fallback: a
  `<recv>.<verb>(path)` whose receiver binds -- same file, named import, or
  default import -- to a client emits HTTP_CALLS to base + path (axios
  combineURLs: trailing '/' trimmed, absolute URLs kept as-is, path kept
  when the base is unknown). Shared helper
  cbm_pipeline_http_client_call_url keeps the two paths identical.
- incremental: the client pair joins the file's LSP surface (only for files
  that have one, so every other surface sha is unchanged), so a baseURL
  edit pulls importers into the closure; delta preseed proxies carry the
  two keys so a caller re-resolved alone still sees its unchanged wrapper.
- result_compact relocates the two new strings (ASan caught the dangling
  worker-arena pointer).

Look-alikes (`factory.create({ baseURL })`, a plain object's `.get`) are
unchanged.

Test: pipeline_axios_wrapper_baseurl_composes_http_calls_issue1916 --
default/named/same-file/dynamic-base/look-alike cases, sequential vs
parallel resolver equality, and same-DB closure-repair reindex (wrapper
edit, then caller-only edit) converging with fresh indexes. RED on origin/
main (seq.wrapper_http == 0); each of the parallel, surface and preseed
pieces reverted alone turns its own assertion RED.

Fixes #1916

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-09-25 18:37:16 +02:00
1 parent 5958f546dc
commit b9ad9ed144
13 files changed
+623 -22

No files matched your search

+9
View File
@@ -238,6 +238,14 @@ typedef struct {
* that declared this method. Kept at the tail so zero-initialised
* callers in every other language remain ABI/source compatible. */
const char *impl_trait;
/* JS/TS only (#1916): a module-level binding initialised by
* `axios.create(...)` records the client library here ("axios"), and the
* literal `baseURL` string (or NULL when absent / not a string literal).
* The Module def carries the pair when its default export is such a
* client. The call resolver composes `<binding>.get('/p')` into an
* HTTP_CALLS edge to base + path. Tail fields: zero-init stays valid. */
const char *http_client;
const char *http_base_url;
} CBMDefinition;
/* Argument captured from a call expression */
@@ -299,6 +307,7 @@ typedef struct {
typedef struct {
const char *local_name; // local alias or name
const char *module_path; // resolved module path / QN
bool is_default; // ES default import (`import X from "Y"`), JS/TS only (#1916)
} CBMImport;
typedef enum {
+129
View File
@@ -5744,6 +5744,123 @@ static bool is_require_import_call(TSNode value, const char *source, CBMArena *a
return false;
}
/* True when text of `node` equals `want` exactly (no arena allocation). */
static bool js_node_text_is(TSNode node, const char *source, const char *want) {
if (ts_node_is_null(node)) {
return false;
}
uint32_t start = ts_node_start_byte(node);
uint32_t end = ts_node_end_byte(node);
size_t len = strlen(want);
return end >= start && (size_t)(end - start) == len && memcmp(source + start, want, len) == 0;
}
/* #1916: `axios.create(...)` — the factory of a configured axios instance
* (the vue-element-admin / RuoYi `request.js` wrapper). Only the literal
* `axios` receiver is recognised: a look-alike `factory.create(...)` is not
* an HTTP client and must not turn its binding into one. */
static bool js_is_axios_create_call(TSNode value, const char *source) {
if (ts_node_is_null(value) || strcmp(ts_node_type(value), "call_expression") != 0) {
return false;
}
TSNode fn = ts_node_child_by_field_name(value, TS_FIELD("function"));
if (ts_node_is_null(fn) || strcmp(ts_node_type(fn), "member_expression") != 0) {
return false;
}
TSNode obj = ts_node_child_by_field_name(fn, TS_FIELD("object"));
TSNode prop = ts_node_child_by_field_name(fn, TS_FIELD("property"));
return !ts_node_is_null(obj) && strcmp(ts_node_type(obj), "identifier") == 0 &&
js_node_text_is(obj, source, "axios") && js_node_text_is(prop, source, "create");
}
/* The literal `baseURL` of `axios.create({ baseURL: '<lit>' })`, or NULL when
* the config is absent, not an object literal, or the value is not a plain
* string literal (process.env.X, a template with substitutions, an escape):
* an unknown base is never guessed. */
static const char *js_axios_create_base_url(CBMArena *a, TSNode call, const char *source) {
TSNode args = ts_node_child_by_field_name(call, TS_FIELD("arguments"));
if (ts_node_is_null(args) || ts_node_named_child_count(args) == 0) {
return NULL;
}
TSNode cfg = ts_node_named_child(args, 0);
if (strcmp(ts_node_type(cfg), "object") != 0) {
return NULL;
}
uint32_t n = ts_node_named_child_count(cfg);
for (uint32_t i = 0; i < n; i++) {
TSNode pair = ts_node_named_child(cfg, i);
if (strcmp(ts_node_type(pair), "pair") != 0) {
continue;
}
TSNode key = ts_node_child_by_field_name(pair, TS_FIELD("key"));
if (!js_node_text_is(key, source, "baseURL") &&
!js_node_text_is(key, source, "'baseURL'") &&
!js_node_text_is(key, source, "\"baseURL\"")) {
continue;
}
TSNode val = ts_node_child_by_field_name(pair, TS_FIELD("value"));
if (ts_node_is_null(val) || strcmp(ts_node_type(val), "string") != 0) {
return NULL;
}
char *text = cbm_node_text(a, val, source);
size_t len = text ? strlen(text) : 0;
if (len < PAIR_LEN || strchr(text, '\\') != NULL) {
return NULL;
}
text[len - SKIP_ONE] = '\0';
return text + SKIP_ONE;
}
return NULL;
}
/* Mark `def` as an axios client instance created by `call` (#1916). */
static void js_mark_axios_client(CBMArena *a, CBMDefinition *def, TSNode call, const char *source) {
def->http_client = "axios";
def->http_base_url = js_axios_create_base_url(a, call, source);
}
/* #1916: `export default api;` (api an axios instance declared above) or
* `export default axios.create({...})` makes the MODULE's default export the
* client — record it on the Module def so a default import can find it. */
static void js_mark_default_export_client(CBMExtractCtx *ctx, int mod_idx) {
if (mod_idx < 0 || mod_idx >= ctx->result->defs.count) {
return;
}
TSTreeCursor cursor = ts_tree_cursor_new(ctx->root);
if (!ts_tree_cursor_goto_first_child(&cursor)) {
ts_tree_cursor_delete(&cursor);
return;
}
do {
TSNode stmt = ts_tree_cursor_current_node(&cursor);
if (strcmp(ts_node_type(stmt), "export_statement") != 0) {
continue;
}
TSNode val = ts_node_child_by_field_name(stmt, TS_FIELD("value"));
if (ts_node_is_null(val)) {
continue;
}
CBMDefinition *mod = &ctx->result->defs.items[mod_idx];
if (js_is_axios_create_call(val, ctx->source)) {
js_mark_axios_client(ctx->arena, mod, val, ctx->source);
continue;
}
if (strcmp(ts_node_type(val), "identifier") != 0) {
continue;
}
for (int d = 0; d < ctx->result->defs.count; d++) {
const CBMDefinition *v = &ctx->result->defs.items[d];
if (v->http_client && v->label && strcmp(v->label, "Variable") == 0 && v->name &&
!v->parent_class && js_node_text_is(val, ctx->source, v->name)) {
mod->http_client = v->http_client;
mod->http_base_url = v->http_base_url;
break;
}
}
} while (ts_tree_cursor_goto_next_sibling(&cursor));
ts_tree_cursor_delete(&cursor);
}
// JS/TS variable extraction: skip function-assigned declarators.
static void extract_js_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) {
uint32_t n = ts_node_named_child_count(node);
@@ -5779,7 +5896,12 @@ static void extract_js_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) {
if (is_require) {
continue;
}
int before = ctx->result->defs.count;
push_var_def(ctx, cbm_node_text(a, vname, ctx->source), child);
if (ctx->result->defs.count > before &&
js_is_axios_create_call(value, ctx->source)) {
js_mark_axios_client(a, &ctx->result->defs.items[before], value, ctx->source);
}
}
}
}
@@ -8212,7 +8334,14 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) {
mod.route_method = "GET"; /* a routable page is reached by navigation */
}
}
int mod_idx = ctx->result->defs.count;
cbm_defs_push(&ctx->result->defs, a, mod);
cbm_extract_definitions_without_module(ctx);
if (ctx->language == CBM_LANG_JAVASCRIPT || ctx->language == CBM_LANG_TYPESCRIPT ||
ctx->language == CBM_LANG_TSX || ctx->language == CBM_LANG_ARKTS) {
/* Same language set as extract_js_vars, which marks the bindings. */
js_mark_default_export_client(ctx, mod_idx);
}
}
+1 -1
View File
@@ -394,7 +394,7 @@ static bool process_import_clause(CBMExtractCtx *ctx, TSNode clause, const char
const char *sk = ts_node_type(sub);
if (strcmp(sk, "identifier") == 0) {
char *name = cbm_node_text(a, sub, ctx->source);
CBMImport imp = {.local_name = name, .module_path = path};
CBMImport imp = {.local_name = name, .module_path = path, .is_default = true};
cbm_imports_push(&ctx->result->imports, a, imp);
found = true;
} else if (strcmp(sk, "namespace_import") == 0) {
+2
View File
@@ -273,6 +273,8 @@ static void cr_walk_def(cr_ctx_t *c, CBMDefinition *d) {
cr_str(c, &d->structural_profile);
cr_str(c, &d->body_tokens);
cr_str(c, &d->impl_trait);
cr_str(c, &d->http_client);
cr_str(c, &d->http_base_url);
}
static void cr_walk_call(cr_ctx_t *c, CBMCall *call) {
+25
View File
@@ -125,6 +125,31 @@ static char *surface_file_to_json(const CBMFileResult *result, const CBMLSPDef *
}
yyjson_mut_obj_add_val(doc, root, "reg", reg);
/* #1916: an axios instance binding's baseURL is consumed by the files
* that import it (their HTTP_CALLS compose base + path), so a changed
* base must change the surface or those importers keep stale edges.
* Written only when the file has such a binding: every other file's
* surface bytes — and so its sha — stay exactly what they were. The
* decoder ignores this key; it feeds the early-cutoff hash only. */
yyjson_mut_val *http = NULL;
for (int i = 0; result && i < result->defs.count; i++) {
const CBMDefinition *d = &result->defs.items[i];
if (!d->http_client || !d->qualified_name) {
continue;
}
if (!http) {
http = yyjson_mut_arr(doc);
}
yyjson_mut_val *o = yyjson_mut_obj(doc);
yyjson_mut_obj_add_str(doc, o, "q", d->qualified_name);
yyjson_mut_obj_add_str(doc, o, "c", d->http_client);
add_str_or_null(doc, o, "b", d->http_base_url);
yyjson_mut_arr_add_val(http, o);
}
if (http) {
yyjson_mut_obj_add_val(doc, root, "http", http);
}
char *json = yyjson_mut_write(doc, 0, out_len);
yyjson_mut_doc_free(doc);
return json;
+189 -7
View File
@@ -204,6 +204,169 @@ static void free_import_map(const char **keys, const char **vals, int count) {
}
}
/* ── #1916: axios client-instance calls ─────────────────────────── */
/* Copy the plain (unescaped) JSON string value of "key" in props into out.
* False when absent, escaped, or too long — an unreadable base is unknown. */
static bool pc_json_str_prop(const char *props, const char *key, char *out, size_t out_sz) {
if (!props) {
return false;
}
char pat[CBM_SZ_64];
snprintf(pat, sizeof(pat), "\"%s\":\"", key);
const char *p = strstr(props, pat);
if (!p) {
return false;
}
p += strlen(pat);
const char *e = strchr(p, '"');
if (!e) {
return false;
}
size_t len = (size_t)(e - p);
if (len >= out_sz || memchr(p, '\\', len) != NULL) {
return false;
}
memcpy(out, p, len);
out[len] = '\0';
return true;
}
/* A node the extractor marked as an axios instance. `base` receives the
* literal baseURL ("" when unknown). */
static bool pc_node_is_http_client(const cbm_gbuf_node_t *n, char *base, size_t base_sz) {
char client[CBM_SZ_32];
if (!n || !pc_json_str_prop(n->properties_json, "http_client", client, sizeof(client))) {
return false;
}
if (!pc_json_str_prop(n->properties_json, "http_base_url", base, base_sz)) {
base[0] = '\0';
}
return true;
}
/* The module-level Variable `name` declared in file `file_rel`. */
static const cbm_gbuf_node_t *pc_module_var(const cbm_gbuf_t *gbuf, const char *project,
const char *file_rel, const char *name) {
if (!file_rel) {
return NULL;
}
char *qn = cbm_pipeline_fqn_compute(project, file_rel, name);
const cbm_gbuf_node_t *n = qn ? cbm_gbuf_find_by_qn(gbuf, qn) : NULL;
free(qn);
return (n && n->label && strcmp(n->label, "Variable") == 0) ? n : NULL;
}
/* Resolve the binding `recv` names in the calling file to a client node:
* a same-file declaration first, then its ES import (default import → the
* module's default export; named import → that module's Variable). */
static bool pc_receiver_client_base(const cbm_gbuf_t *gbuf, const char *project, const char *rel,
const CBMFileResult *result, const char **imp_keys,
const char **imp_vals, int imp_count, const char *recv,
char *base, size_t base_sz) {
if (pc_node_is_http_client(pc_module_var(gbuf, project, rel, recv), base, base_sz)) {
return true;
}
const CBMImport *imp = NULL;
for (int i = 0; result && i < result->imports.count; i++) {
const CBMImport *it = &result->imports.items[i];
if (it->local_name && strcmp(it->local_name, recv) == 0) {
imp = it;
break;
}
}
if (!imp) {
return false;
}
for (int i = 0; i < imp_count; i++) {
if (!imp_keys[i] || strcmp(imp_keys[i], recv) != 0 || !imp_vals[i]) {
continue;
}
const cbm_gbuf_node_t *t = cbm_gbuf_find_by_qn(gbuf, imp_vals[i]);
if (!t || !t->label) {
return false;
}
if (strcmp(t->label, "Variable") == 0) {
return pc_node_is_http_client(t, base, base_sz);
}
if (strcmp(t->label, "Module") != 0) {
return false;
}
if (imp->is_default) {
return pc_node_is_http_client(t, base, base_sz);
}
return pc_node_is_http_client(pc_module_var(gbuf, project, t->file_path, recv), base,
base_sz);
}
return false;
}
/* axios instance request methods (the verb after the receiver). */
static bool pc_is_axios_verb(const char *verb) {
static const char *const verbs[] = {"get", "post", "put", "delete",
"patch", "head", "options", NULL};
for (int i = 0; verbs[i]; i++) {
if (strcmp(verb, verbs[i]) == 0) {
return true;
}
}
return false;
}
/* Copy the plain-identifier receiver of `callee` (text before `dot`) into
* recv. False for `this.api.get`, `a().get` and other non-binding receivers. */
static bool pc_plain_receiver(const char *callee, const char *dot, char *recv, size_t recv_sz) {
size_t rlen = (size_t)(dot - callee);
if (rlen == 0 || rlen >= recv_sz) {
return false;
}
for (size_t i = 0; i < rlen; i++) {
char c = callee[i];
bool ident = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
c == '_' || c == '$';
if (!ident) {
return false;
}
}
memcpy(recv, callee, rlen);
recv[rlen] = '\0';
return true;
}
bool cbm_pipeline_http_client_call_url(const cbm_gbuf_t *gbuf, const char *project, const char *rel,
const CBMFileResult *result, const char **imp_keys,
const char **imp_vals, int imp_count, const CBMCall *call,
char *out, size_t out_sz) {
const char *callee = call ? call->callee_name : NULL;
const char *url = call ? call->first_string_arg : NULL;
if (!gbuf || !project || !callee || !url || (url[0] != '/' && !strstr(url, "://"))) {
return false;
}
const char *dot = strrchr(callee, '.');
char recv[CBM_SZ_128];
if (!dot || !pc_is_axios_verb(dot + SKIP_ONE) ||
!pc_plain_receiver(callee, dot, recv, sizeof(recv))) {
return false;
}
char base[CBM_SZ_256];
if (!pc_receiver_client_base(gbuf, project, rel, result, imp_keys, imp_vals, imp_count, recv,
base, sizeof(base))) {
return false;
}
/* axios combineURLs: an absolute request URL ignores baseURL; otherwise
* the base (trailing '/' trimmed) is joined with the '/'-leading path. */
if (strstr(url, "://") || base[0] == '\0') {
int n = snprintf(out, out_sz, "%s", url);
return n > 0 && (size_t)n < out_sz;
}
size_t blen = strlen(base);
while (blen > 0 && base[blen - SKIP_ONE] == '/') {
base[--blen] = '\0';
}
int n = snprintf(out, out_sz, "%s%s", base, url);
return n > 0 && (size_t)n < out_sz;
}
/* Handle a route registration call: create Route node + HANDLES edge. */
static void handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
const cbm_gbuf_node_t *source_node, const char *module_qn,
@@ -475,10 +638,10 @@ static const cbm_gbuf_node_t *calls_find_source(cbm_pipeline_ctx_t *ctx, const c
}
/* Resolve one call and emit the appropriate edge. Returns 1 if resolved, 0 if not. */
static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
const CBMResolvedCallArray *lsp_calls, const char *rel,
const char *module_qn, const char **imp_keys, const char **imp_vals,
int imp_count, CBMLanguage lang) {
static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBMFileResult *result,
const char *rel, const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count, CBMLanguage lang) {
const CBMResolvedCallArray *lsp_calls = &result->resolved_calls;
const cbm_gbuf_node_t *source_node = calls_find_source(ctx, rel, call->enclosing_func_qn);
if (!source_node) {
return 0;
@@ -527,7 +690,26 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
* and not an HTTP pattern, so BOTH the empty-resolution and resolved-QN
* service checks below miss it and the call is dropped. Detect it on the
* callee_name FIRST so the HTTP_CALLS/ASYNC_CALLS edge is emitted regardless
* (target is a synthesized route node, not the unindexed library). (#523) */
* (target is a synthesized route node, not the unindexed library). (#523)
*
* First, though: `api.get('/orders')` on an axios.create() instance (#1916).
* The receiver's binding decides, not its spelling — without this the
* verb suffix made it an Express route REGISTRATION below (a CALLS edge
* to a phantom server route), and a receiver spelled `axiosInstance`
* substring-matches the #523 check and loses its baseURL. */
char client_url[CBM_SZ_512];
if (cbm_pipeline_http_client_call_url(ctx->gbuf, ctx->project_name, rel, result, imp_keys,
imp_vals, imp_count, call, client_url,
sizeof(client_url))) {
CBMCall routed = *call;
routed.first_string_arg = client_url;
cbm_resolution_t svc_res = {.qualified_name = call->callee_name,
.confidence = PC_SVC_PATTERN_CONF,
.strategy = "http_client_instance",
.candidate_count = 0};
emit_http_async_edge(ctx, &routed, source_node, NULL, &svc_res, CBM_SVC_HTTP, false);
return SKIP_ONE;
}
cbm_svc_kind_t csvc = cbm_service_pattern_match(call->callee_name);
if (csvc == CBM_SVC_HTTP || csvc == CBM_SVC_ASYNC) {
const char *cu = call->first_string_arg;
@@ -841,8 +1023,8 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
continue;
}
total_calls++;
if (resolve_single_call(ctx, call, &result->resolved_calls, rel, module_qn, imp_keys,
imp_vals, imp_count, files[i].language)) {
if (resolve_single_call(ctx, call, result, rel, module_qn, imp_keys, imp_vals,
imp_count, files[i].language)) {
resolved++;
} else {
unresolved++;
+2
View File
@@ -294,6 +294,8 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
append_json_str_array(buf, bufsize, &pos, "param_types", def->param_types);
append_json_string(buf, bufsize, &pos, "route_path", def->route_path);
append_json_string(buf, bufsize, &pos, "route_method", def->route_method);
append_json_string(buf, bufsize, &pos, "http_client", def->http_client);
append_json_string(buf, bufsize, &pos, "http_base_url", def->http_base_url);
/* MinHash fingerprint — append if present and buffer has room. */
if (def->fingerprint && def->fingerprint_k > 0 &&
+18 -1
View File
@@ -522,6 +522,8 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
append_json_str_array(buf, bufsize, &pos, "param_types", def->param_types);
append_json_string(buf, bufsize, &pos, "route_path", def->route_path);
append_json_string(buf, bufsize, &pos, "route_method", def->route_method);
append_json_string(buf, bufsize, &pos, "http_client", def->http_client);
append_json_string(buf, bufsize, &pos, "http_base_url", def->http_base_url);
/* MinHash fingerprint — append if present and buffer has room.
* Hex-encoded K=64 uint32 = 512 chars + key/quotes ≈ 520 chars. */
@@ -2966,7 +2968,22 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
* pattern, so the resolved-QN service checks below miss it and the call
* is dropped. Detect it on the callee_name FIRST so the HTTP_CALLS/
* ASYNC_CALLS edge is emitted regardless (target is a synthesized route
* node, not the unindexed library). Mirrors pass_calls.c. (#523) */
* node, not the unindexed library). Mirrors pass_calls.c. (#523)
*
* First: a call on an axios.create() instance (#1916) — decided by the
* receiver's binding, before the #523 spelling check and the
* route-registration suffix fallback. MUST match pass_calls.c. */
char client_url[CBM_SZ_512];
if (cbm_pipeline_http_client_call_url(rc->main_gbuf, rc->project_name, rel, result,
imp_keys, imp_vals, imp_count, call, client_url,
sizeof(client_url))) {
cbm_resolution_t svc_res = {.qualified_name = call->callee_name,
.confidence = PP_HALF_CONF,
.strategy = "http_client_instance"};
emit_http_async_service_edge(ws->local_edge_buf, source_node, call, &svc_res,
CBM_SVC_HTTP, client_url);
continue;
}
cbm_svc_kind_t csvc = cbm_service_pattern_match(call->callee_name);
if (csvc == CBM_SVC_HTTP || csvc == CBM_SVC_ASYNC) {
const char *cu = call->first_string_arg;
+1 -1
View File
@@ -1180,7 +1180,7 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c
}
str_def_fp += def->fingerprint ? (size_t)def->fingerprint_k * sizeof(uint32_t) : 0;
str_def_misc += census_len(def->route_path) + census_len(def->route_method) +
census_len(def->impl_trait);
census_len(def->impl_trait) + census_len(def->http_base_url);
}
for (int c = 0; c < r->calls.count; c++) {
const CBMCall *call = &r->calls.items[c];
+21 -7
View File
@@ -287,13 +287,25 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
* UNIQUE violation the pre-remap patch would have raised. A resolver that
* only LOOKS UP still needs its target resident, which is why the list
* mirrors the registry's own membership rule instead of guessing. */
/* The one property a resolver READS off a proxy: an axios instance
* binding's client + baseURL (#1916). A caller re-resolved alone must
* compose `api.get('/p')` against its unchanged wrapper exactly as a full
* build does. Only the two keys, only on the rare Module/Variable rows
* that carry them; every other proxy stays "{}". Proxies are never
* written back (cbm_delta_patch skips id <= max_db_id). */
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(db,
"SELECT id, label, name, qualified_name, file_path FROM nodes"
" WHERE project = ?1 AND label NOT IN"
" ('Macro','Comment','Section','Branch','Commit','Tag')"
" ORDER BY id",
CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(
db,
"SELECT id, label, name, qualified_name, file_path,"
" CASE WHEN label IN ('Module','Variable')"
" AND instr(properties, '\"http_client\"') > 0"
" THEN json_object('http_client', json_extract(properties, '$.http_client'),"
" 'http_base_url', json_extract(properties, '$.http_base_url')) END"
" FROM nodes"
" WHERE project = ?1 AND label NOT IN"
" ('Macro','Comment','Section','Branch','Commit','Tag')"
" ORDER BY id",
CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
return -1;
}
sqlite3_bind_text(stmt, 1, project, CBM_NOT_FOUND, SQLITE_TRANSIENT);
@@ -305,9 +317,11 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
const char *name = (const char *)sqlite3_column_text(stmt, 2);
const char *qn = (const char *)sqlite3_column_text(stmt, 3);
const char *fp = (const char *)sqlite3_column_text(stmt, 4);
const char *client_props = (const char *)sqlite3_column_text(stmt, 5);
/* Pin the gbuf id to the database id: proxies ARE their rows. */
cbm_gbuf_set_next_id(gbuf, id);
int64_t got = cbm_gbuf_upsert_node(gbuf, label, name, qn, fp ? fp : "", 0, 0, "{}");
int64_t got = cbm_gbuf_upsert_node(gbuf, label, name, qn, fp ? fp : "", 0, 0,
client_props ? client_props : "{}");
if (got != id) {
/* A QN collision inside the preseed set would silently split
* identity between RAM and disk; the run cannot be trusted. */
+15
View File
@@ -211,6 +211,21 @@ void cbm_pipeline_set_pkgmap(CBMHashTable *map);
char *cbm_pipeline_resolve_module(const cbm_pipeline_ctx_t *ctx, const char *source_rel,
const char *module_path);
/* #1916: HTTP client-instance calls. When `call` is `<recv>.<verb>(url)` with
* an HTTP verb suffix and a path/URL first argument, and `recv` is bound —
* in the calling module itself, or via an ES import (named or default) — to a
* binding the extractor marked as an `axios.create(...)` instance
* (`http_client` node property), write the request URL to `out` (the
* instance's literal `http_base_url` joined with a '/'-leading path, the path
* unchanged when the base is unknown or the URL is absolute) and return true.
* Both call resolvers (pass_calls.c, pass_parallel.c) call this before any
* route-registration or registry fallback, so a wrapper client's `api.get`
* is never mistaken for an Express `app.get` route registration. */
bool cbm_pipeline_http_client_call_url(const cbm_gbuf_t *gbuf, const char *project, const char *rel,
const CBMFileResult *result, const char **imp_keys,
const char **imp_vals, int imp_count, const CBMCall *call,
char *out, size_t out_sz);
/* Resolve an import to its in-graph target node, or NULL if unresolvable.
*
* Resolution order (first hit wins):
+6 -5
View File
@@ -181,11 +181,12 @@ TEST(extract_ts_factory_object_methods_issue341) {
*
* The bound is derived, not tuned. Measured on this source, total_alloc was
* 365984 before the scratch arena and is 87456 after, exactly that difference.
* Of the 87456 that remain, 7680 is the defs item array at GROW_ARRAY's
* starting capacity of 32 times sizeof(CBMDefinition) 240, and the other 79776
* is everything else this file's extraction interns; none of it is traversal
* scratch. So the bound sits above 87456 with room and a factor of four below
* 365984.
* #1916 added two pointers to CBMDefinition (240 -> 256 bytes), so it now
* measures 87968: 8192 is the defs item array at GROW_ARRAY's starting
* capacity of 32 times sizeof(CBMDefinition) 256, and the other 79776 is
* everything else this file's extraction interns; none of it is traversal
* scratch. So the bound sits above 87968 with room and a factor of four below
* the 365984 the scratch stacks cost.
*
* It is a byte budget, not a proof of lifetime; that is
* extract_traversal_stacks_come_from_ctx_scratch_issue2010 in test_mem.c. */
+205
View File
@@ -5585,6 +5585,210 @@ TEST(pipeline_incremental_parallel_result_cache_alloc_failure_preserves_db_and_r
}
#endif
/* #1916: one index of the axios-wrapper fixture, reduced to the edge counts
* the test asserts. */
typedef struct {
int run_rc;
int wrapper_http; /* loadWrapperOrders -HTTP_CALLS-> <base>/orders */
int wrapper_http_v3; /* same call after the wrapper's base moves to /v3 */
int wrapper_http_v3_inv; /* ... and then the caller's path moves to /invoices */
int wrapper_raw_http; /* loadWrapperOrders -HTTP_CALLS-> /orders (uncomposed) */
int wrapper_route_reg; /* loadWrapperOrders -CALLS-> /orders (phantom route reg) */
int named_http; /* named import, base with trailing '/' */
int local_http; /* same-file instance spelled axiosInstance */
int local_raw_http; /* ... uncomposed (the #523 spelling match) */
int dynamic_http; /* non-literal baseURL: path kept, never guessed */
int dynamic_route_reg; /* ... misread as a route registration */
int direct_http; /* control: axios.get('/direct') */
int fake_http; /* look-alike factory.create: never a client */
int provider_handles; /* control: Express app.get stays a registration */
} AxiosWrapperObs;
static AxiosWrapperObs observe_axios_wrapper(const char *repo, const char *db) {
AxiosWrapperObs o;
memset(&o, 0xff, sizeof(o)); /* -1 everywhere: "not observed" */
cbm_pipeline_t *p = cbm_pipeline_new(repo, db, CBM_MODE_FULL);
if (!p) {
return o;
}
o.run_rc = cbm_pipeline_run(p);
const char *proj = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db);
if (s && proj) {
o.wrapper_http =
named_edge_count(s, proj, "HTTP_CALLS", "loadWrapperOrders", "/api/orders");
o.wrapper_http_v3 =
named_edge_count(s, proj, "HTTP_CALLS", "loadWrapperOrders", "/v3/orders");
o.wrapper_http_v3_inv =
named_edge_count(s, proj, "HTTP_CALLS", "loadWrapperOrders", "/v3/invoices");
o.wrapper_raw_http =
named_edge_count(s, proj, "HTTP_CALLS", "loadWrapperOrders", "/orders");
o.wrapper_route_reg = named_edge_count(s, proj, "CALLS", "loadWrapperOrders", "/orders");
o.named_http = named_edge_count(s, proj, "HTTP_CALLS", "loadNamedItems", "/v2/items");
o.local_http = named_edge_count(s, proj, "HTTP_CALLS", "loadLocal", "/local/x");
o.local_raw_http = named_edge_count(s, proj, "HTTP_CALLS", "loadLocal", "/x");
o.dynamic_http = named_edge_count(s, proj, "HTTP_CALLS", "loadDynamic", "/dynamic");
o.dynamic_route_reg = named_edge_count(s, proj, "CALLS", "loadDynamic", "/dynamic");
o.direct_http = named_edge_count(s, proj, "HTTP_CALLS", "loadDirectOrders", "/direct");
o.fake_http = named_edge_count(s, proj, "HTTP_CALLS", "loadFake", "/api/fake");
o.provider_handles = named_edge_count(s, proj, "HANDLES", "provideOrders", "/api/orders");
}
if (s) {
cbm_store_close(s);
}
cbm_pipeline_free(p);
return o;
}
/* #1916: `const api = axios.create({ baseURL: '/api' }); export default api`
* in one module and `api.get('/orders')` in another is THE Vue-admin wrapper
* pattern. The call must become HTTP_CALLS to GET /api/orders — before the
* fix the `.get` suffix made it an Express route REGISTRATION (a CALLS edge
* to a phantom server route /orders) and cross-repo matching found nothing.
* 52 fillers put the default run on the parallel resolver; the
* single-thread run is the sequential oracle; a same-DB reindex after the
* wrapper's base changes must converge with the fresh index. */
TEST(pipeline_axios_wrapper_baseurl_composes_http_calls_issue1916) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_axios_wrapper_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "src/client.ts",
"import axios from 'axios';\n"
"const api = axios.create({ baseURL: '/api', timeout: 5000 });\n"
"export default api;\n");
write_temp_file(
tmp, "src/orders.ts",
"import api from './client';\n"
"import axios from 'axios';\n"
"export function loadWrapperOrders(): unknown { return api.get('/orders'); }\n"
"export function loadDirectOrders(): unknown { return axios.get('/direct'); }\n");
write_temp_file(tmp, "src/named-client.ts",
"import axios from 'axios';\n"
"export const http = axios.create({ baseURL: '/v2/' });\n");
write_temp_file(tmp, "src/named-caller.ts",
"import { http } from './named-client';\n"
"export function loadNamedItems(): unknown { return http.post('/items'); }\n");
write_temp_file(tmp, "src/local.ts",
"import axios from 'axios';\n"
"const axiosInstance = axios.create({ baseURL: '/local' });\n"
"export function loadLocal(): unknown { return axiosInstance.get('/x'); }\n");
write_temp_file(tmp, "src/dynamic-client.ts",
"import axios from 'axios';\n"
"const dynamicApi = axios.create({ baseURL: process.env.API_BASE });\n"
"export default dynamicApi;\n");
write_temp_file(
tmp, "src/dynamic-caller.ts",
"import dynamicApi from './dynamic-client';\n"
"export function loadDynamic(): unknown { return dynamicApi.get('/dynamic'); }\n");
write_temp_file(tmp, "src/fake-client.ts",
"const factory = { create: (config: unknown) => config };\n"
"const fakeApi = factory.create({ baseURL: '/api' });\n"
"export default fakeApi;\n");
write_temp_file(tmp, "src/fake-caller.ts",
"import fakeApi from './fake-client';\n"
"export function loadFake(): unknown { return fakeApi.get('/fake'); }\n");
write_temp_file(tmp, "src/provider.ts",
"import express from 'express';\n"
"const app = express();\n"
"function provideOrders(): void {}\n"
"app.get('/api/orders', provideOrders);\n");
for (int i = 0; i < 52; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "src/wrapper_pad_%02d.ts", i);
snprintf(body, sizeof(body), "export function wrapperPad%02d(): number { return %d; }\n", i,
i);
write_temp_file(tmp, name, body);
}
char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
char *saved_single = old_single ? strdup(old_single) : NULL;
char db_seq[512];
char db_par[512];
char db_fresh[512];
char db_fresh2[512];
snprintf(db_seq, sizeof(db_seq), "%s/seq.db", tmp);
snprintf(db_par, sizeof(db_par), "%s/par.db", tmp);
snprintf(db_fresh, sizeof(db_fresh), "%s/fresh.db", tmp);
snprintf(db_fresh2, sizeof(db_fresh2), "%s/fresh2.db", tmp);
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
AxiosWrapperObs seq = observe_axios_wrapper(tmp, db_seq);
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
cbm_setenv("CBM_WORKERS", "4", 1);
AxiosWrapperObs par = observe_axios_wrapper(tmp, db_par);
/* Only the wrapper changes; the importing caller is untouched. */
write_temp_file(tmp, "src/client.ts",
"import axios from 'axios';\n"
"const api = axios.create({ baseURL: '/v3' });\n"
"export default api;\n");
AxiosWrapperObs incr = observe_axios_wrapper(tmp, db_par);
AxiosWrapperObs fresh = observe_axios_wrapper(tmp, db_fresh);
/* Then only the caller changes; the (unchanged) wrapper's base must
* still be found when the caller alone is re-resolved. */
write_temp_file(
tmp, "src/orders.ts",
"import api from './client';\n"
"import axios from 'axios';\n"
"export function loadWrapperOrders(): unknown { return api.get('/invoices'); }\n"
"export function loadDirectOrders(): unknown { return axios.get('/direct'); }\n");
AxiosWrapperObs incr2 = observe_axios_wrapper(tmp, db_par);
AxiosWrapperObs fresh2 = observe_axios_wrapper(tmp, db_fresh2);
if (saved_workers) {
cbm_setenv("CBM_WORKERS", saved_workers, 1);
free(saved_workers);
} else {
cbm_unsetenv("CBM_WORKERS");
}
if (saved_single) {
cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1);
free(saved_single);
} else {
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
}
th_rmtree(tmp);
ASSERT_EQ(seq.run_rc, 0);
ASSERT_EQ(par.run_rc, 0);
/* Controls: direct axios and an Express registration are unchanged, and
* a look-alike factory never becomes a client. */
ASSERT_EQ(seq.direct_http, 1);
ASSERT_EQ(seq.provider_handles, 1);
ASSERT_EQ(seq.fake_http, 0);
/* The issue: default-imported wrapper composes base + path. */
ASSERT_EQ(seq.wrapper_http, 1);
ASSERT_EQ(seq.wrapper_raw_http, 0);
ASSERT_EQ(seq.wrapper_route_reg, 0);
ASSERT_EQ(seq.named_http, 1);
ASSERT_EQ(seq.local_http, 1);
ASSERT_EQ(seq.local_raw_http, 0);
ASSERT_EQ(seq.dynamic_http, 1);
ASSERT_EQ(seq.dynamic_route_reg, 0);
/* Sequential and parallel resolvers agree edge for edge. */
ASSERT_MEM_EQ(&seq, &par, sizeof(seq));
/* Reindex after the base moved converges with a fresh index. */
ASSERT_EQ(fresh.run_rc, 0);
ASSERT_EQ(fresh.wrapper_http_v3, 1);
ASSERT_EQ(fresh.wrapper_http, 0);
ASSERT_EQ(incr.run_rc, 0);
ASSERT_MEM_EQ(&incr, &fresh, sizeof(fresh));
ASSERT_EQ(fresh2.run_rc, 0);
ASSERT_EQ(fresh2.wrapper_http_v3_inv, 1);
ASSERT_EQ(fresh2.wrapper_http_v3, 0);
ASSERT_MEM_EQ(&incr2, &fresh2, sizeof(fresh2));
PASS();
}
TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_tsjs_recv_XXXXXX");
@@ -15115,6 +15319,7 @@ SUITE(pipeline) {
RUN_TEST(pipeline_incremental_parallel_result_cache_alloc_failure_preserves_db_and_retries);
#endif
RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_axios_wrapper_baseurl_composes_http_calls_issue1916);
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_python_receiver_keeps_specific_unique_name_member_call);
RUN_TEST(pipeline_html_embedded_member_call_stays_unbound);