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.
- package/README.md +12 -3
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.js +1 -1
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.d.ts +3 -1
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/diffCommand.cjs +1 -1
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +1 -1
- package/dist/diffCommand.js.map +1 -1
- package/dist/duplicateSelection.cjs +1 -1
- package/dist/duplicateSelection.cjs.map +1 -1
- package/dist/duplicateSelection.d.ts +13 -4
- 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.d.ts +16 -7
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/languages.cjs +1 -1
- package/dist/languages.cjs.map +1 -1
- package/dist/languages.d.ts +3 -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.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +1 -1
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.js +1 -1
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/types.d.ts +15 -2
- package/native/src/complexity.rs +134 -42
- package/native/src/cyclomatic.rs +79 -0
- package/native/src/dep_degree.rs +102 -11
- package/native/src/functions.rs +828 -22
- package/native/src/languages.rs +16 -0
- package/native/src/lib.rs +2 -1
- package/native/src/measure.rs +2 -0
- package/native/src/types.rs +2 -0
- package/package.json +14 -14
package/native/src/complexity.rs
CHANGED
|
@@ -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
|
|
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
|
|
145
|
-
///
|
|
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 `
|
|
566
|
-
/// `
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
.
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
+
}
|
package/native/src/dep_degree.rs
CHANGED
|
@@ -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 !
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
.
|
|
276
|
-
|
|
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
|
|
315
|
-
//
|
|
316
|
-
|
|
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 {
|