code-gauge 4.6.0 → 4.7.1
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 +25 -0
- package/THIRD-PARTY-NOTICES.txt +8911 -0
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.js +1 -1
- package/dist/diffCommand.cjs +3 -3
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +3 -3
- package/dist/diffCommand.js.map +1 -1
- package/dist/languages.cjs +1 -1
- package/dist/languages.cjs.map +1 -1
- 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 +8 -3
- package/dist/metrics.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +3 -3
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +30 -0
- package/dist/nativeMetrics.js +3 -3
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.d.ts +2 -2
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/wasmBinding.cjs +2 -0
- package/dist/wasmBinding.cjs.map +1 -0
- package/dist/wasmBinding.d.ts +9 -0
- package/dist/wasmBinding.js +2 -0
- package/dist/wasmBinding.js.map +1 -0
- package/dist/worker.cjs +2 -0
- package/dist/worker.cjs.map +1 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +2 -0
- package/dist/worker.js.map +1 -0
- package/native/Cargo.lock +1 -0
- package/native/Cargo.toml +6 -2
- package/native/build.rs +4 -1
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +134 -230
- package/native/src/dep_degree.rs +42 -39
- package/native/src/duplication.rs +65 -63
- package/native/src/functions.rs +169 -152
- package/native/src/languages.rs +20 -0
- package/native/src/lib.rs +41 -42
- package/native/src/measure.rs +109 -123
- package/native/src/napi.rs +104 -0
- package/native/src/ncss.rs +79 -84
- package/native/src/near_miss.rs +7 -7
- package/native/src/tree_index.rs +110 -0
- package/native/src/util.rs +17 -12
- package/native/src/wasm.rs +128 -0
- package/native/src/worker_pool.rs +53 -0
- package/package.json +16 -11
package/native/src/complexity.rs
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
use
|
|
1
|
+
use rustc_hash::{FxHashMap, FxHashSet};
|
|
2
2
|
use tree_sitter::Node;
|
|
3
3
|
|
|
4
|
+
use crate::tree_index::NodeExt;
|
|
4
5
|
use crate::util::{all_children, node_text, Source};
|
|
5
6
|
|
|
6
|
-
pub struct ComplexityResult {
|
|
7
|
-
pub cognitive_complexity: u64,
|
|
8
|
-
pub nesting_depth: u64,
|
|
9
|
-
}
|
|
10
|
-
|
|
11
7
|
/// Node-type lookup sets built once per measurement from the language definition.
|
|
12
8
|
pub struct LanguageSets {
|
|
13
|
-
pub function_nodes:
|
|
14
|
-
pub decision_nodes:
|
|
15
|
-
pub nesting_nodes:
|
|
16
|
-
pub ncss_nodes:
|
|
17
|
-
pub ncss_containers:
|
|
9
|
+
pub function_nodes: FxHashSet<&'static str>,
|
|
10
|
+
pub decision_nodes: FxHashSet<&'static str>,
|
|
11
|
+
pub nesting_nodes: FxHashSet<&'static str>,
|
|
12
|
+
pub ncss_nodes: FxHashSet<&'static str>,
|
|
13
|
+
pub ncss_containers: FxHashSet<&'static str>,
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
impl LanguageSets {
|
|
@@ -44,17 +40,17 @@ const BOOLEAN_OPERATOR_PARENT_TYPES: &[&str] = &[
|
|
|
44
40
|
|
|
45
41
|
/// A Ruby stabby lambda's body block is part of the lambda, not a separate function.
|
|
46
42
|
pub fn is_lambda_body_block(node: Node<'_>) -> bool {
|
|
47
|
-
(node.
|
|
43
|
+
(node.kind_name() == "block" || node.kind_name() == "do_block")
|
|
48
44
|
&& node
|
|
49
|
-
.
|
|
50
|
-
.is_some_and(|parent| parent.
|
|
45
|
+
.parent_node()
|
|
46
|
+
.is_some_and(|parent| parent.kind_name() == "lambda")
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
/// A function node with a body of its own: bodyless declarations (abstract methods, auto-property
|
|
54
50
|
/// accessors, accessor-list properties) open no nesting frame, so members inside them are not
|
|
55
51
|
/// charged as nested functions.
|
|
56
|
-
pub fn is_function_boundary(node: Node<'_>, function_nodes: &
|
|
57
|
-
function_nodes.contains(node.
|
|
52
|
+
pub fn is_function_boundary(node: Node<'_>, function_nodes: &FxHashSet<&'static str>) -> bool {
|
|
53
|
+
function_nodes.contains(node.kind_name())
|
|
58
54
|
&& !is_lambda_body_block(node)
|
|
59
55
|
&& crate::functions::is_implemented_function(node)
|
|
60
56
|
}
|
|
@@ -135,17 +131,25 @@ struct FunctionBodyPass<'sets, 'code, 'source> {
|
|
|
135
131
|
sets: &'sets LanguageSets,
|
|
136
132
|
code: &'code Source<'source>,
|
|
137
133
|
frames: Vec<FunctionBodyFrame>,
|
|
138
|
-
results:
|
|
134
|
+
results: FxHashMap<usize, FunctionBodyMetrics>,
|
|
139
135
|
/// Cyclomatic decisions inside class bodies nested in functions, which no function owns.
|
|
140
136
|
nested_class_decisions: u64,
|
|
137
|
+
/// Deepest structural nesting anywhere in the file.
|
|
138
|
+
max_nesting: u64,
|
|
141
139
|
}
|
|
142
140
|
|
|
143
|
-
/// Per-function body metrics, plus the cyclomatic decisions no function
|
|
141
|
+
/// Per-function body metrics, plus the file-level totals and the cyclomatic decisions no function
|
|
142
|
+
/// body owns.
|
|
144
143
|
pub struct BodyMetrics {
|
|
145
|
-
pub by_function:
|
|
144
|
+
pub by_function: FxHashMap<usize, FunctionBodyMetrics>,
|
|
146
145
|
/// Cyclomatic decisions outside every function body (top-level statements, field initializers,
|
|
147
146
|
/// including those of classes nested in functions).
|
|
148
147
|
pub top_level_decisions: u64,
|
|
148
|
+
/// File-level cognitive complexity: nested function/lambda content is charged one nesting
|
|
149
|
+
/// level deeper per function boundary crossed (Sonar spec).
|
|
150
|
+
pub cognitive_complexity: u64,
|
|
151
|
+
pub nesting_depth: u64,
|
|
152
|
+
pub ncss: u64,
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
/// Per-function complexity and NCSS for every function boundary, in one post-order pass so each
|
|
@@ -164,15 +168,22 @@ pub fn measure_function_body_metrics(
|
|
|
164
168
|
let mut pass = FunctionBodyPass {
|
|
165
169
|
sets,
|
|
166
170
|
code,
|
|
167
|
-
// frames[0] is a sentinel for top-level code
|
|
171
|
+
// frames[0] is a sentinel for top-level code, which ends up holding the file's totals.
|
|
168
172
|
frames: vec![FunctionBodyFrame::new(0, 0)],
|
|
169
|
-
results:
|
|
173
|
+
results: FxHashMap::default(),
|
|
170
174
|
nested_class_decisions: 0,
|
|
175
|
+
max_nesting: 0,
|
|
171
176
|
};
|
|
172
177
|
pass.visit(root, 0, 0, false, false, false);
|
|
178
|
+
// Every closed frame hoists into its parent, so the sentinel's cognitive increments are re-based
|
|
179
|
+
// to absolute nesting (its entry nesting is 0).
|
|
180
|
+
let file_frame = &pass.frames[0];
|
|
173
181
|
BodyMetrics {
|
|
182
|
+
top_level_decisions: file_frame.cyclomatic_complexity - 1 + pass.nested_class_decisions,
|
|
183
|
+
cognitive_complexity: file_frame.cognitive_complexity,
|
|
184
|
+
nesting_depth: pass.max_nesting,
|
|
185
|
+
ncss: file_frame.ncss,
|
|
174
186
|
by_function: pass.results,
|
|
175
|
-
top_level_decisions: pass.frames[0].cyclomatic_complexity - 1 + pass.nested_class_decisions,
|
|
176
187
|
}
|
|
177
188
|
}
|
|
178
189
|
|
|
@@ -189,7 +200,8 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
189
200
|
// A class body nested in a function (anonymous/local classes) raises the cognitive nesting
|
|
190
201
|
// level once for everything inside it — PMD charges the class body, not the methods it
|
|
191
202
|
// holds, so methods directly inside a charged class body skip the function-boundary bonus.
|
|
192
|
-
let
|
|
203
|
+
let parent = current.parent_node();
|
|
204
|
+
let is_charged_class_body = current.kind_name() == "class_body" && inside_function;
|
|
193
205
|
if is_charged_class_body {
|
|
194
206
|
inside_nested_region = true;
|
|
195
207
|
function_nesting_bonus += 1;
|
|
@@ -212,25 +224,25 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
212
224
|
// Anonymous keyword tokens can share a type with named nodes (Ruby's `if` node contains an
|
|
213
225
|
// `if` keyword token), so only named nodes count as decisions.
|
|
214
226
|
let is_decision = current.is_named()
|
|
215
|
-
&& self.sets.decision_nodes.contains(current.
|
|
227
|
+
&& self.sets.decision_nodes.contains(current.kind_name())
|
|
216
228
|
&& !is_pathless_switch_branch(current, self.code);
|
|
217
|
-
let is_case_clause =
|
|
229
|
+
let is_case_clause =
|
|
230
|
+
current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(¤t.kind_name());
|
|
218
231
|
// Ruby's `case ... else` arm is an `else` node; like every other language's default branch
|
|
219
232
|
// it nests its contents inside the switch (it cannot go in the Ruby nesting set because
|
|
220
233
|
// `if`/`begin` else branches would then double-nest under their already-nesting parent).
|
|
221
234
|
let is_nesting = current.is_named()
|
|
222
|
-
&& (self.sets.nesting_nodes.contains(current.
|
|
223
|
-
|| (current.
|
|
224
|
-
&& current.parent().is_some_and(|parent| {
|
|
225
|
-
parent.kind() == "case" || parent.kind() == "case_match"
|
|
226
|
-
})));
|
|
235
|
+
&& (self.sets.nesting_nodes.contains(current.kind_name())
|
|
236
|
+
|| (current.kind_name() == "else" && is_case_else_parent(parent)));
|
|
227
237
|
// `elsif`/`elif`/`else if` continue a flat chain: they add a decision without a nesting
|
|
228
238
|
// surcharge (Sonar cognitive-complexity semantics).
|
|
229
|
-
let is_continuation = is_decision && is_flat_chain_continuation(current);
|
|
239
|
+
let is_continuation = is_decision && is_flat_chain_continuation(current, parent);
|
|
240
|
+
let is_boolean_operator = is_boolean_operator(current, parent, self.code);
|
|
241
|
+
let is_pattern_guard = is_pattern_guard(current, parent);
|
|
230
242
|
|
|
231
243
|
// Each branch, short-circuit operator, and pattern guard adds one path (McCabe; NIST SP
|
|
232
244
|
// 500-235 §4); `else` adds none.
|
|
233
|
-
if is_decision || is_boolean_operator
|
|
245
|
+
if is_decision || is_boolean_operator || is_pattern_guard {
|
|
234
246
|
if counts_for_own_body {
|
|
235
247
|
self.top_frame().cyclomatic_complexity += 1;
|
|
236
248
|
} else if !opens_frame {
|
|
@@ -245,13 +257,13 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
245
257
|
self.top_frame().nesting_sensitive_count += 1;
|
|
246
258
|
}
|
|
247
259
|
}
|
|
248
|
-
if current.is_named() && SWITCH_LIKE_NODE_TYPES.contains(¤t.
|
|
260
|
+
if current.is_named() && SWITCH_LIKE_NODE_TYPES.contains(¤t.kind_name()) {
|
|
249
261
|
self.top_frame().cognitive_complexity += 1 + relative_nesting;
|
|
250
262
|
self.top_frame().nesting_sensitive_count += 1;
|
|
251
263
|
}
|
|
252
264
|
// A plain `else` branch adds one flat cognitive point; `else if` chains are charged on the
|
|
253
265
|
// nested if instead.
|
|
254
|
-
self.top_frame().cognitive_complexity += count_plain_else_branches(current);
|
|
266
|
+
self.top_frame().cognitive_complexity += count_plain_else_branches(current, parent);
|
|
255
267
|
// Sonar charges flow-breaking jumps: goto and labeled break/continue add one flat point.
|
|
256
268
|
if is_flow_breaking_jump(current) {
|
|
257
269
|
self.top_frame().cognitive_complexity += 1;
|
|
@@ -259,14 +271,12 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
259
271
|
|
|
260
272
|
// A sequence of identical boolean operators reads as one condition, so only the operator
|
|
261
273
|
// starting a sequence adds a cognitive point (Sonar spec).
|
|
262
|
-
if is_boolean_operator(
|
|
263
|
-
&& starts_boolean_operator_sequence(current, self.code)
|
|
264
|
-
{
|
|
274
|
+
if is_boolean_operator && starts_boolean_operator_sequence(parent, self.code, current) {
|
|
265
275
|
self.top_frame().cognitive_complexity += 1;
|
|
266
276
|
}
|
|
267
277
|
|
|
268
278
|
// Pattern guards add one independent execution path without nesting.
|
|
269
|
-
if is_pattern_guard
|
|
279
|
+
if is_pattern_guard {
|
|
270
280
|
self.top_frame().cognitive_complexity += 1;
|
|
271
281
|
}
|
|
272
282
|
|
|
@@ -275,6 +285,7 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
275
285
|
} else {
|
|
276
286
|
current_nesting
|
|
277
287
|
};
|
|
288
|
+
self.max_nesting = self.max_nesting.max(child_nesting);
|
|
278
289
|
if counts_for_own_body {
|
|
279
290
|
let frame = self.top_frame();
|
|
280
291
|
frame.nesting_depth = frame
|
|
@@ -292,6 +303,7 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
292
303
|
// frame the node opens, if any (per-function NCSS includes the declaration node itself).
|
|
293
304
|
let own_ncss = crate::ncss::ncss_contribution(
|
|
294
305
|
current,
|
|
306
|
+
parent,
|
|
295
307
|
&self.sets.ncss_nodes,
|
|
296
308
|
&self.sets.ncss_containers,
|
|
297
309
|
);
|
|
@@ -345,145 +357,23 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
345
357
|
}
|
|
346
358
|
}
|
|
347
359
|
|
|
348
|
-
/// File-level complexity over the whole tree. Cognitive complexity charges nested function/lambda
|
|
349
|
-
/// content one nesting level deeper per function boundary crossed (Sonar spec); nesting depth
|
|
350
|
-
/// counts every node once.
|
|
351
|
-
pub fn measure_complexity(
|
|
352
|
-
node: Node<'_>,
|
|
353
|
-
sets: &LanguageSets,
|
|
354
|
-
code: &Source<'_>,
|
|
355
|
-
) -> ComplexityResult {
|
|
356
|
-
let mut result = ComplexityResult {
|
|
357
|
-
cognitive_complexity: 0,
|
|
358
|
-
nesting_depth: 0,
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
#[allow(clippy::too_many_arguments)]
|
|
362
|
-
fn visit(
|
|
363
|
-
current: Node<'_>,
|
|
364
|
-
current_nesting: u64,
|
|
365
|
-
mut function_nesting_bonus: u64,
|
|
366
|
-
mut inside_function: bool,
|
|
367
|
-
inside_charged_class_body: bool,
|
|
368
|
-
sets: &LanguageSets,
|
|
369
|
-
code: &Source<'_>,
|
|
370
|
-
result: &mut ComplexityResult,
|
|
371
|
-
) {
|
|
372
|
-
// A class body nested in a function (anonymous/local classes) raises the cognitive nesting
|
|
373
|
-
// level once for everything inside it — PMD charges the class body, not the methods it
|
|
374
|
-
// holds, so methods directly inside a charged class body skip the function-boundary bonus.
|
|
375
|
-
let is_charged_class_body = current.kind() == "class_body" && inside_function;
|
|
376
|
-
if is_charged_class_body {
|
|
377
|
-
function_nesting_bonus += 1;
|
|
378
|
-
}
|
|
379
|
-
if is_function_boundary(current, &sets.function_nodes) {
|
|
380
|
-
if inside_function && !inside_charged_class_body {
|
|
381
|
-
function_nesting_bonus += 1;
|
|
382
|
-
}
|
|
383
|
-
inside_function = true;
|
|
384
|
-
}
|
|
385
|
-
let cognitive_nesting = current_nesting + function_nesting_bonus;
|
|
386
|
-
|
|
387
|
-
// Anonymous keyword tokens can share a type with named nodes (Ruby's `if` node contains an
|
|
388
|
-
// `if` keyword token), so only named nodes count as decisions.
|
|
389
|
-
let is_decision = current.is_named()
|
|
390
|
-
&& sets.decision_nodes.contains(current.kind())
|
|
391
|
-
&& !is_pathless_switch_branch(current, code);
|
|
392
|
-
let is_case_clause = current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(¤t.kind());
|
|
393
|
-
// Ruby's `case ... else` arm is an `else` node; like every other language's default branch
|
|
394
|
-
// it nests its contents inside the switch (it cannot go in the Ruby nesting set because
|
|
395
|
-
// `if`/`begin` else branches would then double-nest under their already-nesting parent).
|
|
396
|
-
let is_nesting = current.is_named()
|
|
397
|
-
&& (sets.nesting_nodes.contains(current.kind())
|
|
398
|
-
|| (current.kind() == "else"
|
|
399
|
-
&& current.parent().is_some_and(|parent| {
|
|
400
|
-
parent.kind() == "case" || parent.kind() == "case_match"
|
|
401
|
-
})));
|
|
402
|
-
// `elsif`/`elif`/`else if` continue a flat chain: they add a decision without a nesting
|
|
403
|
-
// surcharge (Sonar cognitive-complexity semantics).
|
|
404
|
-
let is_continuation = is_decision && is_flat_chain_continuation(current);
|
|
405
|
-
|
|
406
|
-
if is_decision && !is_case_clause {
|
|
407
|
-
result.cognitive_complexity += if is_continuation {
|
|
408
|
-
1
|
|
409
|
-
} else {
|
|
410
|
-
1 + cognitive_nesting
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
if current.is_named() && SWITCH_LIKE_NODE_TYPES.contains(¤t.kind()) {
|
|
414
|
-
result.cognitive_complexity += 1 + cognitive_nesting;
|
|
415
|
-
}
|
|
416
|
-
// A plain `else` branch adds one flat cognitive point; `else if` chains are charged on the
|
|
417
|
-
// nested if instead.
|
|
418
|
-
result.cognitive_complexity += count_plain_else_branches(current);
|
|
419
|
-
// Sonar charges flow-breaking jumps: goto and labeled break/continue add one flat point.
|
|
420
|
-
if is_flow_breaking_jump(current) {
|
|
421
|
-
result.cognitive_complexity += 1;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// A sequence of identical boolean operators reads as one condition, so only the operator
|
|
425
|
-
// starting a sequence adds a cognitive point (Sonar spec).
|
|
426
|
-
if is_boolean_operator(current, code) && starts_boolean_operator_sequence(current, code) {
|
|
427
|
-
result.cognitive_complexity += 1;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Pattern guards add one independent execution path without nesting.
|
|
431
|
-
if is_pattern_guard(current) {
|
|
432
|
-
result.cognitive_complexity += 1;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
let child_nesting = if is_nesting && !is_continuation {
|
|
436
|
-
current_nesting + 1
|
|
437
|
-
} else {
|
|
438
|
-
current_nesting
|
|
439
|
-
};
|
|
440
|
-
result.nesting_depth = result.nesting_depth.max(child_nesting);
|
|
441
|
-
|
|
442
|
-
for child in all_children(current) {
|
|
443
|
-
visit(
|
|
444
|
-
child,
|
|
445
|
-
child_nesting,
|
|
446
|
-
function_nesting_bonus,
|
|
447
|
-
inside_function,
|
|
448
|
-
is_charged_class_body,
|
|
449
|
-
sets,
|
|
450
|
-
code,
|
|
451
|
-
result,
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
for child in all_children(node) {
|
|
457
|
-
visit(child, 0, 0, false, false, sets, code, &mut result);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
result
|
|
461
|
-
}
|
|
462
|
-
|
|
463
360
|
/// Plain else branches attached to `current`: an `else_clause`/Ruby `else` whose branch is not an
|
|
464
361
|
/// `else if` continuation, or a bare Java/Go `alternative:` statement without a clause wrapper.
|
|
465
|
-
fn count_plain_else_branches(current: Node<'_
|
|
362
|
+
fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64 {
|
|
466
363
|
if !current.is_named() {
|
|
467
364
|
return 0;
|
|
468
365
|
}
|
|
469
|
-
let kind = current.
|
|
366
|
+
let kind = current.kind_name();
|
|
470
367
|
if kind == "else" {
|
|
471
368
|
// A Ruby `case ... else` is the default arm of a switch, which already counts as a whole
|
|
472
369
|
// (sonar-ruby models it as a match case, not an else branch); `if`/`unless`/`begin` else
|
|
473
370
|
// branches count one point each.
|
|
474
|
-
return
|
|
475
|
-
.parent()
|
|
476
|
-
.is_some_and(|parent| parent.kind() == "case" || parent.kind() == "case_match")
|
|
477
|
-
{
|
|
478
|
-
0
|
|
479
|
-
} else {
|
|
480
|
-
1
|
|
481
|
-
};
|
|
371
|
+
return u64::from(!is_case_else_parent(parent));
|
|
482
372
|
}
|
|
483
373
|
if kind == "else_clause" {
|
|
484
374
|
let has_if_like_child = crate::util::named_children(current)
|
|
485
375
|
.iter()
|
|
486
|
-
.any(|child| IF_LIKE_NODE_TYPES.contains(&child.
|
|
376
|
+
.any(|child| IF_LIKE_NODE_TYPES.contains(&child.kind_name()));
|
|
487
377
|
return if has_if_like_child { 0 } else { 1 };
|
|
488
378
|
}
|
|
489
379
|
if kind != "if_statement" && kind != "if_expression" {
|
|
@@ -500,9 +390,9 @@ fn count_plain_else_branches(current: Node<'_>) -> u64 {
|
|
|
500
390
|
.iter()
|
|
501
391
|
.filter(|child| {
|
|
502
392
|
!child.is_extra()
|
|
503
|
-
&& child.
|
|
504
|
-
&& child.
|
|
505
|
-
&& !IF_LIKE_NODE_TYPES.contains(&child.
|
|
393
|
+
&& child.kind_name() != "else_clause"
|
|
394
|
+
&& child.kind_name() != "elif_clause"
|
|
395
|
+
&& !IF_LIKE_NODE_TYPES.contains(&child.kind_name())
|
|
506
396
|
})
|
|
507
397
|
.count() as u64
|
|
508
398
|
}
|
|
@@ -512,29 +402,29 @@ fn is_flow_breaking_jump(node: Node<'_>) -> bool {
|
|
|
512
402
|
if !node.is_named() {
|
|
513
403
|
return false;
|
|
514
404
|
}
|
|
515
|
-
if node.
|
|
405
|
+
if node.kind_name() == "goto_statement" {
|
|
516
406
|
return true;
|
|
517
407
|
}
|
|
518
408
|
// Rust jumps are expressions; `break value` carries a named expression child, so only an
|
|
519
409
|
// explicit `label` child marks a labeled jump.
|
|
520
|
-
if node.
|
|
410
|
+
if node.kind_name() == "break_expression" || node.kind_name() == "continue_expression" {
|
|
521
411
|
return crate::util::named_children(node)
|
|
522
412
|
.iter()
|
|
523
|
-
.any(|child| child.
|
|
413
|
+
.any(|child| child.kind_name() == "label" || child.kind_name() == "loop_label");
|
|
524
414
|
}
|
|
525
415
|
// Kotlin folds every jump into `jump_expression`; the grammar tokenizes a labeled break or
|
|
526
416
|
// continue as `break@`/`continue@` followed by the label (`return@label` is a plain return).
|
|
527
|
-
if node.
|
|
528
|
-
return node
|
|
529
|
-
.
|
|
530
|
-
|
|
417
|
+
if node.kind_name() == "jump_expression" {
|
|
418
|
+
return node.child(0).is_some_and(|keyword| {
|
|
419
|
+
keyword.kind_name() == "break@" || keyword.kind_name() == "continue@"
|
|
420
|
+
});
|
|
531
421
|
}
|
|
532
422
|
// Comments are named children too (`break /* done */;`), so only non-comment children mark a
|
|
533
423
|
// label.
|
|
534
|
-
(node.
|
|
424
|
+
(node.kind_name() == "break_statement" || node.kind_name() == "continue_statement")
|
|
535
425
|
&& crate::util::named_children(node)
|
|
536
426
|
.iter()
|
|
537
|
-
.any(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.
|
|
427
|
+
.any(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind_name()))
|
|
538
428
|
}
|
|
539
429
|
|
|
540
430
|
/// Wrappers that are transparent when locating the enclosing boolean operation: PMD/Sonar keep a
|
|
@@ -549,21 +439,25 @@ const PARENTHESIZED_NODE_TYPES: &[&str] = &[
|
|
|
549
439
|
/// run of same-operator binaries (possibly through parentheses). Only the root operator counts one
|
|
550
440
|
/// cognitive point: `a && b && c` and `a && (b && c)` cost one, `a && b || c` costs two, matching
|
|
551
441
|
/// the Sonar specification and PMD 7.26.0.
|
|
552
|
-
fn starts_boolean_operator_sequence(
|
|
553
|
-
|
|
442
|
+
fn starts_boolean_operator_sequence(
|
|
443
|
+
binary: Option<Node<'_>>,
|
|
444
|
+
code: &Source<'_>,
|
|
445
|
+
token: Node<'_>,
|
|
446
|
+
) -> bool {
|
|
447
|
+
let Some(binary) = binary else {
|
|
554
448
|
return true;
|
|
555
449
|
};
|
|
556
|
-
let mut ancestor = binary.
|
|
450
|
+
let mut ancestor = binary.parent_node();
|
|
557
451
|
while let Some(node) = ancestor {
|
|
558
|
-
if !PARENTHESIZED_NODE_TYPES.contains(&node.
|
|
452
|
+
if !PARENTHESIZED_NODE_TYPES.contains(&node.kind_name()) {
|
|
559
453
|
break;
|
|
560
454
|
}
|
|
561
|
-
ancestor = node.
|
|
455
|
+
ancestor = node.parent_node();
|
|
562
456
|
}
|
|
563
457
|
let Some(ancestor) = ancestor else {
|
|
564
458
|
return true;
|
|
565
459
|
};
|
|
566
|
-
if ancestor.
|
|
460
|
+
if ancestor.kind_name() != binary.kind_name() {
|
|
567
461
|
return true;
|
|
568
462
|
}
|
|
569
463
|
find_boolean_operator_text(ancestor, code).map(normalize_boolean_operator)
|
|
@@ -596,46 +490,44 @@ fn find_boolean_operator_text<'a>(binary_node: Node<'_>, code: &Source<'a>) -> O
|
|
|
596
490
|
/// ternaries and conditions, which are charged. A C# exception filter (`catch (E e) when (...)`, a `catch_filter_clause`) is
|
|
597
491
|
/// deliberately not charged: the catch itself already counts, and the filter is part of the same
|
|
598
492
|
/// handler condition rather than an extra path (SonarC# does not charge it either).
|
|
599
|
-
fn is_pattern_guard(node: Node<'_
|
|
493
|
+
fn is_pattern_guard(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
|
|
600
494
|
if !node.is_named() {
|
|
601
495
|
return false;
|
|
602
496
|
}
|
|
603
|
-
let kind = node.
|
|
497
|
+
let kind = node.kind_name();
|
|
604
498
|
if kind == "guard" || kind == "when_clause" || kind == "if_guard" || kind == "unless_guard" {
|
|
605
499
|
return true;
|
|
606
500
|
}
|
|
607
501
|
if kind == "if_clause" {
|
|
608
|
-
return
|
|
609
|
-
.parent()
|
|
610
|
-
.is_some_and(|parent| parent.kind() == "case_clause");
|
|
502
|
+
return parent.is_some_and(|parent| parent.kind_name() == "case_clause");
|
|
611
503
|
}
|
|
612
504
|
kind == "match_pattern"
|
|
613
505
|
&& all_children(node)
|
|
614
506
|
.iter()
|
|
615
|
-
.any(|child| !child.is_named() && child.
|
|
507
|
+
.any(|child| !child.is_named() && child.kind_name() == "if")
|
|
616
508
|
}
|
|
617
509
|
|
|
618
510
|
/// Ruby `elsif`, Python `elif`, and `else if` (an if node in an else/alternative position).
|
|
619
|
-
fn is_flat_chain_continuation(node: Node<'_
|
|
620
|
-
let kind = node.
|
|
511
|
+
fn is_flat_chain_continuation(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
|
|
512
|
+
let kind = node.kind_name();
|
|
621
513
|
if kind == "elsif" || kind == "elif_clause" {
|
|
622
514
|
return true;
|
|
623
515
|
}
|
|
624
516
|
if kind != "if_statement" && kind != "if_expression" && kind != "if" {
|
|
625
517
|
return false;
|
|
626
518
|
}
|
|
627
|
-
let Some(parent) =
|
|
519
|
+
let Some(parent) = parent else {
|
|
628
520
|
return false;
|
|
629
521
|
};
|
|
630
522
|
// Kotlin puts a braceless `else if` directly in the else branch's control_structure_body.
|
|
631
|
-
if parent.
|
|
523
|
+
if parent.kind_name() == "control_structure_body" {
|
|
632
524
|
return parent
|
|
633
|
-
.
|
|
525
|
+
.parent_node()
|
|
634
526
|
.and_then(crate::util::kotlin_else_body)
|
|
635
527
|
.is_some_and(|else_body| else_body.id() == parent.id());
|
|
636
528
|
}
|
|
637
529
|
// JS/C/C++/Rust/C# wrap `else if` in an else clause or put it directly in `alternative`.
|
|
638
|
-
parent.
|
|
530
|
+
parent.kind_name() == "else_clause"
|
|
639
531
|
|| parent
|
|
640
532
|
.child_by_field_name("alternative")
|
|
641
533
|
.is_some_and(|alternative| alternative.id() == node.id())
|
|
@@ -653,7 +545,7 @@ fn is_pathless_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
653
545
|
let mut stacked = vec![node];
|
|
654
546
|
let mut previous = node.prev_named_sibling();
|
|
655
547
|
while let Some(sibling) = previous {
|
|
656
|
-
if !crate::ncss::COMMENT_NODE_TYPES.contains(&sibling.
|
|
548
|
+
if !crate::ncss::COMMENT_NODE_TYPES.contains(&sibling.kind_name()) {
|
|
657
549
|
if !is_label_only_case(sibling) {
|
|
658
550
|
break;
|
|
659
551
|
}
|
|
@@ -673,10 +565,10 @@ fn is_pathless_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
673
565
|
/// pattern (Java `when`, Rust `if`).
|
|
674
566
|
fn has_pattern_guard(node: Node<'_>) -> bool {
|
|
675
567
|
crate::util::named_children(node).into_iter().any(|child| {
|
|
676
|
-
is_pattern_guard(child)
|
|
568
|
+
is_pattern_guard(child, Some(node))
|
|
677
569
|
|| crate::util::named_children(child)
|
|
678
570
|
.into_iter()
|
|
679
|
-
.any(is_pattern_guard)
|
|
571
|
+
.any(|grandchild| is_pattern_guard(grandchild, Some(child)))
|
|
680
572
|
})
|
|
681
573
|
}
|
|
682
574
|
|
|
@@ -684,28 +576,28 @@ fn has_pattern_guard(node: Node<'_>) -> bool {
|
|
|
684
576
|
/// stacked label as its own case node.
|
|
685
577
|
fn is_label_only_case(node: Node<'_>) -> bool {
|
|
686
578
|
let children = non_comment_children(node);
|
|
687
|
-
match node.
|
|
579
|
+
match node.kind_name() {
|
|
688
580
|
"case_statement" => {
|
|
689
581
|
let value = node.child_by_field_name("value").map(|value| value.id());
|
|
690
582
|
children.iter().all(|child| Some(child.id()) == value)
|
|
691
583
|
}
|
|
692
584
|
"switch_case" | "switch_default" => node.child_by_field_name("body").is_none(),
|
|
693
|
-
"switch_block_statement_group" =>
|
|
694
|
-
|
|
695
|
-
|
|
585
|
+
"switch_block_statement_group" => children
|
|
586
|
+
.iter()
|
|
587
|
+
.all(|child| child.kind_name() == "switch_label"),
|
|
696
588
|
// A C# label is a pattern (`case 1:` parses as a constant pattern) plus an optional `when`
|
|
697
589
|
// guard; anything else, a `#if` block included, is the section's body.
|
|
698
590
|
"switch_section" => children.iter().all(|child| {
|
|
699
|
-
child.
|
|
700
|
-
|| child.
|
|
701
|
-
|| child.
|
|
591
|
+
child.kind_name().ends_with("pattern")
|
|
592
|
+
|| child.kind_name() == "discard"
|
|
593
|
+
|| child.kind_name() == "when_clause"
|
|
702
594
|
}),
|
|
703
595
|
_ => false,
|
|
704
596
|
}
|
|
705
597
|
}
|
|
706
598
|
|
|
707
599
|
fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
708
|
-
let kind = node.
|
|
600
|
+
let kind = node.kind_name();
|
|
709
601
|
if kind == "switch_default" {
|
|
710
602
|
return true;
|
|
711
603
|
}
|
|
@@ -718,12 +610,12 @@ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
718
610
|
if kind == "switch_block_statement_group" || kind == "switch_rule" {
|
|
719
611
|
return non_comment_children(node)
|
|
720
612
|
.into_iter()
|
|
721
|
-
.filter(|child| child.
|
|
613
|
+
.filter(|child| child.kind_name() == "switch_label")
|
|
722
614
|
.any(|label| {
|
|
723
615
|
let parts = non_comment_children(label);
|
|
724
616
|
parts.is_empty()
|
|
725
617
|
|| parts.iter().any(|part| {
|
|
726
|
-
part.
|
|
618
|
+
part.kind_name() == "identifier" && node_text(*part, code) == "default"
|
|
727
619
|
})
|
|
728
620
|
});
|
|
729
621
|
}
|
|
@@ -733,7 +625,9 @@ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
733
625
|
// branches, which is_pattern_guard charges, like Python's `case _ if cond:` and Rust's
|
|
734
626
|
// `_ if cond =>`.
|
|
735
627
|
if kind == "switch_section" {
|
|
736
|
-
return node
|
|
628
|
+
return node
|
|
629
|
+
.child(0)
|
|
630
|
+
.is_some_and(|first| first.kind_name() == "default")
|
|
737
631
|
|| crate::util::named_children(node)
|
|
738
632
|
.into_iter()
|
|
739
633
|
.any(is_csharp_catch_all_pattern);
|
|
@@ -746,7 +640,7 @@ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
746
640
|
if kind == "when_entry" {
|
|
747
641
|
return !crate::util::named_children(node)
|
|
748
642
|
.iter()
|
|
749
|
-
.any(|child| child.
|
|
643
|
+
.any(|child| child.kind_name() == "when_condition");
|
|
750
644
|
}
|
|
751
645
|
|
|
752
646
|
// Python arms with an irrefutable pattern are unconditional like `default`.
|
|
@@ -755,24 +649,26 @@ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
755
649
|
if kind == "case_clause" {
|
|
756
650
|
let patterns: Vec<Node<'_>> = crate::util::named_children(node)
|
|
757
651
|
.into_iter()
|
|
758
|
-
.filter(|child| child.
|
|
652
|
+
.filter(|child| child.kind_name() == "case_pattern")
|
|
759
653
|
.collect();
|
|
760
|
-
return !all_children(node)
|
|
654
|
+
return !all_children(node)
|
|
655
|
+
.iter()
|
|
656
|
+
.any(|child| child.kind_name() == ",")
|
|
761
657
|
&& matches!(patterns[..], [pattern] if is_python_irrefutable_pattern(pattern));
|
|
762
658
|
}
|
|
763
659
|
// Rust `_ =>` (optionally guarded) fallback arms.
|
|
764
660
|
if kind == "match_arm" {
|
|
765
661
|
return crate::util::named_children(node)
|
|
766
662
|
.into_iter()
|
|
767
|
-
.find(|child| child.
|
|
663
|
+
.find(|child| child.kind_name() == "match_pattern")
|
|
768
664
|
.is_some_and(|pattern| {
|
|
769
665
|
// The guard keyword is anonymous, so filter all children, not just named ones.
|
|
770
666
|
let parts: Vec<Node<'_>> = all_children(pattern)
|
|
771
667
|
.into_iter()
|
|
772
|
-
.filter(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.
|
|
668
|
+
.filter(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind_name()))
|
|
773
669
|
.collect();
|
|
774
|
-
matches!(parts[..], [first, ..] if first.
|
|
775
|
-
&& parts.get(1).is_none_or(|second| second.
|
|
670
|
+
matches!(parts[..], [first, ..] if first.kind_name() == "_")
|
|
671
|
+
&& parts.get(1).is_none_or(|second| second.kind_name() == "if")
|
|
776
672
|
});
|
|
777
673
|
}
|
|
778
674
|
|
|
@@ -780,7 +676,7 @@ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
780
676
|
if kind == "in_clause" {
|
|
781
677
|
return node
|
|
782
678
|
.named_child(0)
|
|
783
|
-
.is_some_and(|first| first.
|
|
679
|
+
.is_some_and(|first| first.kind_name() == "identifier");
|
|
784
680
|
}
|
|
785
681
|
|
|
786
682
|
false
|
|
@@ -790,19 +686,23 @@ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
790
686
|
/// `p | q` when `p` (or, for `|`, any alternative) is irrefutable. A group parses as a one-element
|
|
791
687
|
/// tuple pattern that differs from the real tuple `(p,)` only by the comma token.
|
|
792
688
|
fn is_python_irrefutable_pattern(node: Node<'_>) -> bool {
|
|
793
|
-
match node.
|
|
689
|
+
match node.kind_name() {
|
|
794
690
|
"_" => true,
|
|
795
691
|
"case_pattern" => match non_comment_children(node)[..] {
|
|
796
|
-
[] => all_children(node)
|
|
692
|
+
[] => all_children(node)
|
|
693
|
+
.iter()
|
|
694
|
+
.any(|child| child.kind_name() == "_"),
|
|
797
695
|
[inner] => is_python_irrefutable_pattern(inner),
|
|
798
696
|
_ => false,
|
|
799
697
|
},
|
|
800
698
|
"dotted_name" => matches!(
|
|
801
699
|
non_comment_children(node)[..],
|
|
802
|
-
[name] if name.
|
|
700
|
+
[name] if name.kind_name() == "identifier"
|
|
803
701
|
),
|
|
804
702
|
"tuple_pattern" => {
|
|
805
|
-
!all_children(node)
|
|
703
|
+
!all_children(node)
|
|
704
|
+
.iter()
|
|
705
|
+
.any(|child| child.kind_name() == ",")
|
|
806
706
|
&& matches!(
|
|
807
707
|
non_comment_children(node)[..],
|
|
808
708
|
[inner] if is_python_irrefutable_pattern(inner)
|
|
@@ -821,36 +721,40 @@ fn is_python_irrefutable_pattern(node: Node<'_>) -> bool {
|
|
|
821
721
|
fn non_comment_children<'t>(node: Node<'t>) -> Vec<Node<'t>> {
|
|
822
722
|
crate::util::named_children(node)
|
|
823
723
|
.into_iter()
|
|
824
|
-
.filter(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.
|
|
724
|
+
.filter(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind_name()))
|
|
825
725
|
.collect()
|
|
826
726
|
}
|
|
827
727
|
|
|
828
728
|
/// C# patterns that match every value: the discard `_` and `var x`/`var _`, possibly parenthesized
|
|
829
729
|
/// (but not a `var (a, b)` deconstruction, which requires a deconstructible value).
|
|
830
730
|
fn is_csharp_catch_all_pattern(node: Node<'_>) -> bool {
|
|
831
|
-
if node.
|
|
731
|
+
if node.kind_name() == "parenthesized_pattern" {
|
|
832
732
|
return crate::util::named_children(node)
|
|
833
733
|
.into_iter()
|
|
834
|
-
.find(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.
|
|
734
|
+
.find(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind_name()))
|
|
835
735
|
.is_some_and(is_csharp_catch_all_pattern);
|
|
836
736
|
}
|
|
837
|
-
node.
|
|
838
|
-
|| (node.
|
|
737
|
+
node.kind_name() == "discard"
|
|
738
|
+
|| (node.kind_name() == "declaration_pattern"
|
|
839
739
|
&& node
|
|
840
740
|
.child_by_field_name("type")
|
|
841
|
-
.is_some_and(|ty| ty.
|
|
741
|
+
.is_some_and(|ty| ty.kind_name() == "implicit_type")
|
|
842
742
|
&& !crate::util::named_children(node)
|
|
843
743
|
.iter()
|
|
844
|
-
.any(|child| child.
|
|
744
|
+
.any(|child| child.kind_name() == "parenthesized_variable_designation"))
|
|
845
745
|
}
|
|
846
746
|
|
|
847
747
|
/// The parent guard is required because the same tokens appear in non-boolean syntax (C++ `int&&`,
|
|
848
748
|
/// `operator&&`, Rust's empty closure parameter list `|| 5`).
|
|
849
|
-
fn is_boolean_operator(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
749
|
+
fn is_boolean_operator(node: Node<'_>, parent: Option<Node<'_>>, code: &Source<'_>) -> bool {
|
|
850
750
|
if node.is_named() || !BOOLEAN_OPERATORS.contains(&node_text(node, code)) {
|
|
851
751
|
return false;
|
|
852
752
|
}
|
|
853
753
|
|
|
854
|
-
|
|
855
|
-
|
|
754
|
+
parent.is_some_and(|parent| BOOLEAN_OPERATOR_PARENT_TYPES.contains(&parent.kind_name()))
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/// A Ruby `case ... else` arm (see count_plain_else_branches).
|
|
758
|
+
fn is_case_else_parent(parent: Option<Node<'_>>) -> bool {
|
|
759
|
+
parent.is_some_and(|parent| parent.kind_name() == "case" || parent.kind_name() == "case_match")
|
|
856
760
|
}
|