code-gauge 4.7.0 → 4.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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 +1 -0
- package/native/Cargo.toml +2 -1
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +134 -230
- package/native/src/dep_degree.rs +42 -39
- package/native/src/duplication.rs +65 -63
- package/native/src/functions.rs +169 -152
- package/native/src/languages.rs +20 -0
- package/native/src/lib.rs +10 -5
- package/native/src/measure.rs +109 -123
- package/native/src/napi.rs +61 -2
- package/native/src/ncss.rs +79 -84
- package/native/src/near_miss.rs +7 -7
- package/native/src/tree_index.rs +110 -0
- package/native/src/util.rs +17 -12
- package/native/src/worker_pool.rs +53 -0
- package/package.json +8 -8
package/native/src/ncss.rs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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
7
|
pub const COMMENT_NODE_TYPES: &[&str] = &[
|
|
7
8
|
"comment",
|
|
@@ -56,54 +57,39 @@ const BODYLESS_NCSS_SPECIFIER_TYPES: &[&str] = &[
|
|
|
56
57
|
"class_specifier",
|
|
57
58
|
];
|
|
58
59
|
|
|
59
|
-
///
|
|
60
|
+
/// A node's own non-commenting source statement (NCSS) count, PMD-style: one per declaration,
|
|
60
61
|
/// 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
|
-
|
|
62
|
+
/// resource); `try` itself, braces, blank lines, and comments count 0. `parent` is the node's
|
|
63
|
+
/// `parent_node()`, looked up once by the caller and shared by every check.
|
|
84
64
|
pub fn ncss_contribution(
|
|
85
65
|
node: Node<'_>,
|
|
86
|
-
|
|
87
|
-
|
|
66
|
+
parent: Option<Node<'_>>,
|
|
67
|
+
countable: &FxHashSet<&'static str>,
|
|
68
|
+
containers: &FxHashSet<&'static str>,
|
|
88
69
|
) -> u64 {
|
|
89
|
-
if !node.is_named()
|
|
70
|
+
if !node.is_named()
|
|
71
|
+
|| COMMENT_NODE_TYPES.contains(&node.kind_name())
|
|
72
|
+
|| is_for_header_node(node, parent)
|
|
73
|
+
{
|
|
90
74
|
return 0;
|
|
91
75
|
}
|
|
92
76
|
// A Kotlin accessor without a body (`private set`) only changes visibility and declares
|
|
93
77
|
// nothing of its own; it parses as a sibling of its property and must not count positionally.
|
|
94
|
-
if (node.
|
|
78
|
+
if (node.kind_name() == "getter" || node.kind_name() == "setter")
|
|
95
79
|
&& !crate::functions::is_implemented_function(node)
|
|
96
80
|
{
|
|
97
81
|
return 0;
|
|
98
82
|
}
|
|
99
83
|
|
|
100
84
|
let mut contribution = 0;
|
|
101
|
-
let positional = is_in_container_position(
|
|
102
|
-
&& !containers.contains(node.
|
|
103
|
-
&& !POSITIONAL_EXCLUSION_TYPES.contains(&node.
|
|
85
|
+
let positional = is_in_container_position(parent, containers)
|
|
86
|
+
&& !containers.contains(node.kind_name())
|
|
87
|
+
&& !POSITIONAL_EXCLUSION_TYPES.contains(&node.kind_name())
|
|
104
88
|
&& !crate::util::is_kotlin_try_expression(node)
|
|
105
|
-
&& !(node.
|
|
106
|
-
if (counts_through_node_type(node, countable)
|
|
89
|
+
&& !(node.kind_name() == "label" && node.child_count() == 0);
|
|
90
|
+
if (counts_through_node_type(node, countable)
|
|
91
|
+
|| positional
|
|
92
|
+
|| counts_contextually(node, parent))
|
|
107
93
|
&& !is_declaration_wrapper(node, countable)
|
|
108
94
|
{
|
|
109
95
|
contribution += 1;
|
|
@@ -112,7 +98,7 @@ pub fn ncss_contribution(
|
|
|
112
98
|
// A bare else branch (Java/Go `alternative:` without an else-clause wrapper, or Kotlin's bare
|
|
113
99
|
// `else` keyword) counts 1 like the `else` keyword does in PMD; an `else if` chain charges the
|
|
114
100
|
// nested if separately on top.
|
|
115
|
-
if IF_NODE_TYPES.contains(&node.
|
|
101
|
+
if IF_NODE_TYPES.contains(&node.kind_name()) {
|
|
116
102
|
contribution += count_bare_alternatives(node)
|
|
117
103
|
+ u64::from(crate::util::kotlin_else_body(node).is_some());
|
|
118
104
|
}
|
|
@@ -120,16 +106,16 @@ pub fn ncss_contribution(
|
|
|
120
106
|
contribution
|
|
121
107
|
}
|
|
122
108
|
|
|
123
|
-
fn counts_through_node_type(node: Node<'_>, countable: &
|
|
124
|
-
if !countable.contains(node.
|
|
109
|
+
fn counts_through_node_type(node: Node<'_>, countable: &FxHashSet<&'static str>) -> bool {
|
|
110
|
+
if !countable.contains(node.kind_name()) {
|
|
125
111
|
return false;
|
|
126
112
|
}
|
|
127
|
-
if BODYLESS_NCSS_SPECIFIER_TYPES.contains(&node.
|
|
113
|
+
if BODYLESS_NCSS_SPECIFIER_TYPES.contains(&node.kind_name()) {
|
|
128
114
|
return node.child_by_field_name("body").is_some();
|
|
129
115
|
}
|
|
130
116
|
// A try-with-resources `resource` counts only when it declares a variable; `try (r)` reuses an
|
|
131
117
|
// existing one and adds no statement (matching PMD).
|
|
132
|
-
if node.
|
|
118
|
+
if node.kind_name() == "resource" {
|
|
133
119
|
return node.child_by_field_name("name").is_some();
|
|
134
120
|
}
|
|
135
121
|
true
|
|
@@ -137,28 +123,31 @@ fn counts_through_node_type(node: Node<'_>, countable: &HashSet<&'static str>) -
|
|
|
137
123
|
|
|
138
124
|
/// Direct container children count positionally; Ruby's `(foo; bar)` statement parentheses are
|
|
139
125
|
/// transparent, so their children count when the parentheses themselves sit in a container.
|
|
140
|
-
fn is_in_container_position(
|
|
141
|
-
|
|
126
|
+
fn is_in_container_position(
|
|
127
|
+
parent: Option<Node<'_>>,
|
|
128
|
+
containers: &FxHashSet<&'static str>,
|
|
129
|
+
) -> bool {
|
|
130
|
+
let mut ancestor = parent;
|
|
142
131
|
while let Some(current) = ancestor {
|
|
143
|
-
if current.
|
|
144
|
-
return containers.contains(current.
|
|
132
|
+
if current.kind_name() != "parenthesized_statements" {
|
|
133
|
+
return containers.contains(current.kind_name());
|
|
145
134
|
}
|
|
146
|
-
ancestor = current.
|
|
135
|
+
ancestor = current.parent_node();
|
|
147
136
|
}
|
|
148
137
|
false
|
|
149
138
|
}
|
|
150
139
|
|
|
151
140
|
/// `export const x = 1` nests a countable declaration inside `export_statement`; only the inner
|
|
152
141
|
/// declaration counts, mirroring how PMD counts one statement per declared entity.
|
|
153
|
-
fn is_declaration_wrapper(node: Node<'_>, countable: &
|
|
154
|
-
if node.
|
|
142
|
+
fn is_declaration_wrapper(node: Node<'_>, countable: &FxHashSet<&'static str>) -> bool {
|
|
143
|
+
if node.kind_name() != "export_statement" {
|
|
155
144
|
return false;
|
|
156
145
|
}
|
|
157
146
|
node.child_by_field_name("declaration")
|
|
158
147
|
.is_some_and(|declaration| {
|
|
159
|
-
countable.contains(declaration.
|
|
160
|
-
|| declaration.
|
|
161
|
-
|| declaration.
|
|
148
|
+
countable.contains(declaration.kind_name())
|
|
149
|
+
|| declaration.kind_name() == "internal_module"
|
|
150
|
+
|| declaration.kind_name() == "ambient_declaration"
|
|
162
151
|
})
|
|
163
152
|
}
|
|
164
153
|
|
|
@@ -169,21 +158,21 @@ const FOR_HEADER_FIELD_NAMES: &[&str] =
|
|
|
169
158
|
/// statement, which already counts; PMD does not count them separately. JavaScript parses the
|
|
170
159
|
/// condition as an `expression_statement` and Go parses the update as an `inc_statement`, so all
|
|
171
160
|
/// header fields must be excluded, not just the initializer.
|
|
172
|
-
fn is_for_header_node(node: Node<'_
|
|
173
|
-
let Some(parent) =
|
|
161
|
+
fn is_for_header_node(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
|
|
162
|
+
let Some(parent) = parent else {
|
|
174
163
|
return false;
|
|
175
164
|
};
|
|
176
165
|
// C++20 range-for initializers nest one level deeper: for_range_loop > init_statement > node.
|
|
177
|
-
if parent.
|
|
166
|
+
if parent.kind_name() == "init_statement"
|
|
178
167
|
&& parent
|
|
179
|
-
.
|
|
180
|
-
.is_some_and(|grandparent| grandparent.
|
|
168
|
+
.parent_node()
|
|
169
|
+
.is_some_and(|grandparent| grandparent.kind_name() == "for_range_loop")
|
|
181
170
|
{
|
|
182
171
|
return true;
|
|
183
172
|
}
|
|
184
|
-
if parent.
|
|
185
|
-
&& parent.
|
|
186
|
-
&& parent.
|
|
173
|
+
if parent.kind_name() != "for_statement"
|
|
174
|
+
&& parent.kind_name() != "for_clause"
|
|
175
|
+
&& parent.kind_name() != "for_range_loop"
|
|
187
176
|
{
|
|
188
177
|
return false;
|
|
189
178
|
}
|
|
@@ -195,52 +184,54 @@ fn is_for_header_node(node: Node<'_>) -> bool {
|
|
|
195
184
|
}
|
|
196
185
|
|
|
197
186
|
/// Statements only countable by their position: constructs without a dedicated statement node.
|
|
198
|
-
fn counts_contextually(node: Node<'_
|
|
199
|
-
let parent_kind =
|
|
187
|
+
fn counts_contextually(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
|
|
188
|
+
let parent_kind = parent.map(|parent| parent.kind_name());
|
|
200
189
|
// A Java instance initializer is a bare `block` in the class body; PMD counts it like the
|
|
201
190
|
// `static_initializer` declaration it parallels.
|
|
202
|
-
if node.
|
|
191
|
+
if node.kind_name() == "block" && parent_kind == Some("class_body") {
|
|
203
192
|
return true;
|
|
204
193
|
}
|
|
205
194
|
// TypeScript interface members (see INTERFACE_MEMBER_NODE_TYPES).
|
|
206
|
-
if INTERFACE_MEMBER_NODE_TYPES.contains(&node.
|
|
195
|
+
if INTERFACE_MEMBER_NODE_TYPES.contains(&node.kind_name())
|
|
196
|
+
&& parent_kind == Some("interface_body")
|
|
197
|
+
{
|
|
207
198
|
return true;
|
|
208
199
|
}
|
|
209
200
|
// A braceless Rust match-arm body (`1 => foo()`) has no expression_statement wrapper; count the
|
|
210
201
|
// value expression so braced and unbraced arms measure alike.
|
|
211
202
|
if parent_kind == Some("match_arm")
|
|
212
|
-
&& node.
|
|
213
|
-
&& is_field_of_parent(node, "value")
|
|
203
|
+
&& node.kind_name() != "block"
|
|
204
|
+
&& is_field_of_parent(node, parent, "value")
|
|
214
205
|
{
|
|
215
206
|
return true;
|
|
216
207
|
}
|
|
217
208
|
// A TypeScript class-body method overload signature declares a member like its interface twin.
|
|
218
|
-
if node.
|
|
209
|
+
if node.kind_name() == "method_signature" && parent_kind == Some("class_body") {
|
|
219
210
|
return true;
|
|
220
211
|
}
|
|
221
212
|
// An ambient `declare namespace M { ... }` is a bare `internal_module`; the non-ambient
|
|
222
213
|
// `namespace N { ... }` is wrapped in an `expression_statement`, which already counts.
|
|
223
|
-
if node.
|
|
214
|
+
if node.kind_name() == "internal_module" && parent_kind != Some("expression_statement") {
|
|
224
215
|
return true;
|
|
225
216
|
}
|
|
226
217
|
// A Ruby endless method (`def f(x) = expr`) stores its single-statement body directly in the
|
|
227
218
|
// `body` field instead of a positional `body_statement` container.
|
|
228
219
|
if (parent_kind == Some("method") || parent_kind == Some("singleton_method"))
|
|
229
|
-
&& node.
|
|
230
|
-
&& is_field_of_parent(node, "body")
|
|
220
|
+
&& node.kind_name() != "body_statement"
|
|
221
|
+
&& is_field_of_parent(node, parent, "body")
|
|
231
222
|
{
|
|
232
223
|
return true;
|
|
233
224
|
}
|
|
234
225
|
// C++ `friend class X;` declares on its own; `friend void g() { ... }` merely wraps a counted
|
|
235
226
|
// definition.
|
|
236
|
-
if node.
|
|
237
|
-
return !crate::util::named_children(node)
|
|
238
|
-
.
|
|
239
|
-
|
|
227
|
+
if node.kind_name() == "friend_declaration" {
|
|
228
|
+
return !crate::util::named_children(node).iter().any(|child| {
|
|
229
|
+
child.kind_name() == "declaration" || child.kind_name() == "function_definition"
|
|
230
|
+
});
|
|
240
231
|
}
|
|
241
232
|
// A Rust item-position macro invocation (`foo! {}` at module level) has no expression_statement
|
|
242
233
|
// wrapper; the semicolon form does and already counts through it.
|
|
243
|
-
if node.
|
|
234
|
+
if node.kind_name() == "macro_invocation"
|
|
244
235
|
&& (parent_kind == Some("source_file") || parent_kind == Some("declaration_list"))
|
|
245
236
|
{
|
|
246
237
|
return true;
|
|
@@ -248,29 +239,33 @@ fn counts_contextually(node: Node<'_>) -> bool {
|
|
|
248
239
|
// Go struct fields and interface members count like other languages' member declarations, but
|
|
249
240
|
// only inside a named type declaration; inline anonymous types (`var x struct{ ... }`,
|
|
250
241
|
// `func f(h interface{ ... })`) are part of one declaration.
|
|
251
|
-
if node.
|
|
242
|
+
if node.kind_name() == "field_declaration" && parent_kind == Some("field_declaration_list") {
|
|
252
243
|
return is_go_declared_type_body(
|
|
253
|
-
|
|
244
|
+
parent.and_then(|parent| parent.parent_node()),
|
|
254
245
|
"struct_type",
|
|
255
246
|
);
|
|
256
247
|
}
|
|
257
|
-
if node.
|
|
258
|
-
|
|
248
|
+
if node.kind_name() == "method_elem"
|
|
249
|
+
|| node.kind_name() == "method_spec"
|
|
250
|
+
|| node.kind_name() == "type_elem"
|
|
251
|
+
{
|
|
252
|
+
return is_go_declared_type_body(parent, "interface_type");
|
|
259
253
|
}
|
|
260
254
|
false
|
|
261
255
|
}
|
|
262
256
|
|
|
263
257
|
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.
|
|
258
|
+
let Some(type_node) = type_node.filter(|type_node| type_node.kind_name() == expected_kind)
|
|
259
|
+
else {
|
|
265
260
|
return false;
|
|
266
261
|
};
|
|
267
|
-
type_node
|
|
268
|
-
.parent()
|
|
269
|
-
|
|
262
|
+
type_node.parent_node().is_some_and(|parent| {
|
|
263
|
+
parent.kind_name() == "type_spec" || parent.kind_name() == "type_alias"
|
|
264
|
+
})
|
|
270
265
|
}
|
|
271
266
|
|
|
272
|
-
fn is_field_of_parent(node: Node<'_>, field_name: &str) -> bool {
|
|
273
|
-
let Some(parent) =
|
|
267
|
+
fn is_field_of_parent(node: Node<'_>, parent: Option<Node<'_>>, field_name: &str) -> bool {
|
|
268
|
+
let Some(parent) = parent else {
|
|
274
269
|
return false;
|
|
275
270
|
};
|
|
276
271
|
find_children_by_field_name(parent, field_name)
|
|
@@ -283,6 +278,6 @@ fn count_bare_alternatives(node: Node<'_>) -> u64 {
|
|
|
283
278
|
// comment between an `elif_clause` and `else_clause` must not be miscounted as a bare branch.
|
|
284
279
|
find_children_by_field_name(node, "alternative")
|
|
285
280
|
.iter()
|
|
286
|
-
.filter(|child| !child.is_extra() && !ELSE_CLAUSE_NODE_TYPES.contains(&child.
|
|
281
|
+
.filter(|child| !child.is_extra() && !ELSE_CLAUSE_NODE_TYPES.contains(&child.kind_name()))
|
|
287
282
|
.count() as u64
|
|
288
283
|
}
|
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,110 @@
|
|
|
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
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Makes NodeExt lookups O(1) while the tree is being measured: tree-sitter's `Node::parent`
|
|
18
|
+
/// rescans from the root on every call (linear in the sibling counts along the path), and
|
|
19
|
+
/// `Node::kind` measures and UTF-8-validates the C string on every call.
|
|
20
|
+
pub struct TreeIndex<'t> {
|
|
21
|
+
tree: PhantomData<&'t Tree>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// The metric passes recurse per tree level and would overflow the native stack (a
|
|
25
|
+
/// process-killing SIGSEGV, not a catchable error) around depth ~20k, so deeper trees are refused.
|
|
26
|
+
const MAX_TREE_DEPTH: usize = 5_000;
|
|
27
|
+
|
|
28
|
+
impl<'t> TreeIndex<'t> {
|
|
29
|
+
pub fn new(tree: &'t Tree, language: &LanguageDefinition) -> Result<TreeIndex<'t>, String> {
|
|
30
|
+
// Constructed first so that the depth error below also clears the partial index on drop.
|
|
31
|
+
let index = TreeIndex { tree: PhantomData };
|
|
32
|
+
KIND_NAMES.set(language.kind_names());
|
|
33
|
+
PARENTS.with_borrow_mut(|parents| {
|
|
34
|
+
assert!(
|
|
35
|
+
parents.is_empty(),
|
|
36
|
+
"one tree is indexed per thread at a time"
|
|
37
|
+
);
|
|
38
|
+
let mut cursor = tree.walk();
|
|
39
|
+
let mut stack: Vec<Node<'static>> = Vec::new();
|
|
40
|
+
loop {
|
|
41
|
+
let node = erase_lifetime(cursor.node());
|
|
42
|
+
if let Some(&parent) = stack.last() {
|
|
43
|
+
parents.insert(node.id(), parent);
|
|
44
|
+
}
|
|
45
|
+
if cursor.goto_first_child() {
|
|
46
|
+
stack.push(node);
|
|
47
|
+
if stack.len() > MAX_TREE_DEPTH {
|
|
48
|
+
return Err(format!("tree depth exceeds {MAX_TREE_DEPTH}"));
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
while !cursor.goto_next_sibling() {
|
|
53
|
+
if !cursor.goto_parent() {
|
|
54
|
+
return Ok(());
|
|
55
|
+
}
|
|
56
|
+
stack.pop();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
})?;
|
|
60
|
+
Ok(index)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl Drop for TreeIndex<'_> {
|
|
65
|
+
fn drop(&mut self) {
|
|
66
|
+
PARENTS.with_borrow_mut(|parents| parents.clear());
|
|
67
|
+
KIND_NAMES.set(&[]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn erase_lifetime(node: Node<'_>) -> Node<'static> {
|
|
72
|
+
// SAFETY: Node only borrows its tree; the erased node is stored in PARENTS, which the
|
|
73
|
+
// TreeIndex borrowing that tree empties before the tree can be dropped.
|
|
74
|
+
unsafe { std::mem::transmute::<Node<'_>, Node<'static>>(node) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pub trait NodeExt<'t> {
|
|
78
|
+
/// `Node::parent`, answered from the TreeIndex when one is installed.
|
|
79
|
+
fn parent_node(self) -> Option<Node<'t>>;
|
|
80
|
+
/// `Node::kind`, answered from the TreeIndex when one is installed.
|
|
81
|
+
fn kind_name(self) -> &'static str;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl<'t> NodeExt<'t> for Node<'t> {
|
|
85
|
+
fn parent_node(self) -> Option<Node<'t>> {
|
|
86
|
+
// tree-sitter locates the parent by byte range, which for a zero-width node (a MISSING
|
|
87
|
+
// token, an empty construct) can select a sibling touching the same offset instead of the
|
|
88
|
+
// structural parent; those rare nodes keep tree-sitter's answer so metrics stay unchanged.
|
|
89
|
+
if self.start_byte() == self.end_byte() {
|
|
90
|
+
return self.parent();
|
|
91
|
+
}
|
|
92
|
+
PARENTS
|
|
93
|
+
.with_borrow(|parents| {
|
|
94
|
+
parents.get(&self.id()).map(|parent| {
|
|
95
|
+
// SAFETY: ids are addresses inside the indexed tree, which outlives the
|
|
96
|
+
// index, so a hit means `parent` belongs to the tree `self` borrows.
|
|
97
|
+
unsafe { std::mem::transmute::<Node<'static>, Node<'t>>(*parent) }
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
.or_else(|| self.parent())
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fn kind_name(self) -> &'static str {
|
|
104
|
+
KIND_NAMES
|
|
105
|
+
.get()
|
|
106
|
+
.get(usize::from(self.kind_id()))
|
|
107
|
+
.copied()
|
|
108
|
+
.unwrap_or_else(|| self.kind())
|
|
109
|
+
}
|
|
110
|
+
}
|
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
|
|
@@ -55,9 +57,9 @@ pub fn node_text<'a>(node: Node<'_>, code: &Source<'a>) -> &'a str {
|
|
|
55
57
|
/// the same kind as an unbound type (`List::size`); the receiver position is what distinguishes
|
|
56
58
|
/// it, and only a visible definition then tells a variable from a type.
|
|
57
59
|
pub fn is_kotlin_callable_receiver(node: Node<'_>) -> bool {
|
|
58
|
-
node.
|
|
59
|
-
&& node.
|
|
60
|
-
parent.
|
|
60
|
+
node.kind_name() == "type_identifier"
|
|
61
|
+
&& node.parent_node().is_some_and(|parent| {
|
|
62
|
+
parent.kind_name() == "callable_reference"
|
|
61
63
|
&& parent
|
|
62
64
|
.named_child(0)
|
|
63
65
|
.is_some_and(|first| first.id() == node.id())
|
|
@@ -72,7 +74,7 @@ pub fn is_kotlin_callable_receiver(node: Node<'_>) -> bool {
|
|
|
72
74
|
pub fn is_identifier_leaf(node: Node<'_>) -> bool {
|
|
73
75
|
node.child_count() == 0
|
|
74
76
|
|| matches!(
|
|
75
|
-
node.
|
|
77
|
+
node.kind_name(),
|
|
76
78
|
"simple_identifier" | "interpolated_identifier" | "type_identifier"
|
|
77
79
|
)
|
|
78
80
|
}
|
|
@@ -167,26 +169,29 @@ pub fn to_int32(value: i64) -> i32 {
|
|
|
167
169
|
/// The body following a Kotlin `if_expression`'s bare `else` keyword (the grammar has no else
|
|
168
170
|
/// clause node and no fields), or None for other languages' if nodes and else-less ifs.
|
|
169
171
|
pub fn kotlin_else_body(if_node: Node<'_>) -> Option<Node<'_>> {
|
|
170
|
-
if if_node.
|
|
172
|
+
if if_node.kind_name() != "if_expression" {
|
|
171
173
|
return None;
|
|
172
174
|
}
|
|
173
175
|
let children = all_children(if_node);
|
|
174
176
|
let else_index = children
|
|
175
177
|
.iter()
|
|
176
|
-
.position(|child| !child.is_named() && child.
|
|
178
|
+
.position(|child| !child.is_named() && child.kind_name() == "else")?;
|
|
177
179
|
children[else_index + 1..]
|
|
178
180
|
.iter()
|
|
179
181
|
.copied()
|
|
180
|
-
.find(|child| child.
|
|
182
|
+
.find(|child| child.kind_name() == "control_structure_body")
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
/// Kotlin's `try { } catch { }` shares its node kind with Rust's `?` operator; only the Kotlin form
|
|
184
186
|
/// holds a body or clause child.
|
|
185
187
|
pub fn is_kotlin_try_expression(node: Node<'_>) -> bool {
|
|
186
|
-
node.
|
|
187
|
-
&& named_children(node)
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
node.kind_name() == "try_expression"
|
|
189
|
+
&& named_children(node).iter().any(|child| {
|
|
190
|
+
matches!(
|
|
191
|
+
child.kind_name(),
|
|
192
|
+
"statements" | "catch_block" | "finally_block"
|
|
193
|
+
)
|
|
194
|
+
})
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
/// Whether a Kotlin else body is a braceless `else if`: the nested if sits directly in the
|
|
@@ -194,5 +199,5 @@ pub fn is_kotlin_try_expression(node: Node<'_>) -> bool {
|
|
|
194
199
|
pub fn is_kotlin_else_if_body(else_body: Node<'_>) -> bool {
|
|
195
200
|
named_children(else_body)
|
|
196
201
|
.iter()
|
|
197
|
-
.any(|child| child.
|
|
202
|
+
.any(|child| child.kind_name() == "if_expression")
|
|
198
203
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
use std::num::NonZeroUsize;
|
|
2
|
+
use std::sync::{mpsc, Arc, Mutex, OnceLock};
|
|
3
|
+
use std::thread;
|
|
4
|
+
|
|
5
|
+
type Job = Box<dyn FnOnce() + Send>;
|
|
6
|
+
|
|
7
|
+
/// The metric passes recurse per tree level (TreeIndex refuses trees deeper than 5,000), so workers
|
|
8
|
+
/// get the stack size of a typical main thread instead of Rust's 2 MiB default.
|
|
9
|
+
const STACK_SIZE: usize = 8 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
/// Runs the job on a process-wide pool with one thread per available core. A dedicated pool rather
|
|
12
|
+
/// than libuv's (4 threads by default) lets measurements use every core without the embedding
|
|
13
|
+
/// process having to raise UV_THREADPOOL_SIZE before its first I/O. The pool keeps however many
|
|
14
|
+
/// workers the OS allows (a thread or PID limit may refuse some); with none, the job runs on the
|
|
15
|
+
/// calling thread instead.
|
|
16
|
+
pub fn spawn(job: impl FnOnce() + Send + 'static) {
|
|
17
|
+
static SENDER: OnceLock<Option<mpsc::Sender<Job>>> = OnceLock::new();
|
|
18
|
+
let sender = SENDER.get_or_init(|| {
|
|
19
|
+
let (sender, receiver) = mpsc::channel::<Job>();
|
|
20
|
+
let receiver = Arc::new(Mutex::new(receiver));
|
|
21
|
+
let mut started = 0;
|
|
22
|
+
for _ in 0..thread::available_parallelism().map_or(1, NonZeroUsize::get) {
|
|
23
|
+
let receiver = Arc::clone(&receiver);
|
|
24
|
+
let spawned = thread::Builder::new()
|
|
25
|
+
.name("code-gauge-worker".to_string())
|
|
26
|
+
.stack_size(STACK_SIZE)
|
|
27
|
+
.spawn(move || loop {
|
|
28
|
+
let job = receiver
|
|
29
|
+
.lock()
|
|
30
|
+
.expect("jobs run after the lock is released, so it is never poisoned")
|
|
31
|
+
.recv();
|
|
32
|
+
match job {
|
|
33
|
+
Ok(job) => job(),
|
|
34
|
+
Err(_) => return,
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
if spawned.is_err() {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
started += 1;
|
|
41
|
+
}
|
|
42
|
+
(started > 0).then_some(sender)
|
|
43
|
+
});
|
|
44
|
+
let job: Job = Box::new(job);
|
|
45
|
+
match sender {
|
|
46
|
+
Some(sender) => {
|
|
47
|
+
if let Err(mpsc::SendError(job)) = sender.send(job) {
|
|
48
|
+
job();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
None => job(),
|
|
52
|
+
}
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-gauge",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.1",
|
|
4
4
|
"description": "Measure code metrics with tree-sitter.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -94,12 +94,12 @@
|
|
|
94
94
|
"registry": "https://registry.npmjs.org/"
|
|
95
95
|
},
|
|
96
96
|
"optionalDependencies": {
|
|
97
|
-
"code-gauge-linux-x64-gnu": "4.7.
|
|
98
|
-
"code-gauge-linux-arm64-gnu": "4.7.
|
|
99
|
-
"code-gauge-linux-x64-musl": "4.7.
|
|
100
|
-
"code-gauge-linux-arm64-musl": "4.7.
|
|
101
|
-
"code-gauge-darwin-x64": "4.7.
|
|
102
|
-
"code-gauge-darwin-arm64": "4.7.
|
|
103
|
-
"code-gauge-win32-x64-msvc": "4.7.
|
|
97
|
+
"code-gauge-linux-x64-gnu": "4.7.1",
|
|
98
|
+
"code-gauge-linux-arm64-gnu": "4.7.1",
|
|
99
|
+
"code-gauge-linux-x64-musl": "4.7.1",
|
|
100
|
+
"code-gauge-linux-arm64-musl": "4.7.1",
|
|
101
|
+
"code-gauge-darwin-x64": "4.7.1",
|
|
102
|
+
"code-gauge-darwin-arm64": "4.7.1",
|
|
103
|
+
"code-gauge-win32-x64-msvc": "4.7.1"
|
|
104
104
|
}
|
|
105
105
|
}
|