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