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/languages.rs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
use std::sync::OnceLock;
|
|
1
2
|
use tree_sitter::Language;
|
|
2
3
|
|
|
3
4
|
/// Per-language node-type configuration mirroring src/languages.ts.
|
|
@@ -32,6 +33,8 @@ enum GrammarId {
|
|
|
32
33
|
Tsx,
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
const GRAMMAR_COUNT: usize = GrammarId::Tsx as usize + 1;
|
|
37
|
+
|
|
35
38
|
impl LanguageDefinition {
|
|
36
39
|
pub fn grammar(&self) -> Language {
|
|
37
40
|
match self.grammar_id {
|
|
@@ -49,6 +52,23 @@ impl LanguageDefinition {
|
|
|
49
52
|
GrammarId::Tsx => tree_sitter_typescript::language_tsx(),
|
|
50
53
|
}
|
|
51
54
|
}
|
|
55
|
+
|
|
56
|
+
/// The grammar's node kind names, indexed by kind id.
|
|
57
|
+
pub fn kind_names(&self) -> &'static [&'static str] {
|
|
58
|
+
static KIND_NAMES: [OnceLock<Vec<&'static str>>; GRAMMAR_COUNT] =
|
|
59
|
+
[const { OnceLock::new() }; GRAMMAR_COUNT];
|
|
60
|
+
KIND_NAMES[self.grammar_id as usize].get_or_init(|| {
|
|
61
|
+
let grammar = self.grammar();
|
|
62
|
+
(0..grammar.node_kind_count())
|
|
63
|
+
.map(|id| {
|
|
64
|
+
u16::try_from(id)
|
|
65
|
+
.ok()
|
|
66
|
+
.and_then(|id| grammar.node_kind_for_id(id))
|
|
67
|
+
.unwrap_or_default()
|
|
68
|
+
})
|
|
69
|
+
.collect()
|
|
70
|
+
})
|
|
71
|
+
}
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
const COMMON_FUNCTION_NODES: &[&str] = &[
|
package/native/src/lib.rs
CHANGED
|
@@ -12,18 +12,23 @@ mod measure;
|
|
|
12
12
|
mod napi;
|
|
13
13
|
mod ncss;
|
|
14
14
|
mod near_miss;
|
|
15
|
+
mod tree_index;
|
|
15
16
|
mod types;
|
|
16
17
|
mod util;
|
|
17
18
|
#[cfg(target_family = "wasm")]
|
|
18
19
|
mod wasm;
|
|
20
|
+
#[cfg(not(target_family = "wasm"))]
|
|
21
|
+
mod worker_pool;
|
|
19
22
|
|
|
20
|
-
/// Version of the native payload schema. The TypeScript wrapper refuses a
|
|
21
|
-
/// differs from the one it expects, so a stale prebuilt addon fails with a
|
|
22
|
-
/// instead of silently returning an incompatible payload
|
|
23
|
-
///
|
|
23
|
+
/// Version of the native payload schema and binding functions. The TypeScript wrapper refuses a
|
|
24
|
+
/// binding whose version differs from the one it expects, so a stale prebuilt addon fails with a
|
|
25
|
+
/// clear rebuild message instead of silently returning an incompatible payload or lacking a
|
|
26
|
+
/// function (a failure the scan would only report per file). Bump on every payload-shape change
|
|
27
|
+
/// and every change to the exported binding functions, together with `expectedPayloadVersion` in
|
|
28
|
+
/// src/nativeMetrics.ts.
|
|
24
29
|
/// scripts/installNative.mjs parses the literal from this function's source.
|
|
25
30
|
pub fn payload_version() -> u32 {
|
|
26
|
-
|
|
31
|
+
8
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
/// Measures code metrics for the given source, returning the NativeMetrics payload as JSON; with
|
package/native/src/measure.rs
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
use
|
|
1
|
+
use rustc_hash::{FxHashMap, FxHashSet};
|
|
2
2
|
use std::sync::OnceLock;
|
|
3
3
|
use tree_sitter::Node;
|
|
4
4
|
|
|
5
|
-
use crate::complexity::{
|
|
6
|
-
is_lambda_body_block, measure_complexity, measure_function_body_metrics, LanguageSets,
|
|
7
|
-
};
|
|
5
|
+
use crate::complexity::{is_lambda_body_block, measure_function_body_metrics, LanguageSets};
|
|
8
6
|
use crate::dep_degree::measure_dep_degree;
|
|
9
7
|
use crate::duplication::{
|
|
10
8
|
collect_cross_file_file_data, hash_text, measure_duplication, tokenize, DuplicationSettings,
|
|
@@ -14,6 +12,7 @@ use crate::functions::{
|
|
|
14
12
|
collect_nodes, count_parameters, find_function_name, is_implemented_function,
|
|
15
13
|
};
|
|
16
14
|
use crate::languages::LanguageDefinition;
|
|
15
|
+
use crate::tree_index::{NodeExt, TreeIndex};
|
|
17
16
|
use crate::types::{
|
|
18
17
|
CrossFileFileData, FunctionMetrics, HalsteadCounts, LineMetrics, NativeMetrics,
|
|
19
18
|
};
|
|
@@ -31,13 +30,23 @@ pub fn measure(
|
|
|
31
30
|
) -> Result<NativeMetrics, String> {
|
|
32
31
|
let source = Source::new(code);
|
|
33
32
|
let tree = parse_source(&source, language)?;
|
|
33
|
+
let _index = TreeIndex::new(&tree, language)?;
|
|
34
34
|
let root = tree.root_node();
|
|
35
35
|
let code = &source;
|
|
36
36
|
let sets = LanguageSets::new(language);
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// One walk collects both function and initializer-block candidates (Ruby's `block` is both).
|
|
39
|
+
let candidates = collect_nodes(root, |kind| {
|
|
40
|
+
sets.function_nodes.contains(kind) || INITIALIZER_NODE_TYPES.contains(&kind)
|
|
41
|
+
});
|
|
42
|
+
let initializer_block_count = count_initializer_blocks(&candidates);
|
|
43
|
+
let functions: Vec<Node<'_>> = candidates
|
|
39
44
|
.into_iter()
|
|
40
|
-
.filter(|node|
|
|
45
|
+
.filter(|node| {
|
|
46
|
+
sets.function_nodes.contains(node.kind_name())
|
|
47
|
+
&& !is_lambda_body_block(*node)
|
|
48
|
+
&& is_implemented_function(*node)
|
|
49
|
+
})
|
|
41
50
|
.collect();
|
|
42
51
|
|
|
43
52
|
let body_metrics = measure_function_body_metrics(root, &sets, code);
|
|
@@ -50,7 +59,7 @@ pub fn measure(
|
|
|
50
59
|
.expect("every collected function node opens a frame in the body-metrics pass");
|
|
51
60
|
FunctionMetrics {
|
|
52
61
|
name: find_function_name(*node, code),
|
|
53
|
-
node_type: node.
|
|
62
|
+
node_type: node.kind_name().to_string(),
|
|
54
63
|
start_line: node.start_position().row + 1,
|
|
55
64
|
// The tree is parsed from UTF-16, so columns are UTF-16 code units x 2 — halving
|
|
56
65
|
// yields the JavaScript string (UTF-16 code unit) column.
|
|
@@ -71,7 +80,6 @@ pub fn measure(
|
|
|
71
80
|
})
|
|
72
81
|
.collect();
|
|
73
82
|
|
|
74
|
-
let global_complexity = measure_complexity(root, &sets, code);
|
|
75
83
|
let (lines, code_line_numbers) = classify_lines(code, root);
|
|
76
84
|
let halstead_counts = measure_halstead(root, code);
|
|
77
85
|
let tokenized = tokenize(root, code);
|
|
@@ -89,15 +97,15 @@ pub fn measure(
|
|
|
89
97
|
.sum::<u64>()
|
|
90
98
|
+ body_metrics.top_level_decisions
|
|
91
99
|
+ u64::from(language.executes_top_level || has_top_level_statements(root, language))
|
|
92
|
-
+
|
|
93
|
-
cognitive_complexity:
|
|
100
|
+
+ initializer_block_count,
|
|
101
|
+
cognitive_complexity: body_metrics.cognitive_complexity,
|
|
94
102
|
max_cognitive_complexity: function_metrics
|
|
95
103
|
.iter()
|
|
96
104
|
.map(|function| function.cognitive_complexity)
|
|
97
105
|
.max()
|
|
98
106
|
.unwrap_or(0),
|
|
99
|
-
nesting_depth:
|
|
100
|
-
ncss_count:
|
|
107
|
+
nesting_depth: body_metrics.nesting_depth,
|
|
108
|
+
ncss_count: body_metrics.ncss,
|
|
101
109
|
duplication: measure_duplication(&tokenized, &code_line_numbers, duplication_settings),
|
|
102
110
|
cross_file_data: include_cross_file_data.then(|| {
|
|
103
111
|
to_cross_file_data(
|
|
@@ -119,22 +127,22 @@ pub fn measure(
|
|
|
119
127
|
/// Initializer blocks run code of their own, so each is a component like a function: Java static
|
|
120
128
|
/// and instance initializers, Kotlin `init` blocks, and JavaScript/TypeScript class `static`
|
|
121
129
|
/// blocks. Their decisions already count as decisions outside functions.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
.
|
|
130
|
+
const INITIALIZER_NODE_TYPES: &[&str] = &[
|
|
131
|
+
"static_initializer",
|
|
132
|
+
"anonymous_initializer",
|
|
133
|
+
"class_static_block",
|
|
134
|
+
"block",
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
fn count_initializer_blocks(candidates: &[Node<'_>]) -> u64 {
|
|
138
|
+
candidates
|
|
139
|
+
.iter()
|
|
140
|
+
.filter(|node| INITIALIZER_NODE_TYPES.contains(&node.kind_name()))
|
|
133
141
|
.filter(|node| {
|
|
134
142
|
// A bare block is an initializer only as a direct member of a Java class or enum body.
|
|
135
|
-
node.
|
|
136
|
-
|| node.
|
|
137
|
-
matches!(parent.
|
|
143
|
+
node.kind_name() != "block"
|
|
144
|
+
|| node.parent_node().is_some_and(|parent| {
|
|
145
|
+
matches!(parent.kind_name(), "class_body" | "enum_body_declarations")
|
|
138
146
|
})
|
|
139
147
|
})
|
|
140
148
|
.count() as u64
|
|
@@ -143,14 +151,14 @@ fn count_initializer_blocks(root: Node<'_>) -> u64 {
|
|
|
143
151
|
/// A C# top-level statement: a `global_statement`, or a statement inside a top-level `#if` block,
|
|
144
152
|
/// which the grammar does not wrap in `global_statement`; preprocessor blocks are transparent.
|
|
145
153
|
fn is_csharp_top_level_statement(node: Node<'_>) -> bool {
|
|
146
|
-
if node.
|
|
154
|
+
if node.kind_name().starts_with("preproc_") {
|
|
147
155
|
return named_children(node)
|
|
148
156
|
.into_iter()
|
|
149
157
|
.any(is_csharp_top_level_statement);
|
|
150
158
|
}
|
|
151
|
-
node.
|
|
152
|
-
|| node.
|
|
153
|
-
|| node.
|
|
159
|
+
node.kind_name() == "global_statement"
|
|
160
|
+
|| node.kind_name() == "block"
|
|
161
|
+
|| node.kind_name().ends_with("_statement")
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
/// Whether a C# or Kotlin file runs top-level code: C# top-level statements, or a Kotlin script's
|
|
@@ -178,8 +186,8 @@ fn has_top_level_statements(root: Node<'_>, language: &LanguageDefinition) -> bo
|
|
|
178
186
|
"kotlin" => {
|
|
179
187
|
!root.has_error()
|
|
180
188
|
&& children.iter().any(|child| {
|
|
181
|
-
!KOTLIN_DECLARATIONS.contains(&child.
|
|
182
|
-
&& !crate::ncss::COMMENT_NODE_TYPES.contains(&child.
|
|
189
|
+
!KOTLIN_DECLARATIONS.contains(&child.kind_name())
|
|
190
|
+
&& !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind_name())
|
|
183
191
|
})
|
|
184
192
|
}
|
|
185
193
|
_ => false,
|
|
@@ -194,6 +202,7 @@ pub fn collect_cross_file_data(
|
|
|
194
202
|
) -> Result<CrossFileFileData, String> {
|
|
195
203
|
let source = Source::new(code);
|
|
196
204
|
let tree = parse_source(&source, language)?;
|
|
205
|
+
let _index = TreeIndex::new(&tree, language)?;
|
|
197
206
|
let root = tree.root_node();
|
|
198
207
|
let (_, code_line_numbers) = classify_lines(&source, root);
|
|
199
208
|
Ok(to_cross_file_data(
|
|
@@ -205,7 +214,7 @@ pub fn collect_cross_file_data(
|
|
|
205
214
|
|
|
206
215
|
fn to_cross_file_data(
|
|
207
216
|
tokenized: &TokenizedSource<'_>,
|
|
208
|
-
code_line_numbers: &
|
|
217
|
+
code_line_numbers: &FxHashSet<usize>,
|
|
209
218
|
min_tokens: usize,
|
|
210
219
|
) -> CrossFileFileData {
|
|
211
220
|
let (candidates, tokens, container_statements, near_miss_blocks) =
|
|
@@ -244,34 +253,37 @@ pub fn collect_function_token_sequences(
|
|
|
244
253
|
) -> Result<Vec<Vec<i32>>, String> {
|
|
245
254
|
let source = Source::new(code);
|
|
246
255
|
let tree = parse_source(&source, language)?;
|
|
256
|
+
let _index = TreeIndex::new(&tree, language)?;
|
|
247
257
|
let root = tree.root_node();
|
|
248
258
|
let sets = LanguageSets::new(language);
|
|
249
|
-
Ok(
|
|
250
|
-
.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
+
Ok(
|
|
260
|
+
collect_nodes(root, |kind| sets.function_nodes.contains(kind))
|
|
261
|
+
.into_iter()
|
|
262
|
+
.filter(|node| !is_lambda_body_block(*node) && is_implemented_function(*node))
|
|
263
|
+
.map(|node| {
|
|
264
|
+
let mut symbols = Vec::new();
|
|
265
|
+
let mut id_index_by_name: FxHashMap<String, usize> = FxHashMap::default();
|
|
266
|
+
collect_token_symbols(node, &source, &mut symbols, &mut id_index_by_name);
|
|
267
|
+
symbols
|
|
268
|
+
})
|
|
269
|
+
.collect(),
|
|
270
|
+
)
|
|
259
271
|
}
|
|
260
272
|
|
|
261
273
|
fn collect_token_symbols(
|
|
262
274
|
node: Node<'_>,
|
|
263
275
|
code: &Source<'_>,
|
|
264
276
|
symbols: &mut Vec<i32>,
|
|
265
|
-
id_index_by_name: &mut
|
|
277
|
+
id_index_by_name: &mut FxHashMap<String, usize>,
|
|
266
278
|
) {
|
|
267
279
|
if matches!(
|
|
268
|
-
node.
|
|
280
|
+
node.kind_name(),
|
|
269
281
|
"comment" | "line_comment" | "block_comment" | "multiline_comment"
|
|
270
282
|
) {
|
|
271
283
|
return;
|
|
272
284
|
}
|
|
273
|
-
if atomic_operand_node_types().contains(node.
|
|
274
|
-
symbols.push(hash_text(node.
|
|
285
|
+
if atomic_operand_node_types().contains(node.kind_name()) {
|
|
286
|
+
symbols.push(hash_text(node.kind_name()));
|
|
275
287
|
return;
|
|
276
288
|
}
|
|
277
289
|
if !is_identifier_leaf(node) {
|
|
@@ -280,7 +292,7 @@ fn collect_token_symbols(
|
|
|
280
292
|
}
|
|
281
293
|
return;
|
|
282
294
|
}
|
|
283
|
-
if IDENTIFIER_LEAF_NODE_TYPES.contains(&node.
|
|
295
|
+
if IDENTIFIER_LEAF_NODE_TYPES.contains(&node.kind_name()) {
|
|
284
296
|
let next_index = id_index_by_name.len();
|
|
285
297
|
let index = *id_index_by_name
|
|
286
298
|
.entry(node_text(node, code).to_string())
|
|
@@ -290,17 +302,17 @@ fn collect_token_symbols(
|
|
|
290
302
|
}
|
|
291
303
|
// Remaining operand leaves are literals, normalized by kind; everything else (keywords,
|
|
292
304
|
// operators, punctuation) is kept verbatim.
|
|
293
|
-
symbols.push(hash_text(
|
|
294
|
-
node.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
305
|
+
symbols.push(hash_text(
|
|
306
|
+
if operand_node_types().contains(node.kind_name()) {
|
|
307
|
+
node.kind_name()
|
|
308
|
+
} else {
|
|
309
|
+
node_text(node, code)
|
|
310
|
+
},
|
|
311
|
+
));
|
|
298
312
|
}
|
|
299
313
|
|
|
300
|
-
/// Parses the source from UTF-16
|
|
301
|
-
/// tree-sitter's error recovery differs between input encodings for malformed non-ASCII source
|
|
302
|
-
/// and refuses pathologically deep trees: the metric passes recurse per tree level and would
|
|
303
|
-
/// overflow the native stack (a process-killing SIGSEGV, not a catchable error) around depth ~20k.
|
|
314
|
+
/// Parses the source from UTF-16, matching node-tree-sitter's JavaScript string semantics:
|
|
315
|
+
/// tree-sitter's error recovery differs between input encodings for malformed non-ASCII source.
|
|
304
316
|
fn parse_source(
|
|
305
317
|
source: &Source<'_>,
|
|
306
318
|
language: &LanguageDefinition,
|
|
@@ -309,38 +321,9 @@ fn parse_source(
|
|
|
309
321
|
parser
|
|
310
322
|
.set_language(&language.grammar())
|
|
311
323
|
.map_err(|error| error.to_string())?;
|
|
312
|
-
|
|
324
|
+
parser
|
|
313
325
|
.parse_utf16(source.to_utf16(), None)
|
|
314
|
-
.ok_or_else(|| "parse failed".to_string())
|
|
315
|
-
if tree_depth(tree.root_node()) > MAX_TREE_DEPTH {
|
|
316
|
-
return Err(format!("tree depth exceeds {MAX_TREE_DEPTH}"));
|
|
317
|
-
}
|
|
318
|
-
Ok(tree)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/// See the depth check in parse_source(); computed iteratively so the check itself cannot overflow.
|
|
322
|
-
const MAX_TREE_DEPTH: usize = 5_000;
|
|
323
|
-
|
|
324
|
-
fn tree_depth(root: Node<'_>) -> usize {
|
|
325
|
-
let mut cursor = root.walk();
|
|
326
|
-
let mut depth = 0;
|
|
327
|
-
let mut max_depth = 0;
|
|
328
|
-
loop {
|
|
329
|
-
if cursor.goto_first_child() {
|
|
330
|
-
depth += 1;
|
|
331
|
-
max_depth = max_depth.max(depth);
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
loop {
|
|
335
|
-
if cursor.goto_next_sibling() {
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
if !cursor.goto_parent() {
|
|
339
|
-
return max_depth;
|
|
340
|
-
}
|
|
341
|
-
depth -= 1;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
326
|
+
.ok_or_else(|| "parse failed".to_string())
|
|
344
327
|
}
|
|
345
328
|
|
|
346
329
|
struct CommentSpan {
|
|
@@ -352,10 +335,10 @@ struct CommentSpan {
|
|
|
352
335
|
/// Line metrics plus the 1-based numbers of lines that are neither blank nor comment-only, shared
|
|
353
336
|
/// by the line counts and duplication line coverage so the coverage and its code-line denominator
|
|
354
337
|
/// agree.
|
|
355
|
-
fn classify_lines(code: &Source<'_>, root: Node<'_>) -> (LineMetrics,
|
|
338
|
+
fn classify_lines(code: &Source<'_>, root: Node<'_>) -> (LineMetrics, FxHashSet<usize>) {
|
|
356
339
|
let source_lines = split_lines(code.code);
|
|
357
340
|
// Spans are bucketed by line so classification stays linear.
|
|
358
|
-
let mut comment_spans_by_line:
|
|
341
|
+
let mut comment_spans_by_line: FxHashMap<usize, Vec<CommentSpan>> = FxHashMap::default();
|
|
359
342
|
for span in collect_comment_spans(root) {
|
|
360
343
|
comment_spans_by_line
|
|
361
344
|
.entry(span.line)
|
|
@@ -364,7 +347,7 @@ fn classify_lines(code: &Source<'_>, root: Node<'_>) -> (LineMetrics, HashSet<us
|
|
|
364
347
|
}
|
|
365
348
|
let mut blank = 0;
|
|
366
349
|
let mut comment = 0;
|
|
367
|
-
let mut code_line_numbers =
|
|
350
|
+
let mut code_line_numbers = FxHashSet::default();
|
|
368
351
|
|
|
369
352
|
for (index, line) in source_lines.iter().enumerate() {
|
|
370
353
|
if line.chars().all(is_js_whitespace) {
|
|
@@ -396,7 +379,7 @@ fn collect_comment_spans(root: Node<'_>) -> Vec<CommentSpan> {
|
|
|
396
379
|
|
|
397
380
|
fn visit(node: Node<'_>, spans: &mut Vec<CommentSpan>) {
|
|
398
381
|
if matches!(
|
|
399
|
-
node.
|
|
382
|
+
node.kind_name(),
|
|
400
383
|
"comment" | "line_comment" | "block_comment" | "multiline_comment"
|
|
401
384
|
) {
|
|
402
385
|
for row in node.start_position().row..=node.end_position().row {
|
|
@@ -646,42 +629,40 @@ const ATOMIC_OPERAND_NODE_TYPES: &[&str] = &[
|
|
|
646
629
|
"placeholder_type_specifier",
|
|
647
630
|
];
|
|
648
631
|
|
|
649
|
-
fn operator_texts() -> &'static
|
|
650
|
-
static SET: OnceLock<
|
|
632
|
+
fn operator_texts() -> &'static FxHashSet<&'static str> {
|
|
633
|
+
static SET: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
|
|
651
634
|
SET.get_or_init(|| OPERATOR_TEXTS.iter().copied().collect())
|
|
652
635
|
}
|
|
653
636
|
|
|
654
|
-
fn operand_node_types() -> &'static
|
|
655
|
-
static SET: OnceLock<
|
|
637
|
+
fn operand_node_types() -> &'static FxHashSet<&'static str> {
|
|
638
|
+
static SET: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
|
|
656
639
|
SET.get_or_init(|| OPERAND_NODE_TYPES.iter().copied().collect())
|
|
657
640
|
}
|
|
658
641
|
|
|
659
|
-
fn atomic_operand_node_types() -> &'static
|
|
660
|
-
static SET: OnceLock<
|
|
642
|
+
fn atomic_operand_node_types() -> &'static FxHashSet<&'static str> {
|
|
643
|
+
static SET: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
|
|
661
644
|
SET.get_or_init(|| ATOMIC_OPERAND_NODE_TYPES.iter().copied().collect())
|
|
662
645
|
}
|
|
663
646
|
|
|
664
647
|
fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
|
|
665
|
-
let mut operators:
|
|
666
|
-
let mut operands:
|
|
648
|
+
let mut operators: FxHashMap<&str, u64> = FxHashMap::default();
|
|
649
|
+
let mut operands: FxHashMap<&str, u64> = FxHashMap::default();
|
|
667
650
|
|
|
668
|
-
fn visit(
|
|
651
|
+
fn visit<'a>(
|
|
669
652
|
node: Node<'_>,
|
|
670
|
-
code: &Source<'
|
|
671
|
-
operators: &mut
|
|
672
|
-
operands: &mut
|
|
653
|
+
code: &Source<'a>,
|
|
654
|
+
operators: &mut FxHashMap<&'a str, u64>,
|
|
655
|
+
operands: &mut FxHashMap<&'a str, u64>,
|
|
673
656
|
) {
|
|
674
657
|
if matches!(
|
|
675
|
-
node.
|
|
658
|
+
node.kind_name(),
|
|
676
659
|
"comment" | "line_comment" | "block_comment" | "multiline_comment"
|
|
677
660
|
) {
|
|
678
661
|
return;
|
|
679
662
|
}
|
|
680
663
|
|
|
681
|
-
if atomic_operand_node_types().contains(node.
|
|
682
|
-
*operands
|
|
683
|
-
.entry(node_text(node, code).to_string())
|
|
684
|
-
.or_insert(0) += 1;
|
|
664
|
+
if atomic_operand_node_types().contains(node.kind_name()) {
|
|
665
|
+
*operands.entry(node_text(node, code)).or_insert(0) += 1;
|
|
685
666
|
return;
|
|
686
667
|
}
|
|
687
668
|
|
|
@@ -692,14 +673,19 @@ fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
|
|
|
692
673
|
// Operands win over text matches so identifiers spelled like word operators stay operands;
|
|
693
674
|
// C# `nameof(x)` is the one keyword operator the grammar parses as a plain callee.
|
|
694
675
|
if is_csharp_nameof_callee(node, text) {
|
|
695
|
-
*operators.entry(text
|
|
696
|
-
} else if operand_node_types().contains(node.
|
|
697
|
-
*operands.entry(text
|
|
698
|
-
} else if (operator_texts().contains(text)
|
|
676
|
+
*operators.entry(text).or_insert(0) += 1;
|
|
677
|
+
} else if operand_node_types().contains(node.kind_name()) {
|
|
678
|
+
*operands.entry(text).or_insert(0) += 1;
|
|
679
|
+
} else if (operator_texts().contains(text)
|
|
680
|
+
|| operator_texts().contains(node.kind_name()))
|
|
699
681
|
&& is_countable_contextual_token(node, text)
|
|
700
682
|
{
|
|
701
|
-
let key = if text.is_empty() {
|
|
702
|
-
|
|
683
|
+
let key = if text.is_empty() {
|
|
684
|
+
node.kind_name()
|
|
685
|
+
} else {
|
|
686
|
+
text
|
|
687
|
+
};
|
|
688
|
+
*operators.entry(key).or_insert(0) += 1;
|
|
703
689
|
}
|
|
704
690
|
return;
|
|
705
691
|
}
|
|
@@ -722,9 +708,9 @@ fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
|
|
|
722
708
|
/// tree-sitter-c-sharp parses `nameof(x)` as an invocation of an identifier named `nameof`.
|
|
723
709
|
fn is_csharp_nameof_callee(node: Node<'_>, text: &str) -> bool {
|
|
724
710
|
text == "nameof"
|
|
725
|
-
&& node.
|
|
726
|
-
&& node.
|
|
727
|
-
parent.
|
|
711
|
+
&& node.kind_name() == "identifier"
|
|
712
|
+
&& node.parent_node().is_some_and(|parent| {
|
|
713
|
+
parent.kind_name() == "invocation_expression"
|
|
728
714
|
&& parent
|
|
729
715
|
.child_by_field_name("function")
|
|
730
716
|
.is_some_and(|callee| callee.id() == node.id())
|
|
@@ -746,18 +732,18 @@ const QUESTION_OPERATOR_PARENT_TYPES: &[&str] = &[
|
|
|
746
732
|
fn is_countable_contextual_token(node: Node<'_>, text: &str) -> bool {
|
|
747
733
|
if text == "@" {
|
|
748
734
|
// Python matrix multiplication only; decorator/annotation `@` marks are not operators.
|
|
749
|
-
let parent_type = node.
|
|
735
|
+
let parent_type = node.parent_node().map(|parent| parent.kind_name());
|
|
750
736
|
return parent_type == Some("binary_operator")
|
|
751
737
|
|| parent_type == Some("augmented_assignment");
|
|
752
738
|
}
|
|
753
739
|
if text == "default" {
|
|
754
740
|
return node
|
|
755
|
-
.
|
|
756
|
-
.is_some_and(|parent| parent.
|
|
741
|
+
.parent_node()
|
|
742
|
+
.is_some_and(|parent| parent.kind_name() == "default_expression");
|
|
757
743
|
}
|
|
758
744
|
if text != "?" {
|
|
759
745
|
return true;
|
|
760
746
|
}
|
|
761
|
-
node.
|
|
762
|
-
.is_some_and(|parent| QUESTION_OPERATOR_PARENT_TYPES.contains(&parent.
|
|
747
|
+
node.parent_node()
|
|
748
|
+
.is_some_and(|parent| QUESTION_OPERATOR_PARENT_TYPES.contains(&parent.kind_name()))
|
|
763
749
|
}
|
package/native/src/napi.rs
CHANGED
|
@@ -18,16 +18,75 @@ pub fn measure_code_native(
|
|
|
18
18
|
min_similarity_percent: Option<u32>,
|
|
19
19
|
include_cross_file_data: Option<bool>,
|
|
20
20
|
) -> Result<String> {
|
|
21
|
-
|
|
21
|
+
measure_code(
|
|
22
22
|
&code,
|
|
23
23
|
&language,
|
|
24
|
+
include_syntax_tree,
|
|
25
|
+
min_tokens,
|
|
26
|
+
max_gap_tokens,
|
|
27
|
+
min_similarity_percent,
|
|
28
|
+
include_cross_file_data,
|
|
29
|
+
)
|
|
30
|
+
.map_err(Error::from_reason)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// measure_code_native on the worker pool, resolving with the same JSON payload, so callers can
|
|
34
|
+
/// measure several files in parallel.
|
|
35
|
+
#[napi(ts_return_type = "Promise<string>")]
|
|
36
|
+
#[allow(clippy::too_many_arguments)]
|
|
37
|
+
pub fn measure_code_native_async(
|
|
38
|
+
env: &Env,
|
|
39
|
+
code: String,
|
|
40
|
+
language: String,
|
|
41
|
+
include_syntax_tree: Option<bool>,
|
|
42
|
+
min_tokens: Option<u32>,
|
|
43
|
+
max_gap_tokens: Option<u32>,
|
|
44
|
+
min_similarity_percent: Option<u32>,
|
|
45
|
+
include_cross_file_data: Option<bool>,
|
|
46
|
+
) -> Result<Object<'_>> {
|
|
47
|
+
let (deferred, promise) = env.create_deferred()?;
|
|
48
|
+
crate::worker_pool::spawn(move || {
|
|
49
|
+
// A panic must reject the promise: otherwise it would stay pending and keep the event
|
|
50
|
+
// loop alive forever.
|
|
51
|
+
let result = std::panic::catch_unwind(|| {
|
|
52
|
+
measure_code(
|
|
53
|
+
&code,
|
|
54
|
+
&language,
|
|
55
|
+
include_syntax_tree,
|
|
56
|
+
min_tokens,
|
|
57
|
+
max_gap_tokens,
|
|
58
|
+
min_similarity_percent,
|
|
59
|
+
include_cross_file_data,
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
.unwrap_or_else(|_| Err("measurement panicked".to_string()));
|
|
63
|
+
match result {
|
|
64
|
+
Ok(json) => deferred.resolve(move |_| Ok(json)),
|
|
65
|
+
Err(reason) => deferred.reject(Error::from_reason(reason)),
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
Ok(promise)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The arguments' defaults, applied in one place so the sync and async bindings measure alike.
|
|
72
|
+
fn measure_code(
|
|
73
|
+
code: &str,
|
|
74
|
+
language: &str,
|
|
75
|
+
include_syntax_tree: Option<bool>,
|
|
76
|
+
min_tokens: Option<u32>,
|
|
77
|
+
max_gap_tokens: Option<u32>,
|
|
78
|
+
min_similarity_percent: Option<u32>,
|
|
79
|
+
include_cross_file_data: Option<bool>,
|
|
80
|
+
) -> std::result::Result<String, String> {
|
|
81
|
+
crate::measure_code(
|
|
82
|
+
code,
|
|
83
|
+
language,
|
|
24
84
|
include_syntax_tree.unwrap_or(false),
|
|
25
85
|
min_tokens,
|
|
26
86
|
max_gap_tokens,
|
|
27
87
|
min_similarity_percent,
|
|
28
88
|
include_cross_file_data.unwrap_or(false),
|
|
29
89
|
)
|
|
30
|
-
.map_err(Error::from_reason)
|
|
31
90
|
}
|
|
32
91
|
|
|
33
92
|
#[napi]
|