@@ -9723,6 +9723,24 @@ static TSNode elixir_call_args(TSNode node) {
97239723 return args;
97249724}
97259725
9726+ /* Combine one folded clause's metric with the function's value so far. A clause
9727+ * whose walk could not finish reports CBM_WALK_METRIC_UNAVAILABLE, and so does
9728+ * the folded function: a sum that skipped that clause would read low on exactly
9729+ * the largest functions. */
9730+ static int elixir_fold_metric_sum(int acc, int clause) {
9731+ if (acc < 0 || clause < 0) {
9732+ return CBM_WALK_METRIC_UNAVAILABLE;
9733+ }
9734+ return acc + clause;
9735+ }
9736+
9737+ static int elixir_fold_metric_max(int acc, int clause) {
9738+ if (acc < 0 || clause < 0) {
9739+ return CBM_WALK_METRIC_UNAVAILABLE;
9740+ }
9741+ return clause > acc ? clause : acc;
9742+ }
9743+
97269744// Fold one more clause of an Elixir function into the def already pushed for
97279745// the previous clause. Every clause is its own `def` call, and an Elixir QN
97289746// carries neither module nor arity, so all of them compute one qualified name
@@ -9801,9 +9819,10 @@ static TSNode elixir_call_args(TSNode node) {
98019819// name, where the pre-patch last-clause-wins record reported a publicly
98029820// callable name as unexported. Verified against the source in every one: the
98039821// new value is the correct one.
9822+ // `cx` is this clause's own complexity; a folded clause adds it to the function.
98049823// Returns true when the clause folded.
98059824static bool fold_elixir_clause(CBMExtractCtx *ctx, const char *qn, TSNode scope, TSNode node,
9806- uint32_t end_line, bool is_exported) {
9825+ uint32_t end_line, bool is_exported, const cbm_complexity_t *cx ) {
98079826 if (!qn || ctx->result->defs.count <= 0 || ts_node_is_null(scope)) {
98089827 return false;
98099828 }
@@ -9828,9 +9847,168 @@ static bool fold_elixir_clause(CBMExtractCtx *ctx, const char *qn, TSNode scope,
98289847 prev->end_line = end_line;
98299848 }
98309849 prev->is_exported = prev->is_exported || is_exported;
9850+ /* One node now stands for every clause, so its metrics cover every clause
9851+ * rather than the first: branch and loop counts add up, the nesting depths
9852+ * take the deepest clause, and each clause past the first is one more
9853+ * dispatch decision. A multi-clause function with no branch in any body is
9854+ * still a decision tree over its heads. `lines` follows the widened span. */
9855+ int cyclomatic = elixir_fold_metric_sum(prev->complexity, cx->cyclomatic);
9856+ prev->complexity = cyclomatic < 0 ? cyclomatic : cyclomatic + 1;
9857+ prev->cognitive = elixir_fold_metric_sum(prev->cognitive, cx->cognitive);
9858+ prev->loop_count = elixir_fold_metric_sum(prev->loop_count, cx->loop_count);
9859+ prev->loop_depth = elixir_fold_metric_max(prev->loop_depth, cx->loop_depth);
9860+ prev->max_access_depth = elixir_fold_metric_max(prev->max_access_depth, cx->max_access_depth);
9861+ prev->lines = (int)(prev->end_line - prev->start_line + TS_LINE_OFFSET);
98319862 return true;
98329863}
98339864
9865+ /* True when node `n` spans exactly the text `lit`. Avoids an arena allocation
9866+ * per node in the complexity walk, which visits every node of every def. */
9867+ static bool elixir_node_text_is(TSNode n, const char *source, const char *lit) {
9868+ if (ts_node_is_null(n)) {
9869+ return false;
9870+ }
9871+ uint32_t s = ts_node_start_byte(n);
9872+ uint32_t e = ts_node_end_byte(n);
9873+ size_t len = strlen(lit);
9874+ return (size_t)(e - s) == len && strncmp(source + s, lit, len) == 0;
9875+ }
9876+
9877+ /* Elixir has no branching NODE TYPES. `if`, `unless`, `case`, `cond`, `with`,
9878+ * `for`, `try` and `receive` are ordinary `call` nodes told apart only by the
9879+ * identifier in callee position, and every case/cond/fn/rescue arm is a
9880+ * `stab_clause`. lang_specs.c cannot express that — the Elixir branch table is
9881+ * `{"call"}`, which would score every function call as a decision — and
9882+ * cbm_compute_complexity matches on node type, so the generic path is unusable
9883+ * and every Elixir function shipped complexity 0.
9884+ *
9885+ * Counted here: the branching construct itself, every `stab_clause`, the
9886+ * short-circuit operators, and `when` guards. Container AND arm are both
9887+ * counted, which is the convention the generic tables already use — Go counts
9888+ * `expression_switch_statement` plus each `expression_case`, Python counts
9889+ * `try_statement` plus each `except_clause`. Cognitive weighting is Campbell's,
9890+ * identical to the generic walk (+1 plus the current nesting depth). */
9891+ static bool elixir_is_branch_call(TSNode call, const char *source) {
9892+ if (ts_node_child_count(call) == 0) {
9893+ return false;
9894+ }
9895+ TSNode callee = ts_node_child(call, 0);
9896+ if (ts_node_is_null(callee) || strcmp(ts_node_type(callee), "identifier") != 0) {
9897+ return false;
9898+ }
9899+ return elixir_node_text_is(callee, source, "if") ||
9900+ elixir_node_text_is(callee, source, "unless") ||
9901+ elixir_node_text_is(callee, source, "with") ||
9902+ elixir_node_text_is(callee, source, "for") ||
9903+ elixir_node_text_is(callee, source, "case") ||
9904+ elixir_node_text_is(callee, source, "cond") ||
9905+ elixir_node_text_is(callee, source, "try") ||
9906+ elixir_node_text_is(callee, source, "receive");
9907+ }
9908+
9909+ /* A comprehension (`for x <- xs, do: ...`) is Elixir's only real loop node.
9910+ * Enum.map/reduce iterate too, but they are plain function calls; counting them
9911+ * would make loop_depth meaningless, so they are left out. */
9912+ static bool elixir_is_loop_call(TSNode call, const char *source) {
9913+ if (ts_node_child_count(call) == 0) {
9914+ return false;
9915+ }
9916+ TSNode callee = ts_node_child(call, 0);
9917+ return !ts_node_is_null(callee) && strcmp(ts_node_type(callee), "identifier") == 0 &&
9918+ elixir_node_text_is(callee, source, "for");
9919+ }
9920+
9921+ static bool elixir_is_branch_operator(TSNode node, const char *source) {
9922+ TSNode op = ts_node_child_by_field_name(node, TS_FIELD("operator"));
9923+ return elixir_node_text_is(op, source, "&&") || elixir_node_text_is(op, source, "||") ||
9924+ elixir_node_text_is(op, source, "and") || elixir_node_text_is(op, source, "or") ||
9925+ elixir_node_text_is(op, source, "when");
9926+ }
9927+
9928+ /* The pending frames live on the same growable stack as cbm_compute_complexity:
9929+ * a small on-stack first chunk that spills to the heap through
9930+ * cbm_walk_stack_reserve, so a wide body is never truncated and no thread pays
9931+ * for a large fixed array. If the stack cannot grow, every metric reports
9932+ * CBM_WALK_METRIC_UNAVAILABLE instead of a silently low count. */
9933+ static void elixir_compute_complexity(TSNode node, const char *source, cbm_complexity_t *out) {
9934+ enum { ELIXIR_CX_STACK_INLINE = 512 };
9935+ struct elixir_cx_frame {
9936+ TSNode node;
9937+ int bdepth;
9938+ int ldepth;
9939+ int adepth;
9940+ };
9941+ struct elixir_cx_frame inline_stack[ELIXIR_CX_STACK_INLINE];
9942+ struct elixir_cx_frame *stack = inline_stack;
9943+ int cap = ELIXIR_CX_STACK_INLINE;
9944+ memset(out, 0, sizeof(*out));
9945+ int top = 0;
9946+ stack[top].node = node;
9947+ stack[top].bdepth = 0;
9948+ stack[top].ldepth = 0;
9949+ stack[top].adepth = 0;
9950+ top++;
9951+ while (top > 0) {
9952+ int cur = --top;
9953+ TSNode n = stack[cur].node;
9954+ int bdepth = stack[cur].bdepth;
9955+ int ldepth = stack[cur].ldepth;
9956+ int adepth = stack[cur].adepth;
9957+ const char *kind = ts_node_type(n);
9958+ bool is_branch = false;
9959+ if (strcmp(kind, "stab_clause") == 0) {
9960+ is_branch = true;
9961+ } else if (strcmp(kind, "call") == 0) {
9962+ is_branch = elixir_is_branch_call(n, source);
9963+ } else if (strcmp(kind, "binary_operator") == 0) {
9964+ is_branch = elixir_is_branch_operator(n, source);
9965+ }
9966+ int child_b = bdepth;
9967+ int child_l = ldepth;
9968+ /* `conn.assigns.user` nests as dot(dot(conn)), so consecutive `dot`
9969+ * nodes deepen the chain and anything else resets it. */
9970+ int child_a = 0;
9971+ if (strcmp(kind, "dot") == 0) {
9972+ child_a = adepth + 1;
9973+ if (child_a > out->max_access_depth) {
9974+ out->max_access_depth = child_a;
9975+ }
9976+ }
9977+ if (is_branch) {
9978+ out->cyclomatic++;
9979+ out->cognitive += 1 + bdepth;
9980+ child_b = bdepth + 1;
9981+ }
9982+ if (strcmp(kind, "call") == 0 && elixir_is_loop_call(n, source)) {
9983+ out->loop_count++;
9984+ int d = ldepth + 1;
9985+ if (d > out->loop_depth) {
9986+ out->loop_depth = d;
9987+ }
9988+ child_l = d;
9989+ }
9990+ uint32_t nc = ts_node_child_count(n);
9991+ if (!cbm_walk_stack_reserve((void **)&stack, &cap, top + (int)nc,
9992+ sizeof(struct elixir_cx_frame), inline_stack,
9993+ "elixir_compute_complexity")) {
9994+ out->cyclomatic = CBM_WALK_METRIC_UNAVAILABLE;
9995+ out->cognitive = CBM_WALK_METRIC_UNAVAILABLE;
9996+ out->loop_count = CBM_WALK_METRIC_UNAVAILABLE;
9997+ out->loop_depth = CBM_WALK_METRIC_UNAVAILABLE;
9998+ out->max_access_depth = CBM_WALK_METRIC_UNAVAILABLE;
9999+ break;
10000+ }
10001+ for (int i = (int)nc - 1; i >= 0; i--) {
10002+ stack[top].node = ts_node_child(n, (uint32_t)i);
10003+ stack[top].bdepth = child_b;
10004+ stack[top].ldepth = child_l;
10005+ stack[top].adepth = child_a;
10006+ top++;
10007+ }
10008+ }
10009+ cbm_walk_stack_release(stack, inline_stack);
10010+ }
10011+
983410012// Handle Elixir def/defp/defmacro — extract function definition. `scope` is the
983510013// module body the previous clause was extracted from, and is updated to this
983610014// def's own module body; see fold_elixir_clause for what it bounds.
@@ -9872,7 +10050,9 @@ static void extract_elixir_func_def(CBMExtractCtx *ctx, TSNode node, const char
987210050 uint32_t start_line = ts_node_start_point(node).row + TS_LINE_OFFSET;
987310051 uint32_t end_line = ts_node_end_point(node).row + TS_LINE_OFFSET;
987410052 bool is_exported = (strcmp(macro, "def") == 0 || strcmp(macro, "defmacro") == 0);
9875- bool folded = fold_elixir_clause(ctx, qn, *scope, node, end_line, is_exported);
10053+ cbm_complexity_t cx;
10054+ elixir_compute_complexity(node, ctx->source, &cx);
10055+ bool folded = fold_elixir_clause(ctx, qn, *scope, node, end_line, is_exported, &cx);
987610056 *scope = ts_node_parent(node);
987710057 if (folded) {
987810058 return;
@@ -9887,6 +10067,19 @@ static void extract_elixir_func_def(CBMExtractCtx *ctx, TSNode node, const char
988710067 def.start_line = start_line;
988810068 def.end_line = end_line;
988910069 def.is_exported = is_exported;
10070+ def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
10071+ def.complexity = cx.cyclomatic;
10072+ def.cognitive = cx.cognitive;
10073+ def.loop_count = cx.loop_count;
10074+ def.loop_depth = cx.loop_depth;
10075+ def.max_access_depth = cx.max_access_depth;
10076+ /* Fingerprint off the body, not the whole def call: including the head
10077+ * would make every clause of a name look alike. Elixir was the only
10078+ * language emitting Functions with no fingerprint at all, so it never
10079+ * appeared in similarity or duplicate results. Runs after `lines` — the
10080+ * AST profile reads it. */
10081+ TSNode fp_body = cbm_find_child_by_kind(node, "do_block");
10082+ compute_fingerprint(ctx, &def, ts_node_is_null(fp_body) ? node : fp_body);
989010083 cbm_defs_push(&ctx->result->defs, a, def);
989110084}
989210085
0 commit comments