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.
Files changed (57) hide show
  1. package/README.md +25 -0
  2. package/THIRD-PARTY-NOTICES.txt +8911 -0
  3. package/dist/cliConfig.cjs +1 -1
  4. package/dist/cliConfig.cjs.map +1 -1
  5. package/dist/cliConfig.js +1 -1
  6. package/dist/diffCommand.cjs +3 -3
  7. package/dist/diffCommand.cjs.map +1 -1
  8. package/dist/diffCommand.js +3 -3
  9. package/dist/diffCommand.js.map +1 -1
  10. package/dist/languages.cjs +1 -1
  11. package/dist/languages.cjs.map +1 -1
  12. package/dist/languages.js +1 -1
  13. package/dist/languages.js.map +1 -1
  14. package/dist/metrics.cjs +1 -1
  15. package/dist/metrics.cjs.map +1 -1
  16. package/dist/metrics.d.ts +8 -3
  17. package/dist/metrics.js +1 -1
  18. package/dist/metrics.js.map +1 -1
  19. package/dist/nativeMetrics.cjs +3 -3
  20. package/dist/nativeMetrics.cjs.map +1 -1
  21. package/dist/nativeMetrics.d.ts +30 -0
  22. package/dist/nativeMetrics.js +3 -3
  23. package/dist/nativeMetrics.js.map +1 -1
  24. package/dist/scan.cjs +1 -1
  25. package/dist/scan.cjs.map +1 -1
  26. package/dist/scan.d.ts +2 -2
  27. package/dist/scan.js +1 -1
  28. package/dist/scan.js.map +1 -1
  29. package/dist/wasmBinding.cjs +2 -0
  30. package/dist/wasmBinding.cjs.map +1 -0
  31. package/dist/wasmBinding.d.ts +9 -0
  32. package/dist/wasmBinding.js +2 -0
  33. package/dist/wasmBinding.js.map +1 -0
  34. package/dist/worker.cjs +2 -0
  35. package/dist/worker.cjs.map +1 -0
  36. package/dist/worker.d.ts +1 -0
  37. package/dist/worker.js +2 -0
  38. package/dist/worker.js.map +1 -0
  39. package/native/Cargo.lock +1 -0
  40. package/native/Cargo.toml +6 -2
  41. package/native/build.rs +4 -1
  42. package/native/code-gauge.wasm +0 -0
  43. package/native/src/complexity.rs +134 -230
  44. package/native/src/dep_degree.rs +42 -39
  45. package/native/src/duplication.rs +65 -63
  46. package/native/src/functions.rs +169 -152
  47. package/native/src/languages.rs +20 -0
  48. package/native/src/lib.rs +41 -42
  49. package/native/src/measure.rs +109 -123
  50. package/native/src/napi.rs +104 -0
  51. package/native/src/ncss.rs +79 -84
  52. package/native/src/near_miss.rs +7 -7
  53. package/native/src/tree_index.rs +110 -0
  54. package/native/src/util.rs +17 -12
  55. package/native/src/wasm.rs +128 -0
  56. package/native/src/worker_pool.rs +53 -0
  57. package/package.json +16 -11
@@ -1,20 +1,16 @@
1
- use std::collections::{HashMap, HashSet};
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: 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>,
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.kind() == "block" || node.kind() == "do_block")
43
+ (node.kind_name() == "block" || node.kind_name() == "do_block")
48
44
  && node
49
- .parent()
50
- .is_some_and(|parent| parent.kind() == "lambda")
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: &HashSet<&'static str>) -> bool {
57
- function_nodes.contains(node.kind())
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: HashMap<usize, FunctionBodyMetrics>,
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 body owns.
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: HashMap<usize, FunctionBodyMetrics>,
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; only its cyclomatic decisions are kept.
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: HashMap::new(),
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 is_charged_class_body = current.kind() == "class_body" && inside_function;
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.kind())
227
+ && self.sets.decision_nodes.contains(current.kind_name())
216
228
  && !is_pathless_switch_branch(current, self.code);
217
- let is_case_clause = current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(&current.kind());
229
+ let is_case_clause =
230
+ current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(&current.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.kind())
223
- || (current.kind() == "else"
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(current, self.code) || is_pattern_guard(current) {
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(&current.kind()) {
260
+ if current.is_named() && SWITCH_LIKE_NODE_TYPES.contains(&current.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(current, self.code)
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(current) {
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(&current.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(&current.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<'_>) -> u64 {
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.kind();
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 if current
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.kind()));
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.kind() != "else_clause"
504
- && child.kind() != "elif_clause"
505
- && !IF_LIKE_NODE_TYPES.contains(&child.kind())
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.kind() == "goto_statement" {
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.kind() == "break_expression" || node.kind() == "continue_expression" {
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.kind() == "label" || child.kind() == "loop_label");
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.kind() == "jump_expression" {
528
- return node
529
- .child(0)
530
- .is_some_and(|keyword| keyword.kind() == "break@" || keyword.kind() == "continue@");
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.kind() == "break_statement" || node.kind() == "continue_statement")
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.kind()))
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(token: Node<'_>, code: &Source<'_>) -> bool {
553
- let Some(binary) = token.parent() else {
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.parent();
450
+ let mut ancestor = binary.parent_node();
557
451
  while let Some(node) = ancestor {
558
- if !PARENTHESIZED_NODE_TYPES.contains(&node.kind()) {
452
+ if !PARENTHESIZED_NODE_TYPES.contains(&node.kind_name()) {
559
453
  break;
560
454
  }
561
- ancestor = node.parent();
455
+ ancestor = node.parent_node();
562
456
  }
563
457
  let Some(ancestor) = ancestor else {
564
458
  return true;
565
459
  };
566
- if ancestor.kind() != binary.kind() {
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<'_>) -> bool {
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.kind();
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 node
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.kind() == "if")
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<'_>) -> bool {
620
- let kind = node.kind();
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) = node.parent() else {
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.kind() == "control_structure_body" {
523
+ if parent.kind_name() == "control_structure_body" {
632
524
  return parent
633
- .parent()
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.kind() == "else_clause"
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.kind()) {
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.kind() {
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
- children.iter().all(|child| child.kind() == "switch_label")
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.kind().ends_with("pattern")
700
- || child.kind() == "discard"
701
- || child.kind() == "when_clause"
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.kind();
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.kind() == "switch_label")
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.kind() == "identifier" && node_text(*part, code) == "default"
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.child(0).is_some_and(|first| first.kind() == "default")
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.kind() == "when_condition");
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.kind() == "case_pattern")
652
+ .filter(|child| child.kind_name() == "case_pattern")
759
653
  .collect();
760
- return !all_children(node).iter().any(|child| child.kind() == ",")
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.kind() == "match_pattern")
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.kind()))
668
+ .filter(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind_name()))
773
669
  .collect();
774
- matches!(parts[..], [first, ..] if first.kind() == "_")
775
- && parts.get(1).is_none_or(|second| second.kind() == "if")
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.kind() == "identifier");
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.kind() {
689
+ match node.kind_name() {
794
690
  "_" => true,
795
691
  "case_pattern" => match non_comment_children(node)[..] {
796
- [] => all_children(node).iter().any(|child| child.kind() == "_"),
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.kind() == "identifier"
700
+ [name] if name.kind_name() == "identifier"
803
701
  ),
804
702
  "tuple_pattern" => {
805
- !all_children(node).iter().any(|child| child.kind() == ",")
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.kind()))
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.kind() == "parenthesized_pattern" {
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.kind()))
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.kind() == "discard"
838
- || (node.kind() == "declaration_pattern"
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.kind() == "implicit_type")
741
+ .is_some_and(|ty| ty.kind_name() == "implicit_type")
842
742
  && !crate::util::named_children(node)
843
743
  .iter()
844
- .any(|child| child.kind() == "parenthesized_variable_designation"))
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
- node.parent()
855
- .is_some_and(|parent| BOOLEAN_OPERATOR_PARENT_TYPES.contains(&parent.kind()))
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
  }