Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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 {
Expand Down
129 changes: 129 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
}
}
Expand Down Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion internal/cbm/extract_imports.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions internal/cbm/result_compact.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion scripts/memory-core-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ src/pipeline/artifact.c 38
src/pipeline/fqn.c 23
src/pipeline/lsp_resolve.h 4
src/pipeline/lsp_surface.c 5
src/pipeline/pass_calls.c 23
src/pipeline/pass_calls.c 22
src/pipeline/pass_compile_commands.c 17
src/pipeline/pass_complexity.c 15
src/pipeline/pass_configlink.c 3
Expand Down
25 changes: 25 additions & 0 deletions src/pipeline/lsp_surface.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading