diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index c93e46a44..e7ee992ad 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -515,6 +515,17 @@ static char *extract_objc_callee(CBMArena *a, TSNode node, const char *source, c return ts_node_is_null(selector) ? NULL : cbm_node_text(a, selector, source); } +/* tree-sitter-elixir gives a call's arguments node no field name, so it is + * found positionally. Mirrors elixir_call_args() in extract_defs.c, which the + * definition side has always used for the same reason. */ +static TSNode elixir_call_arguments_fallback(TSNode node) { + TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments")); + if (ts_node_is_null(args) && ts_node_child_count(node) > 1) { + args = ts_node_child(node, 1); + } + return args; +} + // Erlang: extract callee from call node's first child. static char *extract_erlang_callee(CBMArena *a, TSNode node, const char *source, const char *nk) { if (strcmp(nk, "call") != 0 || ts_node_child_count(node) == 0) { @@ -3311,6 +3322,21 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML } TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments")); + /* tree-sitter-elixir attaches NO field name to a call's arguments + * node — its whole field set is key, left, operand, operator, + * quoted_start, quoted_end, right, target, value — so the lookup + * above is always null for Elixir and first_string_arg was never + * populated for ANY Elixir call. That silently disabled every + * downstream signal keyed off a call's string argument: Phoenix + * route paths, HTTP/async service URLs, and config keys. + * cbm_elixir_call_args is the shared second-child fallback the + * definition side already uses. Restricted to `call` nodes — + * Elixir's other call kinds (`dot`, the `|>` binary_operator) have + * no arguments node in that position. */ + if (ts_node_is_null(args) && ctx->language == CBM_LANG_ELIXIR && + strcmp(ts_node_type(node), "call") == 0) { + args = elixir_call_arguments_fallback(node); + } // ObjectScript stores args under oref_method/method_args, not the // generic "arguments" field; macro arguments add one wrapper. if (ts_node_is_null(args) && is_objectscript_language(ctx->language)) { diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 447e7c104..40b8497b3 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -1005,6 +1005,33 @@ TEST(elixir_function) { PASS(); } +/* tree-sitter-elixir gives a call's arguments node no field name, so the + * generic `arguments` field lookup returns null and first_string_arg was never + * populated for any Elixir call — Phoenix route paths, service URLs and config + * keys all key off it. */ +TEST(elixir_call_string_argument) { + CBMFileResult *r = extract("defmodule Sample do\n" + " def run do\n" + " get(\"/wallets\", WalletController)\n" + " end\n" + "end\n", + CBM_LANG_ELIXIR, "t", "sample.ex"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + int seen = 0; + for (int i = 0; i < r->calls.count; i++) { + if (strcmp(r->calls.items[i].callee_name, "get") != 0) { + continue; + } + seen = 1; + ASSERT_NOT_NULL(r->calls.items[i].first_string_arg); + ASSERT_STR_EQ("/wallets", r->calls.items[i].first_string_arg); + } + ASSERT_EQ(1, seen); + cbm_free_result(r); + PASS(); +} + /* --- Haskell --- */ TEST(haskell_function) { CBMFileResult *r = extract("add :: Int -> Int -> Int\nadd x y = x + y\n\nmultiply :: Int -> " @@ -5432,6 +5459,7 @@ SUITE(extraction) { /* Functional */ RUN_TEST(elixir_function); + RUN_TEST(elixir_call_string_argument); RUN_TEST(haskell_function); RUN_TEST(ocaml_function); RUN_TEST(erlang_function);