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::{HashMap, HashSet};
1
+ use rustc_hash::{FxHashMap, FxHashSet};
2
2
  use tree_sitter::Node;
3
3
 
4
4
  use crate::complexity::is_function_boundary;
5
+ use crate::tree_index::NodeExt;
5
6
  use crate::util::{is_identifier_leaf, node_text, Source};
6
7
 
7
8
  /// Leaf node types treated as variable references by the def-use approximation.
@@ -112,7 +113,7 @@ struct DepDegreeLeaf<'t> {
112
113
  pub fn measure_dep_degree(
113
114
  function_node: Node<'_>,
114
115
  code: &Source<'_>,
115
- function_nodes: &HashSet<&'static str>,
116
+ function_nodes: &FxHashSet<&'static str>,
116
117
  ) -> u64 {
117
118
  let mut leaves = Vec::new();
118
119
  let mut next_scope_id = 0usize;
@@ -125,7 +126,7 @@ pub fn measure_dep_degree(
125
126
  &mut leaves,
126
127
  true,
127
128
  );
128
- let mut definition_scopes_by_name: HashMap<&str, Vec<String>> = HashMap::new();
129
+ let mut definition_scopes_by_name: FxHashMap<&str, Vec<String>> = FxHashMap::default();
129
130
  for name in implicit_accessor_definitions(function_node, code) {
130
131
  add_definition(&mut definition_scopes_by_name, name, "");
131
132
  }
@@ -162,20 +163,20 @@ pub fn measure_dep_degree(
162
163
  /// `type_identifier` under the `pointer_type_declarator` spelling `C::*`, possibly through further
163
164
  /// declarator wrappers; every other `type_identifier` names a type, not a variable.
164
165
  fn is_variable_leaf(node: Node<'_>) -> bool {
165
- VARIABLE_NODE_TYPES.contains(&node.kind())
166
+ VARIABLE_NODE_TYPES.contains(&node.kind_name())
166
167
  || crate::util::is_kotlin_callable_receiver(node)
167
- || (node.kind() == "type_identifier" && is_member_pointer_name(node))
168
+ || (node.kind_name() == "type_identifier" && is_member_pointer_name(node))
168
169
  }
169
170
 
170
171
  fn is_member_pointer_name(node: Node<'_>) -> bool {
171
172
  let mut current = node;
172
- while let Some(parent) = current.parent() {
173
- if parent.kind() == "pointer_type_declarator" {
173
+ while let Some(parent) = current.parent_node() {
174
+ if parent.kind_name() == "pointer_type_declarator" {
174
175
  return true;
175
176
  }
176
177
  // The climb follows the declared-name position only, exactly like unwrap_declarator_wrappers.
177
- if !DECLARATOR_WRAPPER_TYPES.contains(&parent.kind())
178
- || parent.kind() == "qualified_identifier"
178
+ if !DECLARATOR_WRAPPER_TYPES.contains(&parent.kind_name())
179
+ || parent.kind_name() == "qualified_identifier"
179
180
  || field_name_in_parent(current, parent).is_some_and(|field| field != "declarator")
180
181
  {
181
182
  return false;
@@ -189,7 +190,7 @@ fn is_member_pointer_name(node: Node<'_>) -> bool {
189
190
  /// indexer's parameters (including a `params` array, which the grammar names directly on the
190
191
  /// parameter list) and, in a setter, initializer, or event accessor, the implicit `value`.
191
192
  fn implicit_accessor_definitions<'a>(function_node: Node<'_>, code: &Source<'a>) -> Vec<&'a str> {
192
- if function_node.kind() != "accessor_declaration" {
193
+ if function_node.kind_name() != "accessor_declaration" {
193
194
  return Vec::new();
194
195
  }
195
196
  let mut names = Vec::new();
@@ -201,9 +202,11 @@ fn implicit_accessor_definitions<'a>(function_node: Node<'_>, code: &Source<'a>)
201
202
  {
202
203
  names.push("value");
203
204
  }
204
- let owner = function_node.parent().and_then(|list| list.parent());
205
+ let owner = function_node
206
+ .parent_node()
207
+ .and_then(|list| list.parent_node());
205
208
  if let Some(parameters) = owner
206
- .filter(|owner| owner.kind() == "indexer_declaration")
209
+ .filter(|owner| owner.kind_name() == "indexer_declaration")
207
210
  .and_then(|owner| owner.child_by_field_name("parameters"))
208
211
  {
209
212
  let declared = crate::util::named_children(parameters)
@@ -223,12 +226,12 @@ fn collect_dep_degree_leaves<'t>(
223
226
  field_name: Option<&'static str>,
224
227
  scope: &str,
225
228
  next_scope_id: &mut usize,
226
- function_nodes: &HashSet<&'static str>,
229
+ function_nodes: &FxHashSet<&'static str>,
227
230
  leaves: &mut Vec<DepDegreeLeaf<'t>>,
228
231
  is_measured_root: bool,
229
232
  ) {
230
233
  if matches!(
231
- node.kind(),
234
+ node.kind_name(),
232
235
  "comment" | "line_comment" | "block_comment" | "multiline_comment"
233
236
  ) {
234
237
  return;
@@ -268,7 +271,7 @@ fn collect_dep_degree_leaves<'t>(
268
271
  }
269
272
 
270
273
  fn add_definition<'a>(
271
- definition_scopes_by_name: &mut HashMap<&'a str, Vec<String>>,
274
+ definition_scopes_by_name: &mut FxHashMap<&'a str, Vec<String>>,
272
275
  name: &'a str,
273
276
  scope: &str,
274
277
  ) {
@@ -291,37 +294,37 @@ fn is_definition_visible(definition_scopes: Option<&Vec<String>>, scope: &str) -
291
294
  }
292
295
 
293
296
  fn is_structural_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
294
- let Some(parent) = leaf.node.parent() else {
297
+ let Some(parent) = leaf.node.parent_node() else {
295
298
  return false;
296
299
  };
297
- if leaf.node.kind() == "simple_identifier"
298
- && KOTLIN_DEFINITION_PARENT_TYPES.contains(&parent.kind())
300
+ if leaf.node.kind_name() == "simple_identifier"
301
+ && KOTLIN_DEFINITION_PARENT_TYPES.contains(&parent.kind_name())
299
302
  {
300
303
  return true;
301
304
  }
302
- if leaf.node.kind() == "identifier"
303
- && CSHARP_QUERY_BINDING_PARENT_TYPES.contains(&parent.kind())
305
+ if leaf.node.kind_name() == "identifier"
306
+ && CSHARP_QUERY_BINDING_PARENT_TYPES.contains(&parent.kind_name())
304
307
  && crate::util::named_children(parent)
305
308
  .into_iter()
306
- .find(|child| child.kind() == "identifier")
309
+ .find(|child| child.kind_name() == "identifier")
307
310
  .is_some_and(|first| first.id() == leaf.node.id())
308
311
  {
309
312
  return true;
310
313
  }
311
314
  let (declared, declared_field) = unwrap_declarator_wrappers(leaf);
312
315
  if declared
313
- .parent()
316
+ .parent_node()
314
317
  .is_some_and(|holder| is_definition_field(holder, declared_field))
315
318
  {
316
319
  return true;
317
320
  }
318
- if !DEFINITION_LIST_NODE_TYPES.contains(&parent.kind()) {
321
+ if !DEFINITION_LIST_NODE_TYPES.contains(&parent.kind_name()) {
319
322
  return false;
320
323
  }
321
- let Some(holder) = parent.parent() else {
324
+ let Some(holder) = parent.parent_node() else {
322
325
  return false;
323
326
  };
324
- DEFINITION_LIST_HOLDER_TYPES.contains(&holder.kind())
327
+ DEFINITION_LIST_HOLDER_TYPES.contains(&holder.kind_name())
325
328
  && field_name_in_parent(parent, holder) == Some("left")
326
329
  }
327
330
 
@@ -329,7 +332,7 @@ fn is_definition_field(holder: Node<'_>, field_name: Option<&str>) -> bool {
329
332
  DEFINITION_FIELD_BY_PARENT_TYPE
330
333
  .iter()
331
334
  .any(|(parent_type, definition_field)| {
332
- *parent_type == holder.kind() && field_name == Some(definition_field)
335
+ *parent_type == holder.kind_name() && field_name == Some(definition_field)
333
336
  })
334
337
  }
335
338
 
@@ -342,10 +345,10 @@ fn unwrap_declarator_wrappers<'t>(leaf: &DepDegreeLeaf<'t>) -> (Node<'t>, Option
342
345
  let mut current = leaf.node;
343
346
  let mut field_name = leaf.field_name;
344
347
  while let Some(parent) = current
345
- .parent()
346
- .filter(|parent| DECLARATOR_WRAPPER_TYPES.contains(&parent.kind()))
348
+ .parent_node()
349
+ .filter(|parent| DECLARATOR_WRAPPER_TYPES.contains(&parent.kind_name()))
347
350
  {
348
- let declared_field = if parent.kind() == "qualified_identifier" {
351
+ let declared_field = if parent.kind_name() == "qualified_identifier" {
349
352
  "name"
350
353
  } else {
351
354
  "declarator"
@@ -354,7 +357,7 @@ fn unwrap_declarator_wrappers<'t>(leaf: &DepDegreeLeaf<'t>) -> (Node<'t>, Option
354
357
  break;
355
358
  }
356
359
  field_name = parent
357
- .parent()
360
+ .parent_node()
358
361
  .and_then(|grandparent| field_name_in_parent(parent, grandparent));
359
362
  current = parent;
360
363
  }
@@ -368,12 +371,12 @@ fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
368
371
  // Kotlin has no `type`/`value` fields to veto default values: a function parameter's default
369
372
  // sits in the parameter list (`fun f(b: Int = a)`), a class parameter's inside the parameter
370
373
  // node (`class A(val y: Int = a)`), so only a parameter node's first identifier child binds.
371
- if leaf.node.kind() == "simple_identifier" {
372
- return leaf.node.parent().is_some_and(|parent| {
373
- KOTLIN_PARAMETER_TYPES.contains(&parent.kind())
374
+ if leaf.node.kind_name() == "simple_identifier" {
375
+ return leaf.node.parent_node().is_some_and(|parent| {
376
+ KOTLIN_PARAMETER_TYPES.contains(&parent.kind_name())
374
377
  && crate::util::named_children(parent)
375
378
  .into_iter()
376
- .find(|child| child.kind() == "simple_identifier")
379
+ .find(|child| child.kind_name() == "simple_identifier")
377
380
  .is_some_and(|first| first.id() == leaf.node.id())
378
381
  });
379
382
  }
@@ -384,10 +387,10 @@ fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
384
387
  let declares_member_pointer = is_member_pointer_name(leaf.node);
385
388
  let mut in_member_pointer = false;
386
389
  loop {
387
- let Some(parent) = current.parent() else {
390
+ let Some(parent) = current.parent_node() else {
388
391
  return false;
389
392
  };
390
- let parent_is_parameterish = parent.kind().contains("parameter");
393
+ let parent_is_parameterish = parent.kind_name().contains("parameter");
391
394
  // Beyond the grandparent, only declarator wrappers keep climbing, plus the
392
395
  // `qualified_identifier` nodes that spell a member-pointer parameter's class (`int C::* q`,
393
396
  // `int N::C::* q`) — entered from the pointer declarator and continued through the `name`
@@ -395,13 +398,13 @@ fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
395
398
  // a read. Checking this before the field lookup also keeps reads inside high-arity nodes
396
399
  // O(1).
397
400
  let continues_member_pointer = declares_member_pointer
398
- && parent.kind() == "qualified_identifier"
399
- && (current.kind() == "pointer_type_declarator" || in_member_pointer)
401
+ && parent.kind_name() == "qualified_identifier"
402
+ && (current.kind_name() == "pointer_type_declarator" || in_member_pointer)
400
403
  && field_name_in_parent(current, parent) == Some("name");
401
404
  in_member_pointer = continues_member_pointer;
402
405
  if depth >= 1
403
406
  && !parent_is_parameterish
404
- && !parent.kind().contains("declarator")
407
+ && !parent.kind_name().contains("declarator")
405
408
  && !continues_member_pointer
406
409
  {
407
410
  return false;
@@ -1,10 +1,11 @@
1
1
  use indexmap::IndexMap;
2
+ use rustc_hash::{FxHashMap, FxHashSet};
2
3
  use std::borrow::Cow;
3
- use std::collections::{HashMap, HashSet};
4
4
  use std::sync::OnceLock;
5
5
  use tree_sitter::Node;
6
6
 
7
7
  use crate::near_miss::{Block, Matcher, PairMatch, FILTRATION_PERCENT, MAX_LENGTH_RATIO};
8
+ use crate::tree_index::NodeExt;
8
9
  use crate::types::{
9
10
  CrossFileCandidate, CrossFileToken, CrossFileTokenRange, DuplicateBlockOccurrence,
10
11
  DuplicationMetrics,
@@ -127,22 +128,22 @@ fn is_duplicate_block(node: Node<'_>) -> bool {
127
128
  if !node.is_named() {
128
129
  return false;
129
130
  }
130
- if node.kind() == "try_expression" {
131
+ if node.kind_name() == "try_expression" {
131
132
  return crate::util::is_kotlin_try_expression(node);
132
133
  }
133
- DUPLICATE_BLOCK_TYPES.contains(&node.kind())
134
+ DUPLICATE_BLOCK_TYPES.contains(&node.kind_name())
134
135
  }
135
136
 
136
137
  fn is_statement_container(node: Node<'_>) -> bool {
137
138
  if !node.is_named() {
138
139
  return false;
139
140
  }
140
- if node.kind() == "declaration_list" {
141
- return node
142
- .parent()
143
- .is_some_and(|parent| CSHARP_DECLARATION_LIST_PARENT_TYPES.contains(&parent.kind()));
141
+ if node.kind_name() == "declaration_list" {
142
+ return node.parent_node().is_some_and(|parent| {
143
+ CSHARP_DECLARATION_LIST_PARENT_TYPES.contains(&parent.kind_name())
144
+ });
144
145
  }
145
- STATEMENT_CONTAINER_TYPES.contains(&node.kind())
146
+ STATEMENT_CONTAINER_TYPES.contains(&node.kind_name())
146
147
  }
147
148
 
148
149
  /// Identifier leaves anonymized by occurrence order so consistently renamed copies still match.
@@ -320,13 +321,13 @@ fn is_literal_dense(literal_count: usize, token_count: usize) -> bool {
320
321
  literal_count * 5 >= token_count
321
322
  }
322
323
 
323
- fn literal_kind_by_type() -> &'static HashMap<&'static str, &'static str> {
324
- static MAP: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
324
+ fn literal_kind_by_type() -> &'static FxHashMap<&'static str, &'static str> {
325
+ static MAP: OnceLock<FxHashMap<&'static str, &'static str>> = OnceLock::new();
325
326
  MAP.get_or_init(|| LITERAL_KIND_BY_TYPE.iter().copied().collect())
326
327
  }
327
328
 
328
- fn semantic_name_field_by_parent_type() -> &'static HashMap<&'static str, &'static str> {
329
- static MAP: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
329
+ fn semantic_name_field_by_parent_type() -> &'static FxHashMap<&'static str, &'static str> {
330
+ static MAP: OnceLock<FxHashMap<&'static str, &'static str>> = OnceLock::new();
330
331
  MAP.get_or_init(|| SEMANTIC_NAME_FIELD_BY_PARENT_TYPE.iter().copied().collect())
331
332
  }
332
333
 
@@ -405,7 +406,7 @@ pub fn tokenize<'a>(root: Node<'_>, code: &Source<'a>) -> TokenizedSource<'a> {
405
406
  /// candidates cross-file matching fingerprints in TypeScript.
406
407
  pub fn measure_duplication(
407
408
  source: &TokenizedSource<'_>,
408
- code_line_numbers: &HashSet<usize>,
409
+ code_line_numbers: &FxHashSet<usize>,
409
410
  settings: &DuplicationSettings,
410
411
  ) -> DuplicationMetrics {
411
412
  let tokens = &source.tokens;
@@ -568,7 +569,7 @@ fn collect_tokens<'a>(
568
569
  node.start_position().row,
569
570
  node.end_position().row,
570
571
  ));
571
- } else if !COMMENT_TYPES.contains(&node.kind()) {
572
+ } else if !COMMENT_TYPES.contains(&node.kind_name()) {
572
573
  let mut statement_ranges: Vec<TokenRange> = Vec::new();
573
574
  let is_container = is_statement_container(node);
574
575
  for child in all_children(node) {
@@ -579,7 +580,7 @@ fn collect_tokens<'a>(
579
580
  block_ranges,
580
581
  container_statement_ranges,
581
582
  );
582
- if is_container && child.is_named() && !COMMENT_TYPES.contains(&child.kind()) {
583
+ if is_container && child.is_named() && !COMMENT_TYPES.contains(&child.kind_name()) {
583
584
  statement_ranges.push(child_range);
584
585
  }
585
586
  }
@@ -611,14 +612,14 @@ fn collect_tokens<'a>(
611
612
  /// The kind tag of a string-like node with no interpolation, or None to descend normally.
612
613
  fn atomic_literal_kind(node: Node<'_>) -> Option<&'static str> {
613
614
  let kind = if node.is_named() {
614
- literal_kind_by_type().get(node.kind()).copied()
615
+ literal_kind_by_type().get(node.kind_name()).copied()
615
616
  } else {
616
617
  None
617
618
  };
618
619
  let kind = kind?;
619
620
  if named_children(node)
620
621
  .iter()
621
- .all(|child| STRING_FRAGMENT_TYPES.contains(&child.kind()))
622
+ .all(|child| STRING_FRAGMENT_TYPES.contains(&child.kind_name()))
622
623
  {
623
624
  Some(kind)
624
625
  } else {
@@ -627,13 +628,13 @@ fn atomic_literal_kind(node: Node<'_>) -> Option<&'static str> {
627
628
  }
628
629
 
629
630
  fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Token<'a>>) {
630
- if COMMENT_TYPES.contains(&node.kind()) {
631
+ if COMMENT_TYPES.contains(&node.kind_name()) {
631
632
  return;
632
633
  }
633
634
 
634
635
  let start_row = node.start_position().row;
635
636
  let end_row = node.end_position().row;
636
- if node.is_named() && SHORTHAND_PROPERTY_TYPES.contains(&node.kind()) {
637
+ if node.is_named() && SHORTHAND_PROPERTY_TYPES.contains(&node.kind_name()) {
637
638
  let text = node_text(node, code);
638
639
  tokens.push(make_text_token(
639
640
  Cow::Borrowed(text),
@@ -669,7 +670,7 @@ fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Tok
669
670
  && !pascal_case_regex().is_match(node_text(node, code));
670
671
  if node.is_named()
671
672
  && (is_variable_receiver
672
- || (ANONYMIZED_IDENTIFIER_TYPES.contains(&node.kind())
673
+ || (ANONYMIZED_IDENTIFIER_TYPES.contains(&node.kind_name())
673
674
  && !is_semantic_name_leaf(node, code)))
674
675
  {
675
676
  tokens.push(Token {
@@ -688,7 +689,7 @@ fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Tok
688
689
 
689
690
  // Anything else keeps its text: keywords, operators, punctuation, and semantic names.
690
691
  let literal_kind = if node.is_named() {
691
- literal_kind_by_type().get(node.kind()).copied()
692
+ literal_kind_by_type().get(node.kind_name()).copied()
692
693
  } else {
693
694
  None
694
695
  };
@@ -745,12 +746,12 @@ fn literal_value_text<'a>(node: Node<'_>, kind: &str, code: &Source<'a>) -> Cow<
745
746
  return Cow::Borrowed(node_text(node, code));
746
747
  }
747
748
  // Fragment leaves already carry bare content; a quote appearing there is content.
748
- if STRING_CONTENT_FRAGMENT_TYPES.contains(&node.kind()) {
749
+ if STRING_CONTENT_FRAGMENT_TYPES.contains(&node.kind_name()) {
749
750
  return Cow::Borrowed(node_text(node, code));
750
751
  }
751
752
  let fragments: Vec<&str> = named_children(node)
752
753
  .iter()
753
- .filter(|child| STRING_CONTENT_FRAGMENT_TYPES.contains(&child.kind()))
754
+ .filter(|child| STRING_CONTENT_FRAGMENT_TYPES.contains(&child.kind_name()))
754
755
  .map(|child| node_text(*child, code))
755
756
  .collect();
756
757
  if !fragments.is_empty() {
@@ -758,7 +759,7 @@ fn literal_value_text<'a>(node: Node<'_>, kind: &str, code: &Source<'a>) -> Cow<
758
759
  }
759
760
  // A C# verbatim string (`@"..."`) carries the same value as its ordinary spelling.
760
761
  let text = node_text(node, code);
761
- let text = if node.kind() == "verbatim_string_literal" {
762
+ let text = if node.kind_name() == "verbatim_string_literal" {
762
763
  text.strip_prefix('@').unwrap_or(text)
763
764
  } else {
764
765
  text
@@ -789,13 +790,13 @@ fn build_literal_count_prefix(tokens: &[Token<'_>]) -> Vec<usize> {
789
790
  }
790
791
 
791
792
  fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
792
- let Some(parent) = node.parent() else {
793
+ let Some(parent) = node.parent_node() else {
793
794
  return false;
794
795
  };
795
796
 
796
797
  // Java method references (`Foo::bar`) and Kotlin callable references (`::bar`) name their
797
798
  // identifiers without grammar fields.
798
- if parent.kind() == "method_reference" || parent.kind() == "callable_reference" {
799
+ if parent.kind_name() == "method_reference" || parent.kind_name() == "callable_reference" {
799
800
  return true;
800
801
  }
801
802
 
@@ -803,18 +804,19 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
803
804
  // `new Foo()`) and attribute names (`[Obsolete]`, an `attribute` inside an `attribute_list`, or
804
805
  // `[assembly: Foo]` inside a `global_attribute`; C/C++ attributes hang off other parents and
805
806
  // Python's `attribute` has no `name` field).
806
- if node.kind() == "identifier" {
807
+ if node.kind_name() == "identifier" {
807
808
  let occupies = |field: &str| {
808
809
  parent
809
810
  .child_by_field_name(field)
810
811
  .is_some_and(|field_node| field_node.id() == node.id())
811
812
  };
812
- let is_csharp_attribute = parent.kind() == "attribute"
813
- && parent
814
- .parent()
815
- .is_some_and(|list| matches!(list.kind(), "attribute_list" | "global_attribute"));
816
- if CSHARP_TYPE_PARENT_TYPES.contains(&parent.kind())
817
- || (occupies("type") && !NON_CSHARP_TYPE_FIELD_PARENT_TYPES.contains(&parent.kind()))
813
+ let is_csharp_attribute = parent.kind_name() == "attribute"
814
+ && parent.parent_node().is_some_and(|list| {
815
+ matches!(list.kind_name(), "attribute_list" | "global_attribute")
816
+ });
817
+ if CSHARP_TYPE_PARENT_TYPES.contains(&parent.kind_name())
818
+ || (occupies("type")
819
+ && !NON_CSHARP_TYPE_FIELD_PARENT_TYPES.contains(&parent.kind_name()))
818
820
  || (is_csharp_attribute && occupies("name"))
819
821
  {
820
822
  return true;
@@ -823,35 +825,35 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
823
825
 
824
826
  // Kotlin (no grammar fields): a callee (`foo(...)`), a member name (`a.foo`), an infix function
825
827
  // (`a shl b`), and a named argument (`foo(name = x)`) are API names.
826
- if node.kind() == "simple_identifier" {
827
- if parent.kind() == "navigation_suffix" {
828
+ if node.kind_name() == "simple_identifier" {
829
+ if parent.kind_name() == "navigation_suffix" {
828
830
  return true;
829
831
  }
830
832
  let is_first_named = parent
831
833
  .named_child(0)
832
834
  .is_some_and(|first| first.id() == node.id());
833
- if parent.kind() == "call_expression" && is_first_named {
835
+ if parent.kind_name() == "call_expression" && is_first_named {
834
836
  return true;
835
837
  }
836
- if parent.kind() == "infix_expression"
838
+ if parent.kind_name() == "infix_expression"
837
839
  && parent
838
840
  .named_child(1)
839
841
  .is_some_and(|operator| operator.id() == node.id())
840
842
  {
841
843
  return true;
842
844
  }
843
- if parent.kind() == "value_argument"
845
+ if parent.kind_name() == "value_argument"
844
846
  && is_first_named
845
847
  && node
846
848
  .next_sibling()
847
- .is_some_and(|next| !next.is_named() && next.kind() == "=")
849
+ .is_some_and(|next| !next.is_named() && next.kind_name() == "=")
848
850
  {
849
851
  return true;
850
852
  }
851
853
  }
852
854
 
853
855
  // `call` names its callee `method` in Ruby but `function` in Python; accept both fields.
854
- if parent.kind() == "call"
856
+ if parent.kind_name() == "call"
855
857
  && parent
856
858
  .child_by_field_name("function")
857
859
  .is_some_and(|function| function.id() == node.id())
@@ -860,8 +862,8 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
860
862
  }
861
863
 
862
864
  // A Ruby constant receiving a call (`Alpha.new(...)`) names the invoked API.
863
- if node.kind() == "constant"
864
- && parent.kind() == "call"
865
+ if node.kind_name() == "constant"
866
+ && parent.kind_name() == "call"
865
867
  && parent
866
868
  .child_by_field_name("receiver")
867
869
  .is_some_and(|receiver| receiver.id() == node.id())
@@ -871,7 +873,7 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
871
873
 
872
874
  // Java/C# static receivers (`Alpha.run(...)`, `Console.WriteLine(...)`) name the invoked type;
873
875
  // PascalCase is the discriminator because the tokenizer has no symbol table.
874
- let is_static_receiver = match parent.kind() {
876
+ let is_static_receiver = match parent.kind_name() {
875
877
  "method_invocation" => parent
876
878
  .child_by_field_name("object")
877
879
  .is_some_and(|object| object.id() == node.id()),
@@ -889,7 +891,7 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
889
891
  }
890
892
 
891
893
  // Qualified/generic callees are semantic in call position only.
892
- if (parent.kind() == "scoped_identifier" || parent.kind() == "qualified_identifier")
894
+ if (parent.kind_name() == "scoped_identifier" || parent.kind_name() == "qualified_identifier")
893
895
  && (parent
894
896
  .child_by_field_name("name")
895
897
  .is_some_and(|name| name.id() == node.id())
@@ -898,9 +900,9 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
898
900
  .is_some_and(|path| path.id() == node.id()))
899
901
  {
900
902
  let mut outer = parent;
901
- while let Some(outer_parent) = outer.parent() {
903
+ while let Some(outer_parent) = outer.parent_node() {
902
904
  if matches!(
903
- outer_parent.kind(),
905
+ outer_parent.kind_name(),
904
906
  "scoped_identifier"
905
907
  | "qualified_identifier"
906
908
  | "generic_function"
@@ -911,8 +913,8 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
911
913
  break;
912
914
  }
913
915
  }
914
- if outer.parent().is_some_and(|call| {
915
- call.kind() == "call_expression"
916
+ if outer.parent_node().is_some_and(|call| {
917
+ call.kind_name() == "call_expression"
916
918
  && call
917
919
  .child_by_field_name("function")
918
920
  .is_some_and(|function| function.id() == outer.id())
@@ -922,9 +924,9 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
922
924
  }
923
925
 
924
926
  // Go struct-literal keys (`Config{Timeout: ...}`) have no `key` field in the grammar.
925
- if parent.kind() == "literal_element"
926
- && parent.parent().is_some_and(|grandparent| {
927
- grandparent.kind() == "keyed_element"
927
+ if parent.kind_name() == "literal_element"
928
+ && parent.parent_node().is_some_and(|grandparent| {
929
+ grandparent.kind_name() == "keyed_element"
928
930
  && grandparent
929
931
  .named_child(0)
930
932
  .is_some_and(|first| first.id() == parent.id())
@@ -934,7 +936,7 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
934
936
  }
935
937
 
936
938
  semantic_name_field_by_parent_type()
937
- .get(parent.kind())
939
+ .get(parent.kind_name())
938
940
  .is_some_and(|field| {
939
941
  parent
940
942
  .child_by_field_name(*field)
@@ -1005,7 +1007,7 @@ fn collect_sequence_candidates(
1005
1007
  min_tokens: usize,
1006
1008
  ) -> Vec<DuplicateCandidate> {
1007
1009
  let mut candidates = Vec::new();
1008
- let mut occurrences_by_window_key: HashMap<i64, WindowOccurrences> = HashMap::new();
1010
+ let mut occurrences_by_window_key: FxHashMap<i64, WindowOccurrences> = FxHashMap::default();
1009
1011
  let container_windows: Vec<ContainerWindows> = containers
1010
1012
  .iter()
1011
1013
  .map(|statements| enumerate_container_windows(tokens, statements, min_tokens))
@@ -1111,7 +1113,7 @@ fn collect_sequence_candidates(
1111
1113
 
1112
1114
  // Every emitted window exposes its repeating, unvisited sub-windows; lengths strictly
1113
1115
  // decrease, so the worklist terminates.
1114
- let mut visited: HashSet<SequenceWindow> = maximal_windows.iter().copied().collect();
1116
+ let mut visited: FxHashSet<SequenceWindow> = maximal_windows.iter().copied().collect();
1115
1117
  let mut frontier = maximal_windows;
1116
1118
  while !frontier.is_empty() {
1117
1119
  let mut emitted: Vec<SequenceWindow> = Vec::new();
@@ -1269,7 +1271,7 @@ fn fingerprint_hash_pair(
1269
1271
  fold_literal_values: bool,
1270
1272
  ) -> (i32, i32) {
1271
1273
  let clamped_end = end_token_index.min(tokens.len());
1272
- let mut index_by_identifier: HashMap<&str, usize> = HashMap::new();
1274
+ let mut index_by_identifier: FxHashMap<&str, usize> = FxHashMap::default();
1273
1275
  let mut index_hashes: Vec<(i32, i32)> = Vec::new();
1274
1276
  let mut primary: i32 = 5381;
1275
1277
  let mut secondary: i32 = 52_711;
@@ -1344,7 +1346,7 @@ fn select_maximal_duplicates(
1344
1346
  .filter(|group| group.len() >= 2)
1345
1347
  .collect();
1346
1348
  // Greedy order ranks by total coverage (region size × copies).
1347
- let group_size_by_fingerprint: HashMap<std::rc::Rc<str>, usize> = groups
1349
+ let group_size_by_fingerprint: FxHashMap<std::rc::Rc<str>, usize> = groups
1348
1350
  .iter()
1349
1351
  .map(|group| {
1350
1352
  (
@@ -1925,7 +1927,7 @@ fn collect_near_miss_groups(
1925
1927
  // Rebuild the component as ONE group with one coalesced occurrence per member block:
1926
1928
  // the fragments every node of a block overlaps are collected together, since a block's
1927
1929
  // cores are parts of one copy.
1928
- let mut consumed: HashSet<(usize, usize)> = HashSet::new();
1930
+ let mut consumed: FxHashSet<(usize, usize)> = FxHashSet::default();
1929
1931
  let mut merged: Vec<CountedOccurrence> = Vec::new();
1930
1932
  let mut unanchored_nodes: Vec<usize> = Vec::new();
1931
1933
  let mut nodes_by_block: IndexMap<usize, Vec<usize>> = IndexMap::new();
@@ -1974,7 +1976,7 @@ fn collect_near_miss_groups(
1974
1976
  (occurrence.start_token_index, occurrence.end_token_index)
1975
1977
  });
1976
1978
  let mut copy_parts: Vec<CountedOccurrence> = Vec::new();
1977
- let mut copy_groups: HashSet<usize> = HashSet::new();
1979
+ let mut copy_groups: FxHashSet<usize> = FxHashSet::default();
1978
1980
  for (occurrence, group_index) in fragments {
1979
1981
  if copy_groups.contains(&group_index) {
1980
1982
  merged.push(coalesce_occurrences(std::mem::take(&mut copy_parts)));
@@ -2156,8 +2158,8 @@ fn merge_overlapping_cores(cores: &[(usize, usize)]) -> Vec<(usize, usize)> {
2156
2158
  /// Interned per call so a file's symbol ids (and thus its n-gram hashes) never depend on which
2157
2159
  /// other files the process measured before it.
2158
2160
  fn to_symbol_stream(tokens: &[Token<'_>]) -> (Vec<i32>, Vec<bool>) {
2159
- let mut symbol_by_token_hashes: HashMap<(i32, i32, i32, i32), i32> = HashMap::new();
2160
- let mut id_by_identifier: HashMap<&str, i32> = HashMap::new();
2161
+ let mut symbol_by_token_hashes: FxHashMap<(i32, i32, i32, i32), i32> = FxHashMap::default();
2162
+ let mut id_by_identifier: FxHashMap<&str, i32> = FxHashMap::default();
2161
2163
  tokens
2162
2164
  .iter()
2163
2165
  .map(|token| {
@@ -2222,7 +2224,7 @@ fn for_each_candidate_pair(
2222
2224
  ) {
2223
2225
  let mut order: Vec<usize> = (0..blocks.len()).collect();
2224
2226
  order.sort_by_key(|&index| blocks[index].len());
2225
- let mut postings: HashMap<i32, Vec<usize>> = HashMap::new();
2227
+ let mut postings: FxHashMap<i32, Vec<usize>> = FxHashMap::default();
2226
2228
  let mut shared_counts = vec![0usize; blocks.len()];
2227
2229
  let mut touched: Vec<usize> = Vec::new();
2228
2230
  for right in order {
@@ -2284,13 +2286,13 @@ fn count_redundant_fragments(group: &[CountedOccurrence]) -> usize {
2284
2286
 
2285
2287
  fn summarize_duplicates(
2286
2288
  groups: &[Vec<CountedOccurrence>],
2287
- code_line_numbers: &HashSet<usize>,
2289
+ code_line_numbers: &FxHashSet<usize>,
2288
2290
  tokens: &[Token<'_>],
2289
2291
  ) -> DuplicationMetrics {
2290
2292
  let mut duplicate_block_count = 0;
2291
2293
  let mut max_duplicate_block_size = 0;
2292
2294
  let mut duplicate_block_groups: Vec<Vec<DuplicateBlockOccurrence>> = Vec::new();
2293
- let mut duplicated_lines: HashSet<usize> = HashSet::new();
2295
+ let mut duplicated_lines: FxHashSet<usize> = FxHashSet::default();
2294
2296
  for group in groups {
2295
2297
  duplicate_block_count += count_redundant_fragments(group);
2296
2298
  for occurrence in group {