Skip to content

Commit cfd474d

Browse files
authored
Merge pull request #840 from DeusData/recover/836-tsjs-suppression
fix(resolver): suppress weak short-name strategies for TS/JS (recovers #836)
2 parents d2a5975 + 26689e7 commit cfd474d

10 files changed

Lines changed: 504 additions & 24 deletions

File tree

internal/cbm/cbm.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,9 @@ typedef struct {
236236
int loop_depth; // enclosing loop nesting at the call site
237237
int branch_depth; // enclosing branch nesting at the call site
238238
int start_line; // 1-based source line of the call (for def range-match)
239-
bool is_method; // Perl-only: arrow/method call ($obj->m). Default false.
239+
bool is_method; // method/member call with a non-self receiver. Perl:
240+
// arrow/method call ($obj->m). TS/JS/TSX: member call
241+
// x.foo() whose receiver is not this/super. Default false.
240242
} CBMCall;
241243

242244
typedef struct {

internal/cbm/extract_calls.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1892,6 +1892,29 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk
18921892
strcmp(ts_node_type(node), "method_call_expression") == 0) {
18931893
call.is_method = true;
18941894
}
1895+
// TS/JS/TSX receiver-aware guard (#592/#606 direction; same intent
1896+
// as the Perl flag above). Flag a member call x.foo() whose receiver
1897+
// is NOT `this`/`super`. When the TS-LSP cannot resolve the receiver
1898+
// type, the call-resolution pass suppresses weak short-name matches
1899+
// for these (so `re.test()` cannot fabricate an edge to a project
1900+
// `test`). this/super receivers stay unflagged — their target is the
1901+
// enclosing class, where a namespace-proximity weak match is usually
1902+
// right. Bare calls (helper()) and new_expression have no member
1903+
// receiver, so they keep is_method=false (struct is zero-init).
1904+
if ((ctx->language == CBM_LANG_JAVASCRIPT || ctx->language == CBM_LANG_TYPESCRIPT ||
1905+
ctx->language == CBM_LANG_TSX) &&
1906+
strcmp(ts_node_type(node), "call_expression") == 0) {
1907+
TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function"));
1908+
if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "member_expression") == 0) {
1909+
TSNode obj = ts_node_child_by_field_name(fn, TS_FIELD("object"));
1910+
if (!ts_node_is_null(obj)) {
1911+
const char *ok = ts_node_type(obj);
1912+
if (strcmp(ok, "this") != 0 && strcmp(ok, "super") != 0) {
1913+
call.is_method = true;
1914+
}
1915+
}
1916+
}
1917+
}
18951918

18961919
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
18971920
if (!ts_node_is_null(args)) {

src/pipeline/pass_calls.c

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -327,13 +327,21 @@ static void calls_emit_edge(cbm_gbuf_t *gbuf, int64_t src, int64_t tgt, const ch
327327

328328
static void emit_http_async_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
329329
const cbm_gbuf_node_t *source, const cbm_gbuf_node_t *target,
330-
const cbm_resolution_t *res, cbm_svc_kind_t svc) {
330+
const cbm_resolution_t *res, cbm_svc_kind_t svc,
331+
bool suppress_plain_calls) {
331332
const char *url_or_topic = call->first_string_arg;
332333
bool is_url = (url_or_topic && url_or_topic[0] != '\0' &&
333334
(url_or_topic[0] == '/' || strstr(url_or_topic, "://") != NULL));
334335
bool is_topic = (url_or_topic && url_or_topic[0] != '\0' && svc == CBM_SVC_ASYNC &&
335336
strlen(url_or_topic) > PAIR_LEN);
336337
if (!is_url && !is_topic) {
338+
/* No URL/topic → this is not a real service call; the svc kind was a
339+
* substring coincidence in the resolved QN (e.g. "SalesforceRestClient"
340+
* matches the "RestClient" HTTP lib). Emit a plain CALLS edge — unless a
341+
* weak TS/JS member-call match should be suppressed (#592/#606). */
342+
if (suppress_plain_calls) {
343+
return;
344+
}
337345
char esc_callee[CBM_SZ_256];
338346
cbm_json_escape(esc_callee, sizeof(esc_callee), call->callee_name);
339347
char props[CBM_SZ_512];
@@ -368,17 +376,22 @@ static void emit_http_async_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
368376
}
369377

370378
/* Classify a resolved call and emit the appropriate edge. */
379+
/* When suppress_plain_calls is true (a TS/JS/TSX weak short-name member-call
380+
* match, #592/#606), the route/HTTP/ASYNC/CONFIG service classifications below
381+
* still run — only the plain CALLS fall-through is skipped, so a fabricated
382+
* project edge is dropped while every service edge stays main-identical. */
371383
static void emit_classified_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
372384
const cbm_gbuf_node_t *source, const cbm_gbuf_node_t *target,
373385
const cbm_resolution_t *res, const char *module_qn,
374-
const char **imp_keys, const char **imp_vals, int imp_count) {
386+
const char **imp_keys, const char **imp_vals, int imp_count,
387+
bool suppress_plain_calls) {
375388
cbm_svc_kind_t svc = cbm_service_pattern_match(res->qualified_name);
376389
if (svc == CBM_SVC_ROUTE_REG && call->first_string_arg && call->first_string_arg[0] == '/') {
377390
handle_route_registration(ctx, call, source, module_qn, imp_keys, imp_vals, imp_count);
378391
return;
379392
}
380393
if (svc == CBM_SVC_HTTP || svc == CBM_SVC_ASYNC) {
381-
emit_http_async_edge(ctx, call, source, target, res, svc);
394+
emit_http_async_edge(ctx, call, source, target, res, svc, suppress_plain_calls);
382395
return;
383396
}
384397
if (svc == CBM_SVC_CONFIG) {
@@ -393,6 +406,9 @@ static void emit_classified_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
393406
call);
394407
return;
395408
}
409+
if (suppress_plain_calls) {
410+
return; /* weak TS/JS member-call match with an unresolved receiver (#606) */
411+
}
396412
char esc_c2[CBM_SZ_256];
397413
cbm_json_escape(esc_c2, sizeof(esc_c2), call->callee_name);
398414
char props[CBM_SZ_512];
@@ -450,7 +466,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
450466
res.strategy = lsp->strategy;
451467
res.candidate_count = 1;
452468
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys,
453-
imp_vals, imp_count);
469+
imp_vals, imp_count, false);
454470
return SKIP_ONE;
455471
}
456472
}
@@ -474,7 +490,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
474490
.confidence = PC_SVC_PATTERN_CONF,
475491
.strategy = "service_pattern",
476492
.candidate_count = 0};
477-
emit_http_async_edge(ctx, call, source_node, NULL, &svc_res, csvc);
493+
emit_http_async_edge(ctx, call, source_node, NULL, &svc_res, csvc, false);
478494
return SKIP_ONE;
479495
}
480496
}
@@ -502,7 +518,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
502518
.confidence = PC_SVC_PATTERN_CONF,
503519
.strategy = "service_pattern",
504520
.candidate_count = 0};
505-
emit_http_async_edge(ctx, call, source_node, NULL, &svc_res, esvc);
521+
emit_http_async_edge(ctx, call, source_node, NULL, &svc_res, esvc, false);
506522
return SKIP_ONE;
507523
}
508524
}
@@ -522,6 +538,21 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
522538
return 0;
523539
}
524540

541+
/* TS/JS/TSX weak-method suppression (#592/#606). A member call x.foo() only
542+
* reaches the registry when the TS-LSP could not resolve the receiver type
543+
* (the LSP block above already returned for type-resolved calls, including
544+
* the "resolved but target out of gbuf" fall-through). Binding such a call
545+
* by a weak short-name strategy fabricates an edge (`re.test()` -> a project
546+
* `test`). Rather than drop it here — which would also skip the service
547+
* bypasses below and emit_classified_edge's route/HTTP/CONFIG branches —
548+
* defer to emit_classified_edge and suppress ONLY the plain-CALLS
549+
* fall-through, so every service edge stays main-identical. res.strategy may
550+
* be lsp_* here; the helper's explicit drop-list leaves lsp_* untouched. */
551+
bool is_tsjs =
552+
lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX;
553+
bool tsjs_drop_plain_call =
554+
cbm_tsjs_suppress_weak_method_match(is_tsjs, call->is_method, res.strategy);
555+
525556
/* Service-pattern HTTP/ASYNC calls to an EXTERNAL client library (e.g.
526557
* `requests.get("/api/orders/{id}")`) resolve to a QN containing the library
527558
* 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,
537568
(u[0] == '/' || strstr(u, "://") != NULL ||
538569
(svc == CBM_SVC_ASYNC && strlen(u) > PAIR_LEN));
539570
if (has_url_or_topic) {
540-
emit_http_async_edge(ctx, call, source_node, NULL, &res, svc);
571+
emit_http_async_edge(ctx, call, source_node, NULL, &res, svc, false);
541572
return SKIP_ONE;
542573
}
543574
}
@@ -547,7 +578,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
547578
return 0;
548579
}
549580
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys, imp_vals,
550-
imp_count);
581+
imp_count, tsjs_drop_plain_call);
551582
return SKIP_ONE;
552583
}
553584

src/pipeline/pass_parallel.c

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1765,11 +1765,18 @@ static void emit_trpc_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, cons
17651765
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "TRPC_CALLS", props);
17661766
}
17671767

1768+
/* When suppress_plain_calls is true (a TS/JS/TSX weak short-name member-call
1769+
* match, #592/#606), every service classification below still runs — only the
1770+
* plain CALLS fall-through (emit_normal_calls_edge) is skipped. detect_url_in_args
1771+
* and the HTTP/ASYNC/gRPC/GraphQL/tRPC/CONFIG/route branches are unaffected, so
1772+
* a verb-suffix HTTP client (api.patch('/x')), broker, or route registration
1773+
* keeps its edge; only the fabricated project CALLS edge is dropped. */
17681774
static void emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
17691775
const cbm_gbuf_node_t *target, const CBMCall *call,
17701776
const cbm_resolution_t *res, const char *module_qn,
17711777
const cbm_registry_t *registry, const cbm_gbuf_t *main_gbuf,
1772-
const char **imp_keys, const char **imp_vals, int imp_count) {
1778+
const char **imp_keys, const char **imp_vals, int imp_count,
1779+
bool suppress_plain_calls) {
17731780
cbm_svc_kind_t svc = cbm_service_pattern_match(res->qualified_name);
17741781
const char *arg = call->first_string_arg;
17751782

@@ -1815,7 +1822,7 @@ static void emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
18151822
emit_trpc_edge(gbuf, source, call, res);
18161823
} else if (svc == CBM_SVC_CONFIG) {
18171824
emit_config_edge(gbuf, source, target, call, res, arg);
1818-
} else {
1825+
} else if (!suppress_plain_calls) {
18191826
emit_normal_calls_edge(gbuf, source, target, call, res);
18201827
}
18211828

@@ -2034,6 +2041,21 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
20342041
continue;
20352042
}
20362043

2044+
/* TS/JS/TSX weak-method suppression (#592/#606). The receiver-aware guard
2045+
* must NOT drop this call here: doing so would also skip the #523
2046+
* callee-name service bypass below, emit_service_edge's route/gRPC/config
2047+
* branches, and its unconditional detect_url_in_args (which classifies
2048+
* verb-suffix HTTP clients like api.patch('/x')). Instead, defer to the
2049+
* emit path and suppress ONLY the plain-CALLS fall-through
2050+
* (emit_normal_calls_edge), so every service edge stays main-identical by
2051+
* construction. res.strategy may carry an lsp_* value here (LSP-resolved
2052+
* calls keep res through this point); the helper's EXPLICIT drop-list
2053+
* leaves lsp_ts_method / lsp_cross untouched. See #606 direction. */
2054+
bool is_tsjs =
2055+
lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX;
2056+
bool tsjs_drop_plain_call =
2057+
cbm_tsjs_suppress_weak_method_match(is_tsjs, call->is_method, res.strategy);
2058+
20372059
/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
20382060
* service signal lives in the callee_name. The registry can mis-resolve
20392061
* 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
20542076
.strategy = "service_pattern"};
20552077
emit_service_edge(ws->local_edge_buf, source_node, source_node, call, &svc_res,
20562078
module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals,
2057-
imp_count);
2079+
imp_count, false);
20582080
continue;
20592081
}
20602082
}
@@ -2066,7 +2088,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
20662088
.strategy = "callee_suffix"};
20672089
emit_service_edge(ws->local_edge_buf, source_node, source_node, call, &fake_res,
20682090
module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals,
2069-
imp_count);
2091+
imp_count, false);
20702092
}
20712093
continue;
20722094
}
@@ -2097,15 +2119,17 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
20972119
(psvc == CBM_SVC_ASYNC && strlen(u) > PP_ESC_SPACE));
20982120
if (url_or_topic) {
20992121
emit_service_edge(ws->local_edge_buf, source_node, NULL, call, &res, module_qn,
2100-
rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count);
2122+
rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count,
2123+
false);
21012124
ws->calls_resolved++;
21022125
}
21032126
}
21042127
continue;
21052128
}
21062129
_rc_t0 = extract_now_ns();
21072130
emit_service_edge(ws->local_edge_buf, source_node, target_node, call, &res, module_qn,
2108-
rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count);
2131+
rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count,
2132+
tsjs_drop_plain_call);
21092133
atomic_fetch_add_explicit(&rc->time_ns_rc_emit, extract_now_ns() - _rc_t0,
21102134
memory_order_relaxed);
21112135
ws->calls_resolved++;

src/pipeline/pipeline.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ bool cbm_perl_is_builtin(const char *name);
223223
bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *callee_name,
224224
const char *strategy);
225225

226+
/* Decide whether a resolved TS/JS/TSX member-call edge is weak-strategy noise to
227+
* drop (#592/#606): true only for TS/JS, only for a member call with a
228+
* non-this/super receiver (is_method), and only when the match used a weak
229+
* short-name strategy (suffix_match / unique_name / field_type_hint / fuzzy).
230+
* Explicit drop-list keeps every lsp_* / import / same-module / qualified match.
231+
* Pure; unit-tested in test_registry.c. */
232+
bool cbm_tsjs_suppress_weak_method_match(bool is_tsjs, bool is_method, const char *strategy);
233+
226234
/* Get the label of a qualified name, or NULL if not found. */
227235
const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn);
228236

src/pipeline/registry.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,33 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c
423423
return true; /* weak short-name match (suffix_match / unique_name / …) → drop */
424424
}
425425

426+
/* TS/JS analogue of the Perl guard above (#592/#606 direction; precedent #477).
427+
* A member call `x.foo()` reaches the weak textual cascade ONLY when the TS-LSP
428+
* could not resolve the receiver type — type-resolved calls win via lsp_*
429+
* strategies before the registry runs. Binding such a call to a project symbol
430+
* by a weak short-name strategy fabricates a CALLS edge (`re.test()` ->
431+
* SalesforceRestClient.test, `date.toISOString()` -> any project toISOString).
432+
* Drop ONLY the weak strategies; keep import/same-module/qualified-tail matches
433+
* and every lsp_* strategy. Uses an EXPLICIT drop-list (not keep-list +
434+
* default-drop) because the parallel resolver runs lsp_* strategies through the
435+
* same guard variable — a default-drop would silently kill lsp_ts_method. Pure
436+
* + side-effect-free so the contract is unit-testable without a full pipeline. */
437+
bool cbm_tsjs_suppress_weak_method_match(bool is_tsjs, bool is_method, const char *strategy) {
438+
if (!is_tsjs || !is_method || !strategy || !strategy[0]) {
439+
return false;
440+
}
441+
/* Weak short-name strategies that actually reach the call-resolution guards:
442+
* the registry's suffix_match / unique_name and the parallel field_type_hint.
443+
* "fuzzy" is listed as defensive insurance only — cbm_registry_fuzzy_resolve
444+
* is not wired into the sequential/parallel resolvers today, so it never
445+
* reaches this helper, but naming it keeps a future wiring from silently
446+
* reintroducing the noise. Everything else — same_module / import_map /
447+
* import_map_suffix / qualified_suffix / callee_suffix / service_pattern /
448+
* lsp_* — is a receiver- or import-aware match and is KEPT. */
449+
return strcmp(strategy, "suffix_match") == 0 || strcmp(strategy, "unique_name") == 0 ||
450+
strcmp(strategy, "field_type_hint") == 0 || strcmp(strategy, "fuzzy") == 0;
451+
}
452+
426453
/* ── Lifecycle ──────────────────────────────────────────────────── */
427454

428455
cbm_registry_t *cbm_registry_new(void) {

0 commit comments

Comments
 (0)