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