From 0b94f44f966dfe47a1b4c945d1102e67da595002 Mon Sep 17 00:00:00 2001 From: Thomas Dyar Date: Mon, 22 Jun 2026 18:16:25 -0400 Subject: [PATCH] fix(extract): string-dispatch calls in dead code path, never linked to target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python calls of the form iris_obj.classMethodValue('Pkg.Class', 'Method') emitted no CALLS edge. The detection existed in walk_calls, which is dead code — the pipeline uses cbm_extract_unified → handle_calls, not walk_calls. Move the IRIS Native API string-dispatch detection to handle_calls. When a Python call's callee ends in .classMethodValue, .classMethodVoid, .classMethodBoolean, or .classMethodObject, and both string-literal arguments can be extracted, emit a synthetic CALLS edge to Arg1.Arg2. The pattern generalizes beyond this specific API: any library that takes class/method names as string literals follows the same shape. Regression test: python_iris_classMethodValue in test_extraction.c. Signed-off-by: Thomas Dyar --- internal/cbm/cbm.h | 1 - internal/cbm/extract_calls.c | 128 ++++++++++++++--------------------- internal/cbm/lang_specs.h | 7 ++ tests/test_extraction.c | 15 ++++ 4 files changed, 71 insertions(+), 80 deletions(-) diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index 7b89bd4fa..39ddb96b0 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -584,7 +584,6 @@ void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch); // --- Sub-extractor entry points --- void cbm_extract_definitions(CBMExtractCtx *ctx); -void cbm_extract_calls(CBMExtractCtx *ctx); void cbm_extract_imports(CBMExtractCtx *ctx); void cbm_extract_usages(CBMExtractCtx *ctx); void cbm_extract_semantic(CBMExtractCtx *ctx); diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 583fbe6d7..80c31d05c 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -58,10 +58,25 @@ static const char *strip_quotes(CBMArena *a, const char *text) { return text; } +// Callee suffixes for IRIS Python interop string-dispatch. Kept at file scope +// (not inside the function) to satisfy cppcheck variableScope. +static const char *s_py_dispatch_suffixes[] = {".classMethodValue", ".classMethodVoid", + ".classMethodBoolean", ".classMethodObject", NULL}; + +// Per-language callee-suffix dispatch table — returns a NULL-terminated list of +// method-name suffixes whose calls should be resolved by extracting class+method +// from the first two string arguments (e.g. IRIS Python interop). Kept here +// rather than in CBMLangSpec to avoid -Wmissing-field-initializers across 155 +// language rows. +const char **cbm_string_dispatch_suffixes(CBMLanguage lang) { + if (lang == CBM_LANG_PYTHON) { + return s_py_dispatch_suffixes; + } + return NULL; +} + // Forward declarations -static void walk_calls(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec); static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, CBMLanguage lang); -static void extract_jsx_refs(CBMExtractCtx *ctx, TSNode node); static char *gotemplate_callee(CBMArena *a, TSNode node, const char *source); // Lean 4: check if an apply node is inside a type annotation. @@ -735,20 +750,6 @@ static const char *strip_and_validate_string_arg(CBMArena *a, char *text) { return text; } -// Extract first string argument from a call's arguments node. -static const char *extract_first_string_arg(CBMExtractCtx *ctx, TSNode args) { - uint32_t nc = ts_node_named_child_count(args); - for (uint32_t ai = 0; ai < nc && ai < MAX_POSITIONAL_SCAN; ai++) { - TSNode arg = ts_node_named_child(args, ai); - const char *ak = ts_node_type(arg); - if (is_string_like(ak)) { - char *text = cbm_node_text(ctx->arena, arg, ctx->source); - return strip_and_validate_string_arg(ctx->arena, text); - } - } - return NULL; -} - // Return the (dequoted) first string-literal child of a node, or NULL. static char *gotemplate_string_child(CBMArena *a, TSNode parent, const char *source) { TSNode s = cbm_find_child_by_kind(parent, "interpreted_string_literal"); @@ -791,72 +792,21 @@ static char *gotemplate_callee(CBMArena *a, TSNode node, const char *source) { return NULL; } -// Walk AST for call nodes (iterative) -static void walk_calls(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) { - TSNodeStack stack; - ts_nstack_init(&stack, ctx->arena, CBM_SZ_512); - ts_nstack_push(&stack, ctx->arena, root); - - while (stack.count > 0) { - TSNode node = ts_nstack_pop(&stack); - const char *kind = ts_node_type(node); - - if (cbm_kind_in_set(node, spec->call_node_types)) { - char *callee = extract_callee_name(ctx->arena, node, ctx->source, ctx->language); - if (callee && callee[0] && !cbm_is_keyword(callee, ctx->language)) { - CBMCall call = {0}; - call.callee_name = callee; - call.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node); - - TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments")); - if (!ts_node_is_null(args)) { - call.first_string_arg = extract_first_string_arg(ctx, args); - } - cbm_calls_push(&ctx->result->calls, ctx->arena, call); +static const char *extract_nth_string_arg(CBMExtractCtx *ctx, TSNode args, uint32_t n) { + uint32_t nc = ts_node_named_child_count(args); + uint32_t found = 0; + for (uint32_t ai = 0; ai < nc && ai < MAX_POSITIONAL_SCAN + n; ai++) { + TSNode arg = ts_node_named_child(args, ai); + const char *ak = ts_node_type(arg); + if (is_string_like(ak)) { + if (found == n) { + char *text = cbm_node_text(ctx->arena, arg, ctx->source); + return strip_and_validate_string_arg(ctx->arena, text); } + found++; } - - if (ctx->language == CBM_LANG_TSX || ctx->language == CBM_LANG_JAVASCRIPT) { - if (strcmp(kind, "jsx_self_closing_element") == 0 || - strcmp(kind, "jsx_opening_element") == 0) { - extract_jsx_refs(ctx, node); - } - } - - ts_nstack_push_children(&stack, ctx->arena, node); } -} - -// Extract JSX component references (uppercase = component, lowercase = HTML) -static void extract_jsx_refs(CBMExtractCtx *ctx, TSNode node) { - TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); - if (ts_node_is_null(name_node)) { - return; - } - - char *name = cbm_node_text(ctx->arena, name_node, ctx->source); - if (!name || !name[0]) { - return; - } - - // Only uppercase names are components - if (name[0] < 'A' || name[0] > 'Z') { - return; - } - - CBMCall call = {0}; - call.callee_name = name; - call.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node); - cbm_calls_push(&ctx->result->calls, ctx->arena, call); -} - -void cbm_extract_calls(CBMExtractCtx *ctx) { - const CBMLangSpec *spec = cbm_lang_spec(ctx->language); - if (!spec || !spec->call_node_types || !spec->call_node_types[0]) { - return; - } - - walk_calls(ctx, ctx->root, spec); + return NULL; } // --- Unified handler: called once per node by the cursor walk --- @@ -1204,6 +1154,26 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk } cbm_calls_push(&ctx->result->calls, ctx->arena, call); + + const char **dispatch_suffixes = cbm_string_dispatch_suffixes(ctx->language); + if (dispatch_suffixes && !ts_node_is_null(args)) { + const char *cn = call.callee_name; + size_t len = cn ? strlen(cn) : 0; + for (const char **nm = dispatch_suffixes; *nm; nm++) { + size_t nlen = strlen(*nm); + if (len >= nlen && strcmp(cn + len - nlen, *nm) == 0) { + const char *cls = extract_nth_string_arg(ctx, args, 0); + const char *mth = extract_nth_string_arg(ctx, args, 1); + if (cls && mth) { + CBMCall xcall = {0}; + xcall.callee_name = cbm_arena_sprintf(ctx->arena, "%s.%s", cls, mth); + xcall.enclosing_func_qn = call.enclosing_func_qn; + cbm_calls_push(&ctx->result->calls, ctx->arena, xcall); + } + break; + } + } + } } } diff --git a/internal/cbm/lang_specs.h b/internal/cbm/lang_specs.h index 61d97b4df..ab823557b 100644 --- a/internal/cbm/lang_specs.h +++ b/internal/cbm/lang_specs.h @@ -41,6 +41,13 @@ typedef struct { const CBMEmbeddedLangSpec *embedded_imports; } CBMLangSpec; +// Returns a NULL-terminated list of callee-name suffixes that indicate a +// string-dispatch call for a given language (e.g. ".classMethodValue" for +// Python/IRIS), or NULL if the language has no such dispatch pattern. +// Kept out of CBMLangSpec to avoid -Wmissing-field-initializers across 155 +// language rows; the table lives in extract_calls.c next to the dispatch code. +const char **cbm_string_dispatch_suffixes(CBMLanguage lang); + // Get the language spec for a given language. Returns NULL for unsupported. const CBMLangSpec *cbm_lang_spec(CBMLanguage lang); diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 4bad219ba..d06b2a506 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -2019,6 +2019,20 @@ TEST(python_calls) { PASS(); } +TEST(python_iris_classMethodValue) { + CBMFileResult *r = extract( + "import iris\n" + "iris_obj = iris.cls('%Library.ObjectScript')\n" + "def call_bfs(n):\n" + " return iris_obj.classMethodValue('Graph.KG.TraversalBFS', 'BFSFastJson', n)\n", + CBM_LANG_PYTHON, "t", "store.py"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_call(r, "Graph.KG.TraversalBFS.BFSFastJson")); + cbm_free_result(r); + PASS(); +} + TEST(go_calls) { CBMFileResult *r = extract("package main\nimport \"fmt\"\nfunc main() { fmt.Println(\"hello\") }\n", @@ -3100,6 +3114,7 @@ SUITE(extraction) { /* Cross-cutting */ RUN_TEST(python_calls); + RUN_TEST(python_iris_classMethodValue); RUN_TEST(go_calls); RUN_TEST(python_imports); RUN_TEST(js_imports);