code-gauge 4.2.1 → 4.3.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.
Files changed (53) hide show
  1. package/README.md +12 -3
  2. package/dist/cliConfig.cjs +1 -1
  3. package/dist/cliConfig.cjs.map +1 -1
  4. package/dist/cliConfig.js +1 -1
  5. package/dist/crossFileDuplication.cjs +1 -1
  6. package/dist/crossFileDuplication.cjs.map +1 -1
  7. package/dist/crossFileDuplication.d.ts +3 -1
  8. package/dist/crossFileDuplication.js +1 -1
  9. package/dist/crossFileDuplication.js.map +1 -1
  10. package/dist/diffCommand.cjs +1 -1
  11. package/dist/diffCommand.cjs.map +1 -1
  12. package/dist/diffCommand.js +1 -1
  13. package/dist/diffCommand.js.map +1 -1
  14. package/dist/duplicateSelection.cjs +1 -1
  15. package/dist/duplicateSelection.cjs.map +1 -1
  16. package/dist/duplicateSelection.d.ts +13 -4
  17. package/dist/duplicateSelection.js +1 -1
  18. package/dist/duplicateSelection.js.map +1 -1
  19. package/dist/duplication.cjs +1 -1
  20. package/dist/duplication.cjs.map +1 -1
  21. package/dist/duplication.d.ts +16 -7
  22. package/dist/duplication.js +1 -1
  23. package/dist/duplication.js.map +1 -1
  24. package/dist/index.cjs +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/languages.cjs +1 -1
  28. package/dist/languages.cjs.map +1 -1
  29. package/dist/languages.d.ts +3 -1
  30. package/dist/languages.js +1 -1
  31. package/dist/languages.js.map +1 -1
  32. package/dist/metrics.cjs +1 -1
  33. package/dist/metrics.cjs.map +1 -1
  34. package/dist/metrics.js +1 -1
  35. package/dist/metrics.js.map +1 -1
  36. package/dist/nativeMetrics.cjs +1 -1
  37. package/dist/nativeMetrics.cjs.map +1 -1
  38. package/dist/nativeMetrics.js +1 -1
  39. package/dist/nativeMetrics.js.map +1 -1
  40. package/dist/scan.cjs +1 -1
  41. package/dist/scan.cjs.map +1 -1
  42. package/dist/scan.js +1 -1
  43. package/dist/scan.js.map +1 -1
  44. package/dist/types.d.ts +15 -2
  45. package/native/src/complexity.rs +134 -42
  46. package/native/src/cyclomatic.rs +79 -0
  47. package/native/src/dep_degree.rs +102 -11
  48. package/native/src/functions.rs +828 -22
  49. package/native/src/languages.rs +16 -0
  50. package/native/src/lib.rs +2 -1
  51. package/native/src/measure.rs +2 -0
  52. package/native/src/types.rs +2 -0
  53. package/package.json +14 -14
@@ -12,6 +12,7 @@ pub struct ComplexityResult {
12
12
  pub struct LanguageSets {
13
13
  pub function_nodes: HashSet<&'static str>,
14
14
  pub decision_nodes: HashSet<&'static str>,
15
+ pub pmd_cyclomatic: bool,
15
16
  pub nesting_nodes: HashSet<&'static str>,
16
17
  pub ncss_nodes: HashSet<&'static str>,
17
18
  pub ncss_containers: HashSet<&'static str>,
@@ -22,6 +23,7 @@ impl LanguageSets {
22
23
  LanguageSets {
23
24
  function_nodes: language.function_node_types.iter().copied().collect(),
24
25
  decision_nodes: language.decision_node_types.iter().copied().collect(),
26
+ pmd_cyclomatic: language.pmd_cyclomatic,
25
27
  nesting_nodes: language.nesting_node_types.iter().copied().collect(),
26
28
  ncss_nodes: language.ncss_node_types.iter().copied().collect(),
27
29
  ncss_containers: language.ncss_container_node_types.iter().copied().collect(),
@@ -74,7 +76,7 @@ const SWITCH_LIKE_NODE_TYPES: &[&str] = &[
74
76
  "case_match",
75
77
  ];
76
78
 
77
- // Per-case decision nodes add no cognitive point because the switch itself carries the cost.
79
+ // Per-case decision nodes: cyclomatic-only, because the switch itself carries the cognitive cost.
78
80
  const CASE_CLAUSE_NODE_TYPES: &[&str] = &[
79
81
  "case_clause",
80
82
  "switch_case",
@@ -95,6 +97,7 @@ const CASE_CLAUSE_NODE_TYPES: &[&str] = &[
95
97
  const IF_LIKE_NODE_TYPES: &[&str] = &["if_statement", "if_expression", "if", "unless"];
96
98
 
97
99
  pub struct FunctionBodyMetrics {
100
+ pub cyclomatic_complexity: u64,
98
101
  pub cognitive_complexity: u64,
99
102
  pub nesting_depth: u64,
100
103
  pub ncss: u64,
@@ -102,6 +105,7 @@ pub struct FunctionBodyMetrics {
102
105
 
103
106
  /// Accumulator for one function body during measure_function_body_metrics' post-order pass.
104
107
  struct FunctionBodyFrame {
108
+ cyclomatic_complexity: u64,
105
109
  cognitive_complexity: u64,
106
110
  /// Count of `1 + nesting` cognitive increments, for re-basing on hoist into the parent frame.
107
111
  nesting_sensitive_count: u64,
@@ -117,6 +121,7 @@ struct FunctionBodyFrame {
117
121
  impl FunctionBodyFrame {
118
122
  fn new(entry_cognitive_nesting: u64, entry_structural_nesting: u64) -> Self {
119
123
  FunctionBodyFrame {
124
+ cyclomatic_complexity: 1,
120
125
  cognitive_complexity: 0,
121
126
  nesting_sensitive_count: 0,
122
127
  nesting_depth: 0,
@@ -141,8 +146,8 @@ struct FunctionBodyPass<'sets, 'code, 'source> {
141
146
  /// already-computed totals: NCSS hoists as-is; cognitive complexity re-bases the nested function's
142
147
  /// nesting-sensitive increments (each worth `1 + nesting`) by the nesting offset at the embedding
143
148
  /// site, while flat increments (else branches, boolean-operator sequences, chain continuations,
144
- /// jumps, guards) hoist unchanged; nesting depth describes the own body only, so it does not
145
- /// hoist.
149
+ /// jumps, guards) hoist unchanged; cyclomatic complexity and nesting depth describe the own body
150
+ /// only, so nothing hoists.
146
151
  pub fn measure_function_body_metrics(
147
152
  root: Node<'_>,
148
153
  sets: &LanguageSets,
@@ -186,7 +191,7 @@ impl FunctionBodyPass<'_, '_, '_> {
186
191
  }
187
192
  // The node's own increments target the frame it is embedded in, not the one it opens; a
188
193
  // frame-opening or charged-class-body node contributes nothing to that frame's own body
189
- // (nesting), matching the per-function traversal this pass replaces.
194
+ // (cyclomatic/nesting), matching the per-function traversal this pass replaces.
190
195
  let entry_cognitive_nesting = self.top_frame().entry_cognitive_nesting;
191
196
  let entry_structural_nesting = self.top_frame().entry_structural_nesting;
192
197
  let relative_nesting = current_nesting + function_nesting_bonus - entry_cognitive_nesting;
@@ -211,6 +216,19 @@ impl FunctionBodyPass<'_, '_, '_> {
211
216
  // surcharge (Sonar cognitive-complexity semantics).
212
217
  let is_continuation = is_decision && is_flat_chain_continuation(current);
213
218
 
219
+ if counts_for_own_body {
220
+ self.top_frame().cyclomatic_complexity += if self.sets.pmd_cyclomatic {
221
+ crate::cyclomatic::pmd_cyclomatic_increment(current, self.code)
222
+ } else {
223
+ // Each boolean operator and pattern guard is one more execution path; `else` adds
224
+ // none.
225
+ u64::from(
226
+ is_decision
227
+ || is_boolean_operator(current, self.code)
228
+ || is_pattern_guard(current),
229
+ )
230
+ };
231
+ }
214
232
  if is_decision && !is_case_clause {
215
233
  if is_continuation {
216
234
  self.top_frame().cognitive_complexity += 1;
@@ -295,6 +313,7 @@ impl FunctionBodyPass<'_, '_, '_> {
295
313
  self.results.insert(
296
314
  current.id(),
297
315
  FunctionBodyMetrics {
316
+ cyclomatic_complexity: closed.cyclomatic_complexity,
298
317
  cognitive_complexity: closed.cognitive_complexity,
299
318
  nesting_depth: closed.nesting_depth,
300
319
  // A function node without a countable declaration of its own (arrow functions,
@@ -562,8 +581,11 @@ fn find_boolean_operator_text<'a>(binary_node: Node<'_>, code: &Source<'a>) -> O
562
581
  .map(|child| node_text(child, code))
563
582
  }
564
583
 
565
- /// Java `guard`, C# `when_clause`, Ruby `if_guard`, Python `if_clause`, and Rust guards inside
566
- /// `match_pattern`. A C# exception filter (`catch (E e) when (...)`, a `catch_filter_clause`) is
584
+ /// Java `guard`, C# `when_clause`, Ruby `if_guard`, a Python `case` guard (an `if_clause` under a
585
+ /// `case_clause`), and Rust guards inside `match_pattern`. A Python comprehension filter shares
586
+ /// the `if_clause` type but is a per-element predicate of one expression, not an extra execution
587
+ /// path: Sonar ports differ on it (complexipy charges it), and code-gauge does not, unlike
588
+ /// ternaries and conditions, which are charged. A C# exception filter (`catch (E e) when (...)`, a `catch_filter_clause`) is
567
589
  /// deliberately not charged: the catch itself already counts, and the filter is part of the same
568
590
  /// handler condition rather than an extra path (SonarC# does not charge it either).
569
591
  fn is_pattern_guard(node: Node<'_>) -> bool {
@@ -571,14 +593,14 @@ fn is_pattern_guard(node: Node<'_>) -> bool {
571
593
  return false;
572
594
  }
573
595
  let kind = node.kind();
574
- if kind == "guard"
575
- || kind == "when_clause"
576
- || kind == "if_guard"
577
- || kind == "unless_guard"
578
- || kind == "if_clause"
579
- {
596
+ if kind == "guard" || kind == "when_clause" || kind == "if_guard" || kind == "unless_guard" {
580
597
  return true;
581
598
  }
599
+ if kind == "if_clause" {
600
+ return node
601
+ .parent()
602
+ .is_some_and(|parent| parent.kind() == "case_clause");
603
+ }
582
604
  kind == "match_pattern"
583
605
  && all_children(node)
584
606
  .iter()
@@ -618,39 +640,51 @@ fn is_default_switch_branch(node: Node<'_>) -> bool {
618
640
  return node.child_by_field_name("value").is_none();
619
641
  }
620
642
 
621
- if kind == "switch_block_statement_group" || kind == "switch_rule" {
622
- let label = crate::util::named_children(node)
623
- .into_iter()
624
- .find(|child| child.kind() == "switch_label");
625
- return label.is_some_and(|label| label.named_child_count() == 0);
643
+ // C# `default:` sections and catch-all (`_`, `var x`) labels and arms, and Kotlin `else ->`
644
+ // entries. A guarded catch-all (`_ when cond =>`) is still a default arm: only its guard
645
+ // branches, which is_pattern_guard charges, like Python's `case _ if cond:` and Rust's
646
+ // `_ if cond =>`.
647
+ if kind == "switch_section" {
648
+ return node.child(0).is_some_and(|first| first.kind() == "default")
649
+ || crate::util::named_children(node)
650
+ .into_iter()
651
+ .any(is_csharp_catch_all_pattern);
652
+ }
653
+ if kind == "switch_expression_arm" {
654
+ return crate::util::named_children(node)
655
+ .first()
656
+ .is_some_and(|first| is_csharp_catch_all_pattern(*first));
657
+ }
658
+ if kind == "when_entry" {
659
+ return !crate::util::named_children(node)
660
+ .iter()
661
+ .any(|child| child.kind() == "when_condition");
626
662
  }
627
663
 
628
- // Python `case _:` / `case y:` and Rust `_ =>` fallback arms are unconditional like `default`.
629
- if kind == "case_clause" || kind == "match_arm" {
630
- let pattern = crate::util::named_children(node)
664
+ // Python arms with an irrefutable pattern are unconditional like `default`.
665
+ // A bare `case y, z:` or `case y,:` is a sequence pattern: its elements are direct
666
+ // case_pattern children separated by comma tokens of the clause itself.
667
+ if kind == "case_clause" {
668
+ let patterns: Vec<Node<'_>> = crate::util::named_children(node)
631
669
  .into_iter()
632
- .find(|child| child.kind() == "case_pattern" || child.kind() == "match_pattern");
633
- let Some(pattern) = pattern else {
634
- return false;
635
- };
636
- if pattern.child(0).is_some_and(|first| first.kind() == "_")
637
- && (pattern.child_count() == 1
638
- || pattern.child(1).is_some_and(|second| second.kind() == "if"))
639
- {
640
- return true;
641
- }
642
- let sole_child = if pattern.named_child_count() == 1 {
643
- pattern.named_child(0)
644
- } else {
645
- None
646
- };
647
- return kind == "case_clause"
648
- && sole_child.is_some_and(|child| {
649
- child.kind() == "dotted_name"
650
- && child.named_child_count() == 1
651
- && child
652
- .named_child(0)
653
- .is_some_and(|inner| inner.kind() == "identifier")
670
+ .filter(|child| child.kind() == "case_pattern")
671
+ .collect();
672
+ return !all_children(node).iter().any(|child| child.kind() == ",")
673
+ && matches!(patterns[..], [pattern] if is_python_irrefutable_pattern(pattern));
674
+ }
675
+ // Rust `_ =>` (optionally guarded) fallback arms.
676
+ if kind == "match_arm" {
677
+ return crate::util::named_children(node)
678
+ .into_iter()
679
+ .find(|child| child.kind() == "match_pattern")
680
+ .is_some_and(|pattern| {
681
+ // The guard keyword is anonymous, so filter all children, not just named ones.
682
+ let parts: Vec<Node<'_>> = all_children(pattern)
683
+ .into_iter()
684
+ .filter(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind()))
685
+ .collect();
686
+ matches!(parts[..], [first, ..] if first.kind() == "_")
687
+ && parts.get(1).is_none_or(|second| second.kind() == "if")
654
688
  });
655
689
  }
656
690
 
@@ -664,6 +698,64 @@ fn is_default_switch_branch(node: Node<'_>) -> bool {
664
698
  false
665
699
  }
666
700
 
701
+ /// PEP 634's irrefutable patterns: the wildcard `_`, a capture `y`, a group `(p)`, `p as y`, and
702
+ /// `p | q` when `p` (or, for `|`, any alternative) is irrefutable. A group parses as a one-element
703
+ /// tuple pattern that differs from the real tuple `(p,)` only by the comma token.
704
+ fn is_python_irrefutable_pattern(node: Node<'_>) -> bool {
705
+ match node.kind() {
706
+ "_" => true,
707
+ "case_pattern" => match non_comment_children(node)[..] {
708
+ [] => all_children(node).iter().any(|child| child.kind() == "_"),
709
+ [inner] => is_python_irrefutable_pattern(inner),
710
+ _ => false,
711
+ },
712
+ "dotted_name" => matches!(
713
+ non_comment_children(node)[..],
714
+ [name] if name.kind() == "identifier"
715
+ ),
716
+ "tuple_pattern" => {
717
+ !all_children(node).iter().any(|child| child.kind() == ",")
718
+ && matches!(
719
+ non_comment_children(node)[..],
720
+ [inner] if is_python_irrefutable_pattern(inner)
721
+ )
722
+ }
723
+ "as_pattern" => non_comment_children(node)
724
+ .first()
725
+ .is_some_and(|pattern| is_python_irrefutable_pattern(*pattern)),
726
+ "union_pattern" => all_children(node)
727
+ .into_iter()
728
+ .any(is_python_irrefutable_pattern),
729
+ _ => false,
730
+ }
731
+ }
732
+
733
+ fn non_comment_children<'t>(node: Node<'t>) -> Vec<Node<'t>> {
734
+ crate::util::named_children(node)
735
+ .into_iter()
736
+ .filter(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind()))
737
+ .collect()
738
+ }
739
+
740
+ /// C# patterns that match every value: the discard `_` and `var x`/`var _`, possibly parenthesized
741
+ /// (but not a `var (a, b)` deconstruction, which requires a deconstructible value).
742
+ fn is_csharp_catch_all_pattern(node: Node<'_>) -> bool {
743
+ if node.kind() == "parenthesized_pattern" {
744
+ return crate::util::named_children(node)
745
+ .into_iter()
746
+ .find(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind()))
747
+ .is_some_and(is_csharp_catch_all_pattern);
748
+ }
749
+ node.kind() == "discard"
750
+ || (node.kind() == "declaration_pattern"
751
+ && node
752
+ .child_by_field_name("type")
753
+ .is_some_and(|ty| ty.kind() == "implicit_type")
754
+ && !crate::util::named_children(node)
755
+ .iter()
756
+ .any(|child| child.kind() == "parenthesized_variable_designation"))
757
+ }
758
+
667
759
  /// The parent guard is required because the same tokens appear in non-boolean syntax (C++ `int&&`,
668
760
  /// `operator&&`, Rust's empty closure parameter list `|| 5`).
669
761
  fn is_boolean_operator(node: Node<'_>, code: &Source<'_>) -> bool {
@@ -0,0 +1,79 @@
1
+ use tree_sitter::Node;
2
+
3
+ use crate::util::{named_children, node_text, Source};
4
+
5
+ /// Cyclomatic paths a Java node adds to its function, following PMD 7.26.0's CycloVisitor with
6
+ /// its default options: branches and loops add 1 plus the boolean paths of their condition,
7
+ /// `throw`/`catch`/enhanced `for` add 1, and a switch adds the boolean paths of its tested
8
+ /// expression plus the expression alternatives of each non-default label. `&&`/`||` outside these
9
+ /// conditions (initializers, returns, arguments) and pattern labels and guards add nothing.
10
+ pub fn pmd_cyclomatic_increment(node: Node<'_>, code: &Source<'_>) -> u64 {
11
+ match node.kind() {
12
+ "if_statement" | "while_statement" | "do_statement" | "for_statement"
13
+ | "ternary_expression" => {
14
+ 1 + node
15
+ .child_by_field_name("condition")
16
+ .map_or(0, |condition| {
17
+ boolean_expression_complexity(condition, code)
18
+ })
19
+ }
20
+ "enhanced_for_statement" | "catch_clause" | "throw_statement" => 1,
21
+ "switch_expression" => node
22
+ .child_by_field_name("condition")
23
+ .map_or(0, |condition| {
24
+ boolean_expression_complexity(condition, code)
25
+ }),
26
+ "switch_block_statement_group" | "switch_rule" => named_children(node)
27
+ .into_iter()
28
+ .filter(|child| child.kind() == "switch_label")
29
+ .flat_map(named_children)
30
+ .filter(|child| {
31
+ child.kind() != "guard"
32
+ && child.kind() != "pattern"
33
+ && !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind())
34
+ })
35
+ .count() as u64,
36
+ _ => 0,
37
+ }
38
+ }
39
+
40
+ /// PMD's booleanExpressionComplexity: a conditional expression costs 2 plus its parts; any other
41
+ /// expression costs its `&&`/`||` operators. PMD has no parenthesis nodes, so they are unwrapped.
42
+ fn boolean_expression_complexity(expression: Node<'_>, code: &Source<'_>) -> u64 {
43
+ let mut expression = expression;
44
+ while expression.kind() == "parenthesized_expression" {
45
+ let Some(inner) = named_children(expression)
46
+ .into_iter()
47
+ .find(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind()))
48
+ else {
49
+ return 0;
50
+ };
51
+ expression = inner;
52
+ }
53
+ if expression.kind() == "ternary_expression" {
54
+ return 2 + ["condition", "consequence", "alternative"]
55
+ .iter()
56
+ .filter_map(|field| expression.child_by_field_name(field))
57
+ .map(|part| boolean_expression_complexity(part, code))
58
+ .sum::<u64>();
59
+ }
60
+ count_conditional_operators(expression, code)
61
+ }
62
+
63
+ /// `&&`/`||` binaries in the subtree, not descending into lambdas or class bodies, which PMD treats
64
+ /// as find boundaries.
65
+ fn count_conditional_operators(node: Node<'_>, code: &Source<'_>) -> u64 {
66
+ if node.kind() == "lambda_expression" || node.kind() == "class_body" {
67
+ return 0;
68
+ }
69
+ let own = u64::from(
70
+ node.kind() == "binary_expression"
71
+ && node
72
+ .child_by_field_name("operator")
73
+ .is_some_and(|operator| matches!(node_text(operator, code), "&&" | "||")),
74
+ );
75
+ own + named_children(node)
76
+ .into_iter()
77
+ .map(|child| count_conditional_operators(child, code))
78
+ .sum::<u64>()
79
+ }
@@ -72,6 +72,20 @@ const CSHARP_QUERY_BINDING_PARENT_TYPES: &[&str] = &[
72
72
  "query_expression",
73
73
  ];
74
74
 
75
+ /// C/C++ declarators wrapping the declared name (`int* p = q;`, `int& r = *q;`, `int a[n] = {};`,
76
+ /// `int (*fp)(int) = g;`, member pointer `int C::* p = q;`): the definition field is checked on the
77
+ /// outermost wrapper.
78
+ const DECLARATOR_WRAPPER_TYPES: &[&str] = &[
79
+ "pointer_declarator",
80
+ "reference_declarator",
81
+ "array_declarator",
82
+ "parenthesized_declarator",
83
+ "function_declarator",
84
+ "attributed_declarator",
85
+ "pointer_type_declarator",
86
+ "qualified_identifier",
87
+ ];
88
+
75
89
  /// Multi-target lists (`a, b = ...`, `a, b := ...`) whose holder's `left` field marks definitions.
76
90
  const DEFINITION_LIST_NODE_TYPES: &[&str] = &["expression_list", "pattern_list", "tuple_pattern"];
77
91
  const DEFINITION_LIST_HOLDER_TYPES: &[&str] = &[
@@ -119,9 +133,7 @@ pub fn measure_dep_degree(
119
133
  let mut pairs = 0u64;
120
134
  for index in 0..leaves.len() {
121
135
  let leaf = &leaves[index];
122
- if !VARIABLE_NODE_TYPES.contains(&leaf.node.kind())
123
- && !crate::util::is_kotlin_callable_receiver(leaf.node)
124
- {
136
+ if !is_variable_leaf(leaf.node) {
125
137
  continue;
126
138
  }
127
139
  let name = node_text(leaf.node, code);
@@ -147,6 +159,33 @@ pub fn measure_dep_degree(
147
159
  pairs
148
160
  }
149
161
 
162
+ /// A C++ member-pointer variable (`int C::* p`, also `int C::* arr[1]`) is declared as a
163
+ /// `type_identifier` under the `pointer_type_declarator` spelling `C::*`, possibly through further
164
+ /// declarator wrappers; every other `type_identifier` names a type, not a variable.
165
+ fn is_variable_leaf(node: Node<'_>) -> bool {
166
+ VARIABLE_NODE_TYPES.contains(&node.kind())
167
+ || crate::util::is_kotlin_callable_receiver(node)
168
+ || (node.kind() == "type_identifier" && is_member_pointer_name(node))
169
+ }
170
+
171
+ fn is_member_pointer_name(node: Node<'_>) -> bool {
172
+ let mut current = node;
173
+ while let Some(parent) = current.parent() {
174
+ if parent.kind() == "pointer_type_declarator" {
175
+ return true;
176
+ }
177
+ // The climb follows the declared-name position only, exactly like unwrap_declarator_wrappers.
178
+ if !DECLARATOR_WRAPPER_TYPES.contains(&parent.kind())
179
+ || parent.kind() == "qualified_identifier"
180
+ || field_name_in_parent(current, parent).is_some_and(|field| field != "declarator")
181
+ {
182
+ return false;
183
+ }
184
+ current = parent;
185
+ }
186
+ false
187
+ }
188
+
150
189
  /// Names a C# accessor body can read without declaring them in its own subtree: the owning
151
190
  /// indexer's parameters (including a `params` array, which the grammar names directly on the
152
191
  /// parameter list) and, in a setter, initializer, or event accessor, the implicit `value`.
@@ -270,11 +309,10 @@ fn is_structural_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
270
309
  {
271
310
  return true;
272
311
  }
273
- if DEFINITION_FIELD_BY_PARENT_TYPE
274
- .iter()
275
- .any(|(parent_type, definition_field)| {
276
- *parent_type == parent.kind() && leaf.field_name == Some(definition_field)
277
- })
312
+ let (declared, declared_field) = unwrap_declarator_wrappers(leaf);
313
+ if declared
314
+ .parent()
315
+ .is_some_and(|holder| is_definition_field(holder, declared_field))
278
316
  {
279
317
  return true;
280
318
  }
@@ -288,6 +326,42 @@ fn is_structural_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
288
326
  && field_name_in_parent(parent, holder) == Some("left")
289
327
  }
290
328
 
329
+ fn is_definition_field(holder: Node<'_>, field_name: Option<&str>) -> bool {
330
+ DEFINITION_FIELD_BY_PARENT_TYPE
331
+ .iter()
332
+ .any(|(parent_type, definition_field)| {
333
+ *parent_type == holder.kind() && field_name == Some(definition_field)
334
+ })
335
+ }
336
+
337
+ /// Climbs from the identifier through the C/C++ declarator wrappers it is the declared name of
338
+ /// (`reference_declarator` and `parenthesized_declarator` expose no field, so their name is the
339
+ /// fieldless child; a member pointer's `pointer_type_declarator` is the `name` of a
340
+ /// `qualified_identifier`; an `array_declarator` size or a nested parameter has another field and
341
+ /// stops the climb) to the outermost wrapper and its field in the declaration.
342
+ fn unwrap_declarator_wrappers<'t>(leaf: &DepDegreeLeaf<'t>) -> (Node<'t>, Option<&'static str>) {
343
+ let mut current = leaf.node;
344
+ let mut field_name = leaf.field_name;
345
+ while let Some(parent) = current
346
+ .parent()
347
+ .filter(|parent| DECLARATOR_WRAPPER_TYPES.contains(&parent.kind()))
348
+ {
349
+ let declared_field = if parent.kind() == "qualified_identifier" {
350
+ "name"
351
+ } else {
352
+ "declarator"
353
+ };
354
+ if field_name.is_some_and(|name| name != declared_field) {
355
+ break;
356
+ }
357
+ field_name = parent
358
+ .parent()
359
+ .and_then(|grandparent| field_name_in_parent(parent, grandparent));
360
+ current = parent;
361
+ }
362
+ (current, field_name)
363
+ }
364
+
291
365
  /// Mirrors isParameterDefinition in metrics.ts: an ancestor reached through declarator wrappers
292
366
  /// (C/C++ function-pointer or array parameters) is a parameter-ish node, or the identifier
293
367
  /// directly occupies a parameter field; type annotations and default values bind nothing.
@@ -306,14 +380,31 @@ fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
306
380
  }
307
381
  let mut current = leaf.node;
308
382
  let mut depth = 0usize;
383
+ // Only the member pointer's own declared name may climb past its qualified name; a size or
384
+ // other expression inside the same declarator (`int C::* a[n]`) is a read.
385
+ let declares_member_pointer = is_member_pointer_name(leaf.node);
386
+ let mut in_member_pointer = false;
309
387
  loop {
310
388
  let Some(parent) = current.parent() else {
311
389
  return false;
312
390
  };
313
391
  let parent_is_parameterish = parent.kind().contains("parameter");
314
- // Beyond the grandparent, only declarator wrappers keep climbing; checking this before
315
- // the field lookup also keeps reads inside high-arity nodes O(1).
316
- if depth >= 1 && !parent_is_parameterish && !parent.kind().contains("declarator") {
392
+ // Beyond the grandparent, only declarator wrappers keep climbing, plus the
393
+ // `qualified_identifier` nodes that spell a member-pointer parameter's class (`int C::* q`,
394
+ // `int N::C::* q`) — entered from the pointer declarator and continued through the `name`
395
+ // side, so a qualified constant in a default value or array size (`int x = N::M::C`) stays
396
+ // a read. Checking this before the field lookup also keeps reads inside high-arity nodes
397
+ // O(1).
398
+ let continues_member_pointer = declares_member_pointer
399
+ && parent.kind() == "qualified_identifier"
400
+ && (current.kind() == "pointer_type_declarator" || in_member_pointer)
401
+ && field_name_in_parent(current, parent) == Some("name");
402
+ in_member_pointer = continues_member_pointer;
403
+ if depth >= 1
404
+ && !parent_is_parameterish
405
+ && !parent.kind().contains("declarator")
406
+ && !continues_member_pointer
407
+ {
317
408
  return false;
318
409
  }
319
410
  let field = if depth == 0 {