code-gauge 4.7.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/THIRD-PARTY-NOTICES.txt +595 -29
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.js +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/crossFileNearMiss.cjs +1 -1
- package/dist/crossFileNearMiss.cjs.map +1 -1
- package/dist/crossFileNearMiss.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/dist/diffCommand.cjs +3 -3
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +3 -3
- package/dist/diffCommand.js.map +1 -1
- package/dist/duplicateSelection.cjs +1 -1
- package/dist/duplicateSelection.cjs.map +1 -1
- package/dist/duplicateSelection.js +1 -1
- package/dist/duplicateSelection.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/git.cjs +1 -1
- package/dist/git.cjs.map +1 -1
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/languages.cjs +1 -1
- package/dist/languages.cjs.map +1 -1
- package/dist/languages.js +1 -1
- package/dist/languages.js.map +1 -1
- package/dist/metrics.cjs +1 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.ts +8 -3
- package/dist/metrics.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +3 -3
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +19 -3
- package/dist/nativeMetrics.js +3 -3
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/regressionGate.cjs +1 -1
- package/dist/regressionGate.cjs.map +1 -1
- package/dist/regressionGate.js +1 -1
- package/dist/regressionGate.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.d.ts +2 -2
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/wasmBinding.cjs +1 -1
- package/dist/wasmBinding.cjs.map +1 -1
- package/dist/wasmBinding.js +1 -1
- package/dist/wasmBinding.js.map +1 -1
- package/native/Cargo.lock +48 -15
- package/native/Cargo.toml +11 -10
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +152 -255
- package/native/src/dep_degree.rs +79 -65
- package/native/src/duplication.rs +114 -97
- package/native/src/functions.rs +181 -206
- package/native/src/languages.rs +48 -7
- package/native/src/lib.rs +10 -5
- package/native/src/measure.rs +125 -150
- package/native/src/napi.rs +61 -2
- package/native/src/ncss.rs +108 -104
- package/native/src/near_miss.rs +7 -7
- package/native/src/tree_index.rs +119 -0
- package/native/src/util.rs +15 -55
- package/native/src/worker_pool.rs +53 -0
- package/package.json +10 -10
package/native/src/ncss.rs
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
use
|
|
1
|
+
use rustc_hash::FxHashSet;
|
|
2
2
|
use tree_sitter::Node;
|
|
3
3
|
|
|
4
|
-
use crate::
|
|
4
|
+
use crate::tree_index::NodeExt;
|
|
5
|
+
use crate::util::find_children_by_field_name;
|
|
5
6
|
|
|
6
|
-
pub const COMMENT_NODE_TYPES: &[&str] = &[
|
|
7
|
-
"comment",
|
|
8
|
-
"line_comment",
|
|
9
|
-
"block_comment",
|
|
10
|
-
"multiline_comment",
|
|
11
|
-
];
|
|
7
|
+
pub const COMMENT_NODE_TYPES: &[&str] = &["comment", "line_comment", "block_comment"];
|
|
12
8
|
|
|
13
9
|
/// Nodes never counted positionally inside NCSS containers: metadata, empty statements, Ruby
|
|
14
10
|
/// heredoc bodies (tree-sitter emits them as siblings of the statement that opened the heredoc),
|
|
15
|
-
/// Ruby statement parentheses (transparent wrappers whose children count instead), Kotlin
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
///
|
|
19
|
-
/// expression) and `label` (a Rust block label `'outer:` stands for the labeled statement the
|
|
20
|
-
/// other languages count, while a Kotlin `outer@` is a childless token beside its statement).
|
|
11
|
+
/// Ruby statement parentheses (transparent wrappers whose children count instead), Kotlin enum
|
|
12
|
+
/// entries (Java enum constants are not counted either), and Kotlin lambda parameters (siblings of
|
|
13
|
+
/// the lambda's statements). Kotlin's `try_expression` is excluded contextually below because Rust
|
|
14
|
+
/// shares its name: Rust's `?` operator must keep counting as a tail expression.
|
|
21
15
|
const POSITIONAL_EXCLUSION_TYPES: &[&str] = &[
|
|
22
16
|
"attribute_item",
|
|
23
17
|
"inner_attribute_item",
|
|
@@ -26,8 +20,9 @@ const POSITIONAL_EXCLUSION_TYPES: &[&str] = &[
|
|
|
26
20
|
"parenthesized_statements",
|
|
27
21
|
"annotation",
|
|
28
22
|
"file_annotation",
|
|
29
|
-
"
|
|
23
|
+
"shebang",
|
|
30
24
|
"enum_entry",
|
|
25
|
+
"lambda_parameters",
|
|
31
26
|
];
|
|
32
27
|
|
|
33
28
|
/// TypeScript interface members count like Java interface members, but the same node types appear
|
|
@@ -56,80 +51,67 @@ const BODYLESS_NCSS_SPECIFIER_TYPES: &[&str] = &[
|
|
|
56
51
|
"class_specifier",
|
|
57
52
|
];
|
|
58
53
|
|
|
59
|
-
///
|
|
54
|
+
/// A node's own non-commenting source statement (NCSS) count, PMD-style: one per declaration,
|
|
60
55
|
/// statement, and clause (`else`, `case`/`default` label, `catch`, `finally`, try-with-resources
|
|
61
|
-
/// resource); `try` itself, braces, blank lines, and comments count 0.
|
|
62
|
-
|
|
63
|
-
node: Node<'_>,
|
|
64
|
-
countable: &HashSet<&'static str>,
|
|
65
|
-
containers: &HashSet<&'static str>,
|
|
66
|
-
) -> u64 {
|
|
67
|
-
fn visit(
|
|
68
|
-
current: Node<'_>,
|
|
69
|
-
countable: &HashSet<&'static str>,
|
|
70
|
-
containers: &HashSet<&'static str>,
|
|
71
|
-
count: &mut u64,
|
|
72
|
-
) {
|
|
73
|
-
*count += ncss_contribution(current, countable, containers);
|
|
74
|
-
for child in all_children(current) {
|
|
75
|
-
visit(child, countable, containers, count);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let mut count = 0;
|
|
80
|
-
visit(node, countable, containers, &mut count);
|
|
81
|
-
count
|
|
82
|
-
}
|
|
83
|
-
|
|
56
|
+
/// resource); `try` itself, braces, blank lines, and comments count 0. `parent` is the node's
|
|
57
|
+
/// `parent_node()`, looked up once by the caller and shared by every check.
|
|
84
58
|
pub fn ncss_contribution(
|
|
85
59
|
node: Node<'_>,
|
|
86
|
-
|
|
87
|
-
|
|
60
|
+
parent: Option<Node<'_>>,
|
|
61
|
+
countable: &FxHashSet<&'static str>,
|
|
62
|
+
containers: &FxHashSet<&'static str>,
|
|
63
|
+
bare_body_parents: &FxHashSet<&'static str>,
|
|
88
64
|
) -> u64 {
|
|
89
|
-
if !node.is_named()
|
|
65
|
+
if !node.is_named()
|
|
66
|
+
|| COMMENT_NODE_TYPES.contains(&node.kind_name())
|
|
67
|
+
|| is_for_header_node(node, parent)
|
|
68
|
+
{
|
|
90
69
|
return 0;
|
|
91
70
|
}
|
|
92
71
|
// A Kotlin accessor without a body (`private set`) only changes visibility and declares
|
|
93
|
-
// nothing of its own
|
|
94
|
-
if (node.
|
|
72
|
+
// nothing of its own, so it must not count through its node type.
|
|
73
|
+
if (node.kind_name() == "getter" || node.kind_name() == "setter")
|
|
95
74
|
&& !crate::functions::is_implemented_function(node)
|
|
96
75
|
{
|
|
97
76
|
return 0;
|
|
98
77
|
}
|
|
99
78
|
|
|
100
79
|
let mut contribution = 0;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
&& !(node.
|
|
106
|
-
|
|
80
|
+
// A Kotlin lambda holds its statements directly, so it is a container, yet it still counts where
|
|
81
|
+
// it stands like any other expression.
|
|
82
|
+
let positional = (is_in_container_position(parent, containers)
|
|
83
|
+
|| is_bare_body(node, parent, bare_body_parents))
|
|
84
|
+
&& (!containers.contains(node.kind_name()) || node.kind_name() == "lambda_literal")
|
|
85
|
+
&& !POSITIONAL_EXCLUSION_TYPES.contains(&node.kind_name())
|
|
86
|
+
&& !crate::util::is_kotlin_try_expression(node);
|
|
87
|
+
if (counts_through_node_type(node, countable)
|
|
88
|
+
|| positional
|
|
89
|
+
|| counts_contextually(node, parent))
|
|
107
90
|
&& !is_declaration_wrapper(node, countable)
|
|
108
91
|
{
|
|
109
92
|
contribution += 1;
|
|
110
93
|
}
|
|
111
94
|
|
|
112
|
-
// A bare else branch (Java/Go `alternative:` without an else-clause wrapper
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
if IF_NODE_TYPES.contains(&node.
|
|
116
|
-
contribution += count_bare_alternatives(node)
|
|
117
|
-
+ u64::from(crate::util::kotlin_else_body(node).is_some());
|
|
95
|
+
// A bare else branch (Java/Go/Kotlin `alternative:` without an else-clause wrapper) counts 1
|
|
96
|
+
// like the `else` keyword does in PMD; an `else if` chain charges the nested if separately on
|
|
97
|
+
// top.
|
|
98
|
+
if IF_NODE_TYPES.contains(&node.kind_name()) {
|
|
99
|
+
contribution += count_bare_alternatives(node);
|
|
118
100
|
}
|
|
119
101
|
|
|
120
102
|
contribution
|
|
121
103
|
}
|
|
122
104
|
|
|
123
|
-
fn counts_through_node_type(node: Node<'_>, countable: &
|
|
124
|
-
if !countable.contains(node.
|
|
105
|
+
fn counts_through_node_type(node: Node<'_>, countable: &FxHashSet<&'static str>) -> bool {
|
|
106
|
+
if !countable.contains(node.kind_name()) {
|
|
125
107
|
return false;
|
|
126
108
|
}
|
|
127
|
-
if BODYLESS_NCSS_SPECIFIER_TYPES.contains(&node.
|
|
109
|
+
if BODYLESS_NCSS_SPECIFIER_TYPES.contains(&node.kind_name()) {
|
|
128
110
|
return node.child_by_field_name("body").is_some();
|
|
129
111
|
}
|
|
130
112
|
// A try-with-resources `resource` counts only when it declares a variable; `try (r)` reuses an
|
|
131
113
|
// existing one and adds no statement (matching PMD).
|
|
132
|
-
if node.
|
|
114
|
+
if node.kind_name() == "resource" {
|
|
133
115
|
return node.child_by_field_name("name").is_some();
|
|
134
116
|
}
|
|
135
117
|
true
|
|
@@ -137,28 +119,44 @@ fn counts_through_node_type(node: Node<'_>, countable: &HashSet<&'static str>) -
|
|
|
137
119
|
|
|
138
120
|
/// Direct container children count positionally; Ruby's `(foo; bar)` statement parentheses are
|
|
139
121
|
/// transparent, so their children count when the parentheses themselves sit in a container.
|
|
140
|
-
fn is_in_container_position(
|
|
141
|
-
|
|
122
|
+
fn is_in_container_position(
|
|
123
|
+
parent: Option<Node<'_>>,
|
|
124
|
+
containers: &FxHashSet<&'static str>,
|
|
125
|
+
) -> bool {
|
|
126
|
+
let mut ancestor = parent;
|
|
142
127
|
while let Some(current) = ancestor {
|
|
143
|
-
if current.
|
|
144
|
-
return containers.contains(current.
|
|
128
|
+
if current.kind_name() != "parenthesized_statements" {
|
|
129
|
+
return containers.contains(current.kind_name());
|
|
145
130
|
}
|
|
146
|
-
ancestor = current.
|
|
131
|
+
ancestor = current.parent_node();
|
|
147
132
|
}
|
|
148
133
|
false
|
|
149
134
|
}
|
|
150
135
|
|
|
136
|
+
/// A braceless branch or loop body (`if (x) foo()`) stands for the single statement a braced body
|
|
137
|
+
/// would hold.
|
|
138
|
+
fn is_bare_body(
|
|
139
|
+
node: Node<'_>,
|
|
140
|
+
parent: Option<Node<'_>>,
|
|
141
|
+
bare_body_parents: &FxHashSet<&'static str>,
|
|
142
|
+
) -> bool {
|
|
143
|
+
parent.is_some_and(|parent| bare_body_parents.contains(parent.kind_name()))
|
|
144
|
+
&& ["consequence", "alternative", "body"]
|
|
145
|
+
.iter()
|
|
146
|
+
.any(|field_name| is_field_of_parent(node, parent, field_name))
|
|
147
|
+
}
|
|
148
|
+
|
|
151
149
|
/// `export const x = 1` nests a countable declaration inside `export_statement`; only the inner
|
|
152
150
|
/// declaration counts, mirroring how PMD counts one statement per declared entity.
|
|
153
|
-
fn is_declaration_wrapper(node: Node<'_>, countable: &
|
|
154
|
-
if node.
|
|
151
|
+
fn is_declaration_wrapper(node: Node<'_>, countable: &FxHashSet<&'static str>) -> bool {
|
|
152
|
+
if node.kind_name() != "export_statement" {
|
|
155
153
|
return false;
|
|
156
154
|
}
|
|
157
155
|
node.child_by_field_name("declaration")
|
|
158
156
|
.is_some_and(|declaration| {
|
|
159
|
-
countable.contains(declaration.
|
|
160
|
-
|| declaration.
|
|
161
|
-
|| declaration.
|
|
157
|
+
countable.contains(declaration.kind_name())
|
|
158
|
+
|| declaration.kind_name() == "internal_module"
|
|
159
|
+
|| declaration.kind_name() == "ambient_declaration"
|
|
162
160
|
})
|
|
163
161
|
}
|
|
164
162
|
|
|
@@ -169,21 +167,21 @@ const FOR_HEADER_FIELD_NAMES: &[&str] =
|
|
|
169
167
|
/// statement, which already counts; PMD does not count them separately. JavaScript parses the
|
|
170
168
|
/// condition as an `expression_statement` and Go parses the update as an `inc_statement`, so all
|
|
171
169
|
/// header fields must be excluded, not just the initializer.
|
|
172
|
-
fn is_for_header_node(node: Node<'_
|
|
173
|
-
let Some(parent) =
|
|
170
|
+
fn is_for_header_node(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
|
|
171
|
+
let Some(parent) = parent else {
|
|
174
172
|
return false;
|
|
175
173
|
};
|
|
176
174
|
// C++20 range-for initializers nest one level deeper: for_range_loop > init_statement > node.
|
|
177
|
-
if parent.
|
|
175
|
+
if parent.kind_name() == "init_statement"
|
|
178
176
|
&& parent
|
|
179
|
-
.
|
|
180
|
-
.is_some_and(|grandparent| grandparent.
|
|
177
|
+
.parent_node()
|
|
178
|
+
.is_some_and(|grandparent| grandparent.kind_name() == "for_range_loop")
|
|
181
179
|
{
|
|
182
180
|
return true;
|
|
183
181
|
}
|
|
184
|
-
if parent.
|
|
185
|
-
&& parent.
|
|
186
|
-
&& parent.
|
|
182
|
+
if parent.kind_name() != "for_statement"
|
|
183
|
+
&& parent.kind_name() != "for_clause"
|
|
184
|
+
&& parent.kind_name() != "for_range_loop"
|
|
187
185
|
{
|
|
188
186
|
return false;
|
|
189
187
|
}
|
|
@@ -195,52 +193,54 @@ fn is_for_header_node(node: Node<'_>) -> bool {
|
|
|
195
193
|
}
|
|
196
194
|
|
|
197
195
|
/// Statements only countable by their position: constructs without a dedicated statement node.
|
|
198
|
-
fn counts_contextually(node: Node<'_
|
|
199
|
-
let parent_kind =
|
|
196
|
+
fn counts_contextually(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
|
|
197
|
+
let parent_kind = parent.map(|parent| parent.kind_name());
|
|
200
198
|
// A Java instance initializer is a bare `block` in the class body; PMD counts it like the
|
|
201
199
|
// `static_initializer` declaration it parallels.
|
|
202
|
-
if node.
|
|
200
|
+
if node.kind_name() == "block" && parent_kind == Some("class_body") {
|
|
203
201
|
return true;
|
|
204
202
|
}
|
|
205
203
|
// TypeScript interface members (see INTERFACE_MEMBER_NODE_TYPES).
|
|
206
|
-
if INTERFACE_MEMBER_NODE_TYPES.contains(&node.
|
|
204
|
+
if INTERFACE_MEMBER_NODE_TYPES.contains(&node.kind_name())
|
|
205
|
+
&& parent_kind == Some("interface_body")
|
|
206
|
+
{
|
|
207
207
|
return true;
|
|
208
208
|
}
|
|
209
209
|
// A braceless Rust match-arm body (`1 => foo()`) has no expression_statement wrapper; count the
|
|
210
210
|
// value expression so braced and unbraced arms measure alike.
|
|
211
211
|
if parent_kind == Some("match_arm")
|
|
212
|
-
&& node.
|
|
213
|
-
&& is_field_of_parent(node, "value")
|
|
212
|
+
&& node.kind_name() != "block"
|
|
213
|
+
&& is_field_of_parent(node, parent, "value")
|
|
214
214
|
{
|
|
215
215
|
return true;
|
|
216
216
|
}
|
|
217
217
|
// A TypeScript class-body method overload signature declares a member like its interface twin.
|
|
218
|
-
if node.
|
|
218
|
+
if node.kind_name() == "method_signature" && parent_kind == Some("class_body") {
|
|
219
219
|
return true;
|
|
220
220
|
}
|
|
221
221
|
// An ambient `declare namespace M { ... }` is a bare `internal_module`; the non-ambient
|
|
222
222
|
// `namespace N { ... }` is wrapped in an `expression_statement`, which already counts.
|
|
223
|
-
if node.
|
|
223
|
+
if node.kind_name() == "internal_module" && parent_kind != Some("expression_statement") {
|
|
224
224
|
return true;
|
|
225
225
|
}
|
|
226
226
|
// A Ruby endless method (`def f(x) = expr`) stores its single-statement body directly in the
|
|
227
227
|
// `body` field instead of a positional `body_statement` container.
|
|
228
228
|
if (parent_kind == Some("method") || parent_kind == Some("singleton_method"))
|
|
229
|
-
&& node.
|
|
230
|
-
&& is_field_of_parent(node, "body")
|
|
229
|
+
&& node.kind_name() != "body_statement"
|
|
230
|
+
&& is_field_of_parent(node, parent, "body")
|
|
231
231
|
{
|
|
232
232
|
return true;
|
|
233
233
|
}
|
|
234
234
|
// C++ `friend class X;` declares on its own; `friend void g() { ... }` merely wraps a counted
|
|
235
235
|
// definition.
|
|
236
|
-
if node.
|
|
237
|
-
return !crate::util::named_children(node)
|
|
238
|
-
.
|
|
239
|
-
|
|
236
|
+
if node.kind_name() == "friend_declaration" {
|
|
237
|
+
return !crate::util::named_children(node).iter().any(|child| {
|
|
238
|
+
child.kind_name() == "declaration" || child.kind_name() == "function_definition"
|
|
239
|
+
});
|
|
240
240
|
}
|
|
241
241
|
// A Rust item-position macro invocation (`foo! {}` at module level) has no expression_statement
|
|
242
242
|
// wrapper; the semicolon form does and already counts through it.
|
|
243
|
-
if node.
|
|
243
|
+
if node.kind_name() == "macro_invocation"
|
|
244
244
|
&& (parent_kind == Some("source_file") || parent_kind == Some("declaration_list"))
|
|
245
245
|
{
|
|
246
246
|
return true;
|
|
@@ -248,29 +248,33 @@ fn counts_contextually(node: Node<'_>) -> bool {
|
|
|
248
248
|
// Go struct fields and interface members count like other languages' member declarations, but
|
|
249
249
|
// only inside a named type declaration; inline anonymous types (`var x struct{ ... }`,
|
|
250
250
|
// `func f(h interface{ ... })`) are part of one declaration.
|
|
251
|
-
if node.
|
|
251
|
+
if node.kind_name() == "field_declaration" && parent_kind == Some("field_declaration_list") {
|
|
252
252
|
return is_go_declared_type_body(
|
|
253
|
-
|
|
253
|
+
parent.and_then(|parent| parent.parent_node()),
|
|
254
254
|
"struct_type",
|
|
255
255
|
);
|
|
256
256
|
}
|
|
257
|
-
if node.
|
|
258
|
-
|
|
257
|
+
if node.kind_name() == "method_elem"
|
|
258
|
+
|| node.kind_name() == "method_spec"
|
|
259
|
+
|| node.kind_name() == "type_elem"
|
|
260
|
+
{
|
|
261
|
+
return is_go_declared_type_body(parent, "interface_type");
|
|
259
262
|
}
|
|
260
263
|
false
|
|
261
264
|
}
|
|
262
265
|
|
|
263
266
|
fn is_go_declared_type_body(type_node: Option<Node<'_>>, expected_kind: &str) -> bool {
|
|
264
|
-
let Some(type_node) = type_node.filter(|type_node| type_node.
|
|
267
|
+
let Some(type_node) = type_node.filter(|type_node| type_node.kind_name() == expected_kind)
|
|
268
|
+
else {
|
|
265
269
|
return false;
|
|
266
270
|
};
|
|
267
|
-
type_node
|
|
268
|
-
.parent()
|
|
269
|
-
|
|
271
|
+
type_node.parent_node().is_some_and(|parent| {
|
|
272
|
+
parent.kind_name() == "type_spec" || parent.kind_name() == "type_alias"
|
|
273
|
+
})
|
|
270
274
|
}
|
|
271
275
|
|
|
272
|
-
fn is_field_of_parent(node: Node<'_>, field_name: &str) -> bool {
|
|
273
|
-
let Some(parent) =
|
|
276
|
+
fn is_field_of_parent(node: Node<'_>, parent: Option<Node<'_>>, field_name: &str) -> bool {
|
|
277
|
+
let Some(parent) = parent else {
|
|
274
278
|
return false;
|
|
275
279
|
};
|
|
276
280
|
find_children_by_field_name(parent, field_name)
|
|
@@ -283,6 +287,6 @@ fn count_bare_alternatives(node: Node<'_>) -> u64 {
|
|
|
283
287
|
// comment between an `elif_clause` and `else_clause` must not be miscounted as a bare branch.
|
|
284
288
|
find_children_by_field_name(node, "alternative")
|
|
285
289
|
.iter()
|
|
286
|
-
.filter(|child| !child.is_extra() && !ELSE_CLAUSE_NODE_TYPES.contains(&child.
|
|
290
|
+
.filter(|child| !child.is_extra() && !ELSE_CLAUSE_NODE_TYPES.contains(&child.kind_name()))
|
|
287
291
|
.count() as u64
|
|
288
292
|
}
|
package/native/src/near_miss.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
use
|
|
1
|
+
use rustc_hash::{FxHashMap, FxHashSet};
|
|
2
2
|
|
|
3
3
|
/// N-gram size for the candidate index and local-match anchors (NIL's default); shared with
|
|
4
4
|
/// crossFileNearMiss.ts.
|
|
@@ -40,7 +40,7 @@ pub(crate) struct Block {
|
|
|
40
40
|
is_content: Vec<bool>,
|
|
41
41
|
/// Identifiers anonymized by first occurrence within the block.
|
|
42
42
|
sequence: Vec<i32>,
|
|
43
|
-
pub ngrams:
|
|
43
|
+
pub ngrams: FxHashSet<i32>,
|
|
44
44
|
/// The n-grams occurring exactly once in the block with their offsets, sorted by hash so two
|
|
45
45
|
/// blocks' local-match anchors intersect by merging.
|
|
46
46
|
unique_ngrams: Vec<(i32, usize)>,
|
|
@@ -77,7 +77,7 @@ impl Block {
|
|
|
77
77
|
})
|
|
78
78
|
})
|
|
79
79
|
.collect();
|
|
80
|
-
let mut occurrence_counts:
|
|
80
|
+
let mut occurrence_counts: FxHashMap<i32, usize> = FxHashMap::default();
|
|
81
81
|
for &hash in &ngram_hashes {
|
|
82
82
|
*occurrence_counts.entry(hash).or_insert(0) += 1;
|
|
83
83
|
}
|
|
@@ -134,13 +134,13 @@ pub(crate) struct Matcher {
|
|
|
134
134
|
/// capped at MAX_CONTENT_WEIGHT.
|
|
135
135
|
/// Rare names and values (the logic a copy preserves) outweigh ubiquitous ones, following
|
|
136
136
|
/// the information-theoretic weighting of ECScan's essence-clone detection (2025).
|
|
137
|
-
weights:
|
|
137
|
+
weights: FxHashMap<i32, u64>,
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
impl Matcher {
|
|
141
141
|
/// Weights every block's content counts, which verification requires.
|
|
142
142
|
pub fn new(blocks: &mut [Block], min_tokens: usize, min_similarity_percent: usize) -> Matcher {
|
|
143
|
-
let mut document_frequencies:
|
|
143
|
+
let mut document_frequencies: FxHashMap<i32, usize> = FxHashMap::default();
|
|
144
144
|
for block in blocks.iter() {
|
|
145
145
|
for &(symbol, _) in &block.content {
|
|
146
146
|
*document_frequencies.entry(symbol).or_insert(0) += 1;
|
|
@@ -347,7 +347,7 @@ fn anchored_token_count(segment: &[(usize, usize)]) -> usize {
|
|
|
347
347
|
/// Identifiers renumbered by first occurrence within `symbols`, so a range compares the same
|
|
348
348
|
/// wherever it sits in its file.
|
|
349
349
|
fn anonymize(symbols: &[i32]) -> Vec<i32> {
|
|
350
|
-
let mut index_by_identifier:
|
|
350
|
+
let mut index_by_identifier: FxHashMap<i32, i32> = FxHashMap::default();
|
|
351
351
|
symbols
|
|
352
352
|
.iter()
|
|
353
353
|
.map(|&symbol| {
|
|
@@ -427,7 +427,7 @@ fn lcs_length(a: &[i32], b: &[i32]) -> usize {
|
|
|
427
427
|
return 0;
|
|
428
428
|
}
|
|
429
429
|
let word_count = a.len().div_ceil(64);
|
|
430
|
-
let mut position_masks:
|
|
430
|
+
let mut position_masks: FxHashMap<i32, Vec<u64>> = FxHashMap::default();
|
|
431
431
|
for (index, &symbol) in a.iter().enumerate() {
|
|
432
432
|
position_masks
|
|
433
433
|
.entry(symbol)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
use std::cell::{Cell, RefCell};
|
|
2
|
+
use std::marker::PhantomData;
|
|
3
|
+
|
|
4
|
+
use rustc_hash::FxHashMap;
|
|
5
|
+
use tree_sitter::{Node, Tree};
|
|
6
|
+
|
|
7
|
+
use crate::languages::LanguageDefinition;
|
|
8
|
+
|
|
9
|
+
thread_local! {
|
|
10
|
+
/// Node id -> parent for the tree indexed on this thread. The lifetime is erased to store the
|
|
11
|
+
/// nodes; TreeIndex borrows the tree and clears the map on drop, so no entry outlives it.
|
|
12
|
+
static PARENTS: RefCell<FxHashMap<usize, Node<'static>>> = RefCell::new(FxHashMap::default());
|
|
13
|
+
/// Node kind names of the indexed tree's language, indexed by kind id.
|
|
14
|
+
static KIND_NAMES: Cell<&'static [&'static str]> = const { Cell::new(&[]) };
|
|
15
|
+
/// Name of the indexed tree's language.
|
|
16
|
+
static LANGUAGE_NAME: Cell<&'static str> = const { Cell::new("") };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// The language of the tree indexed on this thread, or "" when none is.
|
|
20
|
+
pub fn language_name() -> &'static str {
|
|
21
|
+
LANGUAGE_NAME.get()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Makes NodeExt lookups O(1) while the tree is being measured: tree-sitter's `Node::parent`
|
|
25
|
+
/// rescans from the root on every call (linear in the sibling counts along the path), and
|
|
26
|
+
/// `Node::kind` measures and UTF-8-validates the C string on every call.
|
|
27
|
+
pub struct TreeIndex<'t> {
|
|
28
|
+
tree: PhantomData<&'t Tree>,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// The metric passes recurse per tree level and would overflow the native stack (a
|
|
32
|
+
/// process-killing SIGSEGV, not a catchable error) around depth ~20k, so deeper trees are refused.
|
|
33
|
+
const MAX_TREE_DEPTH: usize = 5_000;
|
|
34
|
+
|
|
35
|
+
impl<'t> TreeIndex<'t> {
|
|
36
|
+
pub fn new(tree: &'t Tree, language: &LanguageDefinition) -> Result<TreeIndex<'t>, String> {
|
|
37
|
+
// Constructed first so that the depth error below also clears the partial index on drop.
|
|
38
|
+
let index = TreeIndex { tree: PhantomData };
|
|
39
|
+
KIND_NAMES.set(language.kind_names());
|
|
40
|
+
LANGUAGE_NAME.set(language.name);
|
|
41
|
+
PARENTS.with_borrow_mut(|parents| {
|
|
42
|
+
assert!(
|
|
43
|
+
parents.is_empty(),
|
|
44
|
+
"one tree is indexed per thread at a time"
|
|
45
|
+
);
|
|
46
|
+
let mut cursor = tree.walk();
|
|
47
|
+
let mut stack: Vec<Node<'static>> = Vec::new();
|
|
48
|
+
loop {
|
|
49
|
+
let node = erase_lifetime(cursor.node());
|
|
50
|
+
if let Some(&parent) = stack.last() {
|
|
51
|
+
parents.insert(node.id(), parent);
|
|
52
|
+
}
|
|
53
|
+
if cursor.goto_first_child() {
|
|
54
|
+
stack.push(node);
|
|
55
|
+
if stack.len() > MAX_TREE_DEPTH {
|
|
56
|
+
return Err(format!("tree depth exceeds {MAX_TREE_DEPTH}"));
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
while !cursor.goto_next_sibling() {
|
|
61
|
+
if !cursor.goto_parent() {
|
|
62
|
+
return Ok(());
|
|
63
|
+
}
|
|
64
|
+
stack.pop();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
})?;
|
|
68
|
+
Ok(index)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl Drop for TreeIndex<'_> {
|
|
73
|
+
fn drop(&mut self) {
|
|
74
|
+
PARENTS.with_borrow_mut(|parents| parents.clear());
|
|
75
|
+
KIND_NAMES.set(&[]);
|
|
76
|
+
LANGUAGE_NAME.set("");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn erase_lifetime(node: Node<'_>) -> Node<'static> {
|
|
81
|
+
// SAFETY: Node only borrows its tree; the erased node is stored in PARENTS, which the
|
|
82
|
+
// TreeIndex borrowing that tree empties before the tree can be dropped.
|
|
83
|
+
unsafe { std::mem::transmute::<Node<'_>, Node<'static>>(node) }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pub trait NodeExt<'t> {
|
|
87
|
+
/// `Node::parent`, answered from the TreeIndex when one is installed.
|
|
88
|
+
fn parent_node(self) -> Option<Node<'t>>;
|
|
89
|
+
/// `Node::kind`, answered from the TreeIndex when one is installed.
|
|
90
|
+
fn kind_name(self) -> &'static str;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
impl<'t> NodeExt<'t> for Node<'t> {
|
|
94
|
+
fn parent_node(self) -> Option<Node<'t>> {
|
|
95
|
+
// tree-sitter locates the parent by byte range, which for a zero-width node (a MISSING
|
|
96
|
+
// token, an empty construct) can select a sibling touching the same offset instead of the
|
|
97
|
+
// structural parent; those rare nodes keep tree-sitter's answer so metrics stay unchanged.
|
|
98
|
+
if self.start_byte() == self.end_byte() {
|
|
99
|
+
return self.parent();
|
|
100
|
+
}
|
|
101
|
+
PARENTS
|
|
102
|
+
.with_borrow(|parents| {
|
|
103
|
+
parents.get(&self.id()).map(|parent| {
|
|
104
|
+
// SAFETY: ids are addresses inside the indexed tree, which outlives the
|
|
105
|
+
// index, so a hit means `parent` belongs to the tree `self` borrows.
|
|
106
|
+
unsafe { std::mem::transmute::<Node<'static>, Node<'t>>(*parent) }
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
.or_else(|| self.parent())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn kind_name(self) -> &'static str {
|
|
113
|
+
KIND_NAMES
|
|
114
|
+
.get()
|
|
115
|
+
.get(usize::from(self.kind_id()))
|
|
116
|
+
.copied()
|
|
117
|
+
.unwrap_or_else(|| self.kind())
|
|
118
|
+
}
|
|
119
|
+
}
|
package/native/src/util.rs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
use tree_sitter::Node;
|
|
2
2
|
|
|
3
|
+
use crate::tree_index::NodeExt;
|
|
4
|
+
|
|
3
5
|
/// The measured source. Trees are parsed from UTF-16 (matching node-tree-sitter, which parses
|
|
4
6
|
/// JavaScript strings as UTF-16 — tree-sitter's error recovery can differ between encodings), so
|
|
5
7
|
/// node "byte" offsets and columns are UTF-16 code units x 2; this maps them back to UTF-8 slices
|
|
@@ -51,30 +53,12 @@ pub fn node_text<'a>(node: Node<'_>, code: &Source<'a>) -> &'a str {
|
|
|
51
53
|
&code.code[code.utf8_offset(node.start_byte())..code.utf8_offset(node.end_byte())]
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
/// Kotlin spells
|
|
55
|
-
///
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
&& node
|
|
60
|
-
parent.kind() == "callable_reference"
|
|
61
|
-
&& parent
|
|
62
|
-
.named_child(0)
|
|
63
|
-
.is_some_and(|first| first.id() == node.id())
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/// Whether the node is a leaf for token-level walks. Kotlin soft keywords used as names (`value`,
|
|
68
|
-
/// `data`, `get`, ...) parse as a `simple_identifier` — or its aliases `interpolated_identifier`
|
|
69
|
-
/// (`"$value"`) and `type_identifier` (`value::size`) — wrapping an anonymous keyword token, so a
|
|
70
|
-
/// plain leaf check would see the keyword instead of the identifier. Every other grammar's
|
|
71
|
-
/// identifier kinds are already leaves.
|
|
72
|
-
pub fn is_identifier_leaf(node: Node<'_>) -> bool {
|
|
73
|
-
node.child_count() == 0
|
|
74
|
-
|| matches!(
|
|
75
|
-
node.kind(),
|
|
76
|
-
"simple_identifier" | "interpolated_identifier" | "type_identifier"
|
|
77
|
-
)
|
|
56
|
+
/// Kotlin spells `true`, `false`, and `null` as plain identifiers, which the other grammars give
|
|
57
|
+
/// literal node kinds of their own (an identifier with that text, e.g. Go's `null`, is a variable).
|
|
58
|
+
pub fn is_kotlin_keyword_literal(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
59
|
+
crate::tree_index::language_name() == "kotlin"
|
|
60
|
+
&& node.kind_name() == "identifier"
|
|
61
|
+
&& matches!(node_text(node, code), "true" | "false" | "null")
|
|
78
62
|
}
|
|
79
63
|
|
|
80
64
|
pub fn named_children<'t>(node: Node<'t>) -> Vec<Node<'t>> {
|
|
@@ -89,8 +73,8 @@ pub fn all_children<'t>(node: Node<'t>) -> Vec<Node<'t>> {
|
|
|
89
73
|
|
|
90
74
|
/// Children carrying the field, with node-tree-sitter's vendored-core semantics: extra children
|
|
91
75
|
/// (error-recovery nodes, comments) inherit the field of the preceding structural sibling.
|
|
92
|
-
///
|
|
93
|
-
///
|
|
76
|
+
/// The core instead reports no field for extras (ts_node_field_name_for_child returns early for
|
|
77
|
+
/// them), which would desynchronize field-based extraction on malformed source.
|
|
94
78
|
pub fn find_children_by_field_name<'t>(node: Node<'t>, field_name: &str) -> Vec<Node<'t>> {
|
|
95
79
|
let mut children = Vec::new();
|
|
96
80
|
let mut preceding_structural_field: Option<&'static str> = None;
|
|
@@ -164,35 +148,11 @@ pub fn to_int32(value: i64) -> i32 {
|
|
|
164
148
|
value as i32
|
|
165
149
|
}
|
|
166
150
|
|
|
167
|
-
/// The body following a Kotlin `if_expression`'s bare `else` keyword (the grammar has no else
|
|
168
|
-
/// clause node and no fields), or None for other languages' if nodes and else-less ifs.
|
|
169
|
-
pub fn kotlin_else_body(if_node: Node<'_>) -> Option<Node<'_>> {
|
|
170
|
-
if if_node.kind() != "if_expression" {
|
|
171
|
-
return None;
|
|
172
|
-
}
|
|
173
|
-
let children = all_children(if_node);
|
|
174
|
-
let else_index = children
|
|
175
|
-
.iter()
|
|
176
|
-
.position(|child| !child.is_named() && child.kind() == "else")?;
|
|
177
|
-
children[else_index + 1..]
|
|
178
|
-
.iter()
|
|
179
|
-
.copied()
|
|
180
|
-
.find(|child| child.kind() == "control_structure_body")
|
|
181
|
-
}
|
|
182
|
-
|
|
183
151
|
/// Kotlin's `try { } catch { }` shares its node kind with Rust's `?` operator; only the Kotlin form
|
|
184
|
-
///
|
|
152
|
+
/// starts with the `try` keyword.
|
|
185
153
|
pub fn is_kotlin_try_expression(node: Node<'_>) -> bool {
|
|
186
|
-
node.
|
|
187
|
-
&&
|
|
188
|
-
.
|
|
189
|
-
.
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/// Whether a Kotlin else body is a braceless `else if`: the nested if sits directly in the
|
|
193
|
-
/// control_structure_body, whereas a braced `else { if ... }` wraps it in `statements`.
|
|
194
|
-
pub fn is_kotlin_else_if_body(else_body: Node<'_>) -> bool {
|
|
195
|
-
named_children(else_body)
|
|
196
|
-
.iter()
|
|
197
|
-
.any(|child| child.kind() == "if_expression")
|
|
154
|
+
node.kind_name() == "try_expression"
|
|
155
|
+
&& node
|
|
156
|
+
.child(0)
|
|
157
|
+
.is_some_and(|keyword| !keyword.is_named() && keyword.kind_name() == "try")
|
|
198
158
|
}
|