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.
@@ -1,7 +1,8 @@
1
- use std::collections::HashSet;
1
+ use rustc_hash::FxHashSet;
2
2
  use tree_sitter::Node;
3
3
 
4
- use crate::util::{all_children, find_children_by_field_name};
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
- /// Counts non-commenting source statements (NCSS) in the subtree, PMD-style: one per declaration,
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
- pub fn count_ncss(
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
- countable: &HashSet<&'static str>,
87
- containers: &HashSet<&'static str>,
66
+ parent: Option<Node<'_>>,
67
+ countable: &FxHashSet<&'static str>,
68
+ containers: &FxHashSet<&'static str>,
88
69
  ) -> u64 {
89
- if !node.is_named() || COMMENT_NODE_TYPES.contains(&node.kind()) || is_for_header_node(node) {
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.kind() == "getter" || node.kind() == "setter")
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(node, containers)
102
- && !containers.contains(node.kind())
103
- && !POSITIONAL_EXCLUSION_TYPES.contains(&node.kind())
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.kind() == "label" && node.child_count() == 0);
106
- if (counts_through_node_type(node, countable) || positional || counts_contextually(node))
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.kind()) {
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: &HashSet<&'static str>) -> bool {
124
- if !countable.contains(node.kind()) {
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.kind()) {
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.kind() == "resource" {
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(node: Node<'_>, containers: &HashSet<&'static str>) -> bool {
141
- let mut ancestor = node.parent();
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.kind() != "parenthesized_statements" {
144
- return containers.contains(current.kind());
132
+ if current.kind_name() != "parenthesized_statements" {
133
+ return containers.contains(current.kind_name());
145
134
  }
146
- ancestor = current.parent();
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: &HashSet<&'static str>) -> bool {
154
- if node.kind() != "export_statement" {
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.kind())
160
- || declaration.kind() == "internal_module"
161
- || declaration.kind() == "ambient_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<'_>) -> bool {
173
- let Some(parent) = node.parent() else {
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.kind() == "init_statement"
166
+ if parent.kind_name() == "init_statement"
178
167
  && parent
179
- .parent()
180
- .is_some_and(|grandparent| grandparent.kind() == "for_range_loop")
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.kind() != "for_statement"
185
- && parent.kind() != "for_clause"
186
- && parent.kind() != "for_range_loop"
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<'_>) -> bool {
199
- let parent_kind = node.parent().map(|parent| 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.kind() == "block" && parent_kind == Some("class_body") {
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.kind()) && parent_kind == Some("interface_body") {
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.kind() != "block"
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.kind() == "method_signature" && parent_kind == Some("class_body") {
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.kind() == "internal_module" && parent_kind != Some("expression_statement") {
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.kind() != "body_statement"
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.kind() == "friend_declaration" {
237
- return !crate::util::named_children(node)
238
- .iter()
239
- .any(|child| child.kind() == "declaration" || child.kind() == "function_definition");
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.kind() == "macro_invocation"
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.kind() == "field_declaration" && parent_kind == Some("field_declaration_list") {
242
+ if node.kind_name() == "field_declaration" && parent_kind == Some("field_declaration_list") {
252
243
  return is_go_declared_type_body(
253
- node.parent().and_then(|parent| parent.parent()),
244
+ parent.and_then(|parent| parent.parent_node()),
254
245
  "struct_type",
255
246
  );
256
247
  }
257
- if node.kind() == "method_elem" || node.kind() == "method_spec" || node.kind() == "type_elem" {
258
- return is_go_declared_type_body(node.parent(), "interface_type");
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.kind() == expected_kind) else {
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
- .is_some_and(|parent| parent.kind() == "type_spec" || parent.kind() == "type_alias")
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) = node.parent() else {
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.kind()))
281
+ .filter(|child| !child.is_extra() && !ELSE_CLAUSE_NODE_TYPES.contains(&child.kind_name()))
287
282
  .count() as u64
288
283
  }
@@ -1,4 +1,4 @@
1
- use std::collections::{HashMap, HashSet};
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: HashSet<i32>,
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: HashMap<i32, usize> = HashMap::new();
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: HashMap<i32, u64>,
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: HashMap<i32, usize> = HashMap::new();
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: HashMap<i32, i32> = HashMap::new();
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: HashMap<i32, Vec<u64>> = HashMap::new();
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
+ }
@@ -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.kind() == "type_identifier"
59
- && node.parent().is_some_and(|parent| {
60
- parent.kind() == "callable_reference"
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.kind(),
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.kind() != "if_expression" {
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.kind() == "else")?;
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.kind() == "control_structure_body")
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.kind() == "try_expression"
187
- && named_children(node)
188
- .iter()
189
- .any(|child| matches!(child.kind(), "statements" | "catch_block" | "finally_block"))
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.kind() == "if_expression")
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.0",
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.0",
98
- "code-gauge-linux-arm64-gnu": "4.7.0",
99
- "code-gauge-linux-x64-musl": "4.7.0",
100
- "code-gauge-linux-arm64-musl": "4.7.0",
101
- "code-gauge-darwin-x64": "4.7.0",
102
- "code-gauge-darwin-arm64": "4.7.0",
103
- "code-gauge-win32-x64-msvc": "4.7.0"
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
  }