fix(py-lsp): resolve dict-dispatch calls funcs["a"](v) to the dispatch-table target

A Python call through a dict dispatch table, funcs["a"](v), produced no CALLS
edge at all: the call's function field is a subscript, which the textual
callee extractor did not handle, so no textual call existed for the py-LSP's
lsp_dict_dispatch resolution to attach to (callable=0, strategy absent).

Three coordinated changes, the same reason-field pattern used for func_ptr and
ctor refs:
- extract_calls: for a Python call whose function field is a subscript with an
  identifier base and a literal string key, emit the base identifier ("funcs")
  as the textual callee so a callable-sourced CALLS edge exists. Gated to that
  exact shape so other subscript calls (arr[i]()) are unaffected.
- py_lsp: the dict-dispatch resolution stashes the subscript base identifier in
  reason (the textual callee differs from the resolved target "foo"), via a new
  py_emit_resolved_call_reason; py_emit_resolved_call delegates with NULL.
- lsp_resolve: admit lsp_dict_dispatch into the reason-matched join so the
  resolution binds to the textual call site.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-06-28 04:34:54 +02:00
parent 3b2a291e99
commit 9ef37e89be
3 changed files with 34 additions and 4 deletions
+19
View File
@@ -983,6 +983,25 @@ static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char *
CBMLanguage lang) {
const char *nk = ts_node_type(node);
/* Python dict-dispatch call `funcs["a"](v)`: the call's `function` field is a
* subscript whose base is the identifier holding the dispatch table. Emit the
* base identifier ("funcs") as the textual callee so a CALLS edge exists; the
* py-LSP resolves it to the real target and joins via `reason` (lsp_resolve.h,
* lsp_dict_dispatch). Gated to the literal-string-key shape the LSP handles so
* other subscript calls (arr[i]()) are unaffected. */
if (lang == CBM_LANG_PYTHON && strcmp(nk, "call") == 0) {
TSNode fnf = ts_node_child_by_field_name(node, TS_FIELD("function"));
if (!ts_node_is_null(fnf) && strcmp(ts_node_type(fnf), "subscript") == 0) {
TSNode val = ts_node_child_by_field_name(fnf, TS_FIELD("value"));
TSNode idx = ts_node_child_by_field_name(fnf, TS_FIELD("subscript"));
if (!ts_node_is_null(val) && !ts_node_is_null(idx) &&
strcmp(ts_node_type(val), "identifier") == 0 &&
strcmp(ts_node_type(idx), "string") == 0) {
return cbm_node_text(a, val, source);
}
}
}
if (lang == CBM_LANG_JSONNET) {
char *c = extract_jsonnet_callee(a, node, source, nk);
return c ? c : extract_scripting_callee(a, node, source, lang, nk);
+13 -3
View File
@@ -324,8 +324,9 @@ static const char *py_lookup_dict_dispatch(PyLSPContext *ctx, const char *var, c
return NULL;
}
static void py_emit_resolved_call(PyLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence) {
static void py_emit_resolved_call_reason(PyLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence,
const char *reason) {
if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn)
return;
// Dedupe by (caller, callee). Bounded-window scan: most duplicate
@@ -354,9 +355,15 @@ static void py_emit_resolved_call(PyLSPContext *ctx, const char *callee_qn, cons
rc.callee_qn = cbm_arena_strdup(ctx->arena, callee_qn);
rc.strategy = strategy;
rc.confidence = confidence;
rc.reason = reason ? cbm_arena_strdup(ctx->arena, reason) : NULL;
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
static void py_emit_resolved_call(PyLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence) {
py_emit_resolved_call_reason(ctx, callee_qn, strategy, confidence, NULL);
}
/* ── helpers: registry-driven attribute lookup with depth cap ──── */
static const CBMRegisteredFunc *py_lookup_attribute_depth(PyLSPContext *ctx, const char *type_qn,
@@ -1664,7 +1671,10 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
if (var_name && k_text) {
const char *tgt = py_lookup_dict_dispatch(ctx, var_name, k_text);
if (tgt) {
py_emit_resolved_call(ctx, tgt, "lsp_dict_dispatch", 0.86f);
/* The textual callee of `funcs["a"](v)` is the subscript base
* identifier ("funcs"), not the resolved target ("foo"), so
* stash it in `reason` for the join (see lsp_resolve.h). */
py_emit_resolved_call_reason(ctx, tgt, "lsp_dict_dispatch", 0.86f, var_name);
return;
}
}
+2 -1
View File
@@ -114,7 +114,8 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
(strcmp(rc->strategy, "lsp_func_ptr") == 0 ||
strcmp(rc->strategy, "lsp_dll_resolve") == 0 ||
strcmp(rc->strategy, "lsp_method_ref_ctor") == 0 ||
strcmp(rc->strategy, "lsp_method_ref_ctor_synth") == 0) &&
strcmp(rc->strategy, "lsp_method_ref_ctor_synth") == 0 ||
strcmp(rc->strategy, "lsp_dict_dispatch") == 0) &&
strcmp(cbm_lsp_bare_segment(rc->reason), call_short) == 0)) {
continue;
}