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/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,13 +12,11 @@ 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
|
};
|
|
20
|
-
use crate::util::{
|
|
21
|
-
all_children, is_identifier_leaf, is_js_whitespace, named_children, node_text, split_lines,
|
|
22
|
-
Source,
|
|
23
|
-
};
|
|
19
|
+
use crate::util::{all_children, is_js_whitespace, named_children, node_text, split_lines, Source};
|
|
24
20
|
|
|
25
21
|
pub fn measure(
|
|
26
22
|
code: &str,
|
|
@@ -31,13 +27,23 @@ pub fn measure(
|
|
|
31
27
|
) -> Result<NativeMetrics, String> {
|
|
32
28
|
let source = Source::new(code);
|
|
33
29
|
let tree = parse_source(&source, language)?;
|
|
30
|
+
let _index = TreeIndex::new(&tree, language)?;
|
|
34
31
|
let root = tree.root_node();
|
|
35
32
|
let code = &source;
|
|
36
33
|
let sets = LanguageSets::new(language);
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
// One walk collects both function and initializer-block candidates (Ruby's `block` is both).
|
|
36
|
+
let candidates = collect_nodes(root, |kind| {
|
|
37
|
+
sets.function_nodes.contains(kind) || INITIALIZER_NODE_TYPES.contains(&kind)
|
|
38
|
+
});
|
|
39
|
+
let initializer_block_count = count_initializer_blocks(&candidates);
|
|
40
|
+
let functions: Vec<Node<'_>> = candidates
|
|
39
41
|
.into_iter()
|
|
40
|
-
.filter(|node|
|
|
42
|
+
.filter(|node| {
|
|
43
|
+
sets.function_nodes.contains(node.kind_name())
|
|
44
|
+
&& !is_lambda_body_block(*node)
|
|
45
|
+
&& is_implemented_function(*node)
|
|
46
|
+
})
|
|
41
47
|
.collect();
|
|
42
48
|
|
|
43
49
|
let body_metrics = measure_function_body_metrics(root, &sets, code);
|
|
@@ -50,7 +56,7 @@ pub fn measure(
|
|
|
50
56
|
.expect("every collected function node opens a frame in the body-metrics pass");
|
|
51
57
|
FunctionMetrics {
|
|
52
58
|
name: find_function_name(*node, code),
|
|
53
|
-
node_type: node.
|
|
59
|
+
node_type: node.kind_name().to_string(),
|
|
54
60
|
start_line: node.start_position().row + 1,
|
|
55
61
|
// The tree is parsed from UTF-16, so columns are UTF-16 code units x 2 — halving
|
|
56
62
|
// yields the JavaScript string (UTF-16 code unit) column.
|
|
@@ -71,7 +77,6 @@ pub fn measure(
|
|
|
71
77
|
})
|
|
72
78
|
.collect();
|
|
73
79
|
|
|
74
|
-
let global_complexity = measure_complexity(root, &sets, code);
|
|
75
80
|
let (lines, code_line_numbers) = classify_lines(code, root);
|
|
76
81
|
let halstead_counts = measure_halstead(root, code);
|
|
77
82
|
let tokenized = tokenize(root, code);
|
|
@@ -89,15 +94,15 @@ pub fn measure(
|
|
|
89
94
|
.sum::<u64>()
|
|
90
95
|
+ body_metrics.top_level_decisions
|
|
91
96
|
+ u64::from(language.executes_top_level || has_top_level_statements(root, language))
|
|
92
|
-
+
|
|
93
|
-
cognitive_complexity:
|
|
97
|
+
+ initializer_block_count,
|
|
98
|
+
cognitive_complexity: body_metrics.cognitive_complexity,
|
|
94
99
|
max_cognitive_complexity: function_metrics
|
|
95
100
|
.iter()
|
|
96
101
|
.map(|function| function.cognitive_complexity)
|
|
97
102
|
.max()
|
|
98
103
|
.unwrap_or(0),
|
|
99
|
-
nesting_depth:
|
|
100
|
-
ncss_count:
|
|
104
|
+
nesting_depth: body_metrics.nesting_depth,
|
|
105
|
+
ncss_count: body_metrics.ncss,
|
|
101
106
|
duplication: measure_duplication(&tokenized, &code_line_numbers, duplication_settings),
|
|
102
107
|
cross_file_data: include_cross_file_data.then(|| {
|
|
103
108
|
to_cross_file_data(
|
|
@@ -119,22 +124,22 @@ pub fn measure(
|
|
|
119
124
|
/// Initializer blocks run code of their own, so each is a component like a function: Java static
|
|
120
125
|
/// and instance initializers, Kotlin `init` blocks, and JavaScript/TypeScript class `static`
|
|
121
126
|
/// blocks. Their decisions already count as decisions outside functions.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
.
|
|
127
|
+
const INITIALIZER_NODE_TYPES: &[&str] = &[
|
|
128
|
+
"static_initializer",
|
|
129
|
+
"anonymous_initializer",
|
|
130
|
+
"class_static_block",
|
|
131
|
+
"block",
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
fn count_initializer_blocks(candidates: &[Node<'_>]) -> u64 {
|
|
135
|
+
candidates
|
|
136
|
+
.iter()
|
|
137
|
+
.filter(|node| INITIALIZER_NODE_TYPES.contains(&node.kind_name()))
|
|
133
138
|
.filter(|node| {
|
|
134
139
|
// 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.
|
|
140
|
+
node.kind_name() != "block"
|
|
141
|
+
|| node.parent_node().is_some_and(|parent| {
|
|
142
|
+
matches!(parent.kind_name(), "class_body" | "enum_body_declarations")
|
|
138
143
|
})
|
|
139
144
|
})
|
|
140
145
|
.count() as u64
|
|
@@ -143,14 +148,14 @@ fn count_initializer_blocks(root: Node<'_>) -> u64 {
|
|
|
143
148
|
/// A C# top-level statement: a `global_statement`, or a statement inside a top-level `#if` block,
|
|
144
149
|
/// which the grammar does not wrap in `global_statement`; preprocessor blocks are transparent.
|
|
145
150
|
fn is_csharp_top_level_statement(node: Node<'_>) -> bool {
|
|
146
|
-
if node.
|
|
151
|
+
if node.kind_name().starts_with("preproc_") {
|
|
147
152
|
return named_children(node)
|
|
148
153
|
.into_iter()
|
|
149
154
|
.any(is_csharp_top_level_statement);
|
|
150
155
|
}
|
|
151
|
-
node.
|
|
152
|
-
|| node.
|
|
153
|
-
|| node.
|
|
156
|
+
node.kind_name() == "global_statement"
|
|
157
|
+
|| node.kind_name() == "block"
|
|
158
|
+
|| node.kind_name().ends_with("_statement")
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
/// Whether a C# or Kotlin file runs top-level code: C# top-level statements, or a Kotlin script's
|
|
@@ -158,28 +163,25 @@ fn is_csharp_top_level_statement(node: Node<'_>) -> bool {
|
|
|
158
163
|
fn has_top_level_statements(root: Node<'_>, language: &LanguageDefinition) -> bool {
|
|
159
164
|
const KOTLIN_DECLARATIONS: &[&str] = &[
|
|
160
165
|
"package_header",
|
|
161
|
-
"
|
|
166
|
+
"import",
|
|
162
167
|
"class_declaration",
|
|
163
168
|
"object_declaration",
|
|
164
169
|
"function_declaration",
|
|
165
170
|
"property_declaration",
|
|
166
171
|
"type_alias",
|
|
167
|
-
"
|
|
172
|
+
"shebang",
|
|
168
173
|
"file_annotation",
|
|
169
|
-
"getter",
|
|
170
|
-
"setter",
|
|
171
174
|
];
|
|
172
175
|
let children = named_children(root);
|
|
173
176
|
match language.name {
|
|
174
177
|
"csharp" => children.into_iter().any(is_csharp_top_level_statement),
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// never taken for a script.
|
|
178
|
+
// Error recovery leaves residue at the top level, so a file with parse errors is never taken
|
|
179
|
+
// for a script.
|
|
178
180
|
"kotlin" => {
|
|
179
181
|
!root.has_error()
|
|
180
182
|
&& children.iter().any(|child| {
|
|
181
|
-
!KOTLIN_DECLARATIONS.contains(&child.
|
|
182
|
-
&& !crate::ncss::COMMENT_NODE_TYPES.contains(&child.
|
|
183
|
+
!KOTLIN_DECLARATIONS.contains(&child.kind_name())
|
|
184
|
+
&& !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind_name())
|
|
183
185
|
})
|
|
184
186
|
}
|
|
185
187
|
_ => false,
|
|
@@ -194,6 +196,7 @@ pub fn collect_cross_file_data(
|
|
|
194
196
|
) -> Result<CrossFileFileData, String> {
|
|
195
197
|
let source = Source::new(code);
|
|
196
198
|
let tree = parse_source(&source, language)?;
|
|
199
|
+
let _index = TreeIndex::new(&tree, language)?;
|
|
197
200
|
let root = tree.root_node();
|
|
198
201
|
let (_, code_line_numbers) = classify_lines(&source, root);
|
|
199
202
|
Ok(to_cross_file_data(
|
|
@@ -205,7 +208,7 @@ pub fn collect_cross_file_data(
|
|
|
205
208
|
|
|
206
209
|
fn to_cross_file_data(
|
|
207
210
|
tokenized: &TokenizedSource<'_>,
|
|
208
|
-
code_line_numbers: &
|
|
211
|
+
code_line_numbers: &FxHashSet<usize>,
|
|
209
212
|
min_tokens: usize,
|
|
210
213
|
) -> CrossFileFileData {
|
|
211
214
|
let (candidates, tokens, container_statements, near_miss_blocks) =
|
|
@@ -224,8 +227,6 @@ fn to_cross_file_data(
|
|
|
224
227
|
/// Name-carrying leaf types anonymized by tokenize_function so consistent renames still match.
|
|
225
228
|
const IDENTIFIER_LEAF_NODE_TYPES: &[&str] = &[
|
|
226
229
|
"identifier",
|
|
227
|
-
"simple_identifier",
|
|
228
|
-
"interpolated_identifier",
|
|
229
230
|
"implicit_parameter",
|
|
230
231
|
"property_identifier",
|
|
231
232
|
"field_identifier",
|
|
@@ -244,43 +245,50 @@ pub fn collect_function_token_sequences(
|
|
|
244
245
|
) -> Result<Vec<Vec<i32>>, String> {
|
|
245
246
|
let source = Source::new(code);
|
|
246
247
|
let tree = parse_source(&source, language)?;
|
|
248
|
+
let _index = TreeIndex::new(&tree, language)?;
|
|
247
249
|
let root = tree.root_node();
|
|
248
250
|
let sets = LanguageSets::new(language);
|
|
249
|
-
Ok(
|
|
250
|
-
.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
251
|
+
Ok(
|
|
252
|
+
collect_nodes(root, |kind| sets.function_nodes.contains(kind))
|
|
253
|
+
.into_iter()
|
|
254
|
+
.filter(|node| !is_lambda_body_block(*node) && is_implemented_function(*node))
|
|
255
|
+
.map(|node| {
|
|
256
|
+
let mut symbols = Vec::new();
|
|
257
|
+
let mut id_index_by_name: FxHashMap<String, usize> = FxHashMap::default();
|
|
258
|
+
collect_token_symbols(node, &source, &mut symbols, &mut id_index_by_name);
|
|
259
|
+
symbols
|
|
260
|
+
})
|
|
261
|
+
.collect(),
|
|
262
|
+
)
|
|
259
263
|
}
|
|
260
264
|
|
|
261
265
|
fn collect_token_symbols(
|
|
262
266
|
node: Node<'_>,
|
|
263
267
|
code: &Source<'_>,
|
|
264
268
|
symbols: &mut Vec<i32>,
|
|
265
|
-
id_index_by_name: &mut
|
|
269
|
+
id_index_by_name: &mut FxHashMap<String, usize>,
|
|
266
270
|
) {
|
|
267
271
|
if matches!(
|
|
268
|
-
node.
|
|
269
|
-
"comment" | "line_comment" | "block_comment"
|
|
272
|
+
node.kind_name(),
|
|
273
|
+
"comment" | "line_comment" | "block_comment"
|
|
270
274
|
) {
|
|
271
275
|
return;
|
|
272
276
|
}
|
|
273
|
-
if atomic_operand_node_types().contains(node.
|
|
274
|
-
symbols.push(hash_text(node.
|
|
277
|
+
if atomic_operand_node_types().contains(node.kind_name()) {
|
|
278
|
+
symbols.push(hash_text(node.kind_name()));
|
|
275
279
|
return;
|
|
276
280
|
}
|
|
277
|
-
if
|
|
281
|
+
if node.child_count() != 0 {
|
|
278
282
|
for child in all_children(node) {
|
|
279
283
|
collect_token_symbols(child, code, symbols, id_index_by_name);
|
|
280
284
|
}
|
|
281
285
|
return;
|
|
282
286
|
}
|
|
283
|
-
if
|
|
287
|
+
if crate::util::is_kotlin_keyword_literal(node, code) {
|
|
288
|
+
symbols.push(hash_text(node_text(node, code)));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if IDENTIFIER_LEAF_NODE_TYPES.contains(&node.kind_name()) {
|
|
284
292
|
let next_index = id_index_by_name.len();
|
|
285
293
|
let index = *id_index_by_name
|
|
286
294
|
.entry(node_text(node, code).to_string())
|
|
@@ -290,17 +298,17 @@ fn collect_token_symbols(
|
|
|
290
298
|
}
|
|
291
299
|
// Remaining operand leaves are literals, normalized by kind; everything else (keywords,
|
|
292
300
|
// operators, punctuation) is kept verbatim.
|
|
293
|
-
symbols.push(hash_text(
|
|
294
|
-
node.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
301
|
+
symbols.push(hash_text(
|
|
302
|
+
if operand_node_types().contains(node.kind_name()) {
|
|
303
|
+
node.kind_name()
|
|
304
|
+
} else {
|
|
305
|
+
node_text(node, code)
|
|
306
|
+
},
|
|
307
|
+
));
|
|
298
308
|
}
|
|
299
309
|
|
|
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.
|
|
310
|
+
/// Parses the source from UTF-16, matching node-tree-sitter's JavaScript string semantics:
|
|
311
|
+
/// tree-sitter's error recovery differs between input encodings for malformed non-ASCII source.
|
|
304
312
|
fn parse_source(
|
|
305
313
|
source: &Source<'_>,
|
|
306
314
|
language: &LanguageDefinition,
|
|
@@ -309,38 +317,9 @@ fn parse_source(
|
|
|
309
317
|
parser
|
|
310
318
|
.set_language(&language.grammar())
|
|
311
319
|
.map_err(|error| error.to_string())?;
|
|
312
|
-
|
|
313
|
-
.
|
|
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
|
-
}
|
|
320
|
+
parser
|
|
321
|
+
.parse_utf16_le(source.to_utf16(), None)
|
|
322
|
+
.ok_or_else(|| "parse failed".to_string())
|
|
344
323
|
}
|
|
345
324
|
|
|
346
325
|
struct CommentSpan {
|
|
@@ -352,10 +331,10 @@ struct CommentSpan {
|
|
|
352
331
|
/// Line metrics plus the 1-based numbers of lines that are neither blank nor comment-only, shared
|
|
353
332
|
/// by the line counts and duplication line coverage so the coverage and its code-line denominator
|
|
354
333
|
/// agree.
|
|
355
|
-
fn classify_lines(code: &Source<'_>, root: Node<'_>) -> (LineMetrics,
|
|
334
|
+
fn classify_lines(code: &Source<'_>, root: Node<'_>) -> (LineMetrics, FxHashSet<usize>) {
|
|
356
335
|
let source_lines = split_lines(code.code);
|
|
357
336
|
// Spans are bucketed by line so classification stays linear.
|
|
358
|
-
let mut comment_spans_by_line:
|
|
337
|
+
let mut comment_spans_by_line: FxHashMap<usize, Vec<CommentSpan>> = FxHashMap::default();
|
|
359
338
|
for span in collect_comment_spans(root) {
|
|
360
339
|
comment_spans_by_line
|
|
361
340
|
.entry(span.line)
|
|
@@ -364,7 +343,7 @@ fn classify_lines(code: &Source<'_>, root: Node<'_>) -> (LineMetrics, HashSet<us
|
|
|
364
343
|
}
|
|
365
344
|
let mut blank = 0;
|
|
366
345
|
let mut comment = 0;
|
|
367
|
-
let mut code_line_numbers =
|
|
346
|
+
let mut code_line_numbers = FxHashSet::default();
|
|
368
347
|
|
|
369
348
|
for (index, line) in source_lines.iter().enumerate() {
|
|
370
349
|
if line.chars().all(is_js_whitespace) {
|
|
@@ -396,8 +375,8 @@ fn collect_comment_spans(root: Node<'_>) -> Vec<CommentSpan> {
|
|
|
396
375
|
|
|
397
376
|
fn visit(node: Node<'_>, spans: &mut Vec<CommentSpan>) {
|
|
398
377
|
if matches!(
|
|
399
|
-
node.
|
|
400
|
-
"comment" | "line_comment" | "block_comment"
|
|
378
|
+
node.kind_name(),
|
|
379
|
+
"comment" | "line_comment" | "block_comment"
|
|
401
380
|
) {
|
|
402
381
|
for row in node.start_position().row..=node.end_position().row {
|
|
403
382
|
// Node columns are UTF-16 code units x 2 (the tree is parsed from UTF-16);
|
|
@@ -566,8 +545,6 @@ const OPERATOR_TEXTS: &[&str] = &[
|
|
|
566
545
|
|
|
567
546
|
const OPERAND_NODE_TYPES: &[&str] = &[
|
|
568
547
|
"identifier",
|
|
569
|
-
"simple_identifier",
|
|
570
|
-
"interpolated_identifier",
|
|
571
548
|
"implicit_parameter",
|
|
572
549
|
"property_identifier",
|
|
573
550
|
"field_identifier",
|
|
@@ -595,8 +572,6 @@ const OPERAND_NODE_TYPES: &[&str] = &[
|
|
|
595
572
|
"integer_literal",
|
|
596
573
|
"float_literal",
|
|
597
574
|
"real_literal",
|
|
598
|
-
"hex_literal",
|
|
599
|
-
"bin_literal",
|
|
600
575
|
"int_literal",
|
|
601
576
|
"rune_literal",
|
|
602
577
|
"imaginary_literal",
|
|
@@ -631,13 +606,10 @@ const OPERAND_NODE_TYPES: &[&str] = &[
|
|
|
631
606
|
];
|
|
632
607
|
|
|
633
608
|
/// Non-leaf literals counted as one Halstead operand without descending.
|
|
634
|
-
/// `character_literal` is a leaf in Java
|
|
635
|
-
/// suffixed numbers (`1L`, `1u`) wrap the bare literal, so `1` and `1L` stay distinct.
|
|
609
|
+
/// `character_literal` is a leaf in Java but wraps its content in C# and Kotlin.
|
|
636
610
|
const ATOMIC_OPERAND_NODE_TYPES: &[&str] = &[
|
|
637
611
|
"interpreted_string_literal",
|
|
638
612
|
"character_literal",
|
|
639
|
-
"long_literal",
|
|
640
|
-
"unsigned_literal",
|
|
641
613
|
"regex",
|
|
642
614
|
"user_defined_literal",
|
|
643
615
|
"integral_type",
|
|
@@ -646,60 +618,63 @@ const ATOMIC_OPERAND_NODE_TYPES: &[&str] = &[
|
|
|
646
618
|
"placeholder_type_specifier",
|
|
647
619
|
];
|
|
648
620
|
|
|
649
|
-
fn operator_texts() -> &'static
|
|
650
|
-
static SET: OnceLock<
|
|
621
|
+
fn operator_texts() -> &'static FxHashSet<&'static str> {
|
|
622
|
+
static SET: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
|
|
651
623
|
SET.get_or_init(|| OPERATOR_TEXTS.iter().copied().collect())
|
|
652
624
|
}
|
|
653
625
|
|
|
654
|
-
fn operand_node_types() -> &'static
|
|
655
|
-
static SET: OnceLock<
|
|
626
|
+
fn operand_node_types() -> &'static FxHashSet<&'static str> {
|
|
627
|
+
static SET: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
|
|
656
628
|
SET.get_or_init(|| OPERAND_NODE_TYPES.iter().copied().collect())
|
|
657
629
|
}
|
|
658
630
|
|
|
659
|
-
fn atomic_operand_node_types() -> &'static
|
|
660
|
-
static SET: OnceLock<
|
|
631
|
+
fn atomic_operand_node_types() -> &'static FxHashSet<&'static str> {
|
|
632
|
+
static SET: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
|
|
661
633
|
SET.get_or_init(|| ATOMIC_OPERAND_NODE_TYPES.iter().copied().collect())
|
|
662
634
|
}
|
|
663
635
|
|
|
664
636
|
fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
|
|
665
|
-
let mut operators:
|
|
666
|
-
let mut operands:
|
|
637
|
+
let mut operators: FxHashMap<&str, u64> = FxHashMap::default();
|
|
638
|
+
let mut operands: FxHashMap<&str, u64> = FxHashMap::default();
|
|
667
639
|
|
|
668
|
-
fn visit(
|
|
640
|
+
fn visit<'a>(
|
|
669
641
|
node: Node<'_>,
|
|
670
|
-
code: &Source<'
|
|
671
|
-
operators: &mut
|
|
672
|
-
operands: &mut
|
|
642
|
+
code: &Source<'a>,
|
|
643
|
+
operators: &mut FxHashMap<&'a str, u64>,
|
|
644
|
+
operands: &mut FxHashMap<&'a str, u64>,
|
|
673
645
|
) {
|
|
674
646
|
if matches!(
|
|
675
|
-
node.
|
|
676
|
-
"comment" | "line_comment" | "block_comment"
|
|
647
|
+
node.kind_name(),
|
|
648
|
+
"comment" | "line_comment" | "block_comment"
|
|
677
649
|
) {
|
|
678
650
|
return;
|
|
679
651
|
}
|
|
680
652
|
|
|
681
|
-
if atomic_operand_node_types().contains(node.
|
|
682
|
-
*operands
|
|
683
|
-
.entry(node_text(node, code).to_string())
|
|
684
|
-
.or_insert(0) += 1;
|
|
653
|
+
if atomic_operand_node_types().contains(node.kind_name()) {
|
|
654
|
+
*operands.entry(node_text(node, code)).or_insert(0) += 1;
|
|
685
655
|
return;
|
|
686
656
|
}
|
|
687
657
|
|
|
688
658
|
// Operators are counted from leaf tokens only: keyword-named nodes always contain a
|
|
689
659
|
// same-text anonymous keyword leaf, so counting the named node as well would double-count.
|
|
690
|
-
if
|
|
660
|
+
if node.child_count() == 0 {
|
|
691
661
|
let text = node_text(node, code);
|
|
692
662
|
// Operands win over text matches so identifiers spelled like word operators stay operands;
|
|
693
663
|
// C# `nameof(x)` is the one keyword operator the grammar parses as a plain callee.
|
|
694
664
|
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)
|
|
665
|
+
*operators.entry(text).or_insert(0) += 1;
|
|
666
|
+
} else if operand_node_types().contains(node.kind_name()) {
|
|
667
|
+
*operands.entry(text).or_insert(0) += 1;
|
|
668
|
+
} else if (operator_texts().contains(text)
|
|
669
|
+
|| operator_texts().contains(node.kind_name()))
|
|
699
670
|
&& is_countable_contextual_token(node, text)
|
|
700
671
|
{
|
|
701
|
-
let key = if text.is_empty() {
|
|
702
|
-
|
|
672
|
+
let key = if text.is_empty() {
|
|
673
|
+
node.kind_name()
|
|
674
|
+
} else {
|
|
675
|
+
text
|
|
676
|
+
};
|
|
677
|
+
*operators.entry(key).or_insert(0) += 1;
|
|
703
678
|
}
|
|
704
679
|
return;
|
|
705
680
|
}
|
|
@@ -722,9 +697,9 @@ fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
|
|
|
722
697
|
/// tree-sitter-c-sharp parses `nameof(x)` as an invocation of an identifier named `nameof`.
|
|
723
698
|
fn is_csharp_nameof_callee(node: Node<'_>, text: &str) -> bool {
|
|
724
699
|
text == "nameof"
|
|
725
|
-
&& node.
|
|
726
|
-
&& node.
|
|
727
|
-
parent.
|
|
700
|
+
&& node.kind_name() == "identifier"
|
|
701
|
+
&& node.parent_node().is_some_and(|parent| {
|
|
702
|
+
parent.kind_name() == "invocation_expression"
|
|
728
703
|
&& parent
|
|
729
704
|
.child_by_field_name("function")
|
|
730
705
|
.is_some_and(|callee| callee.id() == node.id())
|
|
@@ -746,18 +721,18 @@ const QUESTION_OPERATOR_PARENT_TYPES: &[&str] = &[
|
|
|
746
721
|
fn is_countable_contextual_token(node: Node<'_>, text: &str) -> bool {
|
|
747
722
|
if text == "@" {
|
|
748
723
|
// Python matrix multiplication only; decorator/annotation `@` marks are not operators.
|
|
749
|
-
let parent_type = node.
|
|
724
|
+
let parent_type = node.parent_node().map(|parent| parent.kind_name());
|
|
750
725
|
return parent_type == Some("binary_operator")
|
|
751
726
|
|| parent_type == Some("augmented_assignment");
|
|
752
727
|
}
|
|
753
728
|
if text == "default" {
|
|
754
729
|
return node
|
|
755
|
-
.
|
|
756
|
-
.is_some_and(|parent| parent.
|
|
730
|
+
.parent_node()
|
|
731
|
+
.is_some_and(|parent| parent.kind_name() == "default_expression");
|
|
757
732
|
}
|
|
758
733
|
if text != "?" {
|
|
759
734
|
return true;
|
|
760
735
|
}
|
|
761
|
-
node.
|
|
762
|
-
.is_some_and(|parent| QUESTION_OPERATOR_PARENT_TYPES.contains(&parent.
|
|
736
|
+
node.parent_node()
|
|
737
|
+
.is_some_and(|parent| QUESTION_OPERATOR_PARENT_TYPES.contains(&parent.kind_name()))
|
|
763
738
|
}
|
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]
|