code-gauge 4.8.0 → 5.1.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 (66) hide show
  1. package/README.md +34 -9
  2. package/THIRD-PARTY-NOTICES.txt +1152 -798
  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/diffCommand.cjs +3 -3
  13. package/dist/diffCommand.cjs.map +1 -1
  14. package/dist/diffCommand.js +3 -3
  15. package/dist/diffCommand.js.map +1 -1
  16. package/dist/duplication.cjs +1 -1
  17. package/dist/duplication.cjs.map +1 -1
  18. package/dist/duplication.js +1 -1
  19. package/dist/duplication.js.map +1 -1
  20. package/dist/exclusion.cjs +2 -0
  21. package/dist/exclusion.cjs.map +1 -0
  22. package/dist/exclusion.d.ts +30 -0
  23. package/dist/exclusion.js +2 -0
  24. package/dist/exclusion.js.map +1 -0
  25. package/dist/git.cjs +1 -1
  26. package/dist/git.cjs.map +1 -1
  27. package/dist/git.d.ts +16 -0
  28. package/dist/git.js +1 -1
  29. package/dist/git.js.map +1 -1
  30. package/dist/index.cjs +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +1 -1
  33. package/dist/nativeMetrics.cjs +3 -3
  34. package/dist/nativeMetrics.cjs.map +1 -1
  35. package/dist/nativeMetrics.js +3 -3
  36. package/dist/nativeMetrics.js.map +1 -1
  37. package/dist/newDuplication.cjs +2 -0
  38. package/dist/newDuplication.cjs.map +1 -0
  39. package/dist/newDuplication.d.ts +40 -0
  40. package/dist/newDuplication.js +2 -0
  41. package/dist/newDuplication.js.map +1 -0
  42. package/dist/regressionGate.cjs +1 -1
  43. package/dist/regressionGate.cjs.map +1 -1
  44. package/dist/regressionGate.d.ts +8 -6
  45. package/dist/regressionGate.js +1 -1
  46. package/dist/regressionGate.js.map +1 -1
  47. package/dist/scan.cjs +1 -1
  48. package/dist/scan.cjs.map +1 -1
  49. package/dist/scan.d.ts +12 -4
  50. package/dist/scan.js +1 -1
  51. package/dist/scan.js.map +1 -1
  52. package/dist/worker.cjs +1 -1
  53. package/dist/worker.cjs.map +1 -1
  54. package/dist/worker.js +1 -1
  55. package/dist/worker.js.map +1 -1
  56. package/native/Cargo.lock +73 -73
  57. package/native/Cargo.toml +16 -21
  58. package/native/code-gauge.wasm +0 -0
  59. package/native/src/dep_degree.rs +6 -6
  60. package/native/src/duplication.rs +143 -14
  61. package/native/src/functions.rs +10 -7
  62. package/native/src/languages.rs +14 -13
  63. package/native/src/measure.rs +16 -8
  64. package/native/src/tree_index.rs +4 -4
  65. package/native/src/util.rs +9 -2
  66. package/package.json +9 -8
package/native/Cargo.lock CHANGED
@@ -45,18 +45,18 @@ dependencies = [
45
45
  "rustc-hash",
46
46
  "serde",
47
47
  "serde_json",
48
- "tree-sitter",
49
- "tree-sitter-c",
50
- "tree-sitter-c-sharp",
51
- "tree-sitter-cpp",
52
48
  "tree-sitter-go",
53
49
  "tree-sitter-java",
54
- "tree-sitter-javascript",
55
50
  "tree-sitter-python",
56
51
  "tree-sitter-ruby",
57
- "tree-sitter-rust",
58
- "tree-sitter-typescript",
52
+ "willbooster-tree-sitter",
53
+ "willbooster-tree-sitter-c",
54
+ "willbooster-tree-sitter-c-sharp",
55
+ "willbooster-tree-sitter-cpp",
56
+ "willbooster-tree-sitter-javascript",
59
57
  "willbooster-tree-sitter-kotlin",
58
+ "willbooster-tree-sitter-rust",
59
+ "willbooster-tree-sitter-typescript",
60
60
  ]
61
61
 
62
62
  [[package]]
@@ -425,141 +425,141 @@ dependencies = [
425
425
  ]
426
426
 
427
427
  [[package]]
428
- name = "tree-sitter"
429
- version = "0.25.10"
428
+ name = "tree-sitter-go"
429
+ version = "0.25.0"
430
430
  source = "registry+https://github.com/rust-lang/crates.io-index"
431
- checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87"
431
+ checksum = "c8560a4d2f835cc0d4d2c2e03cbd0dde2f6114b43bc491164238d333e28b16ea"
432
432
  dependencies = [
433
433
  "cc",
434
- "regex",
435
- "regex-syntax",
436
- "serde_json",
437
- "streaming-iterator",
438
434
  "tree-sitter-language",
439
435
  ]
440
436
 
441
437
  [[package]]
442
- name = "tree-sitter-c"
443
- version = "0.21.4"
438
+ name = "tree-sitter-java"
439
+ version = "0.23.5"
444
440
  source = "registry+https://github.com/rust-lang/crates.io-index"
445
- checksum = "f956d5351d62652864a4ff3ae861747e7a1940dc96c9998ae400ac0d3ce30427"
441
+ checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6"
446
442
  dependencies = [
447
443
  "cc",
448
- "tree-sitter",
444
+ "tree-sitter-language",
449
445
  ]
450
446
 
451
447
  [[package]]
452
- name = "tree-sitter-c-sharp"
453
- version = "0.21.3"
448
+ name = "tree-sitter-language"
449
+ version = "0.1.8"
454
450
  source = "registry+https://github.com/rust-lang/crates.io-index"
455
- checksum = "8997ad04502208449025114e434c9024a33a74e700513c702a9d2cac6522a771"
456
- dependencies = [
457
- "cc",
458
- "tree-sitter",
459
- ]
451
+ checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081"
460
452
 
461
453
  [[package]]
462
- name = "tree-sitter-cpp"
463
- version = "0.22.3"
454
+ name = "tree-sitter-python"
455
+ version = "0.25.0"
464
456
  source = "registry+https://github.com/rust-lang/crates.io-index"
465
- checksum = "1d509a22a992790d38f2c291961ff8a1ff016c437c7ec6befc9220b8eec8918c"
457
+ checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c"
466
458
  dependencies = [
467
459
  "cc",
468
- "tree-sitter",
460
+ "tree-sitter-language",
469
461
  ]
470
462
 
471
463
  [[package]]
472
- name = "tree-sitter-go"
473
- version = "0.21.2"
464
+ name = "tree-sitter-ruby"
465
+ version = "0.23.1"
474
466
  source = "registry+https://github.com/rust-lang/crates.io-index"
475
- checksum = "b8d702a98d3c7e70e466456e58ff2b1ac550bf1e29b97e5770676d2fdabec00d"
467
+ checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95"
476
468
  dependencies = [
477
469
  "cc",
478
- "tree-sitter",
470
+ "tree-sitter-language",
479
471
  ]
480
472
 
481
473
  [[package]]
482
- name = "tree-sitter-java"
483
- version = "0.21.0"
474
+ name = "unicode-ident"
475
+ version = "1.0.24"
484
476
  source = "registry+https://github.com/rust-lang/crates.io-index"
485
- checksum = "33bc21adf831a773c075d9d00107ab43965e6a6ea7607b47fd9ec6f3db4b481b"
486
- dependencies = [
487
- "cc",
488
- "tree-sitter",
489
- ]
477
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
490
478
 
491
479
  [[package]]
492
- name = "tree-sitter-javascript"
493
- version = "0.25.0"
480
+ name = "unicode-segmentation"
481
+ version = "1.13.3"
494
482
  source = "registry+https://github.com/rust-lang/crates.io-index"
495
- checksum = "68204f2abc0627a90bdf06e605f5c470aa26fdcb2081ea553a04bdad756693f5"
483
+ checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
484
+
485
+ [[package]]
486
+ name = "willbooster-tree-sitter"
487
+ version = "1.0.5"
488
+ source = "registry+https://github.com/rust-lang/crates.io-index"
489
+ checksum = "7c6ca3007235ade028b793d6090a5ea16618ea3ab9b53427d1f782b15bf64064"
496
490
  dependencies = [
497
491
  "cc",
492
+ "regex",
493
+ "serde_json",
494
+ "streaming-iterator",
498
495
  "tree-sitter-language",
499
496
  ]
500
497
 
501
498
  [[package]]
502
- name = "tree-sitter-language"
503
- version = "0.1.8"
499
+ name = "willbooster-tree-sitter-c"
500
+ version = "1.1.0"
504
501
  source = "registry+https://github.com/rust-lang/crates.io-index"
505
- checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081"
502
+ checksum = "5cee6f1a4e0f64f337d28aeaed73879d7e8a87d77c287afb989ce29adea54700"
503
+ dependencies = [
504
+ "cc",
505
+ "tree-sitter-language",
506
+ ]
506
507
 
507
508
  [[package]]
508
- name = "tree-sitter-python"
509
- version = "0.21.0"
509
+ name = "willbooster-tree-sitter-c-sharp"
510
+ version = "1.0.3"
510
511
  source = "registry+https://github.com/rust-lang/crates.io-index"
511
- checksum = "b4066c6cf678f962f8c2c4561f205945c84834cce73d981e71392624fdc390a9"
512
+ checksum = "99b224dcd59cda9b24b588557cca3827abaccaf10da72d125c31e7cdb2e6f7fb"
512
513
  dependencies = [
513
514
  "cc",
514
- "tree-sitter",
515
+ "tree-sitter-language",
515
516
  ]
516
517
 
517
518
  [[package]]
518
- name = "tree-sitter-ruby"
519
- version = "0.21.0"
519
+ name = "willbooster-tree-sitter-cpp"
520
+ version = "1.1.1"
520
521
  source = "registry+https://github.com/rust-lang/crates.io-index"
521
- checksum = "c0031f687c0772f2dad7b77104c43428611099a1804c81244ada21560f41f0b1"
522
+ checksum = "72b92a9fcb0dc27bd4af18e06fe6c085dd14d91f009f60ecd92c0308b9cbd068"
522
523
  dependencies = [
523
524
  "cc",
524
- "tree-sitter",
525
+ "tree-sitter-language",
525
526
  ]
526
527
 
527
528
  [[package]]
528
- name = "tree-sitter-rust"
529
- version = "0.21.2"
529
+ name = "willbooster-tree-sitter-javascript"
530
+ version = "1.0.3"
530
531
  source = "registry+https://github.com/rust-lang/crates.io-index"
531
- checksum = "277690f420bf90741dea984f3da038ace46c4fe6047cba57a66822226cde1c93"
532
+ checksum = "e6c40d41b303b525194daa503a71ec94d8269fe8d1648b2d9138fe902623beff"
532
533
  dependencies = [
533
534
  "cc",
534
- "tree-sitter",
535
+ "tree-sitter-language",
535
536
  ]
536
537
 
537
538
  [[package]]
538
- name = "tree-sitter-typescript"
539
- version = "0.21.2"
539
+ name = "willbooster-tree-sitter-kotlin"
540
+ version = "1.4.0"
540
541
  source = "registry+https://github.com/rust-lang/crates.io-index"
541
- checksum = "ecb35d98a688378e56c18c9c159824fd16f730ccbea19aacf4f206e5d5438ed9"
542
+ checksum = "529ae914939b559754e6dd8b34d2705f04d6dcb73f09cb52f024b49eda9b5b88"
542
543
  dependencies = [
543
544
  "cc",
544
- "tree-sitter",
545
+ "tree-sitter-language",
545
546
  ]
546
547
 
547
548
  [[package]]
548
- name = "unicode-ident"
549
- version = "1.0.24"
549
+ name = "willbooster-tree-sitter-rust"
550
+ version = "1.1.1"
550
551
  source = "registry+https://github.com/rust-lang/crates.io-index"
551
- checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
552
+ checksum = "324c9f18cfc29e4da7335ad8cb76beb6ac1ae91a8037a44724bc55fce04f26f7"
553
+ dependencies = [
554
+ "cc",
555
+ "tree-sitter-language",
556
+ ]
552
557
 
553
558
  [[package]]
554
- name = "unicode-segmentation"
555
- version = "1.13.3"
559
+ name = "willbooster-tree-sitter-typescript"
560
+ version = "1.1.1"
556
561
  source = "registry+https://github.com/rust-lang/crates.io-index"
557
- checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
558
-
559
- [[package]]
560
- name = "willbooster-tree-sitter-kotlin"
561
- version = "0.0.0"
562
- source = "git+https://github.com/WillBooster/tree-sitter-kotlin?tag=v1.1.0#a05912e6f07e2cedd44bfb117940361089d4c3e3"
562
+ checksum = "f8c51b6c9e10d6ae34c2f84c4d44fff79a0210890ede63fda558a6a0f042eb28"
563
563
  dependencies = [
564
564
  "cc",
565
565
  "tree-sitter-language",
package/native/Cargo.toml CHANGED
@@ -16,27 +16,22 @@ regex = "1"
16
16
  rustc-hash = "2"
17
17
 
18
18
  # Grammar crates are pinned exactly: every metric (and the golden-based test suite) is calibrated
19
- # against these grammar versions, so upgrades are deliberate, test-verified changes. The Kotlin
20
- # grammar is WillBooster's fork, which is not on crates.io, pinned to a release tag. Core 0.25 is
21
- # the first to load that grammar's ABI 15 parser, and tree-sitter-javascript 0.21.x pins
22
- # `cc ~1.0`, which conflicts with the core's `cc ^1.2`, hence 0.25.0. The core reports no field for
23
- # extra (error-recovery) children in ts_node_field_name_for_child, which
24
- # util::find_children_by_field_name compensates for.
25
- tree-sitter = "=0.25.10"
26
- tree-sitter-c = "=0.21.4"
27
- tree-sitter-c-sharp = "=0.21.3"
28
- tree-sitter-cpp = "=0.22.3"
29
- tree-sitter-go = "=0.21.2"
30
- tree-sitter-java = "=0.21.0"
31
- tree-sitter-kotlin = { package = "willbooster-tree-sitter-kotlin", git = "https://github.com/WillBooster/tree-sitter-kotlin", tag = "v1.1.0" }
32
- tree-sitter-javascript = "=0.25.0"
33
- tree-sitter-python = "=0.21.0"
34
- tree-sitter-ruby = "=0.21.0"
35
- # 0.21.0 breaks MSVC (its build script passes the GCC-only -Wno-unused-parameter
36
- # unconditionally; cl.exe fails with D8021); 0.21.2 gates the flag and the golden-based test
37
- # suite verifies its grammar changes leave every measured metric unchanged.
38
- tree-sitter-rust = "=0.21.2"
39
- tree-sitter-typescript = "=0.21.2"
19
+ # against these grammar versions, so upgrades are deliberate, test-verified changes. The
20
+ # `willbooster-*` crates are WillBooster's forks of the runtime and grammars. Every grammar crate
21
+ # depends only on `tree-sitter-language`, never on upstream's `tree-sitter` crate, whose
22
+ # `links = "tree-sitter"` would conflict with the forked runtime's.
23
+ tree-sitter = { package = "willbooster-tree-sitter", version = "1" }
24
+ tree-sitter-c = { package = "willbooster-tree-sitter-c", version = "=1.1.0" }
25
+ tree-sitter-c-sharp = { package = "willbooster-tree-sitter-c-sharp", version = "=1.0.3" }
26
+ tree-sitter-cpp = { package = "willbooster-tree-sitter-cpp", version = "=1.1.1" }
27
+ tree-sitter-go = "=0.25.0"
28
+ tree-sitter-java = "=0.23.5"
29
+ tree-sitter-javascript = { package = "willbooster-tree-sitter-javascript", version = "=1.0.3" }
30
+ tree-sitter-kotlin = { package = "willbooster-tree-sitter-kotlin", version = "=1.4.0" }
31
+ tree-sitter-python = "=0.25.0"
32
+ tree-sitter-ruby = "=0.23.1"
33
+ tree-sitter-rust = { package = "willbooster-tree-sitter-rust", version = "=1.1.1" }
34
+ tree-sitter-typescript = { package = "willbooster-tree-sitter-typescript", version = "=1.1.1" }
40
35
 
41
36
  # The WebAssembly build (src/wasm.rs) exports a plain C ABI instead of N-API.
42
37
  [target.'cfg(not(target_family = "wasm"))'.dependencies]
Binary file
@@ -93,7 +93,7 @@ const NON_BINDING_PARAMETER_FIELDS: &[&str] = &["type", "value"];
93
93
  /// One leaf of the def-use walk: its field in the parent and its function-scope chain.
94
94
  struct DepDegreeLeaf<'t> {
95
95
  node: Node<'t>,
96
- field_name: Option<&'static str>,
96
+ field_name: Option<&'t str>,
97
97
  /// Chain of nested-function scope ids below the measured function: "" for its own body,
98
98
  /// then "/0", "/0/1", ...
99
99
  scope: String,
@@ -221,7 +221,7 @@ fn implicit_accessor_definitions<'a>(function_node: Node<'_>, code: &Source<'a>)
221
221
  #[allow(clippy::too_many_arguments)]
222
222
  fn collect_dep_degree_leaves<'t>(
223
223
  node: Node<'t>,
224
- field_name: Option<&'static str>,
224
+ field_name: Option<&'t str>,
225
225
  scope: &str,
226
226
  next_scope_id: &mut usize,
227
227
  function_nodes: &FxHashSet<&'static str>,
@@ -234,7 +234,7 @@ fn collect_dep_degree_leaves<'t>(
234
234
  ) {
235
235
  return;
236
236
  }
237
- if node.child_count() == 0 {
237
+ if crate::util::is_token(node) {
238
238
  leaves.push(DepDegreeLeaf {
239
239
  node,
240
240
  field_name,
@@ -341,7 +341,7 @@ fn is_definition_field(holder: Node<'_>, field_name: Option<&str>) -> bool {
341
341
  /// fieldless child; a member pointer's `pointer_type_declarator` is the `name` of a
342
342
  /// `qualified_identifier`; an `array_declarator` size or a nested parameter has another field and
343
343
  /// stops the climb) to the outermost wrapper and its field in the declaration.
344
- fn unwrap_declarator_wrappers<'t>(leaf: &DepDegreeLeaf<'t>) -> (Node<'t>, Option<&'static str>) {
344
+ fn unwrap_declarator_wrappers<'t>(leaf: &DepDegreeLeaf<'t>) -> (Node<'t>, Option<&'t str>) {
345
345
  let mut current = leaf.node;
346
346
  let mut field_name = leaf.field_name;
347
347
  while let Some(parent) = current
@@ -439,11 +439,11 @@ fn is_kotlin_parameter(node: Node<'_>) -> bool {
439
439
  }
440
440
 
441
441
  /// Only called with small-arity parents (declarator wrappers, definition-list holders).
442
- fn field_name_in_parent(node: Node<'_>, parent: Node<'_>) -> Option<&'static str> {
442
+ fn field_name_in_parent<'t>(node: Node<'_>, parent: Node<'t>) -> Option<&'t str> {
443
443
  for index in 0..parent.child_count() {
444
444
  if let Some(child) = parent.child(index) {
445
445
  if child.id() == node.id() {
446
- return parent.field_name_for_child(index as u32);
446
+ return parent.field_name_for_child(index);
447
447
  }
448
448
  }
449
449
  }
@@ -10,7 +10,7 @@ use crate::types::{
10
10
  CrossFileCandidate, CrossFileToken, CrossFileTokenRange, DuplicateBlockOccurrence,
11
11
  DuplicationMetrics,
12
12
  };
13
- use crate::util::{all_children, named_children, node_text, to_int32, Source};
13
+ use crate::util::{all_children, is_token, 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] = &[
@@ -103,6 +103,7 @@ const STATEMENT_CONTAINER_TYPES: &[&str] = &[
103
103
  "default_case",
104
104
  "compilation_unit",
105
105
  "switch_section",
106
+ "extension_body",
106
107
  "enum_class_body",
107
108
  "function_body",
108
109
  "lambda_literal",
@@ -201,6 +202,26 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
201
202
 
202
203
  const COMMENT_TYPES: &[&str] = &["comment", "line_comment", "block_comment"];
203
204
 
205
+ /// Declaration kinds that only name dependencies (imports, package clauses, includes).
206
+ const DEPENDENCY_DECLARATION_TYPES: &[&str] = &[
207
+ "import_statement",
208
+ "import_from_statement",
209
+ "future_import_statement",
210
+ "import_alias",
211
+ "import_declaration",
212
+ "package_clause",
213
+ "package_declaration",
214
+ "package_header",
215
+ "use_declaration",
216
+ "extern_crate_declaration",
217
+ "using_directive",
218
+ "extern_alias_directive",
219
+ "preproc_include",
220
+ ];
221
+
222
+ /// Ruby methods that load another file, called without a receiver.
223
+ const RUBY_REQUIRE_METHODS: &[&str] = &["require", "require_relative"];
224
+
204
225
  /// Children of a string node that carry only literal content; anything else is interpolation.
205
226
  const STRING_FRAGMENT_TYPES: &[&str] = &[
206
227
  "string_fragment",
@@ -209,6 +230,8 @@ const STRING_FRAGMENT_TYPES: &[&str] = &[
209
230
  "string_literal_content",
210
231
  "character_literal_content",
211
232
  "raw_string_content",
233
+ "interpreted_string_literal_content",
234
+ "raw_string_literal_content",
212
235
  "raw_string_start",
213
236
  "raw_string_end",
214
237
  "escape_sequence",
@@ -272,6 +295,8 @@ const STRING_CONTENT_FRAGMENT_TYPES: &[&str] = &[
272
295
  "string_literal_content",
273
296
  "character_literal_content",
274
297
  "raw_string_content",
298
+ "interpreted_string_literal_content",
299
+ "raw_string_literal_content",
275
300
  "escape_sequence",
276
301
  "heredoc_content",
277
302
  ];
@@ -528,20 +553,25 @@ fn collect_tokens<'a>(
528
553
  block_ranges: &mut Vec<TokenRange>,
529
554
  container_statement_ranges: &mut Vec<Vec<TokenRange>>,
530
555
  ) {
556
+ /// `statement_list_statements` receives the statements of a Go `statement_list`, which wraps
557
+ /// the statements of blocks and case clauses and whose statements belong to the run of the
558
+ /// enclosing container.
531
559
  fn visit<'a>(
532
560
  node: Node<'_>,
533
561
  code: &Source<'a>,
534
562
  tokens: &mut Vec<Token<'a>>,
535
563
  block_ranges: &mut Vec<TokenRange>,
536
564
  container_statement_ranges: &mut Vec<Vec<TokenRange>>,
565
+ statement_list_statements: &mut Vec<TokenRange>,
537
566
  ) -> TokenRange {
538
567
  let start_token_index = tokens.len();
539
- let atomic_kind = if node.child_count() == 0 {
568
+ let is_token = is_token(node);
569
+ let atomic_kind = if is_token {
540
570
  None
541
571
  } else {
542
572
  atomic_literal_kind(node)
543
573
  };
544
- if node.child_count() == 0 {
574
+ if is_token {
545
575
  append_leaf_token(node, code, tokens);
546
576
  } else if let Some(atomic_kind) = atomic_kind {
547
577
  // Interpolation-free strings collapse to their kind tag so copies differing only in
@@ -553,30 +583,39 @@ fn collect_tokens<'a>(
553
583
  node.start_position().row,
554
584
  node.end_position().row,
555
585
  ));
556
- } else if !COMMENT_TYPES.contains(&node.kind_name()) {
586
+ } else if !is_tokenless(node, code) {
557
587
  let mut statement_ranges: Vec<TokenRange> = Vec::new();
558
- let is_container = is_statement_container(node);
588
+ let is_statement_list = node.kind_name() == "statement_list";
589
+ let is_container = is_statement_list || is_statement_container(node);
559
590
  for child in all_children(node) {
591
+ let mut child_statements = Vec::new();
560
592
  let child_range = visit(
561
593
  child,
562
594
  code,
563
595
  tokens,
564
596
  block_ranges,
565
597
  container_statement_ranges,
598
+ &mut child_statements,
566
599
  );
567
600
  // A Kotlin lambda's parameters sit beside its statements.
568
601
  if is_container
569
602
  && child.is_named()
570
- && !COMMENT_TYPES.contains(&child.kind_name())
603
+ && !is_tokenless(child, code)
571
604
  && child.kind_name() != "lambda_parameters"
572
605
  {
573
- statement_ranges.push(child_range);
606
+ if child.kind_name() == "statement_list" {
607
+ statement_ranges.append(&mut child_statements);
608
+ } else {
609
+ statement_ranges.push(child_range);
610
+ }
574
611
  }
575
612
  }
576
- // Single-statement containers are recorded too: window enumeration needs two
577
- // statements and yields nothing for them, but cross-file matching catalogues each
578
- // container's full run.
579
- if is_container && !statement_ranges.is_empty() {
613
+ if is_statement_list {
614
+ *statement_list_statements = statement_ranges;
615
+ } else if is_container && !statement_ranges.is_empty() {
616
+ // Single-statement containers are recorded too: window enumeration needs two
617
+ // statements and yields nothing for them, but cross-file matching catalogues each
618
+ // container's full run.
580
619
  container_statement_ranges.push(statement_ranges);
581
620
  }
582
621
  }
@@ -595,7 +634,97 @@ fn collect_tokens<'a>(
595
634
  range
596
635
  }
597
636
 
598
- visit(root, code, tokens, block_ranges, container_statement_ranges);
637
+ visit(
638
+ root,
639
+ code,
640
+ tokens,
641
+ block_ranges,
642
+ container_statement_ranges,
643
+ &mut Vec::new(),
644
+ );
645
+ }
646
+
647
+ /// Comments and dependency declarations contribute no tokens, so no clone consists of them; the
648
+ /// tokens around them stay adjacent, so a clone may still extend across one.
649
+ fn is_tokenless(node: Node<'_>, code: &Source<'_>) -> bool {
650
+ COMMENT_TYPES.contains(&node.kind_name()) || is_dependency_declaration(node, code)
651
+ }
652
+
653
+ /// Every module must spell out its own dependencies, so a clone made of them has no refactoring
654
+ /// that could remove it; they are left out of clone detection entirely.
655
+ fn is_dependency_declaration(node: Node<'_>, code: &Source<'_>) -> bool {
656
+ if !node.is_named() {
657
+ return false;
658
+ }
659
+ let kind = node.kind_name();
660
+ if DEPENDENCY_DECLARATION_TYPES.contains(&kind) {
661
+ return true;
662
+ }
663
+ match (crate::tree_index::language_name(), kind) {
664
+ // JavaScript's dynamic `import(...)` is a named `import` node too.
665
+ ("kotlin", "import") => true,
666
+ ("cpp", "using_declaration") => true,
667
+ // `mod name;` loads a file; a `mod name { ... }` with a body is code.
668
+ ("rust", "mod_item") => node.child_by_field_name("body").is_none(),
669
+ // Re-exports (`export { a } from './a'`); other exports wrap declarations.
670
+ ("javascript" | "jsx" | "typescript" | "tsx", "export_statement") => {
671
+ node.child_by_field_name("source").is_some()
672
+ }
673
+ (
674
+ "javascript" | "jsx" | "typescript" | "tsx",
675
+ "lexical_declaration" | "variable_declaration",
676
+ ) => is_require_declaration(node, code),
677
+ // `require('a');` and CommonJS re-exports such as `module.exports = require('a');`.
678
+ ("javascript" | "jsx" | "typescript" | "tsx", "expression_statement") => {
679
+ named_children(node).first().is_some_and(|expression| {
680
+ if expression.kind_name() == "assignment_expression" {
681
+ expression
682
+ .child_by_field_name("right")
683
+ .is_some_and(|right| is_require_value(right, code))
684
+ } else {
685
+ is_require_value(*expression, code)
686
+ }
687
+ })
688
+ }
689
+ ("ruby", "call") => {
690
+ node.child_by_field_name("receiver").is_none()
691
+ && node
692
+ .child_by_field_name("method")
693
+ .is_some_and(|method| RUBY_REQUIRE_METHODS.contains(&node_text(method, code)))
694
+ }
695
+ _ => false,
696
+ }
697
+ }
698
+
699
+ /// `const a = require('a')` (including destructuring and `require('a').b`) in every declarator.
700
+ fn is_require_declaration(node: Node<'_>, code: &Source<'_>) -> bool {
701
+ let declarators: Vec<Node<'_>> = named_children(node)
702
+ .into_iter()
703
+ .filter(|child| child.kind_name() == "variable_declarator")
704
+ .collect();
705
+ !declarators.is_empty()
706
+ && declarators.iter().all(|declarator| {
707
+ declarator
708
+ .child_by_field_name("value")
709
+ .is_some_and(|value| is_require_value(value, code))
710
+ })
711
+ }
712
+
713
+ /// `require('a')` or a property read from it (`require('a').b`), but not a call on the module.
714
+ fn is_require_value(node: Node<'_>, code: &Source<'_>) -> bool {
715
+ let mut value = node;
716
+ while value.kind_name() == "member_expression" {
717
+ let Some(object) = value.child_by_field_name("object") else {
718
+ return false;
719
+ };
720
+ value = object;
721
+ }
722
+ value.kind_name() == "call_expression"
723
+ && value
724
+ .child_by_field_name("function")
725
+ .is_some_and(|function| {
726
+ function.kind_name() == "identifier" && node_text(function, code) == "require"
727
+ })
599
728
  }
600
729
 
601
730
  /// The kind tag of a string-like node with no interpolation, or None to descend normally.
@@ -832,7 +961,7 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
832
961
  .named_child(0)
833
962
  .is_some_and(|first| first.id() == node.id());
834
963
  let is_last_named = parent
835
- .named_child(parent.named_child_count().saturating_sub(1))
964
+ .named_child(parent.named_child_count().saturating_sub(1) as u32)
836
965
  .is_some_and(|last| last.id() == node.id());
837
966
  if parent.kind_name() == "navigation_expression" && is_last_named && !is_first_named {
838
967
  return true;
@@ -840,7 +969,7 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
840
969
  if parent.kind_name() == "call_expression"
841
970
  && is_first_named
842
971
  && parent
843
- .named_child(parent.named_child_count().saturating_sub(1))
972
+ .named_child(parent.named_child_count().saturating_sub(1) as u32)
844
973
  .is_some_and(|arguments| {
845
974
  matches!(
846
975
  arguments.kind_name(),
@@ -821,13 +821,16 @@ fn find_string_literal_content(literal: Node<'_>, code: &Source<'_>) -> Option<S
821
821
  {
822
822
  return None;
823
823
  }
824
- // Go exposes no content node at all: it names only the escapes, so the concatenation is
825
- // trusted only when the literal really spells its content out.
824
+ // Escapes alone do not spell the content out, so the concatenation is trusted only when a
825
+ // content node takes part in it.
826
826
  let mut content = String::new();
827
827
  let mut has_content_node = false;
828
828
  for child in &children {
829
829
  match child.kind_name() {
830
- "string_content" | "string_fragment" => {
830
+ "string_content"
831
+ | "string_fragment"
832
+ | "interpreted_string_literal_content"
833
+ | "raw_string_literal_content" => {
831
834
  has_content_node = true;
832
835
  content.push_str(node_text(*child, code));
833
836
  }
@@ -838,8 +841,8 @@ fn find_string_literal_content(literal: Node<'_>, code: &Source<'_>) -> Option<S
838
841
  if has_content_node && !content.is_empty() {
839
842
  return Some(content);
840
843
  }
841
- // A grammar that exposes no content child (Go's string literals) keeps its delimiters in the
842
- // text; an empty literal is left without a name.
844
+ // A literal without a content child (Go's rune literal) keeps its delimiters in the text; an
845
+ // empty literal is left without a name.
843
846
  let text = node_text(literal, code);
844
847
  let quote = text
845
848
  .chars()
@@ -880,7 +883,7 @@ fn find_assignment_target_name(assignment: Node<'_>, code: &Source<'_>) -> Optio
880
883
  "field_expression" => target.child_by_field_name("field")?,
881
884
  "member_access_expression" => target.child_by_field_name("name")?,
882
885
  "field_access" => target.child_by_field_name("field")?,
883
- "navigation_expression" => target.named_child(target.named_child_count() - 1)?,
886
+ "navigation_expression" => target.named_child(target.named_child_count() as u32 - 1)?,
884
887
  "qualified_identifier" => return unwrap_declarator_name(Some(target), code),
885
888
  _ => return None,
886
889
  };
@@ -975,7 +978,7 @@ fn is_value_of_parent(node: Node<'_>, parent: Node<'_>) -> bool {
975
978
  .is_some_and(|child| child.id() == node.id())
976
979
  {
977
980
  return matches!(
978
- parent.field_name_for_child(index as u32),
981
+ parent.field_name_for_child(index),
979
982
  None | Some("value")
980
983
  );
981
984
  }