1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//! `Cyclomatic` implementation for Elixir.
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use super::*;
/// Returns `true` when `node` is the first `stab_clause` child of an
/// `anonymous_function` parent — i.e. the closure's head clause rather
/// than a pattern-dispatch branch.
///
/// The grammar shape is `anonymous_function → fn stab_clause+ end`, so
/// the parent's children include the `fn`/`end` keyword tokens and one
/// or more `stab_clause`s. We locate the first child whose kind is
/// `stab_clause` (skipping the `fn` token and any other non-clause
/// sibling) and report whether it is `node`. Multi-clause `fn`s thus
/// skip only their first clause; `case`/`cond`/`with` arms have a
/// `do_block` parent and never match here (issue #776).
fn elixir_is_anonymous_fn_head_clause(node: &Node) -> bool {
use Elixir as E;
let Some(parent) = node.parent() else {
return false;
};
if parent.kind_id() != E::AnonymousFunction as u16 {
return false;
}
parent
.children()
.find(|child| child.kind_id() == E::StabClause as u16)
.is_some_and(|first| first.id() == node.id())
}
impl Cyclomatic for ElixirCode {
// Elixir's control-flow constructs are not distinct grammar
// productions: `if`/`unless`/`for`/`while`/`with`/`case`/`cond`/`try`
// all surface as `Call` nodes whose `target` field is an
// `Identifier` whose text spells the keyword. We must consult the
// source bytes (mirroring `impl Exit for ElixirCode`) to identify
// them.
//
// The split between standard and modified CCN mirrors the C-family
// case/switch treatment: per-arm `stab_clause` nodes contribute
// standard, while the multi-arm container Calls (`case`/`cond`/
// `with`/`try`) contribute modified. Single-branch keyword Calls
// (`if`/`unless`/`for`/`while`) contribute to both. Short-circuit
// booleans (`&&`, `||`, `and`, `or`) contribute to both.
fn compute<'a>(node: &Node<'a>, code: &'a [u8], stats: &mut Stats) {
use Elixir as E;
match node.kind_id().into() {
// Per-arm decisions: each `stab_clause` is one arm of a
// `case`/`cond`/`with`/anonymous-fn body or a `rescue`/
// `catch` handler. Standard-only — modified counts the
// container Call once.
//
// The exception is the *first* `stab_clause` of an
// `anonymous_function` (`fn … -> … end`): it is the
// closure's head/definition, not a pattern-dispatch
// decision. The closure already opens its own function
// space seeded with base cyclomatic 1 (see
// `getter::elixir` → `SpaceKind::Function`), so counting the
// head clause too over-reports a trivial `fn x -> x end` as
// 2 (issue #776). Only the 2nd+ clauses of a multi-clause
// `fn` are real branches. `case`/`cond`/`with` arms have a
// `do_block` parent — not `anonymous_function` — so they are
// unaffected and keep counting.
E::StabClause if elixir_is_anonymous_fn_head_clause(node) => {}
E::StabClause => {
stats.cyclomatic += 1.;
}
// Short-circuit booleans add a decision point in both
// metrics.
E::AMPAMP | E::PIPEPIPE | E::And | E::Or => {
stats.cyclomatic += 1.;
stats.cyclomatic_modified += 1.;
}
E::Call => {
if let Some(target) = node.child_by_field_name("target")
&& target.kind_id() == E::Identifier
&& let Some(name) = target.utf8_text(code)
{
match name {
// Single-branch constructs: count for both.
// There are no per-arm `stab_clause`s exposing
// themselves separately, so the Call itself
// must carry the decision point.
"if" | "unless" | "for" | "while" => {
stats.cyclomatic += 1.;
stats.cyclomatic_modified += 1.;
}
// Multi-arm containers: count once for modified
// (the container collapses to a single decision).
// Per-arm `stab_clause`s already contribute to
// standard above.
"case" | "cond" | "with" | "try" => {
stats.cyclomatic_modified += 1.;
}
_ => {}
}
}
}
_ => {}
}
}
}