code-gauge 4.7.1 → 5.0.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 (80) hide show
  1. package/README.md +35 -10
  2. package/THIRD-PARTY-NOTICES.txt +595 -29
  3. package/dist/cli.cjs +3 -3
  4. package/dist/cli.cjs.map +1 -1
  5. package/dist/cli.js +3 -3
  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.d.ts +13 -3
  10. package/dist/cliConfig.js +1 -1
  11. package/dist/cliConfig.js.map +1 -1
  12. package/dist/crossFileDuplication.cjs +1 -1
  13. package/dist/crossFileDuplication.cjs.map +1 -1
  14. package/dist/crossFileDuplication.js +1 -1
  15. package/dist/crossFileDuplication.js.map +1 -1
  16. package/dist/crossFileNearMiss.cjs +1 -1
  17. package/dist/crossFileNearMiss.cjs.map +1 -1
  18. package/dist/crossFileNearMiss.js +1 -1
  19. package/dist/crossFileNearMiss.js.map +1 -1
  20. package/dist/diffCommand.cjs +3 -3
  21. package/dist/diffCommand.cjs.map +1 -1
  22. package/dist/diffCommand.js +3 -3
  23. package/dist/diffCommand.js.map +1 -1
  24. package/dist/duplicateSelection.cjs +1 -1
  25. package/dist/duplicateSelection.cjs.map +1 -1
  26. package/dist/duplicateSelection.js +1 -1
  27. package/dist/duplicateSelection.js.map +1 -1
  28. package/dist/duplication.cjs +1 -1
  29. package/dist/duplication.cjs.map +1 -1
  30. package/dist/duplication.js +1 -1
  31. package/dist/duplication.js.map +1 -1
  32. package/dist/exclusion.cjs +2 -0
  33. package/dist/exclusion.cjs.map +1 -0
  34. package/dist/exclusion.d.ts +30 -0
  35. package/dist/exclusion.js +2 -0
  36. package/dist/exclusion.js.map +1 -0
  37. package/dist/git.cjs +1 -1
  38. package/dist/git.cjs.map +1 -1
  39. package/dist/git.d.ts +16 -0
  40. package/dist/git.js +1 -1
  41. package/dist/git.js.map +1 -1
  42. package/dist/index.cjs +1 -1
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +1 -1
  45. package/dist/languages.cjs +1 -1
  46. package/dist/languages.cjs.map +1 -1
  47. package/dist/languages.js +1 -1
  48. package/dist/languages.js.map +1 -1
  49. package/dist/newDuplication.cjs +2 -0
  50. package/dist/newDuplication.cjs.map +1 -0
  51. package/dist/newDuplication.d.ts +40 -0
  52. package/dist/newDuplication.js +2 -0
  53. package/dist/newDuplication.js.map +1 -0
  54. package/dist/regressionGate.cjs +1 -1
  55. package/dist/regressionGate.cjs.map +1 -1
  56. package/dist/regressionGate.d.ts +8 -6
  57. package/dist/regressionGate.js +1 -1
  58. package/dist/regressionGate.js.map +1 -1
  59. package/dist/scan.cjs +1 -1
  60. package/dist/scan.cjs.map +1 -1
  61. package/dist/scan.d.ts +12 -3
  62. package/dist/scan.js +1 -1
  63. package/dist/scan.js.map +1 -1
  64. package/dist/worker.cjs +1 -1
  65. package/dist/worker.cjs.map +1 -1
  66. package/dist/worker.js +1 -1
  67. package/dist/worker.js.map +1 -1
  68. package/native/Cargo.lock +47 -15
  69. package/native/Cargo.toml +9 -9
  70. package/native/code-gauge.wasm +0 -0
  71. package/native/src/complexity.rs +26 -33
  72. package/native/src/dep_degree.rs +43 -32
  73. package/native/src/duplication.rs +158 -40
  74. package/native/src/functions.rs +20 -62
  75. package/native/src/languages.rs +28 -7
  76. package/native/src/measure.rs +16 -27
  77. package/native/src/ncss.rs +32 -23
  78. package/native/src/tree_index.rs +9 -0
  79. package/native/src/util.rs +12 -57
  80. package/package.json +10 -10
@@ -11,6 +11,7 @@ pub struct LanguageSets {
11
11
  pub nesting_nodes: FxHashSet<&'static str>,
12
12
  pub ncss_nodes: FxHashSet<&'static str>,
13
13
  pub ncss_containers: FxHashSet<&'static str>,
14
+ pub ncss_bare_body_parents: FxHashSet<&'static str>,
14
15
  }
15
16
 
16
17
  impl LanguageSets {
@@ -21,6 +22,11 @@ impl LanguageSets {
21
22
  nesting_nodes: language.nesting_node_types.iter().copied().collect(),
22
23
  ncss_nodes: language.ncss_node_types.iter().copied().collect(),
23
24
  ncss_containers: language.ncss_container_node_types.iter().copied().collect(),
25
+ ncss_bare_body_parents: language
26
+ .ncss_bare_body_parent_node_types
27
+ .iter()
28
+ .copied()
29
+ .collect(),
24
30
  }
25
31
  }
26
32
  }
@@ -31,8 +37,6 @@ const BOOLEAN_OPERATOR_PARENT_TYPES: &[&str] = &[
31
37
  "binary_expression",
32
38
  "binary",
33
39
  "boolean_operator",
34
- "conjunction_expression",
35
- "disjunction_expression",
36
40
  // C# pattern combinators (`is > 0 and <= 10`) sequence like `&&`/`||` (SonarC#).
37
41
  "and_pattern",
38
42
  "or_pattern",
@@ -306,6 +310,7 @@ impl FunctionBodyPass<'_, '_, '_> {
306
310
  parent,
307
311
  &self.sets.ncss_nodes,
308
312
  &self.sets.ncss_containers,
313
+ &self.sets.ncss_bare_body_parents,
309
314
  );
310
315
  let ncss_frame = self.top_frame();
311
316
  ncss_frame.ncss += own_ncss;
@@ -313,6 +318,9 @@ impl FunctionBodyPass<'_, '_, '_> {
313
318
  ncss_frame.has_own_ncss_contribution = true;
314
319
  }
315
320
 
321
+ // Kotlin accessors hang off their property, yet are members of the class body.
322
+ let is_charged_property =
323
+ inside_charged_class_body && current.kind_name() == "property_declaration";
316
324
  for child in all_children(current) {
317
325
  self.visit(
318
326
  child,
@@ -324,7 +332,8 @@ impl FunctionBodyPass<'_, '_, '_> {
324
332
  } else {
325
333
  inside_nested_region
326
334
  },
327
- is_charged_class_body,
335
+ is_charged_class_body
336
+ || (is_charged_property && matches!(child.kind_name(), "getter" | "setter")),
328
337
  );
329
338
  }
330
339
 
@@ -358,7 +367,7 @@ impl FunctionBodyPass<'_, '_, '_> {
358
367
  }
359
368
 
360
369
  /// Plain else branches attached to `current`: an `else_clause`/Ruby `else` whose branch is not an
361
- /// `else if` continuation, or a bare Java/Go `alternative:` statement without a clause wrapper.
370
+ /// `else if` continuation, or a bare Java/Go/Kotlin `alternative:` branch without a clause wrapper.
362
371
  fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64 {
363
372
  if !current.is_named() {
364
373
  return 0;
@@ -379,11 +388,6 @@ fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64
379
388
  if kind != "if_statement" && kind != "if_expression" {
380
389
  return 0;
381
390
  }
382
- // Kotlin's `else` is a bare keyword token followed by the branch body (no clause wrapper and
383
- // no grammar field); an `else if` continuation is charged on the nested if instead.
384
- if let Some(else_body) = crate::util::kotlin_else_body(current) {
385
- return u64::from(!crate::util::is_kotlin_else_if_body(else_body));
386
- }
387
391
  // Extras (comments) inherit the preceding sibling's field in find_children_by_field_name, so a
388
392
  // comment between an `elif_clause` and `else_clause` must not be miscounted as a bare branch.
389
393
  crate::util::find_children_by_field_name(current, "alternative")
@@ -397,7 +401,10 @@ fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64
397
401
  .count() as u64
398
402
  }
399
403
 
400
- /// goto, and break/continue that jump to a label (their only named child is the label).
404
+ /// goto, and break/continue that jump to a label.
405
+ // Inlined into the recursive visit, its field lookup enlarges every frame enough to cut the depth the
406
+ // Workers runtime's stack can measure by ~5%.
407
+ #[inline(never)]
401
408
  fn is_flow_breaking_jump(node: Node<'_>) -> bool {
402
409
  if !node.is_named() {
403
410
  return false;
@@ -405,19 +412,13 @@ fn is_flow_breaking_jump(node: Node<'_>) -> bool {
405
412
  if node.kind_name() == "goto_statement" {
406
413
  return true;
407
414
  }
408
- // Rust jumps are expressions; `break value` carries a named expression child, so only an
409
- // explicit `label` child marks a labeled jump.
415
+ // Rust and Kotlin jumps are expressions; a Rust `break value` carries a named expression child,
416
+ // so only an explicit Rust `label` child or Kotlin `label` field marks a labeled jump.
410
417
  if node.kind_name() == "break_expression" || node.kind_name() == "continue_expression" {
411
- return crate::util::named_children(node)
412
- .iter()
413
- .any(|child| child.kind_name() == "label" || child.kind_name() == "loop_label");
414
- }
415
- // Kotlin folds every jump into `jump_expression`; the grammar tokenizes a labeled break or
416
- // continue as `break@`/`continue@` followed by the label (`return@label` is a plain return).
417
- if node.kind_name() == "jump_expression" {
418
- return node.child(0).is_some_and(|keyword| {
419
- keyword.kind_name() == "break@" || keyword.kind_name() == "continue@"
420
- });
418
+ return node.child_by_field_name("label").is_some()
419
+ || crate::util::named_children(node)
420
+ .iter()
421
+ .any(|child| child.kind_name() == "label" || child.kind_name() == "loop_label");
421
422
  }
422
423
  // Comments are named children too (`break /* done */;`), so only non-comment children mark a
423
424
  // label.
@@ -519,14 +520,8 @@ fn is_flat_chain_continuation(node: Node<'_>, parent: Option<Node<'_>>) -> bool
519
520
  let Some(parent) = parent else {
520
521
  return false;
521
522
  };
522
- // Kotlin puts a braceless `else if` directly in the else branch's control_structure_body.
523
- if parent.kind_name() == "control_structure_body" {
524
- return parent
525
- .parent_node()
526
- .and_then(crate::util::kotlin_else_body)
527
- .is_some_and(|else_body| else_body.id() == parent.id());
528
- }
529
- // JS/C/C++/Rust/C# wrap `else if` in an else clause or put it directly in `alternative`.
523
+ // JS/C/C++/Rust/C# wrap `else if` in an else clause; Java/Go/Kotlin put it directly in
524
+ // `alternative`.
530
525
  parent.kind_name() == "else_clause"
531
526
  || parent
532
527
  .child_by_field_name("alternative")
@@ -638,9 +633,7 @@ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
638
633
  .is_some_and(|first| is_csharp_catch_all_pattern(*first));
639
634
  }
640
635
  if kind == "when_entry" {
641
- return !crate::util::named_children(node)
642
- .iter()
643
- .any(|child| child.kind_name() == "when_condition");
636
+ return node.child_by_field_name("condition").is_none();
644
637
  }
645
638
 
646
639
  // Python arms with an irrefutable pattern are unconditional like `default`.
@@ -3,13 +3,11 @@ use tree_sitter::Node;
3
3
 
4
4
  use crate::complexity::is_function_boundary;
5
5
  use crate::tree_index::NodeExt;
6
- use crate::util::{is_identifier_leaf, node_text, Source};
6
+ use crate::util::{node_text, Source};
7
7
 
8
8
  /// Leaf node types treated as variable references by the def-use approximation.
9
9
  const VARIABLE_NODE_TYPES: &[&str] = &[
10
10
  "identifier",
11
- "simple_identifier",
12
- "interpolated_identifier",
13
11
  "implicit_parameter",
14
12
  "instance_variable",
15
13
  "class_variable",
@@ -51,18 +49,10 @@ const DEFINITION_FIELD_BY_PARENT_TYPE: &[(&str, &str)] = &[
51
49
  ("from_clause", "name"),
52
50
  ];
53
51
 
54
- /// Kotlin has no grammar fields: an identifier directly under one of these declares a binding
52
+ /// Kotlin marks no binding fields: an identifier directly under one of these declares a binding
55
53
  /// (`val x`, `for (x in xs)`, lambda parameters, and the `catch (e: T)` exception name).
56
54
  const KOTLIN_DEFINITION_PARENT_TYPES: &[&str] = &["variable_declaration", "catch_block"];
57
55
 
58
- /// Kotlin parameter nodes whose identifier child is the declared name; the parameter LIST nodes
59
- /// (`function_value_parameters`) also hold default-value expressions, which are reads.
60
- const KOTLIN_PARAMETER_TYPES: &[&str] = &[
61
- "parameter",
62
- "parameter_with_optional_type",
63
- "class_parameter",
64
- ];
65
-
66
56
  /// C# LINQ clauses that bind a range variable as their first identifier child (no grammar field):
67
57
  /// `join y in ...`, `into ys`, `let z = ...`, and a query continuation `into g`.
68
58
  const CSHARP_QUERY_BINDING_PARENT_TYPES: &[&str] = &[
@@ -133,7 +123,7 @@ pub fn measure_dep_degree(
133
123
  let mut pairs = 0u64;
134
124
  for index in 0..leaves.len() {
135
125
  let leaf = &leaves[index];
136
- if !is_variable_leaf(leaf.node) {
126
+ if !is_variable_leaf(leaf) {
137
127
  continue;
138
128
  }
139
129
  let name = node_text(leaf.node, code);
@@ -161,11 +151,19 @@ pub fn measure_dep_degree(
161
151
 
162
152
  /// A C++ member-pointer variable (`int C::* p`, also `int C::* arr[1]`) is declared as a
163
153
  /// `type_identifier` under the `pointer_type_declarator` spelling `C::*`, possibly through further
164
- /// declarator wrappers; every other `type_identifier` names a type, not a variable.
165
- fn is_variable_leaf(node: Node<'_>) -> bool {
154
+ /// declarator wrappers; every other `type_identifier` names a type, not a variable. Kotlin spells
155
+ /// type names as identifiers: in a `user_type`, or as a type parameter's fieldless name (C# puts its
156
+ /// type parameter in a `name` field).
157
+ fn is_variable_leaf(leaf: &DepDegreeLeaf<'_>) -> bool {
158
+ let node = leaf.node;
159
+ if node.kind_name() == "type_identifier" {
160
+ return is_member_pointer_name(node);
161
+ }
166
162
  VARIABLE_NODE_TYPES.contains(&node.kind_name())
167
- || crate::util::is_kotlin_callable_receiver(node)
168
- || (node.kind_name() == "type_identifier" && is_member_pointer_name(node))
163
+ && !node.parent_node().is_some_and(|parent| {
164
+ parent.kind_name() == "user_type"
165
+ || (parent.kind_name() == "type_parameter" && leaf.field_name.is_none())
166
+ })
169
167
  }
170
168
 
171
169
  fn is_member_pointer_name(node: Node<'_>) -> bool {
@@ -232,11 +230,11 @@ fn collect_dep_degree_leaves<'t>(
232
230
  ) {
233
231
  if matches!(
234
232
  node.kind_name(),
235
- "comment" | "line_comment" | "block_comment" | "multiline_comment"
233
+ "comment" | "line_comment" | "block_comment"
236
234
  ) {
237
235
  return;
238
236
  }
239
- if is_identifier_leaf(node) {
237
+ if node.child_count() == 0 {
240
238
  leaves.push(DepDegreeLeaf {
241
239
  node,
242
240
  field_name,
@@ -297,7 +295,9 @@ fn is_structural_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
297
295
  let Some(parent) = leaf.node.parent_node() else {
298
296
  return false;
299
297
  };
300
- if leaf.node.kind_name() == "simple_identifier"
298
+ // C#'s `variable_declaration` holds its type in the `type` field; Kotlin's bound name has none.
299
+ if leaf.node.kind_name() == "identifier"
300
+ && leaf.field_name.is_none()
301
301
  && KOTLIN_DEFINITION_PARENT_TYPES.contains(&parent.kind_name())
302
302
  {
303
303
  return true;
@@ -368,18 +368,6 @@ fn unwrap_declarator_wrappers<'t>(leaf: &DepDegreeLeaf<'t>) -> (Node<'t>, Option
368
368
  /// (C/C++ function-pointer or array parameters) is a parameter-ish node, or the identifier
369
369
  /// directly occupies a parameter field; type annotations and default values bind nothing.
370
370
  fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
371
- // Kotlin has no `type`/`value` fields to veto default values: a function parameter's default
372
- // sits in the parameter list (`fun f(b: Int = a)`), a class parameter's inside the parameter
373
- // node (`class A(val y: Int = a)`), so only a parameter node's first identifier child binds.
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())
377
- && crate::util::named_children(parent)
378
- .into_iter()
379
- .find(|child| child.kind_name() == "simple_identifier")
380
- .is_some_and(|first| first.id() == leaf.node.id())
381
- });
382
- }
383
371
  let mut current = leaf.node;
384
372
  let mut depth = 0usize;
385
373
  // Only the member pointer's own declared name may climb past its qualified name; a size or
@@ -390,6 +378,19 @@ fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
390
378
  let Some(parent) = current.parent_node() else {
391
379
  return false;
392
380
  };
381
+ // Kotlin has no `type`/`value` fields to veto default values: a function parameter's default
382
+ // sits in the parameter list (`fun f(b: Int = a)`), a class parameter's inside the parameter
383
+ // node (`class A(val y: Int = a)`), so only such a node's first identifier child binds.
384
+ if parent.kind_name() == "function_value_parameters" {
385
+ return false;
386
+ }
387
+ if is_kotlin_parameter(parent) {
388
+ return depth == 0
389
+ && crate::util::named_children(parent)
390
+ .into_iter()
391
+ .find(|child| child.kind_name() == "identifier")
392
+ .is_some_and(|first| first.id() == leaf.node.id());
393
+ }
393
394
  let parent_is_parameterish = parent.kind_name().contains("parameter");
394
395
  // Beyond the grandparent, only declarator wrappers keep climbing, plus the
395
396
  // `qualified_identifier` nodes that spell a member-pointer parameter's class (`int C::* q`,
@@ -427,6 +428,16 @@ fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
427
428
  }
428
429
  }
429
430
 
431
+ /// A Kotlin class parameter, or a setter's parameter list (`set(value)`; Ruby's `setter` is a method
432
+ /// name, never under a property).
433
+ fn is_kotlin_parameter(node: Node<'_>) -> bool {
434
+ node.kind_name() == "class_parameter"
435
+ || (node.kind_name() == "setter"
436
+ && node
437
+ .parent_node()
438
+ .is_some_and(|property| property.kind_name() == "property_declaration"))
439
+ }
440
+
430
441
  /// Only called with small-arity parents (declarator wrappers, definition-list holders).
431
442
  fn field_name_in_parent(node: Node<'_>, parent: Node<'_>) -> Option<&'static str> {
432
443
  for index in 0..parent.child_count() {
@@ -10,7 +10,7 @@ use crate::types::{
10
10
  CrossFileCandidate, CrossFileToken, CrossFileTokenRange, DuplicateBlockOccurrence,
11
11
  DuplicationMetrics,
12
12
  };
13
- 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};
14
14
 
15
15
  /// Block-like nodes considered as whole-subtree duplicate candidates.
16
16
  const DUPLICATE_BLOCK_TYPES: &[&str] = &[
@@ -62,10 +62,7 @@ const DUPLICATE_BLOCK_TYPES: &[&str] = &[
62
62
  "do_while_statement",
63
63
  "catch_block",
64
64
  "finally_block",
65
- "statements",
66
- "control_structure_body",
67
65
  "function_body",
68
- "jump_expression",
69
66
  "jsx_element",
70
67
  "jsx_self_closing_element",
71
68
  "if",
@@ -106,10 +103,9 @@ const STATEMENT_CONTAINER_TYPES: &[&str] = &[
106
103
  "default_case",
107
104
  "compilation_unit",
108
105
  "switch_section",
109
- "statements",
110
106
  "enum_class_body",
111
- "control_structure_body",
112
107
  "function_body",
108
+ "lambda_literal",
113
109
  ];
114
110
 
115
111
  /// C# type bodies are `declaration_list`s, a name Rust also uses for `mod`/`impl`/`trait` bodies;
@@ -122,8 +118,8 @@ const CSHARP_DECLARATION_LIST_PARENT_TYPES: &[&str] = &[
122
118
  "namespace_declaration",
123
119
  ];
124
120
 
125
- /// Whole-subtree duplicate candidates: DUPLICATE_BLOCK_TYPES plus Kotlin's `try_expression`,
126
- /// 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).
127
123
  fn is_duplicate_block(node: Node<'_>) -> bool {
128
124
  if !node.is_named() {
129
125
  return false;
@@ -149,8 +145,6 @@ fn is_statement_container(node: Node<'_>) -> bool {
149
145
  /// Identifier leaves anonymized by occurrence order so consistently renamed copies still match.
150
146
  const ANONYMIZED_IDENTIFIER_TYPES: &[&str] = &[
151
147
  "identifier",
152
- "simple_identifier",
153
- "interpolated_identifier",
154
148
  "implicit_parameter",
155
149
  "constant",
156
150
  "instance_variable",
@@ -172,8 +166,6 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
172
166
  ("integer_literal", "#num"),
173
167
  ("float_literal", "#num"),
174
168
  ("real_literal", "#num"),
175
- ("hex_literal", "#num"),
176
- ("bin_literal", "#num"),
177
169
  ("int_literal", "#num"),
178
170
  ("rune_literal", "#char"),
179
171
  ("imaginary_literal", "#num"),
@@ -188,7 +180,6 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
188
180
  ("string_content", "#str"),
189
181
  ("string_literal_content", "#str"),
190
182
  ("character_literal_content", "#char"),
191
- ("character_escape_seq", "#char"),
192
183
  ("verbatim_string_literal", "#str"),
193
184
  ("interpolated_string_expression", "#str"),
194
185
  ("raw_string_content", "#str"),
@@ -208,13 +199,28 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
208
199
  ("regex_pattern", "#regex"),
209
200
  ];
210
201
 
211
- const COMMENT_TYPES: &[&str] = &[
212
- "comment",
213
- "line_comment",
214
- "block_comment",
215
- "multiline_comment",
202
+ const COMMENT_TYPES: &[&str] = &["comment", "line_comment", "block_comment"];
203
+
204
+ /// Declaration kinds that only name dependencies (imports, package clauses, includes).
205
+ const DEPENDENCY_DECLARATION_TYPES: &[&str] = &[
206
+ "import_statement",
207
+ "import_from_statement",
208
+ "future_import_statement",
209
+ "import_alias",
210
+ "import_declaration",
211
+ "package_clause",
212
+ "package_declaration",
213
+ "package_header",
214
+ "use_declaration",
215
+ "extern_crate_declaration",
216
+ "using_directive",
217
+ "extern_alias_directive",
218
+ "preproc_include",
216
219
  ];
217
220
 
221
+ /// Ruby methods that load another file, called without a receiver.
222
+ const RUBY_REQUIRE_METHODS: &[&str] = &["require", "require_relative"];
223
+
218
224
  /// Children of a string node that carry only literal content; anything else is interpolation.
219
225
  const STRING_FRAGMENT_TYPES: &[&str] = &[
220
226
  "string_fragment",
@@ -222,7 +228,6 @@ const STRING_FRAGMENT_TYPES: &[&str] = &[
222
228
  "string_content",
223
229
  "string_literal_content",
224
230
  "character_literal_content",
225
- "character_escape_seq",
226
231
  "raw_string_content",
227
232
  "raw_string_start",
228
233
  "raw_string_end",
@@ -286,7 +291,6 @@ const STRING_CONTENT_FRAGMENT_TYPES: &[&str] = &[
286
291
  "string_content",
287
292
  "string_literal_content",
288
293
  "character_literal_content",
289
- "character_escape_seq",
290
294
  "raw_string_content",
291
295
  "escape_sequence",
292
296
  "heredoc_content",
@@ -552,12 +556,12 @@ fn collect_tokens<'a>(
552
556
  container_statement_ranges: &mut Vec<Vec<TokenRange>>,
553
557
  ) -> TokenRange {
554
558
  let start_token_index = tokens.len();
555
- let atomic_kind = if is_identifier_leaf(node) {
559
+ let atomic_kind = if node.child_count() == 0 {
556
560
  None
557
561
  } else {
558
562
  atomic_literal_kind(node)
559
563
  };
560
- if is_identifier_leaf(node) {
564
+ if node.child_count() == 0 {
561
565
  append_leaf_token(node, code, tokens);
562
566
  } else if let Some(atomic_kind) = atomic_kind {
563
567
  // Interpolation-free strings collapse to their kind tag so copies differing only in
@@ -569,7 +573,7 @@ fn collect_tokens<'a>(
569
573
  node.start_position().row,
570
574
  node.end_position().row,
571
575
  ));
572
- } else if !COMMENT_TYPES.contains(&node.kind_name()) {
576
+ } else if !is_tokenless(node, code) {
573
577
  let mut statement_ranges: Vec<TokenRange> = Vec::new();
574
578
  let is_container = is_statement_container(node);
575
579
  for child in all_children(node) {
@@ -580,7 +584,12 @@ fn collect_tokens<'a>(
580
584
  block_ranges,
581
585
  container_statement_ranges,
582
586
  );
583
- if is_container && child.is_named() && !COMMENT_TYPES.contains(&child.kind_name()) {
587
+ // A Kotlin lambda's parameters sit beside its statements.
588
+ if is_container
589
+ && child.is_named()
590
+ && !is_tokenless(child, code)
591
+ && child.kind_name() != "lambda_parameters"
592
+ {
584
593
  statement_ranges.push(child_range);
585
594
  }
586
595
  }
@@ -609,6 +618,89 @@ fn collect_tokens<'a>(
609
618
  visit(root, code, tokens, block_ranges, container_statement_ranges);
610
619
  }
611
620
 
621
+ /// Comments and dependency declarations contribute no tokens, so no clone consists of them; the
622
+ /// tokens around them stay adjacent, so a clone may still extend across one.
623
+ fn is_tokenless(node: Node<'_>, code: &Source<'_>) -> bool {
624
+ COMMENT_TYPES.contains(&node.kind_name()) || is_dependency_declaration(node, code)
625
+ }
626
+
627
+ /// Every module must spell out its own dependencies, so a clone made of them has no refactoring
628
+ /// that could remove it; they are left out of clone detection entirely.
629
+ fn is_dependency_declaration(node: Node<'_>, code: &Source<'_>) -> bool {
630
+ if !node.is_named() {
631
+ return false;
632
+ }
633
+ let kind = node.kind_name();
634
+ if DEPENDENCY_DECLARATION_TYPES.contains(&kind) {
635
+ return true;
636
+ }
637
+ match (crate::tree_index::language_name(), kind) {
638
+ // JavaScript's dynamic `import(...)` is a named `import` node too.
639
+ ("kotlin", "import") => true,
640
+ ("cpp", "using_declaration") => true,
641
+ // `mod name;` loads a file; a `mod name { ... }` with a body is code.
642
+ ("rust", "mod_item") => node.child_by_field_name("body").is_none(),
643
+ // Re-exports (`export { a } from './a'`); other exports wrap declarations.
644
+ ("javascript" | "jsx" | "typescript" | "tsx", "export_statement") => {
645
+ node.child_by_field_name("source").is_some()
646
+ }
647
+ (
648
+ "javascript" | "jsx" | "typescript" | "tsx",
649
+ "lexical_declaration" | "variable_declaration",
650
+ ) => is_require_declaration(node, code),
651
+ // `require('a');` and CommonJS re-exports such as `module.exports = require('a');`.
652
+ ("javascript" | "jsx" | "typescript" | "tsx", "expression_statement") => {
653
+ named_children(node).first().is_some_and(|expression| {
654
+ if expression.kind_name() == "assignment_expression" {
655
+ expression
656
+ .child_by_field_name("right")
657
+ .is_some_and(|right| is_require_value(right, code))
658
+ } else {
659
+ is_require_value(*expression, code)
660
+ }
661
+ })
662
+ }
663
+ ("ruby", "call") => {
664
+ node.child_by_field_name("receiver").is_none()
665
+ && node
666
+ .child_by_field_name("method")
667
+ .is_some_and(|method| RUBY_REQUIRE_METHODS.contains(&node_text(method, code)))
668
+ }
669
+ _ => false,
670
+ }
671
+ }
672
+
673
+ /// `const a = require('a')` (including destructuring and `require('a').b`) in every declarator.
674
+ fn is_require_declaration(node: Node<'_>, code: &Source<'_>) -> bool {
675
+ let declarators: Vec<Node<'_>> = named_children(node)
676
+ .into_iter()
677
+ .filter(|child| child.kind_name() == "variable_declarator")
678
+ .collect();
679
+ !declarators.is_empty()
680
+ && declarators.iter().all(|declarator| {
681
+ declarator
682
+ .child_by_field_name("value")
683
+ .is_some_and(|value| is_require_value(value, code))
684
+ })
685
+ }
686
+
687
+ /// `require('a')` or a property read from it (`require('a').b`), but not a call on the module.
688
+ fn is_require_value(node: Node<'_>, code: &Source<'_>) -> bool {
689
+ let mut value = node;
690
+ while value.kind_name() == "member_expression" {
691
+ let Some(object) = value.child_by_field_name("object") else {
692
+ return false;
693
+ };
694
+ value = object;
695
+ }
696
+ value.kind_name() == "call_expression"
697
+ && value
698
+ .child_by_field_name("function")
699
+ .is_some_and(|function| {
700
+ function.kind_name() == "identifier" && node_text(function, code) == "require"
701
+ })
702
+ }
703
+
612
704
  /// The kind tag of a string-like node with no interpolation, or None to descend normally.
613
705
  fn atomic_literal_kind(node: Node<'_>) -> Option<&'static str> {
614
706
  let kind = if node.is_named() {
@@ -664,14 +756,19 @@ fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Tok
664
756
  return;
665
757
  }
666
758
 
667
- // A Kotlin bound callable-reference receiver (`xs::size`) renames like a variable unless it is
668
- // PascalCase, the discriminator used for static receivers everywhere else.
669
- let is_variable_receiver = crate::util::is_kotlin_callable_receiver(node)
670
- && !pascal_case_regex().is_match(node_text(node, code));
759
+ if crate::util::is_kotlin_keyword_literal(node, code) {
760
+ tokens.push(make_text_token(
761
+ Cow::Borrowed(node_text(node, code)),
762
+ None,
763
+ false,
764
+ start_row,
765
+ end_row,
766
+ ));
767
+ return;
768
+ }
671
769
  if node.is_named()
672
- && (is_variable_receiver
673
- || (ANONYMIZED_IDENTIFIER_TYPES.contains(&node.kind_name())
674
- && !is_semantic_name_leaf(node, code)))
770
+ && ANONYMIZED_IDENTIFIER_TYPES.contains(&node.kind_name())
771
+ && !is_semantic_name_leaf(node, code)
675
772
  {
676
773
  tokens.push(Token {
677
774
  is_id: true,
@@ -795,8 +892,15 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
795
892
  };
796
893
 
797
894
  // Java method references (`Foo::bar`) and Kotlin callable references (`::bar`) name their
798
- // identifiers without grammar fields.
799
- if parent.kind_name() == "method_reference" || parent.kind_name() == "callable_reference" {
895
+ // identifiers without grammar fields, and Kotlin spells type names as identifiers.
896
+ if matches!(
897
+ parent.kind_name(),
898
+ "method_reference" | "callable_reference" | "user_type"
899
+ ) {
900
+ return true;
901
+ }
902
+ // A Kotlin type parameter's name has no field (C# puts its own in `name`).
903
+ if parent.kind_name() == "type_parameter" && parent.child_by_field_name("name").is_none() {
800
904
  return true;
801
905
  }
802
906
 
@@ -823,16 +927,30 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
823
927
  }
824
928
  }
825
929
 
826
- // Kotlin (no grammar fields): a callee (`foo(...)`), a member name (`a.foo`), an infix function
827
- // (`a shl b`), and a named argument (`foo(name = x)`) are API names.
828
- if node.kind_name() == "simple_identifier" {
829
- if parent.kind_name() == "navigation_suffix" {
830
- return true;
831
- }
930
+ // Kotlin (no callee or member fields): a callee (`foo(...)`, whose call ends in Kotlin's own
931
+ // argument nodes), a member name (`a.foo`), an infix function (`a shl b`), and a named
932
+ // argument (`foo(name = x)`) are API names.
933
+ if node.kind_name() == "identifier" {
832
934
  let is_first_named = parent
833
935
  .named_child(0)
834
936
  .is_some_and(|first| first.id() == node.id());
835
- if parent.kind_name() == "call_expression" && is_first_named {
937
+ let is_last_named = parent
938
+ .named_child(parent.named_child_count().saturating_sub(1))
939
+ .is_some_and(|last| last.id() == node.id());
940
+ if parent.kind_name() == "navigation_expression" && is_last_named && !is_first_named {
941
+ return true;
942
+ }
943
+ if parent.kind_name() == "call_expression"
944
+ && is_first_named
945
+ && parent
946
+ .named_child(parent.named_child_count().saturating_sub(1))
947
+ .is_some_and(|arguments| {
948
+ matches!(
949
+ arguments.kind_name(),
950
+ "value_arguments" | "annotated_lambda"
951
+ )
952
+ })
953
+ {
836
954
  return true;
837
955
  }
838
956
  if parent.kind_name() == "infix_expression"