mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-27 06:53:11 +08:00
Merge pull request #840 from DeusData/recover/836-tsjs-suppression
fix(resolver): suppress weak short-name strategies for TS/JS (recovers #836)
This commit is contained in:
+3
-1
@@ -236,7 +236,9 @@ typedef struct {
|
||||
int loop_depth; // enclosing loop nesting at the call site
|
||||
int branch_depth; // enclosing branch nesting at the call site
|
||||
int start_line; // 1-based source line of the call (for def range-match)
|
||||
bool is_method; // Perl-only: arrow/method call ($obj->m). Default false.
|
||||
bool is_method; // method/member call with a non-self receiver. Perl:
|
||||
// arrow/method call ($obj->m). TS/JS/TSX: member call
|
||||
// x.foo() whose receiver is not this/super. Default false.
|
||||
} CBMCall;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -1892,6 +1892,29 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk
|
||||
strcmp(ts_node_type(node), "method_call_expression") == 0) {
|
||||
call.is_method = true;
|
||||
}
|
||||
// TS/JS/TSX receiver-aware guard (#592/#606 direction; same intent
|
||||
// as the Perl flag above). Flag a member call x.foo() whose receiver
|
||||
// is NOT `this`/`super`. When the TS-LSP cannot resolve the receiver
|
||||
// type, the call-resolution pass suppresses weak short-name matches
|
||||
// for these (so `re.test()` cannot fabricate an edge to a project
|
||||
// `test`). this/super receivers stay unflagged — their target is the
|
||||
// enclosing class, where a namespace-proximity weak match is usually
|
||||
// right. Bare calls (helper()) and new_expression have no member
|
||||
// receiver, so they keep is_method=false (struct is zero-init).
|
||||
if ((ctx->language == CBM_LANG_JAVASCRIPT || ctx->language == CBM_LANG_TYPESCRIPT ||
|
||||
ctx->language == CBM_LANG_TSX) &&
|
||||
strcmp(ts_node_type(node), "call_expression") == 0) {
|
||||
TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
||||
if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "member_expression") == 0) {
|
||||
TSNode obj = ts_node_child_by_field_name(fn, TS_FIELD("object"));
|
||||
if (!ts_node_is_null(obj)) {
|
||||
const char *ok = ts_node_type(obj);
|
||||
if (strcmp(ok, "this") != 0 && strcmp(ok, "super") != 0) {
|
||||
call.is_method = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
|
||||
if (!ts_node_is_null(args)) {
|
||||
|
||||
@@ -327,13 +327,21 @@ static void calls_emit_edge(cbm_gbuf_t *gbuf, int64_t src, int64_t tgt, const ch
|
||||
|
||||
static void emit_http_async_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
|
||||
const cbm_gbuf_node_t *source, const cbm_gbuf_node_t *target,
|
||||
const cbm_resolution_t *res, cbm_svc_kind_t svc) {
|
||||
const cbm_resolution_t *res, cbm_svc_kind_t svc,
|
||||
bool suppress_plain_calls) {
|
||||
const char *url_or_topic = call->first_string_arg;
|
||||
bool is_url = (url_or_topic && url_or_topic[0] != '\0' &&
|
||||
(url_or_topic[0] == '/' || strstr(url_or_topic, "://") != NULL));
|
||||
bool is_topic = (url_or_topic && url_or_topic[0] != '\0' && svc == CBM_SVC_ASYNC &&
|
||||
strlen(url_or_topic) > PAIR_LEN);
|
||||
if (!is_url && !is_topic) {
|
||||
/* No URL/topic → this is not a real service call; the svc kind was a
|
||||
* substring coincidence in the resolved QN (e.g. "SalesforceRestClient"
|
||||
* matches the "RestClient" HTTP lib). Emit a plain CALLS edge — unless a
|
||||
* weak TS/JS member-call match should be suppressed (#592/#606). */
|
||||
if (suppress_plain_calls) {
|
||||
return;
|
||||
}
|
||||
char esc_callee[CBM_SZ_256];
|
||||
cbm_json_escape(esc_callee, sizeof(esc_callee), call->callee_name);
|
||||
char props[CBM_SZ_512];
|
||||
@@ -368,17 +376,22 @@ static void emit_http_async_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
|
||||
}
|
||||
|
||||
/* Classify a resolved call and emit the appropriate edge. */
|
||||
/* When suppress_plain_calls is true (a TS/JS/TSX weak short-name member-call
|
||||
* match, #592/#606), the route/HTTP/ASYNC/CONFIG service classifications below
|
||||
* still run — only the plain CALLS fall-through is skipped, so a fabricated
|
||||
* project edge is dropped while every service edge stays main-identical. */
|
||||
static void emit_classified_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
|
||||
const cbm_gbuf_node_t *source, const cbm_gbuf_node_t *target,
|
||||
const cbm_resolution_t *res, const char *module_qn,
|
||||
const char **imp_keys, const char **imp_vals, int imp_count) {
|
||||
const char **imp_keys, const char **imp_vals, int imp_count,
|
||||
bool suppress_plain_calls) {
|
||||
cbm_svc_kind_t svc = cbm_service_pattern_match(res->qualified_name);
|
||||
if (svc == CBM_SVC_ROUTE_REG && call->first_string_arg && call->first_string_arg[0] == '/') {
|
||||
handle_route_registration(ctx, call, source, module_qn, imp_keys, imp_vals, imp_count);
|
||||
return;
|
||||
}
|
||||
if (svc == CBM_SVC_HTTP || svc == CBM_SVC_ASYNC) {
|
||||
emit_http_async_edge(ctx, call, source, target, res, svc);
|
||||
emit_http_async_edge(ctx, call, source, target, res, svc, suppress_plain_calls);
|
||||
return;
|
||||
}
|
||||
if (svc == CBM_SVC_CONFIG) {
|
||||
@@ -393,6 +406,9 @@ static void emit_classified_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
|
||||
call);
|
||||
return;
|
||||
}
|
||||
if (suppress_plain_calls) {
|
||||
return; /* weak TS/JS member-call match with an unresolved receiver (#606) */
|
||||
}
|
||||
char esc_c2[CBM_SZ_256];
|
||||
cbm_json_escape(esc_c2, sizeof(esc_c2), call->callee_name);
|
||||
char props[CBM_SZ_512];
|
||||
@@ -450,7 +466,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
res.strategy = lsp->strategy;
|
||||
res.candidate_count = 1;
|
||||
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys,
|
||||
imp_vals, imp_count);
|
||||
imp_vals, imp_count, false);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
}
|
||||
@@ -474,7 +490,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
.confidence = PC_SVC_PATTERN_CONF,
|
||||
.strategy = "service_pattern",
|
||||
.candidate_count = 0};
|
||||
emit_http_async_edge(ctx, call, source_node, NULL, &svc_res, csvc);
|
||||
emit_http_async_edge(ctx, call, source_node, NULL, &svc_res, csvc, false);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
}
|
||||
@@ -502,7 +518,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
.confidence = PC_SVC_PATTERN_CONF,
|
||||
.strategy = "service_pattern",
|
||||
.candidate_count = 0};
|
||||
emit_http_async_edge(ctx, call, source_node, NULL, &svc_res, esvc);
|
||||
emit_http_async_edge(ctx, call, source_node, NULL, &svc_res, esvc, false);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
}
|
||||
@@ -522,6 +538,21 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* TS/JS/TSX weak-method suppression (#592/#606). A member call x.foo() only
|
||||
* reaches the registry when the TS-LSP could not resolve the receiver type
|
||||
* (the LSP block above already returned for type-resolved calls, including
|
||||
* the "resolved but target out of gbuf" fall-through). Binding such a call
|
||||
* by a weak short-name strategy fabricates an edge (`re.test()` -> a project
|
||||
* `test`). Rather than drop it here — which would also skip the service
|
||||
* bypasses below and emit_classified_edge's route/HTTP/CONFIG branches —
|
||||
* defer to emit_classified_edge and suppress ONLY the plain-CALLS
|
||||
* fall-through, so every service edge stays main-identical. res.strategy may
|
||||
* be lsp_* here; the helper's explicit drop-list leaves lsp_* untouched. */
|
||||
bool is_tsjs =
|
||||
lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX;
|
||||
bool tsjs_drop_plain_call =
|
||||
cbm_tsjs_suppress_weak_method_match(is_tsjs, call->is_method, res.strategy);
|
||||
|
||||
/* Service-pattern HTTP/ASYNC calls to an EXTERNAL client library (e.g.
|
||||
* `requests.get("/api/orders/{id}")`) resolve to a QN containing the library
|
||||
* name ("requests"), but that library is not in the indexed tree so
|
||||
@@ -537,7 +568,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
(u[0] == '/' || strstr(u, "://") != NULL ||
|
||||
(svc == CBM_SVC_ASYNC && strlen(u) > PAIR_LEN));
|
||||
if (has_url_or_topic) {
|
||||
emit_http_async_edge(ctx, call, source_node, NULL, &res, svc);
|
||||
emit_http_async_edge(ctx, call, source_node, NULL, &res, svc, false);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
}
|
||||
@@ -547,7 +578,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
return 0;
|
||||
}
|
||||
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys, imp_vals,
|
||||
imp_count);
|
||||
imp_count, tsjs_drop_plain_call);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
|
||||
|
||||
@@ -1765,11 +1765,18 @@ static void emit_trpc_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, cons
|
||||
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "TRPC_CALLS", props);
|
||||
}
|
||||
|
||||
/* When suppress_plain_calls is true (a TS/JS/TSX weak short-name member-call
|
||||
* match, #592/#606), every service classification below still runs — only the
|
||||
* plain CALLS fall-through (emit_normal_calls_edge) is skipped. detect_url_in_args
|
||||
* and the HTTP/ASYNC/gRPC/GraphQL/tRPC/CONFIG/route branches are unaffected, so
|
||||
* a verb-suffix HTTP client (api.patch('/x')), broker, or route registration
|
||||
* keeps its edge; only the fabricated project CALLS edge is dropped. */
|
||||
static void emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
|
||||
const cbm_gbuf_node_t *target, const CBMCall *call,
|
||||
const cbm_resolution_t *res, const char *module_qn,
|
||||
const cbm_registry_t *registry, const cbm_gbuf_t *main_gbuf,
|
||||
const char **imp_keys, const char **imp_vals, int imp_count) {
|
||||
const char **imp_keys, const char **imp_vals, int imp_count,
|
||||
bool suppress_plain_calls) {
|
||||
cbm_svc_kind_t svc = cbm_service_pattern_match(res->qualified_name);
|
||||
const char *arg = call->first_string_arg;
|
||||
|
||||
@@ -1815,7 +1822,7 @@ static void emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
|
||||
emit_trpc_edge(gbuf, source, call, res);
|
||||
} else if (svc == CBM_SVC_CONFIG) {
|
||||
emit_config_edge(gbuf, source, target, call, res, arg);
|
||||
} else {
|
||||
} else if (!suppress_plain_calls) {
|
||||
emit_normal_calls_edge(gbuf, source, target, call, res);
|
||||
}
|
||||
|
||||
@@ -2034,6 +2041,21 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
continue;
|
||||
}
|
||||
|
||||
/* TS/JS/TSX weak-method suppression (#592/#606). The receiver-aware guard
|
||||
* must NOT drop this call here: doing so would also skip the #523
|
||||
* callee-name service bypass below, emit_service_edge's route/gRPC/config
|
||||
* branches, and its unconditional detect_url_in_args (which classifies
|
||||
* verb-suffix HTTP clients like api.patch('/x')). Instead, defer to the
|
||||
* emit path and suppress ONLY the plain-CALLS fall-through
|
||||
* (emit_normal_calls_edge), so every service edge stays main-identical by
|
||||
* construction. res.strategy may carry an lsp_* value here (LSP-resolved
|
||||
* calls keep res through this point); the helper's EXPLICIT drop-list
|
||||
* leaves lsp_ts_method / lsp_cross untouched. See #606 direction. */
|
||||
bool is_tsjs =
|
||||
lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX;
|
||||
bool tsjs_drop_plain_call =
|
||||
cbm_tsjs_suppress_weak_method_match(is_tsjs, call->is_method, res.strategy);
|
||||
|
||||
/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
|
||||
* service signal lives in the callee_name. The registry can mis-resolve
|
||||
* it to a spurious builtin short-name match (`requests.get` ->
|
||||
@@ -2054,7 +2076,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
.strategy = "service_pattern"};
|
||||
emit_service_edge(ws->local_edge_buf, source_node, source_node, call, &svc_res,
|
||||
module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals,
|
||||
imp_count);
|
||||
imp_count, false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -2066,7 +2088,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
.strategy = "callee_suffix"};
|
||||
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);
|
||||
imp_count, false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -2097,7 +2119,8 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
(psvc == CBM_SVC_ASYNC && strlen(u) > PP_ESC_SPACE));
|
||||
if (url_or_topic) {
|
||||
emit_service_edge(ws->local_edge_buf, source_node, NULL, call, &res, module_qn,
|
||||
rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count);
|
||||
rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count,
|
||||
false);
|
||||
ws->calls_resolved++;
|
||||
}
|
||||
}
|
||||
@@ -2105,7 +2128,8 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
}
|
||||
_rc_t0 = extract_now_ns();
|
||||
emit_service_edge(ws->local_edge_buf, source_node, target_node, call, &res, module_qn,
|
||||
rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count);
|
||||
rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count,
|
||||
tsjs_drop_plain_call);
|
||||
atomic_fetch_add_explicit(&rc->time_ns_rc_emit, extract_now_ns() - _rc_t0,
|
||||
memory_order_relaxed);
|
||||
ws->calls_resolved++;
|
||||
|
||||
@@ -223,6 +223,14 @@ bool cbm_perl_is_builtin(const char *name);
|
||||
bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *callee_name,
|
||||
const char *strategy);
|
||||
|
||||
/* Decide whether a resolved TS/JS/TSX member-call edge is weak-strategy noise to
|
||||
* drop (#592/#606): true only for TS/JS, only for a member call with a
|
||||
* non-this/super receiver (is_method), and only when the match used a weak
|
||||
* short-name strategy (suffix_match / unique_name / field_type_hint / fuzzy).
|
||||
* Explicit drop-list keeps every lsp_* / import / same-module / qualified match.
|
||||
* Pure; unit-tested in test_registry.c. */
|
||||
bool cbm_tsjs_suppress_weak_method_match(bool is_tsjs, bool is_method, const char *strategy);
|
||||
|
||||
/* Get the label of a qualified name, or NULL if not found. */
|
||||
const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn);
|
||||
|
||||
|
||||
@@ -423,6 +423,33 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c
|
||||
return true; /* weak short-name match (suffix_match / unique_name / …) → drop */
|
||||
}
|
||||
|
||||
/* TS/JS analogue of the Perl guard above (#592/#606 direction; precedent #477).
|
||||
* A member call `x.foo()` reaches the weak textual cascade ONLY when the TS-LSP
|
||||
* could not resolve the receiver type — type-resolved calls win via lsp_*
|
||||
* strategies before the registry runs. Binding such a call to a project symbol
|
||||
* by a weak short-name strategy fabricates a CALLS edge (`re.test()` ->
|
||||
* SalesforceRestClient.test, `date.toISOString()` -> any project toISOString).
|
||||
* Drop ONLY the weak strategies; keep import/same-module/qualified-tail matches
|
||||
* and every lsp_* strategy. Uses an EXPLICIT drop-list (not keep-list +
|
||||
* default-drop) because the parallel resolver runs lsp_* strategies through the
|
||||
* same guard variable — a default-drop would silently kill lsp_ts_method. Pure
|
||||
* + side-effect-free so the contract is unit-testable without a full pipeline. */
|
||||
bool cbm_tsjs_suppress_weak_method_match(bool is_tsjs, bool is_method, const char *strategy) {
|
||||
if (!is_tsjs || !is_method || !strategy || !strategy[0]) {
|
||||
return false;
|
||||
}
|
||||
/* Weak short-name strategies that actually reach the call-resolution guards:
|
||||
* the registry's suffix_match / unique_name and the parallel field_type_hint.
|
||||
* "fuzzy" is listed as defensive insurance only — cbm_registry_fuzzy_resolve
|
||||
* is not wired into the sequential/parallel resolvers today, so it never
|
||||
* reaches this helper, but naming it keeps a future wiring from silently
|
||||
* reintroducing the noise. Everything else — same_module / import_map /
|
||||
* import_map_suffix / qualified_suffix / callee_suffix / service_pattern /
|
||||
* lsp_* — is a receiver- or import-aware match and is KEPT. */
|
||||
return strcmp(strategy, "suffix_match") == 0 || strcmp(strategy, "unique_name") == 0 ||
|
||||
strcmp(strategy, "field_type_hint") == 0 || strcmp(strategy, "fuzzy") == 0;
|
||||
}
|
||||
|
||||
/* ── Lifecycle ──────────────────────────────────────────────────── */
|
||||
|
||||
cbm_registry_t *cbm_registry_new(void) {
|
||||
|
||||
+90
-6
@@ -3224,11 +3224,12 @@ TEST(extract_perl_method_call_flags_is_method) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Other languages must be unaffected: a JS method call never sets is_method
|
||||
* (the flag is Perl-only). */
|
||||
TEST(extract_non_perl_method_call_not_flagged_is_method) {
|
||||
CBMFileResult *r =
|
||||
extract("function run(o){ o.commit(); helper(); }\n", CBM_LANG_JAVASCRIPT, "t", "x.js");
|
||||
/* Languages OUTSIDE the is_method flag set (only Perl and TS/JS/TSX set it) must
|
||||
* be unaffected: a Go method call never sets is_method. */
|
||||
TEST(extract_flag_exempt_method_call_not_flagged_is_method) {
|
||||
CBMFileResult *r = extract("package m\n"
|
||||
"func run(o Obj) { o.Commit(); helper() }\n",
|
||||
CBM_LANG_GO, "t", "x.go");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
for (int i = 0; i < r->calls.count; i++) {
|
||||
@@ -3238,6 +3239,86 @@ TEST(extract_non_perl_method_call_not_flagged_is_method) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* TS/JS/TSX receiver-aware flag (#592/#606; same intent as the Perl flag above).
|
||||
* A member call x.foo() with a non-this/super receiver is flagged is_method so
|
||||
* the resolver can suppress a weak short-name match (`re.test()` must not bind a
|
||||
* project `test`); a bare call is not flagged. */
|
||||
TEST(extract_ts_member_call_flags_is_method) {
|
||||
CBMFileResult *r = extract("const re = /^a+$/;\n"
|
||||
"export function checkFormat(s: string) { return re.test(s); }\n"
|
||||
"function helper() { return 1; }\n"
|
||||
"export function run() { return helper(); }\n",
|
||||
CBM_LANG_TYPESCRIPT, "t", "a.ts");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
int member = 0;
|
||||
int bare = 0;
|
||||
for (int i = 0; i < r->calls.count; i++) {
|
||||
const char *cn = r->calls.items[i].callee_name;
|
||||
if (strcmp(cn, "re.test") == 0) {
|
||||
member++;
|
||||
ASSERT_TRUE(r->calls.items[i].is_method);
|
||||
}
|
||||
if (strcmp(cn, "helper") == 0) {
|
||||
bare++;
|
||||
ASSERT_FALSE(r->calls.items[i].is_method);
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(member >= 1); /* re.test() flagged */
|
||||
ASSERT_TRUE(bare >= 1); /* helper() not flagged */
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* this/super receivers keep the enclosing-class target, where a weak
|
||||
* namespace-proximity match is usually correct — so they are NOT flagged. A
|
||||
* new_expression has no member receiver and is never flagged either. */
|
||||
TEST(extract_ts_this_super_receiver_not_flagged) {
|
||||
CBMFileResult *r = extract("class A extends B {\n"
|
||||
" m() { this.helper(); super.render(); return new A(); }\n"
|
||||
" helper() {}\n"
|
||||
"}\n",
|
||||
CBM_LANG_TYPESCRIPT, "t", "b.ts");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
/* Every call here has a self receiver (this/super) or is a new-expression,
|
||||
* so NONE may be flagged. */
|
||||
for (int i = 0; i < r->calls.count; i++) {
|
||||
ASSERT_FALSE(r->calls.items[i].is_method);
|
||||
}
|
||||
/* Sanity: the this/super member calls were actually extracted. */
|
||||
ASSERT_TRUE(has_call(r, "helper")); /* this.helper */
|
||||
ASSERT_TRUE(has_call(r, "render")); /* super.render */
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* JS dialect behaves like TS (the flag is gated on the language set, not the
|
||||
* dialect). Replaces the pre-#592 test that asserted JS never flags is_method. */
|
||||
TEST(extract_js_member_call_flags_is_method) {
|
||||
CBMFileResult *r =
|
||||
extract("function run(o){ o.commit(); helper(); }\n", CBM_LANG_JAVASCRIPT, "t", "x.js");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
int member = 0;
|
||||
int bare = 0;
|
||||
for (int i = 0; i < r->calls.count; i++) {
|
||||
const char *cn = r->calls.items[i].callee_name;
|
||||
if (strcmp(cn, "o.commit") == 0) {
|
||||
member++;
|
||||
ASSERT_TRUE(r->calls.items[i].is_method);
|
||||
}
|
||||
if (strcmp(cn, "helper") == 0) {
|
||||
bare++;
|
||||
ASSERT_FALSE(r->calls.items[i].is_method);
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(member >= 1); /* o.commit() flagged */
|
||||
ASSERT_TRUE(bare >= 1); /* helper() not flagged */
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #668: walk_defs used a fixed `walk_defs_frame_t stack[4096]` — a ~160 KB
|
||||
* C-stack frame that overflowed small thread stacks (the reporter's crash was in
|
||||
* the "definitions pass" on a large SQL file), and whose `top < 4096` push guards
|
||||
@@ -3277,7 +3358,10 @@ SUITE(extraction) {
|
||||
RUN_TEST(extract_perl_config_string_not_a_callee);
|
||||
RUN_TEST(extract_perl_builtin_call_is_function_not_method);
|
||||
RUN_TEST(extract_perl_method_call_flags_is_method);
|
||||
RUN_TEST(extract_non_perl_method_call_not_flagged_is_method);
|
||||
RUN_TEST(extract_flag_exempt_method_call_not_flagged_is_method);
|
||||
RUN_TEST(extract_ts_member_call_flags_is_method);
|
||||
RUN_TEST(extract_ts_this_super_receiver_not_flagged);
|
||||
RUN_TEST(extract_js_member_call_flags_is_method);
|
||||
|
||||
/* R box-module imports + member calls */
|
||||
RUN_TEST(extract_r_box_use_imports_issue218);
|
||||
|
||||
@@ -1006,10 +1006,25 @@ TEST(lrp_ts_s6_inherited_method) {
|
||||
"import { Base } from './base';\n\n"
|
||||
"export class Child extends Base {\n extra(): string { return 'child'; }\n}\n\n"
|
||||
"export function run(c: Child): string { return c.describe(); }\n"}};
|
||||
/* Uncertain: c.describe() on Child — ts_lsp_cross must see Child extends Base
|
||||
* (requires TS INHERITS resolution, which has a known extraction bug).
|
||||
* Assert the correct outcome; RED if TS inheritance extraction bug blocks it. */
|
||||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S6/inherited_method", 0));
|
||||
/* RED: c.describe() needs ts_lsp_cross to walk `Child extends Base`. The
|
||||
* INHERITS edge IS extracted (the probe diagnostic shows INHERITS=1); the
|
||||
* gap is in ts_lsp_cross's cross-file inheritance RESOLUTION, not extraction.
|
||||
* Until the receiver-aware guard (#592/#606) landed, this scenario passed via
|
||||
* a unique_name registry fallback — "describe" is unique in this 2-file
|
||||
* fixture, so a weak short-name guess happened to be right. In a real repo the
|
||||
* same guess binds an arbitrary same-named method (the false edges #606
|
||||
* targets), so the guard now suppresses weak member-call matches with an
|
||||
* unresolved receiver. c.describe() is the ONLY call in the fixture, so a
|
||||
* correctly-suppressed run yields exactly zero CALLS.
|
||||
* Tripwire: assert store opened AND calls == 0 exactly (an infra/DB failure
|
||||
* must not vacuously pass); flip to ASSERT calls >= 1 once ts_lsp_cross
|
||||
* resolves inheritance (lsp_ts_*, a strategy the guard keeps). */
|
||||
LRP_Proj lp;
|
||||
cbm_store_t *store = lrp_index(&lp, f, 2);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int calls = cbm_store_count_edges_by_type(store, lp.project, "CALLS");
|
||||
lrp_cleanup(&lp, store);
|
||||
ASSERT_EQ(calls, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
|
||||
@@ -908,6 +908,226 @@ TEST(pipeline_incremental_preserves_cross_file_calls) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* TS/JS receiver-aware weak-strategy suppression (#592/#606 direction; Perl
|
||||
* precedent #477). A member call x.foo() whose receiver TYPE the TS-LSP cannot
|
||||
* resolve (a regex literal `re.test()`) must NOT be bound to a same-named
|
||||
* project method by a weak short-name strategy — that fabricates a CALLS edge.
|
||||
* Type-resolved receivers (`c.test()` on a typed SalesforceRestClient) and bare
|
||||
* local calls must still resolve. < 50 files → sequential path (pass_calls.c).
|
||||
* RED before the fix: checkFormat->test exists via unique_name/suffix_match. */
|
||||
static void write_temp_file(const char *dir, const char *name, const char *content);
|
||||
TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_tsjs_recv_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
|
||||
/* The lone project symbol named "test" — a real method. */
|
||||
write_temp_file(tmp, "src/client.ts",
|
||||
"export class SalesforceRestClient {\n"
|
||||
" test(): boolean {\n"
|
||||
" return true;\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
/* Regex receiver: `re.test(s)` calls RegExp.prototype.test, NOT the method.
|
||||
* The TS-LSP cannot bind it to a project symbol → the registry would guess
|
||||
* "test" by short name (weak). This is the false edge to suppress. */
|
||||
write_temp_file(tmp, "src/caller.ts",
|
||||
"const re = /^[a-z]+$/;\n"
|
||||
"export function checkFormat(s: string): boolean {\n"
|
||||
" return re.test(s);\n"
|
||||
"}\n");
|
||||
/* Typed receiver: `c` is annotated SalesforceRestClient, so the TS-LSP
|
||||
* resolves c.test() to the method (lsp_ts_method, conf 0.95) BEFORE the
|
||||
* registry runs — the guard's explicit drop-list keeps every lsp_* edge. */
|
||||
write_temp_file(tmp, "src/typed.ts",
|
||||
"import { SalesforceRestClient } from './client';\n"
|
||||
"export function runTyped(c: SalesforceRestClient): boolean {\n"
|
||||
" return c.test();\n"
|
||||
"}\n");
|
||||
/* Bare local call: same-module resolution, unaffected by the guard. */
|
||||
write_temp_file(tmp, "src/local.ts",
|
||||
"function localHelper(): number {\n"
|
||||
" return 1;\n"
|
||||
"}\n"
|
||||
"export function callsLocal(): number {\n"
|
||||
" return localHelper();\n"
|
||||
"}\n");
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/tsjs_recv.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);
|
||||
|
||||
/* (1) The false edge is suppressed (reproduce-first: RED before the fix). */
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "checkFormat", "test"));
|
||||
/* (2) The type-resolved receiver call survives (LSP wins before the guard). */
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "runTyped", "test"));
|
||||
/* (3) The bare local call survives (same-module / lsp_ts_local). */
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "callsLocal", "localHelper"));
|
||||
/* (4) breadth insurance: the real edges are still emitted. */
|
||||
ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 2);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Count nodes with the given exact name in the project (e.g. a Route path). */
|
||||
static int count_nodes_named(cbm_store_t *s, const char *project, const char *name) {
|
||||
cbm_node_t *ns = NULL;
|
||||
int n = 0;
|
||||
cbm_store_find_nodes_by_name(s, project, name, &ns, &n);
|
||||
if (ns) {
|
||||
cbm_store_free_nodes(ns, n);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Parallel-resolver regression for the TS/JS receiver guard (>= 50 files forces
|
||||
* pass_parallel.c's resolve_file_calls). The guard must not drop a weak member
|
||||
* match before the service classification runs — it suppresses ONLY the plain
|
||||
* CALLS fall-through, so every service edge (HTTP_CALLS via the #523 callee
|
||||
* bypass or emit_service_edge's unconditional detect_url_in_args, Route via the
|
||||
* ROUTE_REG fall-through, …) is emitted exactly as on main. These callees are
|
||||
* classified by main's verb-suffix + URL-arg heuristic, NOT by an HTTP library
|
||||
* name in the callee — a duplicated predicate keyed on the resolved QN lost them
|
||||
* (axios.get, api.patch on a renamed-axios instance, supertest request(app).get).
|
||||
* The regex false edge must stay suppressed in parallel too. CBM_WORKERS forces
|
||||
* >1 worker so the parallel path is taken regardless of the host core count. */
|
||||
TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_tsjs_par_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
|
||||
/* The lone project symbols named get()/patch()/test() — weak short-name
|
||||
* targets the registry mis-binds the member calls below to. */
|
||||
write_temp_file(tmp, "src/thing.ts",
|
||||
"export class ApiThing {\n"
|
||||
" get(): number {\n"
|
||||
" return 1;\n"
|
||||
" }\n"
|
||||
" patch(): number {\n"
|
||||
" return 2;\n"
|
||||
" }\n"
|
||||
" load(): number {\n"
|
||||
" return 3;\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"export class Other {\n"
|
||||
" test(): boolean {\n"
|
||||
" return true;\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
/* Untyped axios: the HTTP signal is in the callee name, not the resolved QN
|
||||
* (the registry mis-binds "get" to ApiThing.get). Must yield HTTP_CALLS. */
|
||||
write_temp_file(tmp, "src/http.ts",
|
||||
"export function callApi() {\n"
|
||||
" return axios.get('/api/orders');\n"
|
||||
"}\n");
|
||||
/* Renamed axios instance `api`: no HTTP lib id in the callee — classified by
|
||||
* the .patch verb suffix + URL arg. Must yield HTTP_CALLS (was lost when the
|
||||
* exemption keyed on the resolved QN only). */
|
||||
write_temp_file(tmp, "src/http2.ts",
|
||||
"export function callPatch(): unknown {\n"
|
||||
" return api.patch('/plans/:id', {});\n"
|
||||
"}\n");
|
||||
/* Supertest-style chained receiver `request(app).get('/y')` — untyped, verb
|
||||
* suffix + URL arg. */
|
||||
write_temp_file(tmp, "src/supertest.ts",
|
||||
"export function callSup(app: unknown): unknown {\n"
|
||||
" return request(app).get('/y');\n"
|
||||
"}\n");
|
||||
/* `dev.load('/data')`: `.load` is NOT a route suffix and `dev` is not an HTTP
|
||||
* lib, so main classifies it via the unconditional detect_url_in_args (URL
|
||||
* arg) -> HTTP_CALLS, while the weak plain match to ApiThing.load is the false
|
||||
* edge that must be suppressed. This is exactly the detect_url_in_args path
|
||||
* the previous (predicate-duplicating) guard skipped by dropping the call
|
||||
* before emit_service_edge ran — the class of ~399 HTTP_CALLS it lost. */
|
||||
write_temp_file(tmp, "src/load.ts",
|
||||
"export function callLoad(dev: unknown): unknown {\n"
|
||||
" return dev.load('/api/data');\n"
|
||||
"}\n");
|
||||
/* Route registration by callee suffix + a '/'-path arg. Must yield a Route. */
|
||||
write_temp_file(tmp, "src/routes.ts",
|
||||
"function handler() {}\n"
|
||||
"export function reg(router: any) {\n"
|
||||
" router.get('/users', handler);\n"
|
||||
"}\n");
|
||||
/* Regex receiver: the false edge that must stay suppressed in parallel too. */
|
||||
write_temp_file(tmp, "src/re.ts",
|
||||
"const re = /^[a-z]+$/;\n"
|
||||
"export function checkFormat(s: string): boolean {\n"
|
||||
" return re.test(s);\n"
|
||||
"}\n");
|
||||
/* Pad past MIN_FILES_FOR_PARALLEL (50) so the parallel resolver runs. */
|
||||
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/tsjs_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);
|
||||
|
||||
/* (1) Genuine HTTP_CALLS survive under the guard (>= 3):
|
||||
* - axios.get('/api/orders') -> 2 edges (recognized lib #523 callee bypass
|
||||
* + detect_url_in_args), and
|
||||
* - dev.load('/api/data') -> 1 edge via detect_url_in_args, which runs
|
||||
* unconditionally after emit_service_edge's branch even when the plain
|
||||
* fall-through is suppressed.
|
||||
* dev.load is the class the predicate-duplicating guard lost: `.load` is not
|
||||
* a route suffix and `dev` is not an HTTP lib, so it was dropped before
|
||||
* emit_service_edge ran (RED on that guard: only axios's 2). */
|
||||
ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 3);
|
||||
/* (2) The verb-suffix + route-path member calls keep their route
|
||||
* registrations (edge type CALLS -> a Route node named by the path). These
|
||||
* classify as route_registration on main, NOT HTTP_CALLS — Option A preserves
|
||||
* that by construction. Assert the three Route paths survive:
|
||||
* api.patch('/plans/:id'), request(app).get('/y'), router.get('/users'). */
|
||||
ASSERT_GTE(count_nodes_named(s, project, "/plans/:id"), 1);
|
||||
ASSERT_GTE(count_nodes_named(s, project, "/y"), 1);
|
||||
ASSERT_GTE(count_nodes_named(s, project, "/users"), 1);
|
||||
/* (3) The false plain-CALLS edges are suppressed in parallel: the regex
|
||||
* receiver and the dev.load weak match to ApiThing.load. */
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "checkFormat", "test"));
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "callLoad", "load"));
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/* ── Git history pass tests ─────────────────────────────────────── */
|
||||
|
||||
TEST(githistory_is_trackable) {
|
||||
@@ -6356,6 +6576,8 @@ SUITE(pipeline) {
|
||||
/* Calls pass */
|
||||
RUN_TEST(pipeline_calls_resolution);
|
||||
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);
|
||||
/* Git history pass */
|
||||
RUN_TEST(githistory_is_trackable);
|
||||
RUN_TEST(githistory_compute_coupling);
|
||||
|
||||
@@ -742,6 +742,48 @@ TEST(perl_suppress_keeps_high_confidence_and_genuine_calls) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tsjs_suppress_drops_weak_method_matches) {
|
||||
/* #592/#606: a TS/JS member call whose receiver the LSP could not type, that
|
||||
* landed via a WEAK short-name strategy, is generic-resolver noise → drop.
|
||||
* The strategies that actually reach the guards are the registry's
|
||||
* suffix_match / unique_name and the parallel field_type_hint; "fuzzy" is
|
||||
* covered defensively (cbm_registry_fuzzy_resolve is not wired into the
|
||||
* resolvers today) so a future wiring cannot silently reintroduce it. */
|
||||
ASSERT_TRUE(cbm_tsjs_suppress_weak_method_match(true, true, "suffix_match"));
|
||||
ASSERT_TRUE(cbm_tsjs_suppress_weak_method_match(true, true, "unique_name"));
|
||||
ASSERT_TRUE(cbm_tsjs_suppress_weak_method_match(true, true, "field_type_hint"));
|
||||
ASSERT_TRUE(cbm_tsjs_suppress_weak_method_match(true, true, "fuzzy"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tsjs_suppress_keeps_high_confidence_and_non_methods) {
|
||||
/* Keep every receiver-/import-aware strategy. Because the PARALLEL resolver
|
||||
* runs lsp_* strategies through this same guard variable, an explicit
|
||||
* drop-list must never touch them — asserting the lsp_* keeps here is the
|
||||
* regression guard for the "kills lsp edges" failure mode. The keep set
|
||||
* enumerates the resolver's non-weak strategies: registry
|
||||
* {import_map, import_map_suffix, same_module, qualified_suffix}, parallel
|
||||
* {callee_suffix, service_pattern}, and lsp_*. */
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "same_module"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "import_map"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "import_map_suffix"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "qualified_suffix"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "callee_suffix"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "service_pattern"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "lsp_ts_method"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "lsp_cross"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "lsp_ts_local"));
|
||||
/* A bare call (is_method=false) is a free-function call → never suppressed. */
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, false, "unique_name"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, false, "suffix_match"));
|
||||
/* Non-TS/JS languages are never affected. */
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(false, true, "suffix_match"));
|
||||
/* No match (NULL/empty strategy) → nothing to suppress. */
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, NULL));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, ""));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Suite ─────────────────────────────────────────────────────── */
|
||||
|
||||
SUITE(registry) {
|
||||
@@ -808,4 +850,6 @@ SUITE(registry) {
|
||||
RUN_TEST(perl_builtin_set_rejects_project_subs);
|
||||
RUN_TEST(perl_suppress_drops_weak_builtin_and_method_matches);
|
||||
RUN_TEST(perl_suppress_keeps_high_confidence_and_genuine_calls);
|
||||
RUN_TEST(tsjs_suppress_drops_weak_method_matches);
|
||||
RUN_TEST(tsjs_suppress_keeps_high_confidence_and_non_methods);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user