Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -4985,6 +4985,134 @@ static TSNode elixir_call_args(TSNode node) {
}

// Handle Elixir def/defp/defmacro — extract function definition.
/* True when node `n` spans exactly the text `lit`. Avoids an arena allocation
* per node in the complexity walk, which visits every node of every def. */
static bool elixir_node_text_is(TSNode n, const char *source, const char *lit) {
if (ts_node_is_null(n)) {
return false;
}
uint32_t s = ts_node_start_byte(n);
uint32_t e = ts_node_end_byte(n);
size_t len = strlen(lit);
return (size_t)(e - s) == len && strncmp(source + s, lit, len) == 0;
}

/* Elixir has no branching NODE TYPES. `if`, `unless`, `case`, `cond`, `with`,
* `for`, `try` and `receive` are ordinary `call` nodes told apart only by the
* identifier in callee position, and every case/cond/fn/rescue arm is a
* `stab_clause`. lang_specs.c cannot express that — the Elixir branch table is
* `{"call"}`, which would score every function call as a decision — and
* cbm_compute_complexity matches on node type, so the generic path is unusable
* and every Elixir function shipped complexity 0.
*
* Counted here: the branching construct itself, every `stab_clause`, the
* short-circuit operators, and `when` guards. Container AND arm are both
* counted, which is the convention the generic tables already use — Go counts
* `expression_switch_statement` plus each `expression_case`, Python counts
* `try_statement` plus each `except_clause`. Cognitive weighting is Campbell's,
* identical to the generic walk (+1 plus the current nesting depth). */
static bool elixir_is_branch_call(TSNode call, const char *source) {
if (ts_node_child_count(call) == 0) {
return false;
}
TSNode callee = ts_node_child(call, 0);
if (ts_node_is_null(callee) || strcmp(ts_node_type(callee), "identifier") != 0) {
return false;
}
return elixir_node_text_is(callee, source, "if") ||
elixir_node_text_is(callee, source, "unless") ||
elixir_node_text_is(callee, source, "with") ||
elixir_node_text_is(callee, source, "for") ||
elixir_node_text_is(callee, source, "case") ||
elixir_node_text_is(callee, source, "cond") ||
elixir_node_text_is(callee, source, "try") ||
elixir_node_text_is(callee, source, "receive");
}

/* A comprehension (`for x <- xs, do: ...`) is Elixir's only real loop node.
* Enum.map/reduce iterate too, but they are plain function calls; counting them
* would make loop_depth meaningless, so they are left out. */
static bool elixir_is_loop_call(TSNode call, const char *source) {
if (ts_node_child_count(call) == 0) {
return false;
}
TSNode callee = ts_node_child(call, 0);
return !ts_node_is_null(callee) && strcmp(ts_node_type(callee), "identifier") == 0 &&
elixir_node_text_is(callee, source, "for");
}

static bool elixir_is_branch_operator(TSNode node, const char *source) {
TSNode op = ts_node_child_by_field_name(node, TS_FIELD("operator"));
return elixir_node_text_is(op, source, "&&") || elixir_node_text_is(op, source, "||") ||
elixir_node_text_is(op, source, "and") || elixir_node_text_is(op, source, "or") ||
elixir_node_text_is(op, source, "when");
}

static void elixir_compute_complexity(TSNode node, const char *source, cbm_complexity_t *out) {
enum { ELIXIR_CX_STACK_CAP = 4096 };
struct {
TSNode node;
int bdepth;
int ldepth;
int adepth;
} stack[ELIXIR_CX_STACK_CAP];
memset(out, 0, sizeof(*out));
int top = 0;
stack[top].node = node;
stack[top].bdepth = 0;
stack[top].ldepth = 0;
stack[top].adepth = 0;
top++;
while (top > 0) {
int cur = --top;
TSNode n = stack[cur].node;
int bdepth = stack[cur].bdepth;
int ldepth = stack[cur].ldepth;
int adepth = stack[cur].adepth;
const char *kind = ts_node_type(n);
bool is_branch = false;
if (strcmp(kind, "stab_clause") == 0) {
is_branch = true;
} else if (strcmp(kind, "call") == 0) {
is_branch = elixir_is_branch_call(n, source);
} else if (strcmp(kind, "binary_operator") == 0) {
is_branch = elixir_is_branch_operator(n, source);
}
int child_b = bdepth;
int child_l = ldepth;
/* `conn.assigns.user` nests as dot(dot(conn)), so consecutive `dot`
* nodes deepen the chain and anything else resets it. */
int child_a = 0;
if (strcmp(kind, "dot") == 0) {
child_a = adepth + 1;
if (child_a > out->max_access_depth) {
out->max_access_depth = child_a;
}
}
if (is_branch) {
out->cyclomatic++;
out->cognitive += 1 + bdepth;
child_b = bdepth + 1;
}
if (strcmp(kind, "call") == 0 && elixir_is_loop_call(n, source)) {
out->loop_count++;
int d = ldepth + 1;
if (d > out->loop_depth) {
out->loop_depth = d;
}
child_l = d;
}
uint32_t nc = ts_node_child_count(n);
for (int i = (int)nc - 1; i >= 0 && top < ELIXIR_CX_STACK_CAP; i--) {
stack[top].node = ts_node_child(n, (uint32_t)i);
stack[top].bdepth = child_b;
stack[top].ldepth = child_l;
stack[top].adepth = child_a;
top++;
}
}
}

static void extract_elixir_func_def(CBMExtractCtx *ctx, TSNode node, const char *macro) {
CBMArena *a = ctx->arena;
TSNode args = elixir_call_args(node);
Expand Down Expand Up @@ -5017,6 +5145,21 @@ static void extract_elixir_func_def(CBMExtractCtx *ctx, TSNode node, const char
def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET;
def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET;
def.is_exported = (strcmp(macro, "def") == 0 || strcmp(macro, "defmacro") == 0);
def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
cbm_complexity_t cx;
elixir_compute_complexity(node, ctx->source, &cx);
def.complexity = cx.cyclomatic;
def.cognitive = cx.cognitive;
def.loop_count = cx.loop_count;
def.loop_depth = cx.loop_depth;
def.max_access_depth = cx.max_access_depth;
/* Fingerprint off the body, not the whole def call: including the head
* would make every clause of a name look alike. Elixir was the only
* language emitting Functions with no fingerprint at all, so it never
* appeared in similarity or duplicate results. Runs after `lines` — the
* AST profile reads it. */
TSNode fp_body = cbm_find_child_by_kind(node, "do_block");
compute_fingerprint(ctx, &def, ts_node_is_null(fp_body) ? node : fp_body);
cbm_defs_push(&ctx->result->defs, a, def);
}

Expand Down
40 changes: 40 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,45 @@ TEST(elixir_function) {
PASS();
}

/* Elixir control flow is `call` nodes and `stab_clause` arms, not branch node
* TYPES, so the generic complexity walk scored every Elixir function 0 and the
* fingerprint was never computed at all — no similarity or duplicate detection. */
TEST(elixir_complexity_and_fingerprint) {
CBMFileResult *r = extract("defmodule Cx do\n"
" def classify(x) do\n"
" case x do\n"
" 0 -> :zero\n"
" n when n > 0 -> :pos\n"
" _ -> :neg\n"
" end\n"
" end\n"
" def plain(a), do: a\n"
"end\n",
CBM_LANG_ELIXIR, "t", "cx.ex");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
CBMDefinition *cls = NULL;
CBMDefinition *plain = NULL;
for (int i = 0; i < r->defs.count; i++) {
if (strcmp(r->defs.items[i].name, "classify") == 0)
cls = &r->defs.items[i];
if (strcmp(r->defs.items[i].name, "plain") == 0)
plain = &r->defs.items[i];
}
ASSERT_NOT_NULL(cls);
/* The `case` itself + its 3 arms + the second arm's own `when` guard. */
ASSERT_EQ(5, cls->complexity);
ASSERT(cls->cognitive > 0);
ASSERT(cls->lines > 0);
/* A straight-line body has no decisions but still gets a line count — a
* language must not become nonzero everywhere just by being measured. */
ASSERT_NOT_NULL(plain);
ASSERT_EQ(0, plain->complexity);
ASSERT(plain->lines > 0);
cbm_free_result(r);
PASS();
}

/* --- Haskell --- */
TEST(haskell_function) {
CBMFileResult *r = extract("add :: Int -> Int -> Int\nadd x y = x + y\n\nmultiply :: Int -> "
Expand Down Expand Up @@ -5432,6 +5471,7 @@ SUITE(extraction) {

/* Functional */
RUN_TEST(elixir_function);
RUN_TEST(elixir_complexity_and_fingerprint);
RUN_TEST(haskell_function);
RUN_TEST(ocaml_function);
RUN_TEST(erlang_function);
Expand Down
Loading