code-gauge 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -15
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/diffCommand.cjs +1 -1
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +1 -1
- package/dist/diffCommand.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.d.ts +14 -28
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -1
- package/dist/languages.cjs +1 -1
- package/dist/languages.cjs.map +1 -1
- package/dist/languages.d.ts +5 -0
- package/dist/languages.js +1 -1
- package/dist/languages.js.map +1 -1
- package/dist/metrics.cjs +1 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.ts +14 -12
- package/dist/metrics.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +3 -1
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +24 -9
- package/dist/nativeMetrics.js +3 -1
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/types.d.ts +5 -13
- package/native/Cargo.lock +523 -0
- package/native/Cargo.toml +45 -0
- package/native/build.rs +3 -0
- package/native/src/complexity.rs +627 -0
- package/native/src/dep_degree.rs +253 -0
- package/native/src/duplication.rs +2007 -0
- package/native/src/functions.rs +345 -0
- package/native/src/languages.rs +647 -0
- package/native/src/lib.rs +101 -0
- package/native/src/measure.rs +590 -0
- package/native/src/ncss.rs +263 -0
- package/native/src/types.rs +135 -0
- package/native/src/util.rs +139 -0
- package/package.json +16 -19
- package/scripts/buildNative.mjs +25 -0
- package/scripts/installNative.mjs +96 -0
- package/dist/depDegree.cjs +0 -2
- package/dist/depDegree.cjs.map +0 -1
- package/dist/depDegree.d.ts +0 -12
- package/dist/depDegree.js +0 -2
- package/dist/depDegree.js.map +0 -1
- package/dist/ncss.cjs +0 -2
- package/dist/ncss.cjs.map +0 -1
- package/dist/ncss.d.ts +0 -17
- package/dist/ncss.js +0 -2
- package/dist/ncss.js.map +0 -1
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
use std::collections::{HashMap, HashSet};
|
|
2
|
+
use tree_sitter::Node;
|
|
3
|
+
|
|
4
|
+
use crate::util::{all_children, node_text, Source};
|
|
5
|
+
|
|
6
|
+
pub struct ComplexityResult {
|
|
7
|
+
pub cognitive_complexity: u64,
|
|
8
|
+
pub nesting_depth: u64,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// Node-type lookup sets built once per measurement from the language definition.
|
|
12
|
+
pub struct LanguageSets {
|
|
13
|
+
pub function_nodes: HashSet<&'static str>,
|
|
14
|
+
pub decision_nodes: HashSet<&'static str>,
|
|
15
|
+
pub nesting_nodes: HashSet<&'static str>,
|
|
16
|
+
pub ncss_nodes: HashSet<&'static str>,
|
|
17
|
+
pub ncss_containers: HashSet<&'static str>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl LanguageSets {
|
|
21
|
+
pub fn new(language: &crate::languages::LanguageDefinition) -> Self {
|
|
22
|
+
LanguageSets {
|
|
23
|
+
function_nodes: language.function_node_types.iter().copied().collect(),
|
|
24
|
+
decision_nodes: language.decision_node_types.iter().copied().collect(),
|
|
25
|
+
nesting_nodes: language.nesting_node_types.iter().copied().collect(),
|
|
26
|
+
ncss_nodes: language.ncss_node_types.iter().copied().collect(),
|
|
27
|
+
ncss_containers: language.ncss_container_node_types.iter().copied().collect(),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const BOOLEAN_OPERATORS: &[&str] = &["&&", "||", "and", "or"];
|
|
33
|
+
/// Parents under which `&&`/`||`/`and`/`or` tokens are actual boolean operators.
|
|
34
|
+
const BOOLEAN_OPERATOR_PARENT_TYPES: &[&str] = &["binary_expression", "binary", "boolean_operator"];
|
|
35
|
+
|
|
36
|
+
/// A Ruby stabby lambda's body block is part of the lambda, not a separate function.
|
|
37
|
+
pub fn is_lambda_body_block(node: Node<'_>) -> bool {
|
|
38
|
+
(node.kind() == "block" || node.kind() == "do_block")
|
|
39
|
+
&& node
|
|
40
|
+
.parent()
|
|
41
|
+
.is_some_and(|parent| parent.kind() == "lambda")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn is_function_boundary(node: Node<'_>, function_nodes: &HashSet<&'static str>) -> bool {
|
|
45
|
+
function_nodes.contains(node.kind()) && !is_lambda_body_block(node)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Sonar cognitive complexity charges a switch/match once as a whole, not per case label. Only
|
|
49
|
+
// named nodes are consulted, so anonymous keyword tokens never match.
|
|
50
|
+
const SWITCH_LIKE_NODE_TYPES: &[&str] = &[
|
|
51
|
+
"switch_statement",
|
|
52
|
+
"switch_expression",
|
|
53
|
+
"expression_switch_statement",
|
|
54
|
+
"type_switch_statement",
|
|
55
|
+
"select_statement",
|
|
56
|
+
"match_expression",
|
|
57
|
+
"match_statement",
|
|
58
|
+
"case",
|
|
59
|
+
"case_match",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Per-case decision nodes add no cognitive point because the switch itself carries the cost.
|
|
63
|
+
const CASE_CLAUSE_NODE_TYPES: &[&str] = &[
|
|
64
|
+
"case_clause",
|
|
65
|
+
"switch_case",
|
|
66
|
+
"switch_block_statement_group",
|
|
67
|
+
"switch_rule",
|
|
68
|
+
"case_statement",
|
|
69
|
+
"expression_case",
|
|
70
|
+
"type_case",
|
|
71
|
+
"communication_case",
|
|
72
|
+
"match_arm",
|
|
73
|
+
"when",
|
|
74
|
+
"in_clause",
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const IF_LIKE_NODE_TYPES: &[&str] = &["if_statement", "if_expression", "if", "unless"];
|
|
78
|
+
|
|
79
|
+
pub struct FunctionBodyMetrics {
|
|
80
|
+
pub cognitive_complexity: u64,
|
|
81
|
+
pub nesting_depth: u64,
|
|
82
|
+
pub ncss: u64,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Accumulator for one function body during measure_function_body_metrics' post-order pass.
|
|
86
|
+
struct FunctionBodyFrame {
|
|
87
|
+
cognitive_complexity: u64,
|
|
88
|
+
/// Count of `1 + nesting` cognitive increments, for re-basing on hoist into the parent frame.
|
|
89
|
+
nesting_sensitive_count: u64,
|
|
90
|
+
nesting_depth: u64,
|
|
91
|
+
ncss: u64,
|
|
92
|
+
has_own_ncss_contribution: bool,
|
|
93
|
+
/// Cognitive nesting (structural nesting + function/class bonuses) carried into this body.
|
|
94
|
+
entry_cognitive_nesting: u64,
|
|
95
|
+
/// Structural nesting carried into this body (bonuses excluded), for nesting depth.
|
|
96
|
+
entry_structural_nesting: u64,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
impl FunctionBodyFrame {
|
|
100
|
+
fn new(entry_cognitive_nesting: u64, entry_structural_nesting: u64) -> Self {
|
|
101
|
+
FunctionBodyFrame {
|
|
102
|
+
cognitive_complexity: 0,
|
|
103
|
+
nesting_sensitive_count: 0,
|
|
104
|
+
nesting_depth: 0,
|
|
105
|
+
ncss: 0,
|
|
106
|
+
has_own_ncss_contribution: false,
|
|
107
|
+
entry_cognitive_nesting,
|
|
108
|
+
entry_structural_nesting,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
struct FunctionBodyPass<'sets, 'code, 'source> {
|
|
114
|
+
sets: &'sets LanguageSets,
|
|
115
|
+
code: &'code Source<'source>,
|
|
116
|
+
frames: Vec<FunctionBodyFrame>,
|
|
117
|
+
results: HashMap<usize, FunctionBodyMetrics>,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Per-function complexity and NCSS for every function boundary, in one post-order pass so each
|
|
121
|
+
/// node is visited once instead of once per enclosing function (issue #35). A function's metrics
|
|
122
|
+
/// are its own-body contributions plus, per directly nested function, that function's
|
|
123
|
+
/// already-computed totals: NCSS hoists as-is; cognitive complexity re-bases the nested function's
|
|
124
|
+
/// nesting-sensitive increments (each worth `1 + nesting`) by the nesting offset at the embedding
|
|
125
|
+
/// site, while flat increments (else branches, boolean-operator sequences, chain continuations,
|
|
126
|
+
/// jumps, guards) hoist unchanged; nesting depth describes the own body only, so it does not
|
|
127
|
+
/// hoist.
|
|
128
|
+
pub fn measure_function_body_metrics(
|
|
129
|
+
root: Node<'_>,
|
|
130
|
+
sets: &LanguageSets,
|
|
131
|
+
code: &Source<'_>,
|
|
132
|
+
) -> HashMap<usize, FunctionBodyMetrics> {
|
|
133
|
+
let mut pass = FunctionBodyPass {
|
|
134
|
+
sets,
|
|
135
|
+
code,
|
|
136
|
+
// frames[0] is a sentinel for top-level code; its accumulation is discarded.
|
|
137
|
+
frames: vec![FunctionBodyFrame::new(0, 0)],
|
|
138
|
+
results: HashMap::new(),
|
|
139
|
+
};
|
|
140
|
+
pass.visit(root, 0, 0, false, false, false);
|
|
141
|
+
pass.results
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
impl FunctionBodyPass<'_, '_, '_> {
|
|
145
|
+
fn visit(
|
|
146
|
+
&mut self,
|
|
147
|
+
current: Node<'_>,
|
|
148
|
+
current_nesting: u64,
|
|
149
|
+
mut function_nesting_bonus: u64,
|
|
150
|
+
mut inside_function: bool,
|
|
151
|
+
mut inside_nested_region: bool,
|
|
152
|
+
inside_charged_class_body: bool,
|
|
153
|
+
) {
|
|
154
|
+
// A class body nested in a function (anonymous/local classes) raises the cognitive nesting
|
|
155
|
+
// level once for everything inside it — PMD charges the class body, not the methods it
|
|
156
|
+
// holds, so methods directly inside a charged class body skip the function-boundary bonus.
|
|
157
|
+
let is_charged_class_body = current.kind() == "class_body" && inside_function;
|
|
158
|
+
if is_charged_class_body {
|
|
159
|
+
inside_nested_region = true;
|
|
160
|
+
function_nesting_bonus += 1;
|
|
161
|
+
}
|
|
162
|
+
let opens_frame = is_function_boundary(current, &self.sets.function_nodes);
|
|
163
|
+
if opens_frame {
|
|
164
|
+
if inside_function && !inside_charged_class_body {
|
|
165
|
+
function_nesting_bonus += 1;
|
|
166
|
+
}
|
|
167
|
+
inside_function = true;
|
|
168
|
+
}
|
|
169
|
+
// The node's own increments target the frame it is embedded in, not the one it opens; a
|
|
170
|
+
// frame-opening or charged-class-body node contributes nothing to that frame's own body
|
|
171
|
+
// (nesting), matching the per-function traversal this pass replaces.
|
|
172
|
+
let entry_cognitive_nesting = self.top_frame().entry_cognitive_nesting;
|
|
173
|
+
let entry_structural_nesting = self.top_frame().entry_structural_nesting;
|
|
174
|
+
let relative_nesting = current_nesting + function_nesting_bonus - entry_cognitive_nesting;
|
|
175
|
+
let counts_for_own_body = !inside_nested_region && !opens_frame;
|
|
176
|
+
|
|
177
|
+
// Anonymous keyword tokens can share a type with named nodes (Ruby's `if` node contains an
|
|
178
|
+
// `if` keyword token), so only named nodes count as decisions.
|
|
179
|
+
let is_decision = current.is_named()
|
|
180
|
+
&& self.sets.decision_nodes.contains(current.kind())
|
|
181
|
+
&& !is_default_switch_branch(current);
|
|
182
|
+
let is_case_clause = current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(¤t.kind());
|
|
183
|
+
// Ruby's `case ... else` arm is an `else` node; like every other language's default branch
|
|
184
|
+
// it nests its contents inside the switch (it cannot go in the Ruby nesting set because
|
|
185
|
+
// `if`/`begin` else branches would then double-nest under their already-nesting parent).
|
|
186
|
+
let is_nesting = current.is_named()
|
|
187
|
+
&& (self.sets.nesting_nodes.contains(current.kind())
|
|
188
|
+
|| (current.kind() == "else"
|
|
189
|
+
&& current.parent().is_some_and(|parent| {
|
|
190
|
+
parent.kind() == "case" || parent.kind() == "case_match"
|
|
191
|
+
})));
|
|
192
|
+
// `elsif`/`elif`/`else if` continue a flat chain: they add a decision without a nesting
|
|
193
|
+
// surcharge (Sonar cognitive-complexity semantics).
|
|
194
|
+
let is_continuation = is_decision && is_flat_chain_continuation(current);
|
|
195
|
+
|
|
196
|
+
if is_decision && !is_case_clause {
|
|
197
|
+
if is_continuation {
|
|
198
|
+
self.top_frame().cognitive_complexity += 1;
|
|
199
|
+
} else {
|
|
200
|
+
self.top_frame().cognitive_complexity += 1 + relative_nesting;
|
|
201
|
+
self.top_frame().nesting_sensitive_count += 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if current.is_named() && SWITCH_LIKE_NODE_TYPES.contains(¤t.kind()) {
|
|
205
|
+
self.top_frame().cognitive_complexity += 1 + relative_nesting;
|
|
206
|
+
self.top_frame().nesting_sensitive_count += 1;
|
|
207
|
+
}
|
|
208
|
+
// A plain `else` branch adds one flat cognitive point; `else if` chains are charged on the
|
|
209
|
+
// nested if instead.
|
|
210
|
+
self.top_frame().cognitive_complexity += count_plain_else_branches(current);
|
|
211
|
+
// Sonar charges flow-breaking jumps: goto and labeled break/continue add one flat point.
|
|
212
|
+
if is_flow_breaking_jump(current) {
|
|
213
|
+
self.top_frame().cognitive_complexity += 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// A sequence of identical boolean operators reads as one condition, so only the operator
|
|
217
|
+
// starting a sequence adds a cognitive point (Sonar spec).
|
|
218
|
+
if is_boolean_operator(current, self.code)
|
|
219
|
+
&& starts_boolean_operator_sequence(current, self.code)
|
|
220
|
+
{
|
|
221
|
+
self.top_frame().cognitive_complexity += 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Pattern guards add one independent execution path without nesting.
|
|
225
|
+
if is_pattern_guard(current) {
|
|
226
|
+
self.top_frame().cognitive_complexity += 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let child_nesting = if is_nesting && !is_continuation {
|
|
230
|
+
current_nesting + 1
|
|
231
|
+
} else {
|
|
232
|
+
current_nesting
|
|
233
|
+
};
|
|
234
|
+
if counts_for_own_body {
|
|
235
|
+
let frame = self.top_frame();
|
|
236
|
+
frame.nesting_depth = frame
|
|
237
|
+
.nesting_depth
|
|
238
|
+
.max(child_nesting - entry_structural_nesting);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if opens_frame {
|
|
242
|
+
self.frames.push(FunctionBodyFrame::new(
|
|
243
|
+
child_nesting + function_nesting_bonus,
|
|
244
|
+
child_nesting,
|
|
245
|
+
));
|
|
246
|
+
}
|
|
247
|
+
// The node's NCSS contribution belongs to the innermost frame whose subtree holds it — the
|
|
248
|
+
// frame the node opens, if any (per-function NCSS includes the declaration node itself).
|
|
249
|
+
let own_ncss = crate::ncss::ncss_contribution(
|
|
250
|
+
current,
|
|
251
|
+
&self.sets.ncss_nodes,
|
|
252
|
+
&self.sets.ncss_containers,
|
|
253
|
+
);
|
|
254
|
+
let ncss_frame = self.top_frame();
|
|
255
|
+
ncss_frame.ncss += own_ncss;
|
|
256
|
+
if opens_frame && own_ncss > 0 {
|
|
257
|
+
ncss_frame.has_own_ncss_contribution = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for child in all_children(current) {
|
|
261
|
+
self.visit(
|
|
262
|
+
child,
|
|
263
|
+
child_nesting,
|
|
264
|
+
function_nesting_bonus,
|
|
265
|
+
inside_function,
|
|
266
|
+
if opens_frame {
|
|
267
|
+
false
|
|
268
|
+
} else {
|
|
269
|
+
inside_nested_region
|
|
270
|
+
},
|
|
271
|
+
is_charged_class_body,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if opens_frame {
|
|
276
|
+
let closed = self.frames.pop().expect("frame opened above");
|
|
277
|
+
self.results.insert(
|
|
278
|
+
current.id(),
|
|
279
|
+
FunctionBodyMetrics {
|
|
280
|
+
cognitive_complexity: closed.cognitive_complexity,
|
|
281
|
+
nesting_depth: closed.nesting_depth,
|
|
282
|
+
// A function node without a countable declaration of its own (arrow functions,
|
|
283
|
+
// lambdas, blocks) still counts 1 for the declaration itself.
|
|
284
|
+
ncss: closed.ncss + u64::from(!closed.has_own_ncss_contribution),
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
let parent = self.top_frame();
|
|
288
|
+
parent.cognitive_complexity += closed.cognitive_complexity
|
|
289
|
+
+ closed.nesting_sensitive_count
|
|
290
|
+
* (closed.entry_cognitive_nesting - parent.entry_cognitive_nesting);
|
|
291
|
+
parent.nesting_sensitive_count += closed.nesting_sensitive_count;
|
|
292
|
+
parent.ncss += closed.ncss;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn top_frame(&mut self) -> &mut FunctionBodyFrame {
|
|
297
|
+
self.frames
|
|
298
|
+
.last_mut()
|
|
299
|
+
.expect("sentinel frame always present")
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// File-level complexity over the whole tree. Cognitive complexity charges nested function/lambda
|
|
304
|
+
/// content one nesting level deeper per function boundary crossed (Sonar spec); nesting depth
|
|
305
|
+
/// counts every node once.
|
|
306
|
+
pub fn measure_complexity(
|
|
307
|
+
node: Node<'_>,
|
|
308
|
+
sets: &LanguageSets,
|
|
309
|
+
code: &Source<'_>,
|
|
310
|
+
) -> ComplexityResult {
|
|
311
|
+
let mut result = ComplexityResult {
|
|
312
|
+
cognitive_complexity: 0,
|
|
313
|
+
nesting_depth: 0,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
#[allow(clippy::too_many_arguments)]
|
|
317
|
+
fn visit(
|
|
318
|
+
current: Node<'_>,
|
|
319
|
+
current_nesting: u64,
|
|
320
|
+
mut function_nesting_bonus: u64,
|
|
321
|
+
mut inside_function: bool,
|
|
322
|
+
inside_charged_class_body: bool,
|
|
323
|
+
sets: &LanguageSets,
|
|
324
|
+
code: &Source<'_>,
|
|
325
|
+
result: &mut ComplexityResult,
|
|
326
|
+
) {
|
|
327
|
+
// A class body nested in a function (anonymous/local classes) raises the cognitive nesting
|
|
328
|
+
// level once for everything inside it — PMD charges the class body, not the methods it
|
|
329
|
+
// holds, so methods directly inside a charged class body skip the function-boundary bonus.
|
|
330
|
+
let is_charged_class_body = current.kind() == "class_body" && inside_function;
|
|
331
|
+
if is_charged_class_body {
|
|
332
|
+
function_nesting_bonus += 1;
|
|
333
|
+
}
|
|
334
|
+
if is_function_boundary(current, &sets.function_nodes) {
|
|
335
|
+
if inside_function && !inside_charged_class_body {
|
|
336
|
+
function_nesting_bonus += 1;
|
|
337
|
+
}
|
|
338
|
+
inside_function = true;
|
|
339
|
+
}
|
|
340
|
+
let cognitive_nesting = current_nesting + function_nesting_bonus;
|
|
341
|
+
|
|
342
|
+
// Anonymous keyword tokens can share a type with named nodes (Ruby's `if` node contains an
|
|
343
|
+
// `if` keyword token), so only named nodes count as decisions.
|
|
344
|
+
let is_decision = current.is_named()
|
|
345
|
+
&& sets.decision_nodes.contains(current.kind())
|
|
346
|
+
&& !is_default_switch_branch(current);
|
|
347
|
+
let is_case_clause = current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(¤t.kind());
|
|
348
|
+
// Ruby's `case ... else` arm is an `else` node; like every other language's default branch
|
|
349
|
+
// it nests its contents inside the switch (it cannot go in the Ruby nesting set because
|
|
350
|
+
// `if`/`begin` else branches would then double-nest under their already-nesting parent).
|
|
351
|
+
let is_nesting = current.is_named()
|
|
352
|
+
&& (sets.nesting_nodes.contains(current.kind())
|
|
353
|
+
|| (current.kind() == "else"
|
|
354
|
+
&& current.parent().is_some_and(|parent| {
|
|
355
|
+
parent.kind() == "case" || parent.kind() == "case_match"
|
|
356
|
+
})));
|
|
357
|
+
// `elsif`/`elif`/`else if` continue a flat chain: they add a decision without a nesting
|
|
358
|
+
// surcharge (Sonar cognitive-complexity semantics).
|
|
359
|
+
let is_continuation = is_decision && is_flat_chain_continuation(current);
|
|
360
|
+
|
|
361
|
+
if is_decision && !is_case_clause {
|
|
362
|
+
result.cognitive_complexity += if is_continuation {
|
|
363
|
+
1
|
|
364
|
+
} else {
|
|
365
|
+
1 + cognitive_nesting
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if current.is_named() && SWITCH_LIKE_NODE_TYPES.contains(¤t.kind()) {
|
|
369
|
+
result.cognitive_complexity += 1 + cognitive_nesting;
|
|
370
|
+
}
|
|
371
|
+
// A plain `else` branch adds one flat cognitive point; `else if` chains are charged on the
|
|
372
|
+
// nested if instead.
|
|
373
|
+
result.cognitive_complexity += count_plain_else_branches(current);
|
|
374
|
+
// Sonar charges flow-breaking jumps: goto and labeled break/continue add one flat point.
|
|
375
|
+
if is_flow_breaking_jump(current) {
|
|
376
|
+
result.cognitive_complexity += 1;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// A sequence of identical boolean operators reads as one condition, so only the operator
|
|
380
|
+
// starting a sequence adds a cognitive point (Sonar spec).
|
|
381
|
+
if is_boolean_operator(current, code) && starts_boolean_operator_sequence(current, code) {
|
|
382
|
+
result.cognitive_complexity += 1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Pattern guards add one independent execution path without nesting.
|
|
386
|
+
if is_pattern_guard(current) {
|
|
387
|
+
result.cognitive_complexity += 1;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let child_nesting = if is_nesting && !is_continuation {
|
|
391
|
+
current_nesting + 1
|
|
392
|
+
} else {
|
|
393
|
+
current_nesting
|
|
394
|
+
};
|
|
395
|
+
result.nesting_depth = result.nesting_depth.max(child_nesting);
|
|
396
|
+
|
|
397
|
+
for child in all_children(current) {
|
|
398
|
+
visit(
|
|
399
|
+
child,
|
|
400
|
+
child_nesting,
|
|
401
|
+
function_nesting_bonus,
|
|
402
|
+
inside_function,
|
|
403
|
+
is_charged_class_body,
|
|
404
|
+
sets,
|
|
405
|
+
code,
|
|
406
|
+
result,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for child in all_children(node) {
|
|
412
|
+
visit(child, 0, 0, false, false, sets, code, &mut result);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
result
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/// Plain else branches attached to `current`: an `else_clause`/Ruby `else` whose branch is not an
|
|
419
|
+
/// `else if` continuation, or a bare Java/Go `alternative:` statement without a clause wrapper.
|
|
420
|
+
fn count_plain_else_branches(current: Node<'_>) -> u64 {
|
|
421
|
+
if !current.is_named() {
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
let kind = current.kind();
|
|
425
|
+
if kind == "else" {
|
|
426
|
+
// A Ruby `case ... else` is the default arm of a switch, which already counts as a whole
|
|
427
|
+
// (sonar-ruby models it as a match case, not an else branch); `if`/`unless`/`begin` else
|
|
428
|
+
// branches count one point each.
|
|
429
|
+
return if current
|
|
430
|
+
.parent()
|
|
431
|
+
.is_some_and(|parent| parent.kind() == "case" || parent.kind() == "case_match")
|
|
432
|
+
{
|
|
433
|
+
0
|
|
434
|
+
} else {
|
|
435
|
+
1
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if kind == "else_clause" {
|
|
439
|
+
let has_if_like_child = crate::util::named_children(current)
|
|
440
|
+
.iter()
|
|
441
|
+
.any(|child| IF_LIKE_NODE_TYPES.contains(&child.kind()));
|
|
442
|
+
return if has_if_like_child { 0 } else { 1 };
|
|
443
|
+
}
|
|
444
|
+
if kind != "if_statement" && kind != "if_expression" {
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
// Extras (comments) inherit the preceding sibling's field in find_children_by_field_name, so a
|
|
448
|
+
// comment between an `elif_clause` and `else_clause` must not be miscounted as a bare branch.
|
|
449
|
+
crate::util::find_children_by_field_name(current, "alternative")
|
|
450
|
+
.iter()
|
|
451
|
+
.filter(|child| {
|
|
452
|
+
!child.is_extra()
|
|
453
|
+
&& child.kind() != "else_clause"
|
|
454
|
+
&& child.kind() != "elif_clause"
|
|
455
|
+
&& !IF_LIKE_NODE_TYPES.contains(&child.kind())
|
|
456
|
+
})
|
|
457
|
+
.count() as u64
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/// goto, and break/continue that jump to a label (their only named child is the label).
|
|
461
|
+
fn is_flow_breaking_jump(node: Node<'_>) -> bool {
|
|
462
|
+
if !node.is_named() {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
if node.kind() == "goto_statement" {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
// Rust jumps are expressions; `break value` carries a named expression child, so only an
|
|
469
|
+
// explicit `label` child marks a labeled jump.
|
|
470
|
+
if node.kind() == "break_expression" || node.kind() == "continue_expression" {
|
|
471
|
+
return crate::util::named_children(node)
|
|
472
|
+
.iter()
|
|
473
|
+
.any(|child| child.kind() == "label" || child.kind() == "loop_label");
|
|
474
|
+
}
|
|
475
|
+
// Comments are named children too (`break /* done */;`), so only non-comment children mark a
|
|
476
|
+
// label.
|
|
477
|
+
(node.kind() == "break_statement" || node.kind() == "continue_statement")
|
|
478
|
+
&& crate::util::named_children(node)
|
|
479
|
+
.iter()
|
|
480
|
+
.any(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind()))
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/// Wrappers that are transparent when locating the enclosing boolean operation: PMD/Sonar keep a
|
|
484
|
+
/// sequence continuous across parentheses (`a && (b && c)` costs one point).
|
|
485
|
+
const PARENTHESIZED_NODE_TYPES: &[&str] = &["parenthesized_expression", "parenthesized_statements"];
|
|
486
|
+
|
|
487
|
+
/// Whether this boolean operator token starts a new sequence, i.e. its binary node is the root of a
|
|
488
|
+
/// run of same-operator binaries (possibly through parentheses). Only the root operator counts one
|
|
489
|
+
/// cognitive point: `a && b && c` and `a && (b && c)` cost one, `a && b || c` costs two, matching
|
|
490
|
+
/// the Sonar specification and PMD 7.26.0.
|
|
491
|
+
fn starts_boolean_operator_sequence(token: Node<'_>, code: &Source<'_>) -> bool {
|
|
492
|
+
let Some(binary) = token.parent() else {
|
|
493
|
+
return true;
|
|
494
|
+
};
|
|
495
|
+
let mut ancestor = binary.parent();
|
|
496
|
+
while let Some(node) = ancestor {
|
|
497
|
+
if !PARENTHESIZED_NODE_TYPES.contains(&node.kind()) {
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
ancestor = node.parent();
|
|
501
|
+
}
|
|
502
|
+
let Some(ancestor) = ancestor else {
|
|
503
|
+
return true;
|
|
504
|
+
};
|
|
505
|
+
if ancestor.kind() != binary.kind() {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
find_boolean_operator_text(ancestor, code).map(normalize_boolean_operator)
|
|
509
|
+
!= Some(normalize_boolean_operator(node_text(token, code)))
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/// C++ `and`/`or` are alternative spellings of `&&`/`||`, so mixing them keeps one sequence.
|
|
513
|
+
fn normalize_boolean_operator(text: &str) -> &str {
|
|
514
|
+
match text {
|
|
515
|
+
"and" => "&&",
|
|
516
|
+
"or" => "||",
|
|
517
|
+
_ => text,
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
fn find_boolean_operator_text<'a>(binary_node: Node<'_>, code: &Source<'a>) -> Option<&'a str> {
|
|
522
|
+
if let Some(operator) = binary_node.child_by_field_name("operator") {
|
|
523
|
+
return Some(node_text(operator, code));
|
|
524
|
+
}
|
|
525
|
+
all_children(binary_node)
|
|
526
|
+
.into_iter()
|
|
527
|
+
.find(|child| !child.is_named() && BOOLEAN_OPERATORS.contains(&node_text(*child, code)))
|
|
528
|
+
.map(|child| node_text(child, code))
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/// Java `guard`, Ruby `if_guard`, Python `if_clause`, and Rust guards inside `match_pattern`.
|
|
532
|
+
fn is_pattern_guard(node: Node<'_>) -> bool {
|
|
533
|
+
if !node.is_named() {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
let kind = node.kind();
|
|
537
|
+
if kind == "guard" || kind == "if_guard" || kind == "unless_guard" || kind == "if_clause" {
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
kind == "match_pattern"
|
|
541
|
+
&& all_children(node)
|
|
542
|
+
.iter()
|
|
543
|
+
.any(|child| !child.is_named() && child.kind() == "if")
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/// Ruby `elsif`, Python `elif`, and `else if` (an if node in an else/alternative position).
|
|
547
|
+
fn is_flat_chain_continuation(node: Node<'_>) -> bool {
|
|
548
|
+
let kind = node.kind();
|
|
549
|
+
if kind == "elsif" || kind == "elif_clause" {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
if kind != "if_statement" && kind != "if_expression" && kind != "if" {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
let Some(parent) = node.parent() else {
|
|
556
|
+
return false;
|
|
557
|
+
};
|
|
558
|
+
// JS/C/C++/Rust wrap `else if` in an else clause; Java/Go put it directly in `alternative`.
|
|
559
|
+
parent.kind() == "else_clause"
|
|
560
|
+
|| parent
|
|
561
|
+
.child_by_field_name("alternative")
|
|
562
|
+
.is_some_and(|alternative| alternative.id() == node.id())
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/// Default branches of switch-like constructs add no decision.
|
|
566
|
+
fn is_default_switch_branch(node: Node<'_>) -> bool {
|
|
567
|
+
let kind = node.kind();
|
|
568
|
+
if kind == "case_statement" {
|
|
569
|
+
return node.child_by_field_name("value").is_none();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if kind == "switch_block_statement_group" || kind == "switch_rule" {
|
|
573
|
+
let label = crate::util::named_children(node)
|
|
574
|
+
.into_iter()
|
|
575
|
+
.find(|child| child.kind() == "switch_label");
|
|
576
|
+
return label.is_some_and(|label| label.named_child_count() == 0);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Python `case _:` / `case y:` and Rust `_ =>` fallback arms are unconditional like `default`.
|
|
580
|
+
if kind == "case_clause" || kind == "match_arm" {
|
|
581
|
+
let pattern = crate::util::named_children(node)
|
|
582
|
+
.into_iter()
|
|
583
|
+
.find(|child| child.kind() == "case_pattern" || child.kind() == "match_pattern");
|
|
584
|
+
let Some(pattern) = pattern else {
|
|
585
|
+
return false;
|
|
586
|
+
};
|
|
587
|
+
if pattern.child(0).is_some_and(|first| first.kind() == "_")
|
|
588
|
+
&& (pattern.child_count() == 1
|
|
589
|
+
|| pattern.child(1).is_some_and(|second| second.kind() == "if"))
|
|
590
|
+
{
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
let sole_child = if pattern.named_child_count() == 1 {
|
|
594
|
+
pattern.named_child(0)
|
|
595
|
+
} else {
|
|
596
|
+
None
|
|
597
|
+
};
|
|
598
|
+
return kind == "case_clause"
|
|
599
|
+
&& sole_child.is_some_and(|child| {
|
|
600
|
+
child.kind() == "dotted_name"
|
|
601
|
+
&& child.named_child_count() == 1
|
|
602
|
+
&& child
|
|
603
|
+
.named_child(0)
|
|
604
|
+
.is_some_and(|inner| inner.kind() == "identifier")
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Ruby `in y` binds unconditionally.
|
|
609
|
+
if kind == "in_clause" {
|
|
610
|
+
return node
|
|
611
|
+
.named_child(0)
|
|
612
|
+
.is_some_and(|first| first.kind() == "identifier");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
false
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/// The parent guard is required because the same tokens appear in non-boolean syntax (C++ `int&&`,
|
|
619
|
+
/// `operator&&`, Rust's empty closure parameter list `|| 5`).
|
|
620
|
+
fn is_boolean_operator(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
621
|
+
if node.is_named() || !BOOLEAN_OPERATORS.contains(&node_text(node, code)) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
node.parent()
|
|
626
|
+
.is_some_and(|parent| BOOLEAN_OPERATOR_PARENT_TYPES.contains(&parent.kind()))
|
|
627
|
+
}
|