Merge pull request #447 from isc-tdyar/fix/string-dispatch-dead-code

fix(extract): string-dispatch CALLS edges never emitted — detection was in dead code
This commit is contained in:
Martin Vogel
2026-06-23 23:46:13 +02:00
committed by GitHub
4 changed files with 71 additions and 80 deletions
-1
View File
@@ -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);
+49 -79
View File
@@ -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;
}
}
}
}
}
+7
View File
@@ -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);
+15
View File
@@ -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);