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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! `Cognitive` implementation for Ruby.
#![allow(
clippy::enum_glob_use,
clippy::match_same_arms,
clippy::needless_pass_by_value,
clippy::wildcard_imports
)]
#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use super::*;
/// Folds a Ruby `binary`'s short-circuit operator children into the
/// boolean-sequence counter — Ruby has four (`&&`, `||`, word-form
/// `and`, word-form `or`).
fn compute_ruby_booleans(node: &Node, stats: &mut Stats) {
compute_booleans_with(node, stats, |id| {
matches!(
id.into(),
Ruby::AMPAMP | Ruby::PIPEPIPE | Ruby::And | Ruby::Or
)
});
}
impl Cognitive for RubyCode {
fn compute<'a>(
node: &Node<'a>,
_code: &'a [u8],
stats: &mut Stats,
nesting_map: &mut HashMap<usize, (usize, usize, usize)>,
) {
use Ruby as R;
let (mut nesting, mut depth, mut lambda) = get_nesting_from_map(node, nesting_map);
match node.kind_id().into() {
// Nesting-increasing constructs. tree-sitter-ruby models
// `elsif` as its own `Elsif` clause (handled in the
// branch-extension arm below) rather than nesting a second
// `If` inside the outer one, so the `is_else_if` guard is
// defensive only — mirrors the equivalent pattern in the
// Lua impl.
R::If if !Self::is_else_if(node) => {
increase_nesting(stats, &mut nesting, depth, lambda);
}
R::Unless
| R::While
| R::Until
| R::For
| R::Case
| R::CaseMatch
| R::Conditional
| R::IfModifier
| R::UnlessModifier
| R::WhileModifier
| R::UntilModifier
| R::Rescue
| R::RescueModifier
| R::RescueModifier2
| R::RescueModifier3 => {
increase_nesting(stats, &mut nesting, depth, lambda);
}
// `elsif` and the `else` of an `if`/`elsif` chain extend the
// parent branch at the same nesting level (+1).
R::Elsif => {
increment_branch_extension(stats);
}
// The `Else` clause node is shared across three Ruby
// constructs, distinguished here by parent kind:
//
// * `case/when` — the `else` is the default arm of a
// switch-like construct. The `case`/`case_match` parent
// already paid nesting (+1) above, so the default arm adds
// +0; counting it would double-count the construct (#451).
// This matches Kotlin's `when`-`else` (gated on `WhenEntry`
// above), Java/C# switch-`default`, and the SonarSource
// Cognitive Complexity spec, in which the catch-all arm of a
// switch carries no extra cost. See lesson #11 — the same
// logical construct must score identically across languages.
//
// * `if`/`elsif` chain — the `else` is the alternative branch
// of a conditional and adds +1, mirroring `R::Elsif`.
//
// * `begin/rescue` — the `else` runs when no exception was
// raised. Unlike `case`, the `begin` parent pays no nesting
// here (only `R::Rescue` does), so this `else` is *not*
// switch-like; it is the no-exception branch and adds +1,
// matching Python's `try`/`except`/`else` (the `ElseClause`
// arm in the Python impl). Suppressing it would re-introduce
// a cross-language divergence in the opposite direction.
R::Else => {
// The `case`/`case_match` default arm adds +0 (the parent
// construct already paid nesting); every other `else` — the
// `if`/`elsif` alternative and the `begin/rescue` no-exception
// branch — extends the parent branch at +1. Mirrors Kotlin's
// single `Else` arm gated on `WhenEntry`.
let is_case_default = node
.parent()
.is_some_and(|p| matches!(p.kind_id().into(), R::Case | R::CaseMatch));
if !is_case_default {
increment_branch_extension(stats);
}
}
// Ruby has no labeled loops: `break`/`next` are always
// unlabeled (the token's only optional child is a return-value
// expression, never a label). Per SonarSource Cognitive
// Complexity §B2, an unlabeled break/continue adds +0 — the
// enclosing loop's nesting already accounts for it — so they are
// intentionally excluded here. `redo` (restart the current loop
// iteration) and `retry` (re-run a rescued `begin` block) are
// genuinely unstructured jumps with no structured equivalent and
// each add +1.
R::Redo | R::Retry => {
increment_by_one(stats);
}
R::Binary | R::Binary2 | R::Binary3 => {
// Ruby has four short-circuit forms (`&&`, `||`, `and`,
// `or`); use the dedicated helper rather than the
// two-operator `compute_booleans` so word-form
// operators land in the sequence too.
compute_ruby_booleans(node, stats);
}
R::Method | R::SingletonMethod => {
nesting = 0;
increment_function_depth(&mut depth, node, &[R::Method, R::SingletonMethod]);
}
// Blocks, do-blocks and lambdas are the closure/lambda forms.
R::Block | R::DoBlock | R::Lambda => {
lambda += 1;
}
_ => {}
}
nesting_map.insert(node.id(), (nesting, depth, lambda));
}
}