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,15 +1,16 @@
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,
11
12
  };
12
- use crate::util::{all_children, is_identifier_leaf, named_children, node_text, to_int32, Source};
13
+ use crate::util::{all_children, named_children, node_text, to_int32, Source};
13
14
 
14
15
  /// Block-like nodes considered as whole-subtree duplicate candidates.
15
16
  const DUPLICATE_BLOCK_TYPES: &[&str] = &[
@@ -61,10 +62,7 @@ const DUPLICATE_BLOCK_TYPES: &[&str] = &[
61
62
  "do_while_statement",
62
63
  "catch_block",
63
64
  "finally_block",
64
- "statements",
65
- "control_structure_body",
66
65
  "function_body",
67
- "jump_expression",
68
66
  "jsx_element",
69
67
  "jsx_self_closing_element",
70
68
  "if",
@@ -105,10 +103,9 @@ const STATEMENT_CONTAINER_TYPES: &[&str] = &[
105
103
  "default_case",
106
104
  "compilation_unit",
107
105
  "switch_section",
108
- "statements",
109
106
  "enum_class_body",
110
- "control_structure_body",
111
107
  "function_body",
108
+ "lambda_literal",
112
109
  ];
113
110
 
114
111
  /// C# type bodies are `declaration_list`s, a name Rust also uses for `mod`/`impl`/`trait` bodies;
@@ -121,35 +118,33 @@ const CSHARP_DECLARATION_LIST_PARENT_TYPES: &[&str] = &[
121
118
  "namespace_declaration",
122
119
  ];
123
120
 
124
- /// Whole-subtree duplicate candidates: DUPLICATE_BLOCK_TYPES plus Kotlin's `try_expression`,
125
- /// distinguished by its clause children from Rust's `try_expression` (the `?` operator).
121
+ /// Whole-subtree duplicate candidates: DUPLICATE_BLOCK_TYPES plus Kotlin's `try_expression`, which
122
+ /// shares its name with Rust's `?` operator (see is_kotlin_try_expression).
126
123
  fn is_duplicate_block(node: Node<'_>) -> bool {
127
124
  if !node.is_named() {
128
125
  return false;
129
126
  }
130
- if node.kind() == "try_expression" {
127
+ if node.kind_name() == "try_expression" {
131
128
  return crate::util::is_kotlin_try_expression(node);
132
129
  }
133
- DUPLICATE_BLOCK_TYPES.contains(&node.kind())
130
+ DUPLICATE_BLOCK_TYPES.contains(&node.kind_name())
134
131
  }
135
132
 
136
133
  fn is_statement_container(node: Node<'_>) -> bool {
137
134
  if !node.is_named() {
138
135
  return false;
139
136
  }
140
- if node.kind() == "declaration_list" {
141
- return node
142
- .parent()
143
- .is_some_and(|parent| CSHARP_DECLARATION_LIST_PARENT_TYPES.contains(&parent.kind()));
137
+ if node.kind_name() == "declaration_list" {
138
+ return node.parent_node().is_some_and(|parent| {
139
+ CSHARP_DECLARATION_LIST_PARENT_TYPES.contains(&parent.kind_name())
140
+ });
144
141
  }
145
- STATEMENT_CONTAINER_TYPES.contains(&node.kind())
142
+ STATEMENT_CONTAINER_TYPES.contains(&node.kind_name())
146
143
  }
147
144
 
148
145
  /// Identifier leaves anonymized by occurrence order so consistently renamed copies still match.
149
146
  const ANONYMIZED_IDENTIFIER_TYPES: &[&str] = &[
150
147
  "identifier",
151
- "simple_identifier",
152
- "interpolated_identifier",
153
148
  "implicit_parameter",
154
149
  "constant",
155
150
  "instance_variable",
@@ -171,8 +166,6 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
171
166
  ("integer_literal", "#num"),
172
167
  ("float_literal", "#num"),
173
168
  ("real_literal", "#num"),
174
- ("hex_literal", "#num"),
175
- ("bin_literal", "#num"),
176
169
  ("int_literal", "#num"),
177
170
  ("rune_literal", "#char"),
178
171
  ("imaginary_literal", "#num"),
@@ -187,7 +180,6 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
187
180
  ("string_content", "#str"),
188
181
  ("string_literal_content", "#str"),
189
182
  ("character_literal_content", "#char"),
190
- ("character_escape_seq", "#char"),
191
183
  ("verbatim_string_literal", "#str"),
192
184
  ("interpolated_string_expression", "#str"),
193
185
  ("raw_string_content", "#str"),
@@ -207,12 +199,7 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
207
199
  ("regex_pattern", "#regex"),
208
200
  ];
209
201
 
210
- const COMMENT_TYPES: &[&str] = &[
211
- "comment",
212
- "line_comment",
213
- "block_comment",
214
- "multiline_comment",
215
- ];
202
+ const COMMENT_TYPES: &[&str] = &["comment", "line_comment", "block_comment"];
216
203
 
217
204
  /// Children of a string node that carry only literal content; anything else is interpolation.
218
205
  const STRING_FRAGMENT_TYPES: &[&str] = &[
@@ -221,7 +208,6 @@ const STRING_FRAGMENT_TYPES: &[&str] = &[
221
208
  "string_content",
222
209
  "string_literal_content",
223
210
  "character_literal_content",
224
- "character_escape_seq",
225
211
  "raw_string_content",
226
212
  "raw_string_start",
227
213
  "raw_string_end",
@@ -285,7 +271,6 @@ const STRING_CONTENT_FRAGMENT_TYPES: &[&str] = &[
285
271
  "string_content",
286
272
  "string_literal_content",
287
273
  "character_literal_content",
288
- "character_escape_seq",
289
274
  "raw_string_content",
290
275
  "escape_sequence",
291
276
  "heredoc_content",
@@ -320,13 +305,13 @@ fn is_literal_dense(literal_count: usize, token_count: usize) -> bool {
320
305
  literal_count * 5 >= token_count
321
306
  }
322
307
 
323
- fn literal_kind_by_type() -> &'static HashMap<&'static str, &'static str> {
324
- static MAP: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
308
+ fn literal_kind_by_type() -> &'static FxHashMap<&'static str, &'static str> {
309
+ static MAP: OnceLock<FxHashMap<&'static str, &'static str>> = OnceLock::new();
325
310
  MAP.get_or_init(|| LITERAL_KIND_BY_TYPE.iter().copied().collect())
326
311
  }
327
312
 
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();
313
+ fn semantic_name_field_by_parent_type() -> &'static FxHashMap<&'static str, &'static str> {
314
+ static MAP: OnceLock<FxHashMap<&'static str, &'static str>> = OnceLock::new();
330
315
  MAP.get_or_init(|| SEMANTIC_NAME_FIELD_BY_PARENT_TYPE.iter().copied().collect())
331
316
  }
332
317
 
@@ -405,7 +390,7 @@ pub fn tokenize<'a>(root: Node<'_>, code: &Source<'a>) -> TokenizedSource<'a> {
405
390
  /// candidates cross-file matching fingerprints in TypeScript.
406
391
  pub fn measure_duplication(
407
392
  source: &TokenizedSource<'_>,
408
- code_line_numbers: &HashSet<usize>,
393
+ code_line_numbers: &FxHashSet<usize>,
409
394
  settings: &DuplicationSettings,
410
395
  ) -> DuplicationMetrics {
411
396
  let tokens = &source.tokens;
@@ -551,12 +536,12 @@ fn collect_tokens<'a>(
551
536
  container_statement_ranges: &mut Vec<Vec<TokenRange>>,
552
537
  ) -> TokenRange {
553
538
  let start_token_index = tokens.len();
554
- let atomic_kind = if is_identifier_leaf(node) {
539
+ let atomic_kind = if node.child_count() == 0 {
555
540
  None
556
541
  } else {
557
542
  atomic_literal_kind(node)
558
543
  };
559
- if is_identifier_leaf(node) {
544
+ if node.child_count() == 0 {
560
545
  append_leaf_token(node, code, tokens);
561
546
  } else if let Some(atomic_kind) = atomic_kind {
562
547
  // Interpolation-free strings collapse to their kind tag so copies differing only in
@@ -568,7 +553,7 @@ fn collect_tokens<'a>(
568
553
  node.start_position().row,
569
554
  node.end_position().row,
570
555
  ));
571
- } else if !COMMENT_TYPES.contains(&node.kind()) {
556
+ } else if !COMMENT_TYPES.contains(&node.kind_name()) {
572
557
  let mut statement_ranges: Vec<TokenRange> = Vec::new();
573
558
  let is_container = is_statement_container(node);
574
559
  for child in all_children(node) {
@@ -579,7 +564,12 @@ fn collect_tokens<'a>(
579
564
  block_ranges,
580
565
  container_statement_ranges,
581
566
  );
582
- if is_container && child.is_named() && !COMMENT_TYPES.contains(&child.kind()) {
567
+ // A Kotlin lambda's parameters sit beside its statements.
568
+ if is_container
569
+ && child.is_named()
570
+ && !COMMENT_TYPES.contains(&child.kind_name())
571
+ && child.kind_name() != "lambda_parameters"
572
+ {
583
573
  statement_ranges.push(child_range);
584
574
  }
585
575
  }
@@ -611,14 +601,14 @@ fn collect_tokens<'a>(
611
601
  /// The kind tag of a string-like node with no interpolation, or None to descend normally.
612
602
  fn atomic_literal_kind(node: Node<'_>) -> Option<&'static str> {
613
603
  let kind = if node.is_named() {
614
- literal_kind_by_type().get(node.kind()).copied()
604
+ literal_kind_by_type().get(node.kind_name()).copied()
615
605
  } else {
616
606
  None
617
607
  };
618
608
  let kind = kind?;
619
609
  if named_children(node)
620
610
  .iter()
621
- .all(|child| STRING_FRAGMENT_TYPES.contains(&child.kind()))
611
+ .all(|child| STRING_FRAGMENT_TYPES.contains(&child.kind_name()))
622
612
  {
623
613
  Some(kind)
624
614
  } else {
@@ -627,13 +617,13 @@ fn atomic_literal_kind(node: Node<'_>) -> Option<&'static str> {
627
617
  }
628
618
 
629
619
  fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Token<'a>>) {
630
- if COMMENT_TYPES.contains(&node.kind()) {
620
+ if COMMENT_TYPES.contains(&node.kind_name()) {
631
621
  return;
632
622
  }
633
623
 
634
624
  let start_row = node.start_position().row;
635
625
  let end_row = node.end_position().row;
636
- if node.is_named() && SHORTHAND_PROPERTY_TYPES.contains(&node.kind()) {
626
+ if node.is_named() && SHORTHAND_PROPERTY_TYPES.contains(&node.kind_name()) {
637
627
  let text = node_text(node, code);
638
628
  tokens.push(make_text_token(
639
629
  Cow::Borrowed(text),
@@ -663,14 +653,19 @@ fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Tok
663
653
  return;
664
654
  }
665
655
 
666
- // A Kotlin bound callable-reference receiver (`xs::size`) renames like a variable unless it is
667
- // PascalCase, the discriminator used for static receivers everywhere else.
668
- let is_variable_receiver = crate::util::is_kotlin_callable_receiver(node)
669
- && !pascal_case_regex().is_match(node_text(node, code));
656
+ if crate::util::is_kotlin_keyword_literal(node, code) {
657
+ tokens.push(make_text_token(
658
+ Cow::Borrowed(node_text(node, code)),
659
+ None,
660
+ false,
661
+ start_row,
662
+ end_row,
663
+ ));
664
+ return;
665
+ }
670
666
  if node.is_named()
671
- && (is_variable_receiver
672
- || (ANONYMIZED_IDENTIFIER_TYPES.contains(&node.kind())
673
- && !is_semantic_name_leaf(node, code)))
667
+ && ANONYMIZED_IDENTIFIER_TYPES.contains(&node.kind_name())
668
+ && !is_semantic_name_leaf(node, code)
674
669
  {
675
670
  tokens.push(Token {
676
671
  is_id: true,
@@ -688,7 +683,7 @@ fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Tok
688
683
 
689
684
  // Anything else keeps its text: keywords, operators, punctuation, and semantic names.
690
685
  let literal_kind = if node.is_named() {
691
- literal_kind_by_type().get(node.kind()).copied()
686
+ literal_kind_by_type().get(node.kind_name()).copied()
692
687
  } else {
693
688
  None
694
689
  };
@@ -745,12 +740,12 @@ fn literal_value_text<'a>(node: Node<'_>, kind: &str, code: &Source<'a>) -> Cow<
745
740
  return Cow::Borrowed(node_text(node, code));
746
741
  }
747
742
  // Fragment leaves already carry bare content; a quote appearing there is content.
748
- if STRING_CONTENT_FRAGMENT_TYPES.contains(&node.kind()) {
743
+ if STRING_CONTENT_FRAGMENT_TYPES.contains(&node.kind_name()) {
749
744
  return Cow::Borrowed(node_text(node, code));
750
745
  }
751
746
  let fragments: Vec<&str> = named_children(node)
752
747
  .iter()
753
- .filter(|child| STRING_CONTENT_FRAGMENT_TYPES.contains(&child.kind()))
748
+ .filter(|child| STRING_CONTENT_FRAGMENT_TYPES.contains(&child.kind_name()))
754
749
  .map(|child| node_text(*child, code))
755
750
  .collect();
756
751
  if !fragments.is_empty() {
@@ -758,7 +753,7 @@ fn literal_value_text<'a>(node: Node<'_>, kind: &str, code: &Source<'a>) -> Cow<
758
753
  }
759
754
  // A C# verbatim string (`@"..."`) carries the same value as its ordinary spelling.
760
755
  let text = node_text(node, code);
761
- let text = if node.kind() == "verbatim_string_literal" {
756
+ let text = if node.kind_name() == "verbatim_string_literal" {
762
757
  text.strip_prefix('@').unwrap_or(text)
763
758
  } else {
764
759
  text
@@ -789,13 +784,20 @@ fn build_literal_count_prefix(tokens: &[Token<'_>]) -> Vec<usize> {
789
784
  }
790
785
 
791
786
  fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
792
- let Some(parent) = node.parent() else {
787
+ let Some(parent) = node.parent_node() else {
793
788
  return false;
794
789
  };
795
790
 
796
791
  // Java method references (`Foo::bar`) and Kotlin callable references (`::bar`) name their
797
- // identifiers without grammar fields.
798
- if parent.kind() == "method_reference" || parent.kind() == "callable_reference" {
792
+ // identifiers without grammar fields, and Kotlin spells type names as identifiers.
793
+ if matches!(
794
+ parent.kind_name(),
795
+ "method_reference" | "callable_reference" | "user_type"
796
+ ) {
797
+ return true;
798
+ }
799
+ // A Kotlin type parameter's name has no field (C# puts its own in `name`).
800
+ if parent.kind_name() == "type_parameter" && parent.child_by_field_name("name").is_none() {
799
801
  return true;
800
802
  }
801
803
 
@@ -803,55 +805,70 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
803
805
  // `new Foo()`) and attribute names (`[Obsolete]`, an `attribute` inside an `attribute_list`, or
804
806
  // `[assembly: Foo]` inside a `global_attribute`; C/C++ attributes hang off other parents and
805
807
  // Python's `attribute` has no `name` field).
806
- if node.kind() == "identifier" {
808
+ if node.kind_name() == "identifier" {
807
809
  let occupies = |field: &str| {
808
810
  parent
809
811
  .child_by_field_name(field)
810
812
  .is_some_and(|field_node| field_node.id() == node.id())
811
813
  };
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()))
814
+ let is_csharp_attribute = parent.kind_name() == "attribute"
815
+ && parent.parent_node().is_some_and(|list| {
816
+ matches!(list.kind_name(), "attribute_list" | "global_attribute")
817
+ });
818
+ if CSHARP_TYPE_PARENT_TYPES.contains(&parent.kind_name())
819
+ || (occupies("type")
820
+ && !NON_CSHARP_TYPE_FIELD_PARENT_TYPES.contains(&parent.kind_name()))
818
821
  || (is_csharp_attribute && occupies("name"))
819
822
  {
820
823
  return true;
821
824
  }
822
825
  }
823
826
 
824
- // Kotlin (no grammar fields): a callee (`foo(...)`), a member name (`a.foo`), an infix function
825
- // (`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
- return true;
829
- }
827
+ // Kotlin (no callee or member fields): a callee (`foo(...)`, whose call ends in Kotlin's own
828
+ // argument nodes), a member name (`a.foo`), an infix function (`a shl b`), and a named
829
+ // argument (`foo(name = x)`) are API names.
830
+ if node.kind_name() == "identifier" {
830
831
  let is_first_named = parent
831
832
  .named_child(0)
832
833
  .is_some_and(|first| first.id() == node.id());
833
- if parent.kind() == "call_expression" && is_first_named {
834
+ let is_last_named = parent
835
+ .named_child(parent.named_child_count().saturating_sub(1))
836
+ .is_some_and(|last| last.id() == node.id());
837
+ if parent.kind_name() == "navigation_expression" && is_last_named && !is_first_named {
838
+ return true;
839
+ }
840
+ if parent.kind_name() == "call_expression"
841
+ && is_first_named
842
+ && parent
843
+ .named_child(parent.named_child_count().saturating_sub(1))
844
+ .is_some_and(|arguments| {
845
+ matches!(
846
+ arguments.kind_name(),
847
+ "value_arguments" | "annotated_lambda"
848
+ )
849
+ })
850
+ {
834
851
  return true;
835
852
  }
836
- if parent.kind() == "infix_expression"
853
+ if parent.kind_name() == "infix_expression"
837
854
  && parent
838
855
  .named_child(1)
839
856
  .is_some_and(|operator| operator.id() == node.id())
840
857
  {
841
858
  return true;
842
859
  }
843
- if parent.kind() == "value_argument"
860
+ if parent.kind_name() == "value_argument"
844
861
  && is_first_named
845
862
  && node
846
863
  .next_sibling()
847
- .is_some_and(|next| !next.is_named() && next.kind() == "=")
864
+ .is_some_and(|next| !next.is_named() && next.kind_name() == "=")
848
865
  {
849
866
  return true;
850
867
  }
851
868
  }
852
869
 
853
870
  // `call` names its callee `method` in Ruby but `function` in Python; accept both fields.
854
- if parent.kind() == "call"
871
+ if parent.kind_name() == "call"
855
872
  && parent
856
873
  .child_by_field_name("function")
857
874
  .is_some_and(|function| function.id() == node.id())
@@ -860,8 +877,8 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
860
877
  }
861
878
 
862
879
  // A Ruby constant receiving a call (`Alpha.new(...)`) names the invoked API.
863
- if node.kind() == "constant"
864
- && parent.kind() == "call"
880
+ if node.kind_name() == "constant"
881
+ && parent.kind_name() == "call"
865
882
  && parent
866
883
  .child_by_field_name("receiver")
867
884
  .is_some_and(|receiver| receiver.id() == node.id())
@@ -871,7 +888,7 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
871
888
 
872
889
  // Java/C# static receivers (`Alpha.run(...)`, `Console.WriteLine(...)`) name the invoked type;
873
890
  // PascalCase is the discriminator because the tokenizer has no symbol table.
874
- let is_static_receiver = match parent.kind() {
891
+ let is_static_receiver = match parent.kind_name() {
875
892
  "method_invocation" => parent
876
893
  .child_by_field_name("object")
877
894
  .is_some_and(|object| object.id() == node.id()),
@@ -889,7 +906,7 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
889
906
  }
890
907
 
891
908
  // Qualified/generic callees are semantic in call position only.
892
- if (parent.kind() == "scoped_identifier" || parent.kind() == "qualified_identifier")
909
+ if (parent.kind_name() == "scoped_identifier" || parent.kind_name() == "qualified_identifier")
893
910
  && (parent
894
911
  .child_by_field_name("name")
895
912
  .is_some_and(|name| name.id() == node.id())
@@ -898,9 +915,9 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
898
915
  .is_some_and(|path| path.id() == node.id()))
899
916
  {
900
917
  let mut outer = parent;
901
- while let Some(outer_parent) = outer.parent() {
918
+ while let Some(outer_parent) = outer.parent_node() {
902
919
  if matches!(
903
- outer_parent.kind(),
920
+ outer_parent.kind_name(),
904
921
  "scoped_identifier"
905
922
  | "qualified_identifier"
906
923
  | "generic_function"
@@ -911,8 +928,8 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
911
928
  break;
912
929
  }
913
930
  }
914
- if outer.parent().is_some_and(|call| {
915
- call.kind() == "call_expression"
931
+ if outer.parent_node().is_some_and(|call| {
932
+ call.kind_name() == "call_expression"
916
933
  && call
917
934
  .child_by_field_name("function")
918
935
  .is_some_and(|function| function.id() == outer.id())
@@ -922,9 +939,9 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
922
939
  }
923
940
 
924
941
  // 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"
942
+ if parent.kind_name() == "literal_element"
943
+ && parent.parent_node().is_some_and(|grandparent| {
944
+ grandparent.kind_name() == "keyed_element"
928
945
  && grandparent
929
946
  .named_child(0)
930
947
  .is_some_and(|first| first.id() == parent.id())
@@ -934,7 +951,7 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
934
951
  }
935
952
 
936
953
  semantic_name_field_by_parent_type()
937
- .get(parent.kind())
954
+ .get(parent.kind_name())
938
955
  .is_some_and(|field| {
939
956
  parent
940
957
  .child_by_field_name(*field)
@@ -1005,7 +1022,7 @@ fn collect_sequence_candidates(
1005
1022
  min_tokens: usize,
1006
1023
  ) -> Vec<DuplicateCandidate> {
1007
1024
  let mut candidates = Vec::new();
1008
- let mut occurrences_by_window_key: HashMap<i64, WindowOccurrences> = HashMap::new();
1025
+ let mut occurrences_by_window_key: FxHashMap<i64, WindowOccurrences> = FxHashMap::default();
1009
1026
  let container_windows: Vec<ContainerWindows> = containers
1010
1027
  .iter()
1011
1028
  .map(|statements| enumerate_container_windows(tokens, statements, min_tokens))
@@ -1111,7 +1128,7 @@ fn collect_sequence_candidates(
1111
1128
 
1112
1129
  // Every emitted window exposes its repeating, unvisited sub-windows; lengths strictly
1113
1130
  // decrease, so the worklist terminates.
1114
- let mut visited: HashSet<SequenceWindow> = maximal_windows.iter().copied().collect();
1131
+ let mut visited: FxHashSet<SequenceWindow> = maximal_windows.iter().copied().collect();
1115
1132
  let mut frontier = maximal_windows;
1116
1133
  while !frontier.is_empty() {
1117
1134
  let mut emitted: Vec<SequenceWindow> = Vec::new();
@@ -1269,7 +1286,7 @@ fn fingerprint_hash_pair(
1269
1286
  fold_literal_values: bool,
1270
1287
  ) -> (i32, i32) {
1271
1288
  let clamped_end = end_token_index.min(tokens.len());
1272
- let mut index_by_identifier: HashMap<&str, usize> = HashMap::new();
1289
+ let mut index_by_identifier: FxHashMap<&str, usize> = FxHashMap::default();
1273
1290
  let mut index_hashes: Vec<(i32, i32)> = Vec::new();
1274
1291
  let mut primary: i32 = 5381;
1275
1292
  let mut secondary: i32 = 52_711;
@@ -1344,7 +1361,7 @@ fn select_maximal_duplicates(
1344
1361
  .filter(|group| group.len() >= 2)
1345
1362
  .collect();
1346
1363
  // Greedy order ranks by total coverage (region size × copies).
1347
- let group_size_by_fingerprint: HashMap<std::rc::Rc<str>, usize> = groups
1364
+ let group_size_by_fingerprint: FxHashMap<std::rc::Rc<str>, usize> = groups
1348
1365
  .iter()
1349
1366
  .map(|group| {
1350
1367
  (
@@ -1925,7 +1942,7 @@ fn collect_near_miss_groups(
1925
1942
  // Rebuild the component as ONE group with one coalesced occurrence per member block:
1926
1943
  // the fragments every node of a block overlaps are collected together, since a block's
1927
1944
  // cores are parts of one copy.
1928
- let mut consumed: HashSet<(usize, usize)> = HashSet::new();
1945
+ let mut consumed: FxHashSet<(usize, usize)> = FxHashSet::default();
1929
1946
  let mut merged: Vec<CountedOccurrence> = Vec::new();
1930
1947
  let mut unanchored_nodes: Vec<usize> = Vec::new();
1931
1948
  let mut nodes_by_block: IndexMap<usize, Vec<usize>> = IndexMap::new();
@@ -1974,7 +1991,7 @@ fn collect_near_miss_groups(
1974
1991
  (occurrence.start_token_index, occurrence.end_token_index)
1975
1992
  });
1976
1993
  let mut copy_parts: Vec<CountedOccurrence> = Vec::new();
1977
- let mut copy_groups: HashSet<usize> = HashSet::new();
1994
+ let mut copy_groups: FxHashSet<usize> = FxHashSet::default();
1978
1995
  for (occurrence, group_index) in fragments {
1979
1996
  if copy_groups.contains(&group_index) {
1980
1997
  merged.push(coalesce_occurrences(std::mem::take(&mut copy_parts)));
@@ -2156,8 +2173,8 @@ fn merge_overlapping_cores(cores: &[(usize, usize)]) -> Vec<(usize, usize)> {
2156
2173
  /// Interned per call so a file's symbol ids (and thus its n-gram hashes) never depend on which
2157
2174
  /// other files the process measured before it.
2158
2175
  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();
2176
+ let mut symbol_by_token_hashes: FxHashMap<(i32, i32, i32, i32), i32> = FxHashMap::default();
2177
+ let mut id_by_identifier: FxHashMap<&str, i32> = FxHashMap::default();
2161
2178
  tokens
2162
2179
  .iter()
2163
2180
  .map(|token| {
@@ -2222,7 +2239,7 @@ fn for_each_candidate_pair(
2222
2239
  ) {
2223
2240
  let mut order: Vec<usize> = (0..blocks.len()).collect();
2224
2241
  order.sort_by_key(|&index| blocks[index].len());
2225
- let mut postings: HashMap<i32, Vec<usize>> = HashMap::new();
2242
+ let mut postings: FxHashMap<i32, Vec<usize>> = FxHashMap::default();
2226
2243
  let mut shared_counts = vec![0usize; blocks.len()];
2227
2244
  let mut touched: Vec<usize> = Vec::new();
2228
2245
  for right in order {
@@ -2284,13 +2301,13 @@ fn count_redundant_fragments(group: &[CountedOccurrence]) -> usize {
2284
2301
 
2285
2302
  fn summarize_duplicates(
2286
2303
  groups: &[Vec<CountedOccurrence>],
2287
- code_line_numbers: &HashSet<usize>,
2304
+ code_line_numbers: &FxHashSet<usize>,
2288
2305
  tokens: &[Token<'_>],
2289
2306
  ) -> DuplicationMetrics {
2290
2307
  let mut duplicate_block_count = 0;
2291
2308
  let mut max_duplicate_block_size = 0;
2292
2309
  let mut duplicate_block_groups: Vec<Vec<DuplicateBlockOccurrence>> = Vec::new();
2293
- let mut duplicated_lines: HashSet<usize> = HashSet::new();
2310
+ let mut duplicated_lines: FxHashSet<usize> = FxHashSet::default();
2294
2311
  for group in groups {
2295
2312
  duplicate_block_count += count_redundant_fragments(group);
2296
2313
  for occurrence in group {