Skip to content

Commit dc0f8ae

Browse files
committed
fix(elixir): populate first_string_arg for Elixir calls
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 and value, so ts_node_child_by_field_name(node, "arguments") is always null for Elixir and call.first_string_arg was never populated for any Elixir call. Every downstream signal that keys off a call's string argument was therefore dead for Elixir: Phoenix route paths, HTTP/async service URLs, and config keys. Nothing reported an error — the field was simply always NULL. The definition side already knows this: extract_defs.c carries elixir_call_args() with a positional second-child fallback for exactly this reason. This adds the same fallback on the call side, gated to `call` nodes, since Elixir's other call kinds (`dot`, the `|>` binary_operator) carry no arguments node in that position. The new test pins the field directly rather than a downstream effect. Signed-off-by: Henry Hazan <henry@teramine.io>
1 parent 46ae198 commit dc0f8ae

2 files changed

Lines changed: 54 additions & 0 deletions

File tree

‎internal/cbm/extract_calls.c‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,17 @@ static char *extract_objc_callee(CBMArena *a, TSNode node, const char *source, c
515515
return ts_node_is_null(selector) ? NULL : cbm_node_text(a, selector, source);
516516
}
517517

518+
/* tree-sitter-elixir gives a call's arguments node no field name, so it is
519+
* found positionally. Mirrors elixir_call_args() in extract_defs.c, which the
520+
* definition side has always used for the same reason. */
521+
static TSNode elixir_call_arguments_fallback(TSNode node) {
522+
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
523+
if (ts_node_is_null(args) && ts_node_child_count(node) > 1) {
524+
args = ts_node_child(node, 1);
525+
}
526+
return args;
527+
}
528+
518529
// Erlang: extract callee from call node's first child.
519530
static char *extract_erlang_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
520531
if (strcmp(nk, "call") != 0 || ts_node_child_count(node) == 0) {
@@ -3311,6 +3322,21 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
33113322
}
33123323

33133324
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
3325+
/* tree-sitter-elixir attaches NO field name to a call's arguments
3326+
* node — its whole field set is key, left, operand, operator,
3327+
* quoted_start, quoted_end, right, target, value — so the lookup
3328+
* above is always null for Elixir and first_string_arg was never
3329+
* populated for ANY Elixir call. That silently disabled every
3330+
* downstream signal keyed off a call's string argument: Phoenix
3331+
* route paths, HTTP/async service URLs, and config keys.
3332+
* cbm_elixir_call_args is the shared second-child fallback the
3333+
* definition side already uses. Restricted to `call` nodes —
3334+
* Elixir's other call kinds (`dot`, the `|>` binary_operator) have
3335+
* no arguments node in that position. */
3336+
if (ts_node_is_null(args) && ctx->language == CBM_LANG_ELIXIR &&
3337+
strcmp(ts_node_type(node), "call") == 0) {
3338+
args = elixir_call_arguments_fallback(node);
3339+
}
33143340
// ObjectScript stores args under oref_method/method_args, not the
33153341
// generic "arguments" field; macro arguments add one wrapper.
33163342
if (ts_node_is_null(args) && is_objectscript_language(ctx->language)) {

‎tests/test_extraction.c‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,33 @@ TEST(elixir_function) {
10051005
PASS();
10061006
}
10071007

1008+
/* tree-sitter-elixir gives a call's arguments node no field name, so the
1009+
* generic `arguments` field lookup returns null and first_string_arg was never
1010+
* populated for any Elixir call — Phoenix route paths, service URLs and config
1011+
* keys all key off it. */
1012+
TEST(elixir_call_string_argument) {
1013+
CBMFileResult *r = extract("defmodule Sample do\n"
1014+
" def run do\n"
1015+
" get(\"/wallets\", WalletController)\n"
1016+
" end\n"
1017+
"end\n",
1018+
CBM_LANG_ELIXIR, "t", "sample.ex");
1019+
ASSERT_NOT_NULL(r);
1020+
ASSERT_FALSE(r->has_error);
1021+
int seen = 0;
1022+
for (int i = 0; i < r->calls.count; i++) {
1023+
if (strcmp(r->calls.items[i].callee_name, "get") != 0) {
1024+
continue;
1025+
}
1026+
seen = 1;
1027+
ASSERT_NOT_NULL(r->calls.items[i].first_string_arg);
1028+
ASSERT_STR_EQ("/wallets", r->calls.items[i].first_string_arg);
1029+
}
1030+
ASSERT_EQ(1, seen);
1031+
cbm_free_result(r);
1032+
PASS();
1033+
}
1034+
10081035
/* --- Haskell --- */
10091036
TEST(haskell_function) {
10101037
CBMFileResult *r = extract("add :: Int -> Int -> Int\nadd x y = x + y\n\nmultiply :: Int -> "
@@ -5432,6 +5459,7 @@ SUITE(extraction) {
54325459

54335460
/* Functional */
54345461
RUN_TEST(elixir_function);
5462+
RUN_TEST(elixir_call_string_argument);
54355463
RUN_TEST(haskell_function);
54365464
RUN_TEST(ocaml_function);
54375465
RUN_TEST(erlang_function);

0 commit comments

Comments
 (0)