code-gauge 10.3.0 → 10.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/THIRD-PARTY-NOTICES.txt +36 -12
- 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/native/Cargo.lock +24 -24
- package/native/Cargo.toml +12 -12
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +62 -55
- package/native/src/container.rs +7 -8
- package/native/src/dep_degree.rs +190 -97
- package/native/src/duplication.rs +37 -3
- package/native/src/functions.rs +15 -15
- package/native/src/languages.rs +8 -4
- package/native/src/measure.rs +157 -83
- package/native/src/ncss.rs +45 -36
- package/native/src/tree_index.rs +145 -44
- package/native/src/util.rs +42 -5
- package/package.json +13 -13
package/native/src/complexity.rs
CHANGED
|
@@ -1,34 +1,38 @@
|
|
|
1
|
-
use rustc_hash::
|
|
1
|
+
use rustc_hash::FxHashMap;
|
|
2
|
+
use std::sync::OnceLock;
|
|
2
3
|
use tree_sitter::Node;
|
|
3
4
|
|
|
4
|
-
use crate::
|
|
5
|
+
use crate::languages::LANGUAGES;
|
|
6
|
+
use crate::tree_index::{KindSet, NodeExt};
|
|
5
7
|
use crate::types::CognitiveBlock;
|
|
6
8
|
use crate::util::{all_children, node_text, Source};
|
|
7
9
|
|
|
8
|
-
/// Node-type lookup sets
|
|
10
|
+
/// Node-type lookup sets of a language.
|
|
9
11
|
pub struct LanguageSets {
|
|
10
|
-
pub function_nodes:
|
|
11
|
-
pub decision_nodes:
|
|
12
|
-
pub nesting_nodes:
|
|
13
|
-
pub ncss_nodes:
|
|
14
|
-
pub ncss_containers:
|
|
15
|
-
pub ncss_bare_body_parents:
|
|
12
|
+
pub function_nodes: KindSet,
|
|
13
|
+
pub decision_nodes: KindSet,
|
|
14
|
+
pub nesting_nodes: KindSet,
|
|
15
|
+
pub ncss_nodes: KindSet,
|
|
16
|
+
pub ncss_containers: KindSet,
|
|
17
|
+
pub ncss_bare_body_parents: KindSet,
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
impl LanguageSets {
|
|
19
|
-
pub fn
|
|
20
|
-
LanguageSets
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
pub fn of(language: &crate::languages::LanguageDefinition) -> &'static LanguageSets {
|
|
22
|
+
static SETS: [OnceLock<LanguageSets>; LANGUAGES.len()] =
|
|
23
|
+
[const { OnceLock::new() }; LANGUAGES.len()];
|
|
24
|
+
let index = LANGUAGES
|
|
25
|
+
.iter()
|
|
26
|
+
.position(|candidate| candidate.name == language.name)
|
|
27
|
+
.expect("every language is listed in LANGUAGES");
|
|
28
|
+
SETS[index].get_or_init(|| LanguageSets {
|
|
29
|
+
function_nodes: KindSet::new(language.function_node_types),
|
|
30
|
+
decision_nodes: KindSet::new(language.decision_node_types),
|
|
31
|
+
nesting_nodes: KindSet::new(language.nesting_node_types),
|
|
32
|
+
ncss_nodes: KindSet::new(language.ncss_node_types),
|
|
33
|
+
ncss_containers: KindSet::new(language.ncss_container_node_types),
|
|
34
|
+
ncss_bare_body_parents: KindSet::new(language.ncss_bare_body_parent_node_types),
|
|
35
|
+
})
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -54,15 +58,15 @@ pub fn is_lambda_body_block(node: Node<'_>) -> bool {
|
|
|
54
58
|
/// A function node with a body of its own: bodyless declarations (abstract methods, auto-property
|
|
55
59
|
/// accessors, accessor-list properties) open no nesting frame, so members inside them are not
|
|
56
60
|
/// charged as nested functions.
|
|
57
|
-
pub fn is_function_boundary(node: Node<'_>, function_nodes: &
|
|
58
|
-
function_nodes.contains(node
|
|
61
|
+
pub fn is_function_boundary(node: Node<'_>, function_nodes: &KindSet) -> bool {
|
|
62
|
+
function_nodes.contains(node)
|
|
59
63
|
&& !is_lambda_body_block(node)
|
|
60
64
|
&& crate::functions::is_implemented_function(node)
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
// Sonar cognitive complexity charges a switch/match once as a whole, not per case label. Only
|
|
64
68
|
// named nodes are consulted, so anonymous keyword tokens never match.
|
|
65
|
-
|
|
69
|
+
static SWITCH_LIKE_KINDS: KindSet = KindSet::new(&[
|
|
66
70
|
"switch_statement",
|
|
67
71
|
"switch_expression",
|
|
68
72
|
"expression_switch_statement",
|
|
@@ -73,10 +77,10 @@ const SWITCH_LIKE_NODE_TYPES: &[&str] = &[
|
|
|
73
77
|
"when_expression",
|
|
74
78
|
"case",
|
|
75
79
|
"case_match",
|
|
76
|
-
];
|
|
80
|
+
]);
|
|
77
81
|
|
|
78
82
|
// Per-case decision nodes: cyclomatic-only, because the switch itself carries the cognitive cost.
|
|
79
|
-
|
|
83
|
+
static CASE_CLAUSE_KINDS: KindSet = KindSet::new(&[
|
|
80
84
|
"case_clause",
|
|
81
85
|
"switch_case",
|
|
82
86
|
"switch_block_statement_group",
|
|
@@ -91,7 +95,7 @@ const CASE_CLAUSE_NODE_TYPES: &[&str] = &[
|
|
|
91
95
|
"when_entry",
|
|
92
96
|
"when",
|
|
93
97
|
"in_clause",
|
|
94
|
-
];
|
|
98
|
+
]);
|
|
95
99
|
|
|
96
100
|
const IF_LIKE_NODE_TYPES: &[&str] = &["if_statement", "if_expression", "if", "unless"];
|
|
97
101
|
|
|
@@ -302,18 +306,13 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
302
306
|
let relative_nesting = current_nesting + function_nesting_bonus - entry_cognitive_nesting;
|
|
303
307
|
let counts_for_own_body = !inside_nested_region && !opens_frame;
|
|
304
308
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
let is_decision = current.is_named()
|
|
308
|
-
&& self.sets.decision_nodes.contains(current.kind_name())
|
|
309
|
-
&& !is_pathless_switch_branch(current, self.code);
|
|
310
|
-
let is_case_clause =
|
|
311
|
-
current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(¤t.kind_name());
|
|
309
|
+
let is_decision = self.is_decision(current);
|
|
310
|
+
let is_case_clause = current.is_named() && CASE_CLAUSE_KINDS.contains(current);
|
|
312
311
|
// Ruby's `case ... else` arm is an `else` node; like every other language's default branch
|
|
313
312
|
// it nests its contents inside the switch (it cannot go in the Ruby nesting set because
|
|
314
313
|
// `if`/`begin` else branches would then double-nest under their already-nesting parent).
|
|
315
314
|
let is_nesting = current.is_named()
|
|
316
|
-
&& (self.sets.nesting_nodes.contains(current
|
|
315
|
+
&& (self.sets.nesting_nodes.contains(current)
|
|
317
316
|
|| (current.kind_name() == "else" && is_case_else_parent(parent)));
|
|
318
317
|
// `elsif`/`elif`/`else if` continue a flat chain: they add a decision without a nesting
|
|
319
318
|
// surcharge (Sonar cognitive-complexity semantics).
|
|
@@ -321,8 +320,7 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
321
320
|
let is_boolean_operator = is_boolean_operator(current, parent, self.code);
|
|
322
321
|
let is_pattern_guard = is_pattern_guard(current, parent);
|
|
323
322
|
|
|
324
|
-
let is_switch_like =
|
|
325
|
-
current.is_named() && SWITCH_LIKE_NODE_TYPES.contains(¤t.kind_name());
|
|
323
|
+
let is_switch_like = current.is_named() && SWITCH_LIKE_KINDS.contains(current);
|
|
326
324
|
// Opened before the node's own increments so that the block includes them. The sentinel
|
|
327
325
|
// frame's blocks would be those of top-level code, which no function reports.
|
|
328
326
|
if self.frames.len() > 1
|
|
@@ -513,6 +511,14 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
513
511
|
}
|
|
514
512
|
}
|
|
515
513
|
|
|
514
|
+
fn is_decision(&self, node: Node<'_>) -> bool {
|
|
515
|
+
// Anonymous keyword tokens can share a type with named nodes (Ruby's `if` node contains an
|
|
516
|
+
// `if` keyword token), so only named nodes count as decisions.
|
|
517
|
+
node.is_named()
|
|
518
|
+
&& (self.sets.decision_nodes.contains(node) || crate::util::is_else_if_clause(node))
|
|
519
|
+
&& !is_pathless_switch_branch(node)
|
|
520
|
+
}
|
|
521
|
+
|
|
516
522
|
fn top_frame(&mut self) -> &mut FunctionBodyFrame {
|
|
517
523
|
self.frames
|
|
518
524
|
.last_mut()
|
|
@@ -534,6 +540,9 @@ fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64
|
|
|
534
540
|
return u64::from(!is_case_else_parent(parent));
|
|
535
541
|
}
|
|
536
542
|
if kind == "else_clause" {
|
|
543
|
+
if crate::util::is_else_if_clause(current) {
|
|
544
|
+
return 0;
|
|
545
|
+
}
|
|
537
546
|
let has_if_like_child = crate::util::named_children(current)
|
|
538
547
|
.iter()
|
|
539
548
|
.any(|child| IF_LIKE_NODE_TYPES.contains(&child.kind_name()));
|
|
@@ -671,10 +680,11 @@ fn is_pattern_guard(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
|
|
|
671
680
|
.any(|child| !child.is_named() && child.kind_name() == "if")
|
|
672
681
|
}
|
|
673
682
|
|
|
674
|
-
/// Ruby `elsif`, Python `elif`, and `else if` (an if node in an
|
|
683
|
+
/// Ruby `elsif`, Python `elif`, Rust `else if` clauses, and `else if` (an if node in an
|
|
684
|
+
/// else/alternative position).
|
|
675
685
|
fn is_flat_chain_continuation(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
|
|
676
686
|
let kind = node.kind_name();
|
|
677
|
-
if kind == "elsif" || kind == "elif_clause" {
|
|
687
|
+
if kind == "elsif" || kind == "elif_clause" || crate::util::is_else_if_clause(node) {
|
|
678
688
|
return true;
|
|
679
689
|
}
|
|
680
690
|
if kind != "if_statement" && kind != "if_expression" && kind != "if" {
|
|
@@ -683,9 +693,9 @@ fn is_flat_chain_continuation(node: Node<'_>, parent: Option<Node<'_>>) -> bool
|
|
|
683
693
|
let Some(parent) = parent else {
|
|
684
694
|
return false;
|
|
685
695
|
};
|
|
686
|
-
// JS/C/C++/
|
|
687
|
-
// `alternative`.
|
|
688
|
-
parent.kind_name() == "else_clause"
|
|
696
|
+
// JS/C/C++/C# wrap `else if` in an else clause; Java/Go/Kotlin put it directly in
|
|
697
|
+
// `alternative`. An if inside a Rust else-if clause is its condition, not a continuation.
|
|
698
|
+
(parent.kind_name() == "else_clause" && !crate::util::is_else_if_clause(parent))
|
|
689
699
|
|| parent
|
|
690
700
|
.child_by_field_name("alternative")
|
|
691
701
|
.is_some_and(|alternative| alternative.id() == node.id())
|
|
@@ -696,7 +706,7 @@ fn is_flat_chain_continuation(node: Node<'_>, parent: Option<Node<'_>>) -> bool
|
|
|
696
706
|
/// unless one of its stacked labels is an unguarded default or catch-all (`case 3: default: g();`
|
|
697
707
|
/// and `default: case 3: g();` are the default outcome) or all of them are catch-alls. Guards are
|
|
698
708
|
/// charged separately, so a guarded catch-all never absorbs a case stacked with it.
|
|
699
|
-
fn is_pathless_switch_branch(node: Node<'_
|
|
709
|
+
fn is_pathless_switch_branch(node: Node<'_>) -> bool {
|
|
700
710
|
if is_label_only_case(node) {
|
|
701
711
|
return true;
|
|
702
712
|
}
|
|
@@ -713,10 +723,8 @@ fn is_pathless_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
713
723
|
}
|
|
714
724
|
stacked
|
|
715
725
|
.iter()
|
|
716
|
-
.any(|label| is_default_switch_branch(*label
|
|
717
|
-
|| stacked
|
|
718
|
-
.iter()
|
|
719
|
-
.all(|label| is_default_switch_branch(*label, code))
|
|
726
|
+
.any(|label| is_default_switch_branch(*label) && !has_pattern_guard(*label))
|
|
727
|
+
|| stacked.iter().all(|label| is_default_switch_branch(*label))
|
|
720
728
|
}
|
|
721
729
|
|
|
722
730
|
/// A guard directly on the arm (C# `when`, Python `if`, Ruby `if`/`unless`) or inside its label or
|
|
@@ -754,7 +762,7 @@ fn is_label_only_case(node: Node<'_>) -> bool {
|
|
|
754
762
|
}
|
|
755
763
|
}
|
|
756
764
|
|
|
757
|
-
fn is_default_switch_branch(node: Node<'_
|
|
765
|
+
fn is_default_switch_branch(node: Node<'_>) -> bool {
|
|
758
766
|
let kind = node.kind_name();
|
|
759
767
|
if kind == "switch_default" {
|
|
760
768
|
return true;
|
|
@@ -764,17 +772,16 @@ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
764
772
|
}
|
|
765
773
|
|
|
766
774
|
// Java `default:` groups and `default ->` rules: a label with no expression or pattern, or a
|
|
767
|
-
// `case null, default` label,
|
|
775
|
+
// `case null, default` label, which holds the `default` keyword after the `null`.
|
|
768
776
|
if kind == "switch_block_statement_group" || kind == "switch_rule" {
|
|
769
777
|
return non_comment_children(node)
|
|
770
778
|
.into_iter()
|
|
771
779
|
.filter(|child| child.kind_name() == "switch_label")
|
|
772
780
|
.any(|label| {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
part.
|
|
777
|
-
})
|
|
781
|
+
non_comment_children(label).is_empty()
|
|
782
|
+
|| crate::util::all_children(label)
|
|
783
|
+
.iter()
|
|
784
|
+
.any(|part| !part.is_named() && part.kind_name() == "default")
|
|
778
785
|
});
|
|
779
786
|
}
|
|
780
787
|
|
package/native/src/container.rs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
use rustc_hash::FxHashSet;
|
|
2
1
|
use tree_sitter::Node;
|
|
3
2
|
|
|
4
3
|
use crate::functions::{
|
|
@@ -6,7 +5,7 @@ use crate::functions::{
|
|
|
6
5
|
find_string_literal_content, find_value_binding, is_value_group, next_declarator,
|
|
7
6
|
unwrap_transparent_value_wrappers,
|
|
8
7
|
};
|
|
9
|
-
use crate::tree_index::NodeExt;
|
|
8
|
+
use crate::tree_index::{KindSet, NodeExt};
|
|
10
9
|
use crate::util::{named_children, node_text, Source};
|
|
11
10
|
|
|
12
11
|
const CONTAINER_NODE_TYPES: &[&str] = &[
|
|
@@ -58,7 +57,7 @@ const CPP_SCOPE_NODE_TYPES: &[&str] = &[
|
|
|
58
57
|
/// enclosing it.
|
|
59
58
|
fn enclosing_cpp_scopes<'s>(
|
|
60
59
|
node: Node<'_>,
|
|
61
|
-
function_nodes: &
|
|
60
|
+
function_nodes: &KindSet,
|
|
62
61
|
code: &Source<'s>,
|
|
63
62
|
namespaces_only: bool,
|
|
64
63
|
) -> Vec<&'s str> {
|
|
@@ -98,7 +97,7 @@ fn explicit_ruby_owner(object: Option<Node<'_>>, code: &Source<'_>) -> Option<St
|
|
|
98
97
|
/// method); or the path a JavaScript object literal holding it is bound to.
|
|
99
98
|
pub fn find_container_name(
|
|
100
99
|
node: Node<'_>,
|
|
101
|
-
function_nodes: &
|
|
100
|
+
function_nodes: &KindSet,
|
|
102
101
|
code: &Source<'_>,
|
|
103
102
|
inline_namespaces: &InlineNamespaces<'_>,
|
|
104
103
|
) -> Option<String> {
|
|
@@ -201,7 +200,7 @@ fn assigns_attribute(assignment: Node<'_>, target: Node<'_>) -> bool {
|
|
|
201
200
|
/// to nothing the syntax names.
|
|
202
201
|
fn object_owner(
|
|
203
202
|
object: Node<'_>,
|
|
204
|
-
function_nodes: &
|
|
203
|
+
function_nodes: &KindSet,
|
|
205
204
|
code: &Source<'_>,
|
|
206
205
|
inline_namespaces: &InlineNamespaces<'_>,
|
|
207
206
|
) -> Option<String> {
|
|
@@ -293,7 +292,7 @@ fn find_qualified_declarator(node: Node<'_>) -> Option<Node<'_>> {
|
|
|
293
292
|
fn cpp_spelled_owner(
|
|
294
293
|
node: Node<'_>,
|
|
295
294
|
qualified: Node<'_>,
|
|
296
|
-
function_nodes: &
|
|
295
|
+
function_nodes: &KindSet,
|
|
297
296
|
code: &Source<'_>,
|
|
298
297
|
inline_namespaces: &InlineNamespaces<'_>,
|
|
299
298
|
) -> Option<String> {
|
|
@@ -431,7 +430,7 @@ fn holds_namespaces_without_scope(node: Node<'_>) -> bool {
|
|
|
431
430
|
/// the function no member of it.
|
|
432
431
|
fn enclosing_owner(
|
|
433
432
|
node: Node<'_>,
|
|
434
|
-
function_nodes: &
|
|
433
|
+
function_nodes: &KindSet,
|
|
435
434
|
code: &Source<'_>,
|
|
436
435
|
inline_namespaces: &InlineNamespaces<'_>,
|
|
437
436
|
) -> Option<String> {
|
|
@@ -463,7 +462,7 @@ fn enclosing_owner(
|
|
|
463
462
|
fn spell_owner(
|
|
464
463
|
container: Node<'_>,
|
|
465
464
|
is_friend: bool,
|
|
466
|
-
function_nodes: &
|
|
465
|
+
function_nodes: &KindSet,
|
|
467
466
|
code: &Source<'_>,
|
|
468
467
|
inline_namespaces: &InlineNamespaces<'_>,
|
|
469
468
|
) -> Option<String> {
|
package/native/src/dep_degree.rs
CHANGED
|
@@ -2,7 +2,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
|
|
2
2
|
use tree_sitter::Node;
|
|
3
3
|
|
|
4
4
|
use crate::complexity::is_function_boundary;
|
|
5
|
-
use crate::tree_index::NodeExt;
|
|
5
|
+
use crate::tree_index::{KindSet, NodeExt};
|
|
6
6
|
use crate::util::{node_text, Source};
|
|
7
7
|
|
|
8
8
|
/// Leaf node types treated as variable references by the def-use approximation.
|
|
@@ -89,65 +89,153 @@ const DEFINITION_LIST_HOLDER_TYPES: &[&str] = &[
|
|
|
89
89
|
/// Parameter-position fields that annotate or initialize rather than bind (`x: T`, `x = default`).
|
|
90
90
|
const NON_BINDING_PARAMETER_FIELDS: &[&str] = &["type", "value"];
|
|
91
91
|
|
|
92
|
-
/// One leaf of the def-use walk: its field in the parent and its function
|
|
92
|
+
/// One leaf of the def-use walk: its field in the parent and its function scope.
|
|
93
93
|
struct DepDegreeLeaf<'t> {
|
|
94
94
|
node: Node<'t>,
|
|
95
95
|
field_name: Option<&'t str>,
|
|
96
|
-
///
|
|
97
|
-
|
|
98
|
-
scope: String,
|
|
96
|
+
/// Preorder number of the innermost function scope holding the leaf (see LeafWalk).
|
|
97
|
+
scope: usize,
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
///
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
/// The non-comment leaves of a subtree in walk order, with the function scopes they lie in.
|
|
101
|
+
struct LeafWalk<'t> {
|
|
102
|
+
leaves: Vec<DepDegreeLeaf<'t>>,
|
|
103
|
+
/// The last scope nested in each scope, by preorder number; scope 0 is the walked subtree's
|
|
104
|
+
/// own, and scope `outer` encloses scope `inner` exactly when
|
|
105
|
+
/// `outer <= inner && inner <= last_nested_scopes[outer]`.
|
|
106
|
+
last_nested_scopes: Vec<usize>,
|
|
107
|
+
/// The leaf range and body scope of each function, by node id, whose subtree the walk entered.
|
|
108
|
+
function_spans: FxHashMap<usize, (std::ops::Range<usize>, usize)>,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// What a variable leaf does in the def-use walk, which depends only on the leaf and the leaf
|
|
112
|
+
/// after it.
|
|
113
|
+
#[derive(Clone, Copy)]
|
|
114
|
+
struct VariableUse<'a> {
|
|
115
|
+
name: &'a str,
|
|
116
|
+
next_operator: Option<AssignmentOperator>,
|
|
117
|
+
/// Whether the leaf is a definition even when no assignment operator follows it.
|
|
118
|
+
is_binding: bool,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[derive(Clone, Copy, PartialEq)]
|
|
122
|
+
enum AssignmentOperator {
|
|
123
|
+
Pure,
|
|
124
|
+
Compound,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Approximate def-use pairs of the subtree of each function in `functions`, by node id: one walk
|
|
128
|
+
/// of the file gathers every function's leaves, since a function's leaves and the scopes they lie
|
|
129
|
+
/// in are the same wherever the walk starts.
|
|
130
|
+
pub fn measure_dep_degrees(
|
|
131
|
+
root: Node<'_>,
|
|
132
|
+
functions: &[Node<'_>],
|
|
104
133
|
code: &Source<'_>,
|
|
105
|
-
function_nodes: &
|
|
106
|
-
) -> u64 {
|
|
107
|
-
let
|
|
108
|
-
|
|
109
|
-
collect_dep_degree_leaves(
|
|
110
|
-
function_node,
|
|
111
|
-
None,
|
|
112
|
-
"",
|
|
113
|
-
&mut next_scope_id,
|
|
134
|
+
function_nodes: &KindSet,
|
|
135
|
+
) -> FxHashMap<usize, u64> {
|
|
136
|
+
let walk = LeafWalk::new(
|
|
137
|
+
root,
|
|
114
138
|
function_nodes,
|
|
115
|
-
&
|
|
116
|
-
true,
|
|
139
|
+
&functions.iter().map(Node::id).collect(),
|
|
117
140
|
);
|
|
118
|
-
let mut
|
|
141
|
+
let mut variable_uses = vec![None; walk.leaves.len()];
|
|
142
|
+
functions
|
|
143
|
+
.iter()
|
|
144
|
+
.map(|&function_node| {
|
|
145
|
+
let (leaf_range, body_scope) = walk
|
|
146
|
+
.function_spans
|
|
147
|
+
.get(&function_node.id())
|
|
148
|
+
.expect("the walk enters every function boundary");
|
|
149
|
+
let pairs = count_pairs(
|
|
150
|
+
&walk,
|
|
151
|
+
leaf_range.clone(),
|
|
152
|
+
*body_scope,
|
|
153
|
+
function_node,
|
|
154
|
+
code,
|
|
155
|
+
&mut variable_uses,
|
|
156
|
+
);
|
|
157
|
+
(function_node.id(), pairs)
|
|
158
|
+
})
|
|
159
|
+
.collect()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// The def-use pairs among the leaves in `leaf_range` of the function whose own body is
|
|
163
|
+
/// `body_scope`; `variable_uses` caches what each leaf of the walk does, as `Some(None)` for a
|
|
164
|
+
/// leaf that is no variable.
|
|
165
|
+
fn count_pairs<'a>(
|
|
166
|
+
walk: &LeafWalk<'_>,
|
|
167
|
+
leaf_range: std::ops::Range<usize>,
|
|
168
|
+
body_scope: usize,
|
|
169
|
+
function_node: Node<'_>,
|
|
170
|
+
code: &Source<'a>,
|
|
171
|
+
variable_uses: &mut [Option<Option<VariableUse<'a>>>],
|
|
172
|
+
) -> u64 {
|
|
173
|
+
let is_visible = |definition_scopes: Option<&Vec<usize>>, scope: usize| {
|
|
174
|
+
definition_scopes.is_some_and(|scopes| {
|
|
175
|
+
scopes
|
|
176
|
+
.iter()
|
|
177
|
+
.any(|&outer| outer <= scope && scope <= walk.last_nested_scopes[outer])
|
|
178
|
+
})
|
|
179
|
+
};
|
|
180
|
+
let mut definition_scopes_by_name: FxHashMap<&str, Vec<usize>> = FxHashMap::default();
|
|
119
181
|
for name in implicit_accessor_definitions(function_node, code) {
|
|
120
|
-
add_definition(&mut definition_scopes_by_name, name,
|
|
182
|
+
add_definition(&mut definition_scopes_by_name, name, body_scope);
|
|
121
183
|
}
|
|
184
|
+
let leaf_end = leaf_range.end;
|
|
122
185
|
let mut pairs = 0u64;
|
|
123
|
-
for index in
|
|
124
|
-
let leaf = &leaves[index];
|
|
125
|
-
|
|
186
|
+
for index in leaf_range {
|
|
187
|
+
let leaf = &walk.leaves[index];
|
|
188
|
+
let Some(variable_use) =
|
|
189
|
+
*variable_uses[index].get_or_insert_with(|| variable_use(&walk.leaves, index, code))
|
|
190
|
+
else {
|
|
126
191
|
continue;
|
|
127
|
-
}
|
|
128
|
-
let name =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
192
|
+
};
|
|
193
|
+
let name = variable_use.name;
|
|
194
|
+
// The function's last leaf has no next leaf within the function.
|
|
195
|
+
let next_operator = variable_use.next_operator.filter(|_| index + 1 != leaf_end);
|
|
196
|
+
if next_operator == Some(AssignmentOperator::Compound) {
|
|
197
|
+
if is_visible(definition_scopes_by_name.get(name), leaf.scope) {
|
|
132
198
|
pairs += 1;
|
|
133
199
|
}
|
|
134
|
-
add_definition(&mut definition_scopes_by_name, name,
|
|
200
|
+
add_definition(&mut definition_scopes_by_name, name, leaf.scope);
|
|
135
201
|
continue;
|
|
136
202
|
}
|
|
137
|
-
if
|
|
138
|
-
|
|
139
|
-
|| is_parameter_definition(leaf)
|
|
140
|
-
{
|
|
141
|
-
add_definition(&mut definition_scopes_by_name, name, &leaf.scope);
|
|
203
|
+
if next_operator == Some(AssignmentOperator::Pure) || variable_use.is_binding {
|
|
204
|
+
add_definition(&mut definition_scopes_by_name, name, leaf.scope);
|
|
142
205
|
continue;
|
|
143
206
|
}
|
|
144
|
-
if
|
|
207
|
+
if is_visible(definition_scopes_by_name.get(name), leaf.scope) {
|
|
145
208
|
pairs += 1;
|
|
146
209
|
}
|
|
147
210
|
}
|
|
148
211
|
pairs
|
|
149
212
|
}
|
|
150
213
|
|
|
214
|
+
fn variable_use<'a>(
|
|
215
|
+
leaves: &[DepDegreeLeaf<'_>],
|
|
216
|
+
index: usize,
|
|
217
|
+
code: &Source<'a>,
|
|
218
|
+
) -> Option<VariableUse<'a>> {
|
|
219
|
+
let leaf = &leaves[index];
|
|
220
|
+
if !is_variable_leaf(leaf) {
|
|
221
|
+
return None;
|
|
222
|
+
}
|
|
223
|
+
let next_text = leaves.get(index + 1).map(|next| node_text(next.node, code));
|
|
224
|
+
Some(VariableUse {
|
|
225
|
+
name: node_text(leaf.node, code),
|
|
226
|
+
next_operator: next_text.and_then(|next| {
|
|
227
|
+
if COMPOUND_ASSIGNMENT_OPERATORS.contains(&next) {
|
|
228
|
+
Some(AssignmentOperator::Compound)
|
|
229
|
+
} else if PURE_ASSIGNMENT_OPERATORS.contains(&next) {
|
|
230
|
+
Some(AssignmentOperator::Pure)
|
|
231
|
+
} else {
|
|
232
|
+
None
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
235
|
+
is_binding: is_structural_definition(leaf) || is_parameter_definition(leaf),
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
151
239
|
/// Kotlin spells type names as identifiers: in a `user_type`, or as a type parameter's fieldless
|
|
152
240
|
/// name (C# puts its type parameter in a `name` field).
|
|
153
241
|
fn is_variable_leaf(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
@@ -215,81 +303,86 @@ fn implicit_accessor_definitions<'a>(function_node: Node<'_>, code: &Source<'a>)
|
|
|
215
303
|
names
|
|
216
304
|
}
|
|
217
305
|
|
|
218
|
-
|
|
219
|
-
///
|
|
220
|
-
|
|
221
|
-
fn
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
if crate::util::is_token(node) {
|
|
237
|
-
leaves.push(DepDegreeLeaf {
|
|
238
|
-
node,
|
|
239
|
-
field_name,
|
|
240
|
-
scope: scope.to_string(),
|
|
241
|
-
});
|
|
242
|
-
return;
|
|
306
|
+
impl<'t> LeafWalk<'t> {
|
|
307
|
+
/// Walks the subtree of `root`, recording the span of each function in `function_ids` it
|
|
308
|
+
/// enters.
|
|
309
|
+
fn new(
|
|
310
|
+
root: Node<'t>,
|
|
311
|
+
function_nodes: &KindSet,
|
|
312
|
+
function_ids: &FxHashSet<usize>,
|
|
313
|
+
) -> LeafWalk<'t> {
|
|
314
|
+
let mut walk = LeafWalk {
|
|
315
|
+
leaves: Vec::new(),
|
|
316
|
+
last_nested_scopes: vec![0],
|
|
317
|
+
function_spans: FxHashMap::default(),
|
|
318
|
+
};
|
|
319
|
+
walk.visit(root, None, 0, function_nodes, function_ids);
|
|
320
|
+
// The walked subtree's own scope encloses every scope opened inside it.
|
|
321
|
+
walk.last_nested_scopes[0] = walk.last_nested_scopes.len() - 1;
|
|
322
|
+
walk
|
|
243
323
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
scope
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
324
|
+
|
|
325
|
+
fn visit(
|
|
326
|
+
&mut self,
|
|
327
|
+
node: Node<'t>,
|
|
328
|
+
field_name: Option<&'t str>,
|
|
329
|
+
scope: usize,
|
|
330
|
+
function_nodes: &KindSet,
|
|
331
|
+
function_ids: &FxHashSet<usize>,
|
|
332
|
+
) {
|
|
333
|
+
if matches!(
|
|
334
|
+
node.kind_name(),
|
|
335
|
+
"comment" | "line_comment" | "block_comment"
|
|
336
|
+
) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if crate::util::is_token(node) {
|
|
340
|
+
self.leaves.push(DepDegreeLeaf {
|
|
341
|
+
node,
|
|
342
|
+
field_name,
|
|
343
|
+
scope,
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
let opens_scope = is_function_boundary(node, function_nodes);
|
|
348
|
+
let child_scope = if opens_scope {
|
|
349
|
+
self.last_nested_scopes.push(0);
|
|
350
|
+
self.last_nested_scopes.len() - 1
|
|
351
|
+
} else {
|
|
352
|
+
scope
|
|
353
|
+
};
|
|
354
|
+
let first_leaf = self.leaves.len();
|
|
355
|
+
for (child, child_field_name) in crate::util::children_with_field_names(node) {
|
|
356
|
+
self.visit(
|
|
357
|
+
child,
|
|
358
|
+
child_field_name,
|
|
359
|
+
child_scope,
|
|
259
360
|
function_nodes,
|
|
260
|
-
|
|
261
|
-
false,
|
|
361
|
+
function_ids,
|
|
262
362
|
);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
363
|
+
}
|
|
364
|
+
if opens_scope {
|
|
365
|
+
self.last_nested_scopes[child_scope] = self.last_nested_scopes.len() - 1;
|
|
366
|
+
}
|
|
367
|
+
// Every measured function is a function boundary.
|
|
368
|
+
if opens_scope && function_ids.contains(&node.id()) {
|
|
369
|
+
self.function_spans
|
|
370
|
+
.insert(node.id(), (first_leaf..self.leaves.len(), child_scope));
|
|
266
371
|
}
|
|
267
372
|
}
|
|
268
373
|
}
|
|
269
374
|
|
|
270
375
|
fn add_definition<'a>(
|
|
271
|
-
definition_scopes_by_name: &mut FxHashMap<&'a str, Vec<
|
|
376
|
+
definition_scopes_by_name: &mut FxHashMap<&'a str, Vec<usize>>,
|
|
272
377
|
name: &'a str,
|
|
273
|
-
scope:
|
|
378
|
+
scope: usize,
|
|
274
379
|
) {
|
|
275
380
|
let scopes = definition_scopes_by_name.entry(name).or_default();
|
|
276
|
-
if !scopes.
|
|
277
|
-
scopes.push(scope
|
|
381
|
+
if !scopes.contains(&scope) {
|
|
382
|
+
scopes.push(scope);
|
|
278
383
|
}
|
|
279
384
|
}
|
|
280
385
|
|
|
281
|
-
/// A definition reaches a read only from the read's own or an enclosing function scope.
|
|
282
|
-
fn is_definition_visible(definition_scopes: Option<&Vec<String>>, scope: &str) -> bool {
|
|
283
|
-
definition_scopes.is_some_and(|scopes| {
|
|
284
|
-
scopes.iter().any(|definition_scope| {
|
|
285
|
-
scope == definition_scope
|
|
286
|
-
|| (scope.len() > definition_scope.len()
|
|
287
|
-
&& scope.starts_with(definition_scope.as_str())
|
|
288
|
-
&& scope.as_bytes()[definition_scope.len()] == b'/')
|
|
289
|
-
})
|
|
290
|
-
})
|
|
291
|
-
}
|
|
292
|
-
|
|
293
386
|
fn is_structural_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
294
387
|
let Some(parent) = leaf.node.parent_node() else {
|
|
295
388
|
return false;
|