Skip to content

Commit f159b4c

Browse files
authored
Merge pull request #1731 from henry-hz/upstream-pr/elixir-complexity
fix(elixir): functions carry no complexity, fingerprint or line count
2 parents 65acd42 + 3b33bab commit f159b4c

2 files changed

Lines changed: 357 additions & 2 deletions

File tree

‎internal/cbm/extract_defs.c‎

Lines changed: 195 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
98059824
static 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

Comments
 (0)