mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge pull request #1731 from henry-hz/upstream-pr/elixir-complexity
fix(elixir): functions carry no complexity, fingerprint or line count
This commit is contained in:
2 files changed
+357
-2
No files matched your search
+195
-2
@@ -9723,6 +9723,24 @@ static TSNode elixir_call_args(TSNode node) {
|
||||
return args;
|
||||
}
|
||||
|
||||
/* Combine one folded clause's metric with the function's value so far. A clause
|
||||
* whose walk could not finish reports CBM_WALK_METRIC_UNAVAILABLE, and so does
|
||||
* the folded function: a sum that skipped that clause would read low on exactly
|
||||
* the largest functions. */
|
||||
static int elixir_fold_metric_sum(int acc, int clause) {
|
||||
if (acc < 0 || clause < 0) {
|
||||
return CBM_WALK_METRIC_UNAVAILABLE;
|
||||
}
|
||||
return acc + clause;
|
||||
}
|
||||
|
||||
static int elixir_fold_metric_max(int acc, int clause) {
|
||||
if (acc < 0 || clause < 0) {
|
||||
return CBM_WALK_METRIC_UNAVAILABLE;
|
||||
}
|
||||
return clause > acc ? clause : acc;
|
||||
}
|
||||
|
||||
// Fold one more clause of an Elixir function into the def already pushed for
|
||||
// the previous clause. Every clause is its own `def` call, and an Elixir QN
|
||||
// carries neither module nor arity, so all of them compute one qualified name
|
||||
@@ -9801,9 +9819,10 @@ static TSNode elixir_call_args(TSNode node) {
|
||||
// name, where the pre-patch last-clause-wins record reported a publicly
|
||||
// callable name as unexported. Verified against the source in every one: the
|
||||
// new value is the correct one.
|
||||
// `cx` is this clause's own complexity; a folded clause adds it to the function.
|
||||
// Returns true when the clause folded.
|
||||
static bool fold_elixir_clause(CBMExtractCtx *ctx, const char *qn, TSNode scope, TSNode node,
|
||||
uint32_t end_line, bool is_exported) {
|
||||
uint32_t end_line, bool is_exported, const cbm_complexity_t *cx) {
|
||||
if (!qn || ctx->result->defs.count <= 0 || ts_node_is_null(scope)) {
|
||||
return false;
|
||||
}
|
||||
@@ -9828,9 +9847,168 @@ static bool fold_elixir_clause(CBMExtractCtx *ctx, const char *qn, TSNode scope,
|
||||
prev->end_line = end_line;
|
||||
}
|
||||
prev->is_exported = prev->is_exported || is_exported;
|
||||
/* One node now stands for every clause, so its metrics cover every clause
|
||||
* rather than the first: branch and loop counts add up, the nesting depths
|
||||
* take the deepest clause, and each clause past the first is one more
|
||||
* dispatch decision. A multi-clause function with no branch in any body is
|
||||
* still a decision tree over its heads. `lines` follows the widened span. */
|
||||
int cyclomatic = elixir_fold_metric_sum(prev->complexity, cx->cyclomatic);
|
||||
prev->complexity = cyclomatic < 0 ? cyclomatic : cyclomatic + 1;
|
||||
prev->cognitive = elixir_fold_metric_sum(prev->cognitive, cx->cognitive);
|
||||
prev->loop_count = elixir_fold_metric_sum(prev->loop_count, cx->loop_count);
|
||||
prev->loop_depth = elixir_fold_metric_max(prev->loop_depth, cx->loop_depth);
|
||||
prev->max_access_depth = elixir_fold_metric_max(prev->max_access_depth, cx->max_access_depth);
|
||||
prev->lines = (int)(prev->end_line - prev->start_line + TS_LINE_OFFSET);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 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");
|
||||
}
|
||||
|
||||
/* The pending frames live on the same growable stack as cbm_compute_complexity:
|
||||
* a small on-stack first chunk that spills to the heap through
|
||||
* cbm_walk_stack_reserve, so a wide body is never truncated and no thread pays
|
||||
* for a large fixed array. If the stack cannot grow, every metric reports
|
||||
* CBM_WALK_METRIC_UNAVAILABLE instead of a silently low count. */
|
||||
static void elixir_compute_complexity(TSNode node, const char *source, cbm_complexity_t *out) {
|
||||
enum { ELIXIR_CX_STACK_INLINE = 512 };
|
||||
struct elixir_cx_frame {
|
||||
TSNode node;
|
||||
int bdepth;
|
||||
int ldepth;
|
||||
int adepth;
|
||||
};
|
||||
struct elixir_cx_frame inline_stack[ELIXIR_CX_STACK_INLINE];
|
||||
struct elixir_cx_frame *stack = inline_stack;
|
||||
int cap = ELIXIR_CX_STACK_INLINE;
|
||||
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);
|
||||
if (!cbm_walk_stack_reserve((void **)&stack, &cap, top + (int)nc,
|
||||
sizeof(struct elixir_cx_frame), inline_stack,
|
||||
"elixir_compute_complexity")) {
|
||||
out->cyclomatic = CBM_WALK_METRIC_UNAVAILABLE;
|
||||
out->cognitive = CBM_WALK_METRIC_UNAVAILABLE;
|
||||
out->loop_count = CBM_WALK_METRIC_UNAVAILABLE;
|
||||
out->loop_depth = CBM_WALK_METRIC_UNAVAILABLE;
|
||||
out->max_access_depth = CBM_WALK_METRIC_UNAVAILABLE;
|
||||
break;
|
||||
}
|
||||
for (int i = (int)nc - 1; i >= 0; 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++;
|
||||
}
|
||||
}
|
||||
cbm_walk_stack_release(stack, inline_stack);
|
||||
}
|
||||
|
||||
// Handle Elixir def/defp/defmacro — extract function definition. `scope` is the
|
||||
// module body the previous clause was extracted from, and is updated to this
|
||||
// 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
|
||||
uint32_t start_line = ts_node_start_point(node).row + TS_LINE_OFFSET;
|
||||
uint32_t end_line = ts_node_end_point(node).row + TS_LINE_OFFSET;
|
||||
bool is_exported = (strcmp(macro, "def") == 0 || strcmp(macro, "defmacro") == 0);
|
||||
bool folded = fold_elixir_clause(ctx, qn, *scope, node, end_line, is_exported);
|
||||
cbm_complexity_t cx;
|
||||
elixir_compute_complexity(node, ctx->source, &cx);
|
||||
bool folded = fold_elixir_clause(ctx, qn, *scope, node, end_line, is_exported, &cx);
|
||||
*scope = ts_node_parent(node);
|
||||
if (folded) {
|
||||
return;
|
||||
@@ -9887,6 +10067,19 @@ static void extract_elixir_func_def(CBMExtractCtx *ctx, TSNode node, const char
|
||||
def.start_line = start_line;
|
||||
def.end_line = end_line;
|
||||
def.is_exported = is_exported;
|
||||
def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -2736,6 +2736,165 @@ TEST(elixir_channels_pubsub_and_handle_in) {
|
||||
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"
|
||||
" def summarize(orders, currency) do\n"
|
||||
" totals = Enum.map(orders, fn order -> order.amount * order.rate end)\n"
|
||||
" count = length(totals)\n"
|
||||
" sum = Enum.reduce(totals, 0, fn value, acc -> acc + value end)\n"
|
||||
" average = if count > 0, do: sum / count, else: 0\n"
|
||||
" label = String.upcase(currency) <> \" total\"\n"
|
||||
" %{label: label, sum: sum, count: count, average: average}\n"
|
||||
" end\n"
|
||||
" def tally(items, unit) do\n"
|
||||
" values = Enum.map(items, fn item -> item.price * item.qty end)\n"
|
||||
" n = length(values)\n"
|
||||
" total = Enum.reduce(values, 0, fn v, a -> a + v end)\n"
|
||||
" mean = if n > 0, do: total / n, else: 0\n"
|
||||
" tag = String.upcase(unit) <> \" total\"\n"
|
||||
" %{tag: tag, total: total, n: n, mean: mean}\n"
|
||||
" end\n"
|
||||
"end\n",
|
||||
CBM_LANG_ELIXIR, "t", "cx.ex");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
CBMDefinition *cls = NULL;
|
||||
CBMDefinition *plain = NULL;
|
||||
CBMDefinition *summarize = NULL;
|
||||
CBMDefinition *tally = 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];
|
||||
if (strcmp(r->defs.items[i].name, "summarize") == 0)
|
||||
summarize = &r->defs.items[i];
|
||||
if (strcmp(r->defs.items[i].name, "tally") == 0)
|
||||
tally = &r->defs.items[i];
|
||||
}
|
||||
/* A body past the MinHash size gate gets a fingerprint (Elixir used to emit
|
||||
* none at all); a one-expression body stays below the gate and gets none. */
|
||||
ASSERT_NOT_NULL(summarize);
|
||||
ASSERT(summarize->fingerprint_k > 0);
|
||||
ASSERT_NOT_NULL(summarize->fingerprint);
|
||||
/* Duplicate detection is what the fingerprint exists for: the same body
|
||||
* shape under other function and variable names gets the same fingerprint. */
|
||||
ASSERT_NOT_NULL(tally);
|
||||
ASSERT_EQ(summarize->fingerprint_k, tally->fingerprint_k);
|
||||
ASSERT_NOT_NULL(tally->fingerprint);
|
||||
ASSERT(memcmp(summarize->fingerprint, tally->fingerprint,
|
||||
(size_t)summarize->fingerprint_k * sizeof(summarize->fingerprint[0])) == 0);
|
||||
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);
|
||||
ASSERT_EQ(0, plain->fingerprint_k);
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The clauses of a multi-clause function fold into one Function node, so that
|
||||
* node's metrics must cover every clause, not just the first: branch, cognitive
|
||||
* and loop counts add up, loop_depth and max_access_depth take the deepest
|
||||
* clause, every clause past the first is one more dispatch decision, and
|
||||
* `lines` is the folded span. `single` repeats the second clause on its own as
|
||||
* the per-clause reference: it is not folded, so it carries no dispatch charge. */
|
||||
TEST(elixir_folded_clause_metrics_cover_every_clause) {
|
||||
CBMFileResult *r = extract("defmodule Route do\n" /* 1 */
|
||||
" def pick(0), do: Map.new()\n" /* 2 */
|
||||
" def pick(n) when n > 0 do\n" /* 3 */
|
||||
" for x <- [n], do: Map.new([{x, x}])\n" /* 4 */
|
||||
" end\n" /* 5 */
|
||||
" def pick(rows) do\n" /* 6 */
|
||||
" for row <- rows, do: (for x <- row, do: x)\n" /* 7 */
|
||||
" end\n" /* 8 */
|
||||
" def single(n) when n > 0 do\n" /* 9 */
|
||||
" for x <- [n], do: Map.new([{x, x}])\n" /* 10 */
|
||||
" end\n" /* 11 */
|
||||
"end\n", /* 12 */
|
||||
CBM_LANG_ELIXIR, "t", "route.ex");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_EQ(1, count_defs_named(r, "Function", "pick"));
|
||||
const CBMDefinition *one = elixir_first_def(r, "single");
|
||||
ASSERT_NOT_NULL(one);
|
||||
/* `when` + `for`, each at nesting 0; one comprehension; Map.new is one dot */
|
||||
ASSERT_EQ(2, one->complexity);
|
||||
ASSERT_EQ(2, one->cognitive);
|
||||
ASSERT_EQ(1, one->loop_count);
|
||||
ASSERT_EQ(1, one->loop_depth);
|
||||
ASSERT_EQ(1, one->max_access_depth);
|
||||
ASSERT_EQ(3, one->lines);
|
||||
|
||||
const CBMDefinition *pick = elixir_first_def(r, "pick");
|
||||
ASSERT_NOT_NULL(pick);
|
||||
ASSERT_EQ(2, (int)pick->start_line);
|
||||
ASSERT_EQ(8, (int)pick->end_line);
|
||||
/* clauses 0 + 2 + 2, plus 2 for the two clauses past the first */
|
||||
ASSERT_EQ(6, pick->complexity);
|
||||
/* 0 + 2 + (1 + 2): the inner comprehension sits one level deep */
|
||||
ASSERT_EQ(5, pick->cognitive);
|
||||
ASSERT_EQ(3, pick->loop_count);
|
||||
/* the deepest clause, not the sum of the clauses (that would be 3) */
|
||||
ASSERT_EQ(2, pick->loop_depth);
|
||||
/* two clauses with one dot each: the deepest is 1, the sum would be 2 */
|
||||
ASSERT_EQ(1, pick->max_access_depth);
|
||||
ASSERT_EQ(7, pick->lines);
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The Elixir complexity walk keeps its pending nodes on the growable stack
|
||||
* cbm_compute_complexity uses. A fixed 4096-frame stack dropped the FIRST
|
||||
* children of any wider body (they are pushed last-to-first), so the largest
|
||||
* functions silently under-reported. This body is wider than that old cap. */
|
||||
enum { ELIXIR_WIDE_STMTS = 5000 };
|
||||
|
||||
TEST(elixir_complexity_wide_body_exceeds_old_stack_cap) {
|
||||
static const char head[] = "defmodule Wide do\n def wide(x) do\n";
|
||||
static const char if_stmt[] = " if x, do: :a\n";
|
||||
static const char for_stmt[] = " for y <- x, do: y\n";
|
||||
static const char tail[] = " end\nend\n";
|
||||
size_t cap = sizeof(head) + (size_t)ELIXIR_WIDE_STMTS * sizeof(for_stmt) + sizeof(tail);
|
||||
char *src = malloc(cap);
|
||||
ASSERT_NOT_NULL(src);
|
||||
size_t pos = (size_t)snprintf(src, cap, "%s", head);
|
||||
for (int i = 0; i < ELIXIR_WIDE_STMTS; i++) {
|
||||
pos += (size_t)snprintf(src + pos, cap - pos, "%s", (i % 2 == 0) ? if_stmt : for_stmt);
|
||||
}
|
||||
snprintf(src + pos, cap - pos, "%s", tail);
|
||||
CBMFileResult *r = extract(src, CBM_LANG_ELIXIR, "t", "wide.ex");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
const CBMDefinition *d = elixir_first_def(r, "wide");
|
||||
ASSERT_NOT_NULL(d);
|
||||
/* every `if` and every `for` is one branching call at nesting depth 0 */
|
||||
ASSERT_EQ(ELIXIR_WIDE_STMTS, d->complexity);
|
||||
ASSERT_EQ(ELIXIR_WIDE_STMTS, d->cognitive);
|
||||
ASSERT_EQ(ELIXIR_WIDE_STMTS / 2, d->loop_count);
|
||||
ASSERT_EQ(1, d->loop_depth);
|
||||
cbm_free_result(r);
|
||||
free(src);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* --- Haskell --- */
|
||||
TEST(haskell_function) {
|
||||
CBMFileResult *r = extract("add :: Int -> Int -> Int\nadd x y = x + y\n\nmultiply :: Int -> "
|
||||
@@ -14540,6 +14699,9 @@ SUITE(extraction) {
|
||||
RUN_TEST(extract_elixir_guarded_def_head_scope);
|
||||
RUN_TEST(extract_elixir_guarded_def_head_guard_usages);
|
||||
RUN_TEST(elixir_channels_pubsub_and_handle_in);
|
||||
RUN_TEST(elixir_complexity_and_fingerprint);
|
||||
RUN_TEST(elixir_folded_clause_metrics_cover_every_clause);
|
||||
RUN_TEST(elixir_complexity_wide_body_exceeds_old_stack_cap);
|
||||
RUN_TEST(haskell_function);
|
||||
RUN_TEST(haskell_infix_definition_name);
|
||||
RUN_TEST(ocaml_function);
|
||||
|
||||
Reference in new issue
Block a user