mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(pipeline): classify calls through imports of external client libraries
A call no project symbol resolves, whose callee an import binds to an
external library, is now spelled through that import and classified with
the existing service patterns: `send` under
`from paho.mqtt import publish as send` is paho.mqtt.publish (ASYNC_CALLS
to its topic), `navigate` under `import { get as navigate } from 'axios'`
is axios.get (a typed GET HTTP_CALLS edge). Before, such calls had no
service edge at all.
- CBMImport gains `member`: the exported name an ES named import binds
(`get` in `{ get as navigate }`), NULL for default, namespace and other
imports and for every other language. Filled by process_named_imports,
carried through result compaction (and so through spill), counted in the
extraction census with the other import strings.
- One helper, cbm_pipeline_external_import_service, serves both call
resolvers: a per-file index (local name -> binding import), built on the
first unresolved call that asks, then one hash lookup per call.
- Precision: only unresolved calls, after the callee-name, route-suffix and
global-fetch fallbacks; never a relative import, an import an IMPORTS
edge materialized (a project module), a name bound by two different
imports, or a Python callee rebound as a parameter. An HTTP edge needs a
request verb in the spelled name, so urllib.parse.urljoin is not a
request. Python, JavaScript, TypeScript and TSX.
Tests: extraction_imports covers the member and its survival through
compaction; a pipeline test runs both resolvers over a typed GET, a typed
POST and a publish through aliases, and over controls that must stay
silent (urljoin, lodash and json aliases, a local definition and a
parameter shadowing the import, a materialized project package named
httpx). Each guard was reverted once and its control went red.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
79bbb6ee33
commit
3248591c1e
9 files changed
+516
-8
No files matched your search
@@ -386,6 +386,8 @@ typedef enum {
|
||||
typedef struct {
|
||||
const char *local_name; // local alias or name
|
||||
const char *module_path; // resolved module path / QN
|
||||
const char *member; // ES named import: the exported name the local binds (`get` in
|
||||
// `import { get as navigate } from "axios"`); NULL otherwise
|
||||
bool is_default; // ES default import (`import X from "Y"`), JS/TS only (#1916)
|
||||
CBMImportKind kind; // CBM_IMPORT_KIND_DEFAULT unless the syntax names a member kind
|
||||
} CBMImport;
|
||||
|
||||
@@ -377,9 +377,12 @@ static bool process_named_imports(CBMExtractCtx *ctx, TSNode sub, const char *pa
|
||||
orig = ts_node_child(imp_spec, 0);
|
||||
}
|
||||
if (!ts_node_is_null(orig)) {
|
||||
char *local_name = !ts_node_is_null(local) ? cbm_node_text(a, local, ctx->source)
|
||||
: cbm_node_text(a, orig, ctx->source);
|
||||
CBMImport imp = {.local_name = local_name, .module_path = path};
|
||||
char *member = cbm_node_text(a, orig, ctx->source);
|
||||
char *local_name =
|
||||
!ts_node_is_null(local) ? cbm_node_text(a, local, ctx->source) : member;
|
||||
/* The member the local binds survives an alias: a call through
|
||||
* `navigate` from `{ get as navigate }` is the module's `get`. */
|
||||
CBMImport imp = {.local_name = local_name, .module_path = path, .member = member};
|
||||
cbm_imports_push(&ctx->result->imports, a, imp);
|
||||
found = true;
|
||||
}
|
||||
|
||||
@@ -316,6 +316,7 @@ static void cr_walk(cr_ctx_t *c, CBMFileResult *r) {
|
||||
for (int i = 0; i < r->imports.count && r->imports.items; i++) {
|
||||
cr_str(c, &r->imports.items[i].local_name);
|
||||
cr_str(c, &r->imports.items[i].module_path);
|
||||
cr_str(c, &r->imports.items[i].member);
|
||||
}
|
||||
cr_array(c, (void **)&r->usages.items, r->usages.count, sizeof(CBMUsage));
|
||||
for (int i = 0; i < r->usages.count && r->usages.items; i++) {
|
||||
|
||||
+201
-2
@@ -379,6 +379,185 @@ bool cbm_pipeline_http_client_call_url(const cbm_gbuf_t *gbuf, const char *proje
|
||||
return n > 0 && (size_t)n < out_sz;
|
||||
}
|
||||
|
||||
/* ── Calls through an import of an external library ─────────────────
|
||||
* See cbm_pipeline_external_import_service in pipeline_internal.h. */
|
||||
|
||||
/* Index value for a name no external binding may answer for: a project or
|
||||
* relative import, or a name two imports bind differently. */
|
||||
static const char pc_import_blocked[] = "";
|
||||
|
||||
static bool pc_external_import_lang(CBMLanguage lang) {
|
||||
return lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT ||
|
||||
lang == CBM_LANG_TSX;
|
||||
}
|
||||
|
||||
/* Whether an import can name an external module at all: a relative path
|
||||
* never does, nor (JS/TS) an absolute path or a project-root alias. */
|
||||
static bool pc_import_may_be_external(CBMLanguage lang, const CBMImport *imp) {
|
||||
const char *m = imp->module_path;
|
||||
if (!m || !m[0] || m[0] == '.' || strstr(m, " as ")) {
|
||||
return false;
|
||||
}
|
||||
if (lang == CBM_LANG_PYTHON) {
|
||||
return true;
|
||||
}
|
||||
return m[0] != '/' && m[0] != '~' && m[0] != '#' && strncmp(m, "@/", PAIR_LEN) != 0;
|
||||
}
|
||||
|
||||
/* Two imports bind the same name to the same thing. */
|
||||
static bool pc_same_binding(const CBMImport *a, const CBMImport *b) {
|
||||
return a->is_default == b->is_default && strcmp(a->module_path, b->module_path) == 0 &&
|
||||
(a->member == b->member ||
|
||||
(a->member && b->member && strcmp(a->member, b->member) == 0));
|
||||
}
|
||||
|
||||
/* The callee as its import makes it; false when the import adds nothing to
|
||||
* the callee's own spelling.
|
||||
* Python binds `local` to module_path: `from m import x as y` and
|
||||
* `import m.x as y` both store y -> m.x, so `y.f` is m.x.f. A plain
|
||||
* `import a.b` binds the root `a` and the callee already spells its path.
|
||||
* JS/TS: a named import binds the module's member, alias or not
|
||||
* (`navigate` under `import { get as navigate } from 'axios'` is
|
||||
* axios.get); a default or namespace import binds the module itself, so
|
||||
* `ax.get` is axios.get. */
|
||||
static bool pc_spell_through_import(CBMLanguage lang, const CBMImport *imp, const char *callee,
|
||||
size_t root_len, char *out, size_t out_sz) {
|
||||
static const char node_scheme[] = "node:";
|
||||
const char *module = imp->module_path;
|
||||
const char *rest = callee + root_len;
|
||||
int n;
|
||||
if (lang == CBM_LANG_PYTHON) {
|
||||
size_t local_len = strlen(imp->local_name);
|
||||
if (strncmp(module, imp->local_name, local_len) == 0 &&
|
||||
(module[local_len] == '\0' || module[local_len] == '.')) {
|
||||
return false;
|
||||
}
|
||||
n = snprintf(out, out_sz, "%s%s", module, rest);
|
||||
} else {
|
||||
if (strncmp(module, node_scheme, sizeof(node_scheme) - SKIP_ONE) == 0) {
|
||||
module += sizeof(node_scheme) - SKIP_ONE;
|
||||
}
|
||||
n = imp->member ? snprintf(out, out_sz, "%s.%s%s", module, imp->member, rest)
|
||||
: snprintf(out, out_sz, "%s%s", module, rest);
|
||||
}
|
||||
return n > 0 && (size_t)n < out_sz;
|
||||
}
|
||||
|
||||
/* Index every name the file's imports bind; then block each one an IMPORTS
|
||||
* edge materialized (a project module, whatever its spelling) and each one
|
||||
* bound twice to different things. */
|
||||
static void pc_external_import_index_build(cbm_external_import_index_t *index,
|
||||
const cbm_gbuf_t *gbuf, const char *project,
|
||||
const char *rel, CBMLanguage lang,
|
||||
const CBMImportArray *imports) {
|
||||
index->built = true;
|
||||
if (!imports || imports->count <= 0) {
|
||||
return;
|
||||
}
|
||||
index->by_local = cbm_ht_create((uint32_t)imports->count * PAIR_LEN + CBM_SZ_8);
|
||||
if (!index->by_local) {
|
||||
return;
|
||||
}
|
||||
void *blocked = (void *)pc_import_blocked;
|
||||
for (int i = 0; i < imports->count; i++) {
|
||||
const CBMImport *imp = &imports->items[i];
|
||||
if (!imp->local_name || !imp->local_name[0]) {
|
||||
continue;
|
||||
}
|
||||
const CBMImport *prev = cbm_ht_get(index->by_local, imp->local_name);
|
||||
bool external = pc_import_may_be_external(lang, imp);
|
||||
if (!prev) {
|
||||
cbm_ht_set(index->by_local, imp->local_name, external ? (void *)imp : blocked);
|
||||
} else if ((const void *)prev != blocked && (!external || !pc_same_binding(prev, imp))) {
|
||||
cbm_ht_set(index->by_local, imp->local_name, blocked);
|
||||
}
|
||||
}
|
||||
const cbm_gbuf_node_t *file_node = pc_find_by_computed_qn(gbuf, project, rel, "__file__");
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int edge_count = 0;
|
||||
if (!file_node || cbm_gbuf_find_edges_by_source_type(gbuf, file_node->id, "IMPORTS", &edges,
|
||||
&edge_count) != 0) {
|
||||
return;
|
||||
}
|
||||
static const char key_open[] = "\"local_name\":\"";
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
const char *start =
|
||||
edges[i]->properties_json ? strstr(edges[i]->properties_json, key_open) : NULL;
|
||||
const char *end = start ? strchr(start + sizeof(key_open) - SKIP_ONE, '"') : NULL;
|
||||
if (!end) {
|
||||
continue;
|
||||
}
|
||||
start += sizeof(key_open) - SKIP_ONE;
|
||||
char local[CBM_SZ_256];
|
||||
size_t len = (size_t)(end - start);
|
||||
if (len == 0 || len >= sizeof(local)) {
|
||||
continue;
|
||||
}
|
||||
memcpy(local, start, len);
|
||||
local[len] = '\0';
|
||||
const char *key = cbm_ht_get_key(index->by_local, local);
|
||||
if (key) {
|
||||
cbm_ht_set(index->by_local, key, blocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_pipeline_external_import_index_free(cbm_external_import_index_t *index) {
|
||||
if (!index) {
|
||||
return;
|
||||
}
|
||||
if (index->by_local) {
|
||||
cbm_ht_free(index->by_local);
|
||||
}
|
||||
index->by_local = NULL;
|
||||
index->built = false;
|
||||
}
|
||||
|
||||
cbm_svc_kind_t cbm_pipeline_external_import_service(cbm_external_import_index_t *index,
|
||||
const cbm_gbuf_t *gbuf, const char *project,
|
||||
const char *rel, CBMLanguage lang,
|
||||
const CBMImportArray *imports,
|
||||
const CBMCall *call, char *spelled,
|
||||
size_t spelled_sz) {
|
||||
const char *callee = call ? call->callee_name : NULL;
|
||||
const char *u = call ? call->first_string_arg : NULL;
|
||||
if (!index || !gbuf || !spelled || spelled_sz == 0 || !callee || !callee[0] || !u || !u[0] ||
|
||||
!pc_external_import_lang(lang) || call->requires_lsp_resolution ||
|
||||
(lang == CBM_LANG_PYTHON && call->callee_is_locally_bound) ||
|
||||
cbm_service_pattern_route_method(callee) != NULL ||
|
||||
cbm_service_pattern_is_global_fetch(callee)) {
|
||||
return CBM_SVC_NONE;
|
||||
}
|
||||
const char *dot = strchr(callee, '.');
|
||||
size_t root_len = dot ? (size_t)(dot - callee) : strlen(callee);
|
||||
char root[CBM_SZ_256];
|
||||
if (root_len == 0 || root_len >= sizeof(root)) {
|
||||
return CBM_SVC_NONE;
|
||||
}
|
||||
memcpy(root, callee, root_len);
|
||||
root[root_len] = '\0';
|
||||
if (!index->built) {
|
||||
pc_external_import_index_build(index, gbuf, project, rel, lang, imports);
|
||||
}
|
||||
const CBMImport *imp = index->by_local ? cbm_ht_get(index->by_local, root) : NULL;
|
||||
if (!imp || (const void *)imp == (const void *)pc_import_blocked ||
|
||||
!pc_spell_through_import(lang, imp, callee, root_len, spelled, spelled_sz)) {
|
||||
spelled[0] = '\0';
|
||||
return CBM_SVC_NONE;
|
||||
}
|
||||
cbm_svc_kind_t svc = cbm_service_pattern_match(spelled);
|
||||
bool is_url = u[0] == '/' || strstr(u, "://") != NULL;
|
||||
bool is_topic = svc == CBM_SVC_ASYNC && strlen(u) > PAIR_LEN;
|
||||
/* An HTTP library also exports helpers that take a URL without sending
|
||||
* one (urllib.parse.urljoin): only a request verb is a request. */
|
||||
bool is_request = svc == CBM_SVC_HTTP && cbm_service_pattern_http_method(spelled) != NULL;
|
||||
if ((is_request && is_url) || (svc == CBM_SVC_ASYNC && (is_url || is_topic))) {
|
||||
return svc;
|
||||
}
|
||||
spelled[0] = '\0';
|
||||
return CBM_SVC_NONE;
|
||||
}
|
||||
|
||||
/* Handle a route registration call: create Route node + HANDLES edge.
|
||||
* route_mount is the framework mount of the registering file ("/api" for a
|
||||
* Laravel 11+ `withRouting(api: ...)` file, #1146) or "". */
|
||||
@@ -672,7 +851,8 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
|
||||
const char *rel, const char *module_qn, const char **imp_keys,
|
||||
const char **imp_vals, int imp_count, CBMLanguage lang,
|
||||
const CBMImportArray *imports, const char *route_mount,
|
||||
const cbm_php_vendor_names_t *php_vendor) {
|
||||
const cbm_php_vendor_names_t *php_vendor,
|
||||
cbm_external_import_index_t *ext_imports) {
|
||||
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) {
|
||||
@@ -862,6 +1042,23 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
|
||||
return SKIP_ONE;
|
||||
}
|
||||
}
|
||||
/* A callee an import binds to an external client library
|
||||
* (`send` under `from paho.mqtt import publish as send`): spelled
|
||||
* through its import, then classified. MUST match pass_parallel.c. */
|
||||
char spelled[CBM_SZ_512];
|
||||
cbm_svc_kind_t isvc =
|
||||
cbm_pipeline_external_import_service(ext_imports, ctx->gbuf, ctx->project_name, rel,
|
||||
lang, imports, call, spelled, sizeof(spelled));
|
||||
if (isvc != CBM_SVC_NONE) {
|
||||
CBMCall through = *call;
|
||||
through.callee_name = spelled;
|
||||
cbm_resolution_t imp_res = {.qualified_name = spelled,
|
||||
.confidence = PC_SVC_PATTERN_CONF,
|
||||
.strategy = "import_binding",
|
||||
.candidate_count = 0};
|
||||
emit_http_async_edge(ctx, &through, source_node, NULL, &imp_res, isvc, false);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1173,6 +1370,7 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
|
||||
sizeof(route_mount));
|
||||
|
||||
/* Resolve each call */
|
||||
cbm_external_import_index_t ext_imports = {0};
|
||||
for (int c = 0; c < result->calls.count; c++) {
|
||||
CBMCall *call = &result->calls.items[c];
|
||||
if (!call->callee_name) {
|
||||
@@ -1181,12 +1379,13 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
|
||||
total_calls++;
|
||||
if (resolve_single_call(ctx, call, result, rel, module_qn, imp_keys, imp_vals,
|
||||
imp_count, files[i].language, &result->imports, route_mount,
|
||||
&php_vendor)) {
|
||||
&php_vendor, &ext_imports)) {
|
||||
resolved++;
|
||||
} else {
|
||||
unresolved++;
|
||||
}
|
||||
}
|
||||
cbm_pipeline_external_import_index_free(&ext_imports);
|
||||
|
||||
free(module_qn);
|
||||
cbm_pipeline_php_vendor_names_free(&php_vendor);
|
||||
|
||||
@@ -3075,6 +3075,9 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
bool lsp_exact_idx_complete = true;
|
||||
bool lsp_legacy_idx_complete = true;
|
||||
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
|
||||
/* Names this file's imports bind onto external libraries, built on the
|
||||
* first unresolved call that asks (see pass_calls.c). */
|
||||
cbm_external_import_index_t ext_imports = {0};
|
||||
if (result->calls.count > 0 && result->resolved_calls.count > 0) {
|
||||
uint32_t capacity = (uint32_t)result->resolved_calls.count * 2u + 16u;
|
||||
lsp_exact_idx = cbm_ht_create(capacity);
|
||||
@@ -3411,6 +3414,24 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
emit_http_async_service_edge(ws->local_edge_buf, source_node, call, &fake_res,
|
||||
CBM_SVC_HTTP, u);
|
||||
}
|
||||
} else {
|
||||
/* A callee an import binds to an external client library
|
||||
* (`send` under `from paho.mqtt import publish as send`):
|
||||
* spelled through its import, then classified. MUST match
|
||||
* pass_calls.c. */
|
||||
char spelled[CBM_SZ_512];
|
||||
cbm_svc_kind_t isvc = cbm_pipeline_external_import_service(
|
||||
&ext_imports, rc->main_gbuf, rc->project_name, rel, lang, &result->imports,
|
||||
call, spelled, sizeof(spelled));
|
||||
if (isvc != CBM_SVC_NONE) {
|
||||
CBMCall through = *call;
|
||||
through.callee_name = spelled;
|
||||
cbm_resolution_t imp_res = {.qualified_name = spelled,
|
||||
.confidence = PP_HALF_CONF,
|
||||
.strategy = "import_binding"};
|
||||
emit_http_async_service_edge(ws->local_edge_buf, source_node, &through,
|
||||
&imp_res, isvc, through.first_string_arg);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -3498,6 +3519,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
cbm_ht_foreach(lsp_legacy_idx, lsp_idx_free_key, NULL);
|
||||
cbm_ht_free(lsp_legacy_idx);
|
||||
}
|
||||
cbm_pipeline_external_import_index_free(&ext_imports);
|
||||
cbm_pipeline_lsp_field_index_free(&field_index);
|
||||
}
|
||||
|
||||
|
||||
+10
-3
@@ -1978,7 +1978,7 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c
|
||||
size_t str_def_profile = 0, str_def_lists = 0, str_def_fp = 0, str_def_misc = 0;
|
||||
size_t str_call_names = 0, str_call_enclosing = 0, str_call_args = 0;
|
||||
size_t str_usage_names = 0, str_usage_enclosing = 0, str_rw = 0, str_typeref = 0;
|
||||
size_t str_resolved = 0, str_source = 0, str_module = 0;
|
||||
size_t str_resolved = 0, str_source = 0, str_module = 0, str_imports = 0;
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
const CBMFileResult *r = cache ? cache[i] : NULL;
|
||||
if (!r) {
|
||||
@@ -2070,6 +2070,12 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c
|
||||
str_typeref += census_len(r->type_refs.items[t].type_name) +
|
||||
census_len(r->type_refs.items[t].enclosing_func_qn);
|
||||
}
|
||||
for (int m = 0; m < r->imports.count; m++) {
|
||||
const CBMImport *imp = &r->imports.items[m];
|
||||
/* A non-aliased named import's member IS its local name string. */
|
||||
str_imports += census_len(imp->local_name) + census_len(imp->module_path) +
|
||||
(imp->member != imp->local_name ? census_len(imp->member) : 0);
|
||||
}
|
||||
for (int q = 0; q < r->resolved_calls.count; q++) {
|
||||
const CBMResolvedCall *rc = &r->resolved_calls.items[q];
|
||||
str_resolved += census_len(rc->caller_qn) + census_len(rc->callee_qn) +
|
||||
@@ -2102,10 +2108,11 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c
|
||||
cbm_log_info("extract.census.def_strings_mb", "tag", tag, "v", line);
|
||||
snprintf(line, sizeof(line),
|
||||
"call_names=%lu call_enclosing=%lu call_args=%lu usage_names=%lu "
|
||||
"usage_enclosing=%lu rw=%lu type_refs=%lu resolved=%lu source=%lu module=%lu",
|
||||
"usage_enclosing=%lu rw=%lu type_refs=%lu resolved=%lu source=%lu module=%lu "
|
||||
"imports=%lu",
|
||||
MB(str_call_names), MB(str_call_enclosing), MB(str_call_args), MB(str_usage_names),
|
||||
MB(str_usage_enclosing), MB(str_rw), MB(str_typeref), MB(str_resolved), MB(str_source),
|
||||
MB(str_module));
|
||||
MB(str_module), MB(str_imports));
|
||||
cbm_log_info("extract.census.ref_strings_mb", "tag", tag, "v", line);
|
||||
#undef MB
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "git/git_context.h"
|
||||
#include "foundation/hash_table.h"
|
||||
#include "spawn_patterns.h"
|
||||
#include "service_patterns.h" /* cbm_svc_kind_t */
|
||||
#include "cbm.h"
|
||||
#include "lsp/php_lsp.h"
|
||||
#include "lsp/go_lsp.h" /* CBMLSPDef for cbm_parallel_resolve cross-LSP inputs */
|
||||
@@ -669,6 +670,42 @@ bool cbm_pipeline_http_client_call_url(const cbm_gbuf_t *gbuf, const char *proje
|
||||
* behind both passes' HTTP_CALLS / ASYNC_CALLS props (pass_parallel.c). */
|
||||
void cbm_pipeline_close_call_props(char *props, int n, const CBMCall *call, bool with_line);
|
||||
|
||||
/* Per-file index of the names a file's imports bind onto EXTERNAL modules:
|
||||
* local name -> the binding import. Built lazily, on the first call that
|
||||
* asks, from the file's extraction imports and its IMPORTS edges; one hash
|
||||
* lookup per call after that. Zero-initialize, pass to
|
||||
* cbm_pipeline_external_import_service for every call of ONE file, then
|
||||
* release with cbm_pipeline_external_import_index_free. */
|
||||
typedef struct {
|
||||
CBMHashTable *by_local;
|
||||
bool built;
|
||||
} cbm_external_import_index_t;
|
||||
|
||||
void cbm_pipeline_external_import_index_free(cbm_external_import_index_t *index);
|
||||
|
||||
/* A call no project symbol resolved, whose callee is bound by an import of an
|
||||
* EXTERNAL library: `send(...)` under `from paho.mqtt import publish as send`,
|
||||
* `navigate(...)` under `import { get as navigate } from 'axios'`. Spells the
|
||||
* callee as its import makes it (paho.mqtt.publish, axios.get), runs the
|
||||
* service patterns on that name, and returns CBM_SVC_HTTP or CBM_SVC_ASYNC
|
||||
* (writing the spelled name to `spelled`) for an HTTP request -- the spelled
|
||||
* name ends in a request verb, so urllib.parse.urljoin is none -- with a URL
|
||||
* argument, or a messaging call with a URL or topic argument; CBM_SVC_NONE
|
||||
* otherwise.
|
||||
*
|
||||
* Only adds what the resolvers had no answer for: a project import (one an
|
||||
* IMPORTS edge materialized, or a relative path), a name bound by two
|
||||
* different imports, a Python callee rebound as a parameter, and every call
|
||||
* the route-suffix and global-fetch fallbacks already own are left alone.
|
||||
* Python and JS/TS today; a language plugs in with its binding rule in
|
||||
* pass_calls.c. Both call resolvers call this, so they cannot diverge. */
|
||||
cbm_svc_kind_t cbm_pipeline_external_import_service(cbm_external_import_index_t *index,
|
||||
const cbm_gbuf_t *gbuf, const char *project,
|
||||
const char *rel, CBMLanguage lang,
|
||||
const CBMImportArray *imports,
|
||||
const CBMCall *call, char *spelled,
|
||||
size_t spelled_sz);
|
||||
|
||||
/* A spawn site (pass_spawns.c): the API the call names and the program it
|
||||
* starts (CBM_SPAWN_DYNAMIC when not a literal). */
|
||||
typedef struct {
|
||||
|
||||
@@ -1254,6 +1254,62 @@ TEST(csharp_local_name_last_segment) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* ES named imports keep the member their local binds
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static const CBMImport *imp_by_local(const CBMFileResult *r, const char *local) {
|
||||
for (int i = 0; i < r->imports.count; i++) {
|
||||
if (r->imports.items[i].local_name && strcmp(r->imports.items[i].local_name, local) == 0) {
|
||||
return &r->imports.items[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* `navigate` from `{ get as navigate }` binds axios's `get`: the call
|
||||
* resolvers spell a call through it as axios.get. The member is set for a
|
||||
* named import only (aliased or not) and survives compaction into the
|
||||
* result's one exact arena block. */
|
||||
TEST(es_named_import_member_survives_compaction) {
|
||||
CBMFileResult *r = do_extract("import ax from 'axios';\n"
|
||||
"import * as ns from 'axios';\n"
|
||||
"import { get as navigate, post } from 'axios';\n",
|
||||
CBM_LANG_TYPESCRIPT, "member.ts");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
cbm_result_compact(r);
|
||||
const CBMImport *aliased = imp_by_local(r, "navigate");
|
||||
const CBMImport *plain = imp_by_local(r, "post");
|
||||
const CBMImport *def = imp_by_local(r, "ax");
|
||||
const CBMImport *ns = imp_by_local(r, "ns");
|
||||
ASSERT_NOT_NULL(aliased);
|
||||
ASSERT_NOT_NULL(plain);
|
||||
ASSERT_NOT_NULL(def);
|
||||
ASSERT_NOT_NULL(ns);
|
||||
ASSERT_STR_EQ(aliased->module_path, "axios");
|
||||
ASSERT_STR_EQ(aliased->member, "get");
|
||||
ASSERT_STR_EQ(plain->member, "post");
|
||||
ASSERT_NULL(def->member);
|
||||
ASSERT_NULL(ns->member);
|
||||
uintptr_t member_addr = (uintptr_t)aliased->member;
|
||||
uintptr_t arena_addr = (uintptr_t)r->arena.blocks[0];
|
||||
ASSERT_EQ(r->arena.nblocks, 1);
|
||||
ASSERT(member_addr >= arena_addr);
|
||||
ASSERT(member_addr - arena_addr < r->arena.used);
|
||||
cbm_free_result(r);
|
||||
|
||||
CBMFileResult *py =
|
||||
do_extract("from paho.mqtt import publish as send\n", CBM_LANG_PYTHON, "member.py");
|
||||
ASSERT_NOT_NULL(py);
|
||||
const CBMImport *send = imp_by_local(py, "send");
|
||||
ASSERT_NOT_NULL(send);
|
||||
ASSERT_STR_EQ(send->module_path, "paho.mqtt.publish");
|
||||
ASSERT_NULL(send->member);
|
||||
cbm_free_result(py);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* SUITE declaration — do NOT register in test_main.c; the main workstream will
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
@@ -1294,4 +1350,7 @@ SUITE(extraction_imports) {
|
||||
RUN_TEST(rust_local_name_last_segment);
|
||||
RUN_TEST(java_local_name_last_segment);
|
||||
RUN_TEST(csharp_local_name_last_segment);
|
||||
|
||||
/* ES named-import member (call resolution spells calls through it) */
|
||||
RUN_TEST(es_named_import_member_survives_compaction);
|
||||
}
|
||||
@@ -10360,6 +10360,183 @@ TEST(pipeline_relative_folded_wrapper_url_candidates) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* HTTP_CALLS + ASYNC_CALLS edges out of the function `source_name`. */
|
||||
static int service_edges_from(cbm_store_t *s, const char *project, const char *source_name) {
|
||||
static const char *const types[] = {"HTTP_CALLS", "ASYNC_CALLS"};
|
||||
int matches = 0;
|
||||
for (size_t t = 0; t < sizeof(types) / sizeof(types[0]); t++) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int edge_count = 0;
|
||||
if (cbm_store_find_edges_by_type(s, project, types[t], &edges, &edge_count) !=
|
||||
CBM_STORE_OK) {
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
cbm_node_t source = {0};
|
||||
if (cbm_store_find_node_by_id(s, edges[i].source_id, &source) == CBM_STORE_OK &&
|
||||
source.name && strcmp(source.name, source_name) == 0) {
|
||||
matches++;
|
||||
}
|
||||
cbm_node_free_fields(&source);
|
||||
}
|
||||
cbm_store_free_edges(edges, edge_count);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
/* IMPORTS edges out of `file_path` that bind `local_name`; -1 on a failed query. */
|
||||
static int imports_binding_count(cbm_store_t *s, const char *project, const char *file_path,
|
||||
const char *local_name) {
|
||||
static const char sql[] =
|
||||
"SELECT count(*) FROM edges e JOIN nodes f ON f.id=e.source_id WHERE e.project=?1 "
|
||||
"AND e.type='IMPORTS' AND f.file_path=?2 AND json_extract(e.properties,'$.local_name')=?3";
|
||||
sqlite3 *db = cbm_store_get_db(s);
|
||||
sqlite3_stmt *st = NULL;
|
||||
if (!db || sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
|
||||
sqlite3_finalize(st);
|
||||
return -1;
|
||||
}
|
||||
int count = -1;
|
||||
if (sqlite3_bind_text(st, 1, project, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
|
||||
sqlite3_bind_text(st, 2, file_path, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
|
||||
sqlite3_bind_text(st, 3, local_name, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
|
||||
sqlite3_step(st) == SQLITE_ROW) {
|
||||
count = sqlite3_column_int(st, 0);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* A call through an import of an EXTERNAL client library is that library's
|
||||
* call: `fetchIt` from `{ get as fetchIt } from 'axios'` is a typed GET,
|
||||
* `http_post` from `from requests import post as http_post` a typed POST,
|
||||
* `emitIt` from `{ publish as emitIt } from 'mqtt'` a publish. Precision
|
||||
* controls, each with no HTTP_CALLS/ASYNC_CALLS edge:
|
||||
* - urljoin from urllib.parse: an HTTP library's helper, not a request;
|
||||
* - lodash's `get`, json's `dumps`: libraries no service pattern knows;
|
||||
* - `send` defined in the importing module shadows
|
||||
* `from paho.mqtt import publish as send`; `relay` rebound as a parameter
|
||||
* shadows `... as relay` (a name no project function has, so only the
|
||||
* parameter binding stands between the call and the import);
|
||||
* - `httpx` is a PROJECT package here: its import is materialized (the
|
||||
* precondition is asserted), so a call it cannot resolve stays unresolved
|
||||
* instead of becoming the external httpx.post.
|
||||
* Both resolvers: sequential and parallel (> 50 files). */
|
||||
TEST(pipeline_external_import_binding_controls_sequential_parallel) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_extimport_XXXXXX");
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
|
||||
write_temp_file(
|
||||
tmp, "src/ext_http.ts",
|
||||
"import { get as fetchIt } from 'axios';\n"
|
||||
"import { get as lodashGet } from 'lodash';\n"
|
||||
"import { publish as emitIt } from 'mqtt';\n"
|
||||
"export function viaAxiosAlias(): unknown { return fetchIt('/api/alias-get'); }\n"
|
||||
"export function viaLodashAlias(o: object): unknown {\n"
|
||||
" return lodashGet(o, '/api/lodash');\n"
|
||||
"}\n"
|
||||
"export function viaMqtt(): void { emitIt('tenant/ts', 'payload'); }\n");
|
||||
write_temp_file(tmp, "src/ext_py.py",
|
||||
"from requests import post as http_post\n"
|
||||
"from urllib.parse import urljoin\n"
|
||||
"from json import dumps as publish_json\n"
|
||||
"from paho.mqtt import publish as send\n"
|
||||
"def py_post():\n return http_post('/api/py-post')\n"
|
||||
"def join_url(base):\n return urljoin(base, '/static/asset.css')\n"
|
||||
"def join_abs(name):\n return urljoin('https://example.com/assets/', name)\n"
|
||||
"def to_json():\n return publish_json('/tenant/json')\n"
|
||||
"def send(topic, payload):\n return topic\n"
|
||||
"def shadowed():\n return send('tenant/shadow', 'payload')\n");
|
||||
write_temp_file(tmp, "src/ext_param.py",
|
||||
"from paho.mqtt import publish as relay\n"
|
||||
"def param_shadow(relay):\n return relay('tenant/param', 'payload')\n");
|
||||
write_temp_file(tmp, "httpx/__init__.py", "def get(url):\n return url\n");
|
||||
write_temp_file(tmp, "src/proj_client.py",
|
||||
"from httpx import post as hx_post\n"
|
||||
"def proj_post():\n return hx_post('/api/project')\n");
|
||||
for (int i = 0; i < 52; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "src/padding%d.ts", i);
|
||||
snprintf(body, sizeof(body), "export function padding%d() { return %d; }\n", i, i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
static const char *const silent[] = {"viaLodashAlias", "join_url", "join_abs", "to_json",
|
||||
"shadowed", "param_shadow", "proj_post"};
|
||||
enum { SILENT = sizeof(silent) / sizeof(silent[0]) };
|
||||
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
|
||||
char *saved_single = old_single ? strdup(old_single) : NULL;
|
||||
const char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
|
||||
int run_rc[2] = {-1, -1};
|
||||
bool opened[2] = {false, false};
|
||||
int typed_get[2] = {-1, -1};
|
||||
int typed_post[2] = {-1, -1};
|
||||
int published[2] = {-1, -1};
|
||||
int shadow_calls[2] = {-1, -1};
|
||||
int project_import[2] = {-1, -1};
|
||||
int silent_edges[2][SILENT];
|
||||
for (int mode = 0; mode < 2; mode++) {
|
||||
for (int i = 0; i < SILENT; i++) {
|
||||
silent_edges[mode][i] = -1;
|
||||
}
|
||||
if (mode == 0) {
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
}
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/extimport_%d.db", tmp, mode);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
run_rc[mode] = p ? cbm_pipeline_run(p) : -1;
|
||||
const char *project = p ? cbm_pipeline_project_name(p) : NULL;
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
opened[mode] = s != NULL;
|
||||
if (s && project) {
|
||||
typed_get[mode] = http_calls_to_route(s, project, "viaAxiosAlias", "__route__GET__");
|
||||
typed_post[mode] = http_calls_to_route(s, project, "py_post", "__route__POST__");
|
||||
published[mode] = named_edge_count(s, project, "ASYNC_CALLS", "viaMqtt", "tenant/ts");
|
||||
shadow_calls[mode] = named_edge_count(s, project, "CALLS", "shadowed", "send");
|
||||
project_import[mode] =
|
||||
imports_binding_count(s, project, "src/proj_client.py", "hx_post");
|
||||
for (int i = 0; i < SILENT; i++) {
|
||||
silent_edges[mode][i] = service_edges_from(s, project, silent[i]);
|
||||
}
|
||||
}
|
||||
if (s) {
|
||||
cbm_store_close(s);
|
||||
}
|
||||
cbm_pipeline_free(p);
|
||||
}
|
||||
if (saved_single) {
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1);
|
||||
free(saved_single);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
}
|
||||
if (saved_workers) {
|
||||
cbm_setenv("CBM_WORKERS", saved_workers, 1);
|
||||
free(saved_workers);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
for (int mode = 0; mode < 2; mode++) {
|
||||
ASSERT_EQ(run_rc[mode], 0);
|
||||
ASSERT_TRUE(opened[mode]);
|
||||
ASSERT_EQ(typed_get[mode], 1);
|
||||
ASSERT_EQ(typed_post[mode], 1);
|
||||
ASSERT_EQ(published[mode], 1);
|
||||
ASSERT_GTE(shadow_calls[mode], 1);
|
||||
ASSERT_GTE(project_import[mode], 1);
|
||||
for (int i = 0; i < SILENT; i++) {
|
||||
ASSERT_EQ(silent_edges[mode][i], 0);
|
||||
}
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Git history pass tests ─────────────────────────────────────── */
|
||||
|
||||
TEST(githistory_is_trackable) {
|
||||
@@ -21472,6 +21649,7 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_url_constant_folding_http_calls_issue706_1147);
|
||||
RUN_TEST(pipeline_folded_topics_preserve_identity_sequential_parallel);
|
||||
RUN_TEST(pipeline_relative_folded_wrapper_url_candidates);
|
||||
RUN_TEST(pipeline_external_import_binding_controls_sequential_parallel);
|
||||
/* Git history pass */
|
||||
RUN_TEST(githistory_is_trackable);
|
||||
RUN_TEST(githistory_compute_coupling);
|
||||
|
||||
Reference in new issue
Block a user