code-gauge 10.1.0 → 10.2.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 (42) hide show
  1. package/README.md +33 -14
  2. package/THIRD-PARTY-NOTICES.txt +8 -8
  3. package/dist/checkCommand.cjs +1 -1
  4. package/dist/checkCommand.cjs.map +1 -1
  5. package/dist/checkCommand.js +1 -1
  6. package/dist/checkCommand.js.map +1 -1
  7. package/dist/crossFileDuplication.cjs +1 -1
  8. package/dist/crossFileDuplication.cjs.map +1 -1
  9. package/dist/crossFileDuplication.d.ts +3 -2
  10. package/dist/crossFileDuplication.js +1 -1
  11. package/dist/crossFileDuplication.js.map +1 -1
  12. package/dist/crossFileNearMiss.cjs +1 -1
  13. package/dist/crossFileNearMiss.cjs.map +1 -1
  14. package/dist/crossFileNearMiss.d.ts +6 -4
  15. package/dist/crossFileNearMiss.js +1 -1
  16. package/dist/crossFileNearMiss.js.map +1 -1
  17. package/dist/duplication.cjs +1 -1
  18. package/dist/duplication.cjs.map +1 -1
  19. package/dist/duplication.d.ts +22 -7
  20. package/dist/duplication.js +1 -1
  21. package/dist/duplication.js.map +1 -1
  22. package/dist/thresholdCheck.cjs +1 -1
  23. package/dist/thresholdCheck.cjs.map +1 -1
  24. package/dist/thresholdCheck.d.ts +2 -0
  25. package/dist/thresholdCheck.js +1 -1
  26. package/dist/thresholdCheck.js.map +1 -1
  27. package/dist/thresholds.cjs.map +1 -1
  28. package/dist/thresholds.d.ts +2 -3
  29. package/dist/thresholds.js.map +1 -1
  30. package/dist/types.d.ts +19 -8
  31. package/native/Cargo.lock +16 -16
  32. package/native/Cargo.toml +8 -8
  33. package/native/code-gauge.wasm +0 -0
  34. package/native/src/complexity.rs +10 -1
  35. package/native/src/container.rs +158 -7
  36. package/native/src/dep_degree.rs +13 -14
  37. package/native/src/duplication.rs +188 -75
  38. package/native/src/functions.rs +112 -24
  39. package/native/src/languages.rs +5 -0
  40. package/native/src/measure.rs +9 -4
  41. package/native/src/near_miss.rs +303 -52
  42. package/package.json +8 -8
@@ -72,7 +72,6 @@ const DECLARATOR_WRAPPER_TYPES: &[&str] = &[
72
72
  "parenthesized_declarator",
73
73
  "function_declarator",
74
74
  "attributed_declarator",
75
- "pointer_type_declarator",
76
75
  "qualified_identifier",
77
76
  ];
78
77
 
@@ -149,16 +148,10 @@ pub fn measure_dep_degree(
149
148
  pairs
150
149
  }
151
150
 
152
- /// A C++ member-pointer variable (`int C::* p`, also `int C::* arr[1]`) is declared as a
153
- /// `type_identifier` under the `pointer_type_declarator` spelling `C::*`, possibly through further
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).
151
+ /// Kotlin spells type names as identifiers: in a `user_type`, or as a type parameter's fieldless
152
+ /// name (C# puts its type parameter in a `name` field).
157
153
  fn is_variable_leaf(leaf: &DepDegreeLeaf<'_>) -> bool {
158
154
  let node = leaf.node;
159
- if node.kind_name() == "type_identifier" {
160
- return is_member_pointer_name(node);
161
- }
162
155
  VARIABLE_NODE_TYPES.contains(&node.kind_name())
163
156
  && !node.parent_node().is_some_and(|parent| {
164
157
  parent.kind_name() == "user_type"
@@ -166,15 +159,21 @@ fn is_variable_leaf(leaf: &DepDegreeLeaf<'_>) -> bool {
166
159
  })
167
160
  }
168
161
 
162
+ /// Whether the identifier is the name a C++ member pointer declares (`int C::* p`, also
163
+ /// `int C::* arr[1]`): its `pointer_declarator`, possibly around further declarator wrappers, is
164
+ /// the `name` of the `qualified_identifier` spelling the class.
169
165
  fn is_member_pointer_name(node: Node<'_>) -> bool {
170
166
  let mut current = node;
171
167
  while let Some(parent) = current.parent_node() {
172
- if parent.kind_name() == "pointer_type_declarator" {
173
- return true;
168
+ // The kind is tested before the field is looked up: this runs for every variable read,
169
+ // and scanning the children of a high-arity parent (a list of arguments) each time would
170
+ // cost the square of its length.
171
+ if parent.kind_name() == "qualified_identifier" {
172
+ return current.kind_name() == "pointer_declarator"
173
+ && field_name_in_parent(current, parent) == Some("name");
174
174
  }
175
175
  // The climb follows the declared-name position only, exactly like unwrap_declarator_wrappers.
176
176
  if !DECLARATOR_WRAPPER_TYPES.contains(&parent.kind_name())
177
- || parent.kind_name() == "qualified_identifier"
178
177
  || field_name_in_parent(current, parent).is_some_and(|field| field != "declarator")
179
178
  {
180
179
  return false;
@@ -338,7 +337,7 @@ fn is_definition_field(holder: Node<'_>, field_name: Option<&str>) -> bool {
338
337
 
339
338
  /// Climbs from the identifier through the C/C++ declarator wrappers it is the declared name of
340
339
  /// (`reference_declarator` and `parenthesized_declarator` expose no field, so their name is the
341
- /// fieldless child; a member pointer's `pointer_type_declarator` is the `name` of a
340
+ /// fieldless child; a member pointer's `pointer_declarator` is the `name` of a
342
341
  /// `qualified_identifier`; an `array_declarator` size or a nested parameter has another field and
343
342
  /// stops the climb) to the outermost wrapper and its field in the declaration.
344
343
  fn unwrap_declarator_wrappers<'t>(leaf: &DepDegreeLeaf<'t>) -> (Node<'t>, Option<&'t str>) {
@@ -400,7 +399,7 @@ fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
400
399
  // O(1).
401
400
  let continues_member_pointer = declares_member_pointer
402
401
  && parent.kind_name() == "qualified_identifier"
403
- && (current.kind_name() == "pointer_type_declarator" || in_member_pointer)
402
+ && (current.kind_name() == "pointer_declarator" || in_member_pointer)
404
403
  && field_name_in_parent(current, parent) == Some("name");
405
404
  in_member_pointer = continues_member_pointer;
406
405
  if depth >= 1
@@ -4,7 +4,9 @@ use std::borrow::Cow;
4
4
  use std::sync::OnceLock;
5
5
  use tree_sitter::Node;
6
6
 
7
- use crate::near_miss::{Block, Matcher, PairMatch, FILTRATION_PERCENT, MAX_LENGTH_RATIO};
7
+ use crate::near_miss::{
8
+ Alignment, Block, CorePair, Matcher, PairMatch, FILTRATION_PERCENT, MAX_LENGTH_RATIO,
9
+ };
8
10
  use crate::tree_index::NodeExt;
9
11
  use crate::types::{
10
12
  CrossFileCandidate, CrossFileToken, CrossFileTokenRange, DuplicateBlockOccurrence,
@@ -1577,9 +1579,13 @@ fn dedupe_by_region(group: Vec<DuplicateCandidate>) -> Vec<DuplicateCandidate> {
1577
1579
  #[derive(Clone)]
1578
1580
  struct CountedOccurrence {
1579
1581
  segments: Vec<(usize, usize)>,
1580
- /// Set on a retained group's occurrences that a partial gapped merge also paired into a
1581
- /// merged group: their spans are counted there, so block counting must not count them again.
1582
+ /// Set on an occurrence whose span another group counts, so block counting must not count it
1583
+ /// again: a retained group's occurrence that a partial gapped merge also paired into a merged
1584
+ /// group, and a near-miss anchor, which overlaps the reported occurrence it was matched through.
1582
1585
  shared_with_merged_group: bool,
1586
+ /// The token runs of a near-miss copy that its partners match, within its segments;
1587
+ /// `None` for an exact or gapped copy, whose segments are matched throughout.
1588
+ matched_runs: Option<Vec<(usize, usize)>>,
1583
1589
  /// Sum of segment token counts (the gap tokens are not matched content).
1584
1590
  token_count: usize,
1585
1591
  start_token_index: usize,
@@ -1597,6 +1603,7 @@ fn to_counted_groups(
1597
1603
  .iter()
1598
1604
  .map(|candidate| CountedOccurrence {
1599
1605
  shared_with_merged_group: false,
1606
+ matched_runs: None,
1600
1607
  segments: vec![(candidate.start_token_index, candidate.end_token_index)],
1601
1608
  token_count: candidate.token_count,
1602
1609
  start_token_index: candidate.start_token_index,
@@ -1785,6 +1792,7 @@ fn merge_groups(
1785
1792
  CountedOccurrence {
1786
1793
  // A merged occurrence is a fresh span combination; it inherits no shared marks.
1787
1794
  shared_with_merged_group: false,
1795
+ matched_runs: None,
1788
1796
  segments: [leading.segments.clone(), trailing.segments.clone()].concat(),
1789
1797
  token_count: leading.token_count + trailing.token_count,
1790
1798
  start_token_index: leading.start_token_index,
@@ -1802,15 +1810,30 @@ fn merge_groups(
1802
1810
  })
1803
1811
  }
1804
1812
 
1805
- /// A verified near-miss pair as (block, core, block, core); a `None` core is a whole-block match.
1806
- type MatchEdge = (usize, Option<(usize, usize)>, usize, Option<(usize, usize)>);
1813
+ /// A verified near-miss pair of blocks; without `cores`, the blocks match as wholes.
1814
+ struct MatchEdge {
1815
+ left: usize,
1816
+ right: usize,
1817
+ cores: Option<CorePair>,
1818
+ alignment: Alignment,
1819
+ }
1820
+
1821
+ impl MatchEdge {
1822
+ /// The (block, core) on each side.
1823
+ fn sides(&self) -> [(usize, Option<(usize, usize)>); 2] {
1824
+ let (left_core, right_core) = self.cores.unzip();
1825
+ [(self.left, left_core), (self.right, right_core)]
1826
+ }
1827
+ }
1807
1828
 
1808
1829
  /// Detects near-miss (Type-3) clone groups among block candidates the exact pipeline left
1809
1830
  /// unreported: NIL-style n-gram filtration, then pair verification (near_miss::Matcher), then
1810
1831
  /// transitive clustering of verified pairs (crossFileNearMiss.ts applies the same model across
1811
1832
  /// files). A block that matched only locally is reported as its matched cores (overlapping cores
1812
1833
  /// merged), each clustered with its own partners, so code no verified pair matched never counts
1813
- /// as duplicated.
1834
+ /// as duplicated. A node overlapping a reported occurrence is an anchor: it joins its group at its
1835
+ /// own size, so the copies of a block are listed at the size they matched it, and is marked as
1836
+ /// counted elsewhere unless the group takes over the reported occurrences it overlaps.
1814
1837
  fn collect_near_miss_groups(
1815
1838
  source: &TokenizedSource<'_>,
1816
1839
  settings: &DuplicationSettings,
@@ -1840,7 +1863,7 @@ fn collect_near_miss_groups(
1840
1863
  .collect()
1841
1864
  };
1842
1865
 
1843
- let (symbols, is_content) = to_symbol_stream(tokens);
1866
+ let (symbols, is_content, token_keys) = to_symbol_stream(tokens);
1844
1867
  let statements = top_level_statement_finder(&source.container_statement_ranges);
1845
1868
  let mut blocks: Vec<Block> = comparable
1846
1869
  .iter()
@@ -1848,6 +1871,7 @@ fn collect_near_miss_groups(
1848
1871
  Block::new(
1849
1872
  &symbols,
1850
1873
  &is_content,
1874
+ &token_keys,
1851
1875
  range.start_token_index,
1852
1876
  range.end_token_index,
1853
1877
  statements(range.start_token_index, range.end_token_index),
@@ -1860,9 +1884,13 @@ fn collect_near_miss_groups(
1860
1884
  settings.min_similarity_percent,
1861
1885
  );
1862
1886
 
1887
+ // Two spans that both overlap reported content have nothing new to contribute to each other.
1888
+ // Their pair is dropped before clustering, so that it neither collapses the blocks' core nodes
1889
+ // nor widens a node beyond what the pairs clustering it matched.
1890
+ let is_touched = |(start, end): (usize, usize)| !touched_groups_in(start, end).is_empty();
1863
1891
  let block_touched: Vec<bool> = comparable
1864
1892
  .iter()
1865
- .map(|range| !touched_groups_in(range.start_token_index, range.end_token_index).is_empty())
1893
+ .map(|range| is_touched((range.start_token_index, range.end_token_index)))
1866
1894
  .collect();
1867
1895
  let mut edges: Vec<MatchEdge> = Vec::new();
1868
1896
  for_each_candidate_pair(
@@ -1870,17 +1898,27 @@ fn collect_near_miss_groups(
1870
1898
  settings.min_similarity_percent,
1871
1899
  |left_index, right_index| match matcher.verify(&blocks[left_index], &blocks[right_index]) {
1872
1900
  None => {}
1873
- // A whole match between two blocks that both overlap reported content could never join
1874
- // a group, and recording it would collapse the blocks' core nodes.
1875
- Some(PairMatch::Whole)
1901
+ Some(PairMatch::Whole(alignment))
1876
1902
  if !(block_touched[left_index] && block_touched[right_index]) =>
1877
1903
  {
1878
- edges.push((left_index, None, right_index, None))
1904
+ edges.push(MatchEdge {
1905
+ left: left_index,
1906
+ right: right_index,
1907
+ cores: None,
1908
+ alignment,
1909
+ })
1879
1910
  }
1880
- Some(PairMatch::Whole) => {}
1911
+ Some(PairMatch::Whole(_)) => {}
1881
1912
  Some(PairMatch::Local(cores)) => {
1882
- for (left_core, right_core) in cores {
1883
- edges.push((left_index, Some(left_core), right_index, Some(right_core)));
1913
+ for (core_pair, alignment) in cores {
1914
+ if !(is_touched(core_pair.0) && is_touched(core_pair.1)) {
1915
+ edges.push(MatchEdge {
1916
+ left: left_index,
1917
+ right: right_index,
1918
+ cores: Some(core_pair),
1919
+ alignment,
1920
+ });
1921
+ }
1884
1922
  }
1885
1923
  }
1886
1924
  },
@@ -1891,8 +1929,8 @@ fn collect_near_miss_groups(
1891
1929
  // cores matched with different partners fall into separate groups.
1892
1930
  let mut matched_whole = vec![false; comparable.len()];
1893
1931
  let mut local_cores: Vec<Vec<(usize, usize)>> = vec![Vec::new(); comparable.len()];
1894
- for &(left_index, left_core, right_index, right_core) in &edges {
1895
- for (index, core) in [(left_index, left_core), (right_index, right_core)] {
1932
+ for edge in &edges {
1933
+ for (index, core) in edge.sides() {
1896
1934
  match core {
1897
1935
  Some(core) => local_cores[index].push(core),
1898
1936
  None => matched_whole[index] = true,
@@ -1958,22 +1996,33 @@ fn collect_near_miss_groups(
1958
1996
  touched_groups_in(start, end)
1959
1997
  })
1960
1998
  .collect();
1961
- for &(left_index, left_core, right_index, right_core) in &edges {
1962
- let (left_node, right_node) = (
1963
- node_of(left_index, left_core),
1964
- node_of(right_index, right_core),
1965
- );
1966
- // Two already-reported nodes have nothing new to contribute to each other.
1967
- if !touched_groups_by_node[left_node].is_empty()
1968
- && !touched_groups_by_node[right_node].is_empty()
1969
- {
1970
- continue;
1971
- }
1999
+ let edge_nodes: Vec<[usize; 2]> = edges
2000
+ .iter()
2001
+ .map(|edge| edge.sides().map(|(index, core)| node_of(index, core)))
2002
+ .collect();
2003
+ for &[left_node, right_node] in &edge_nodes {
1972
2004
  let left_root = find(&mut parent, left_node);
1973
2005
  let right_root = find(&mut parent, right_node);
1974
2006
  parent[left_root.max(right_root)] = left_root.min(right_root);
1975
2007
  }
1976
2008
 
2009
+ // The lines of a near-miss copy that are duplicated are those a verified partner matches,
2010
+ // which takes the pairing itself. Every pair is aligned: verifying it already cost a longest
2011
+ // common subsequence of the same sequences, and any choice among a copy's partners would let
2012
+ // their number or their order decide what the copy counts.
2013
+ let mut matched_runs_by_node: Vec<Vec<(usize, usize)>> = vec![Vec::new(); node_blocks.len()];
2014
+ for (edge, nodes) in edges.iter().zip(&edge_nodes) {
2015
+ let (left_runs, right_runs) = matcher.align(
2016
+ &blocks[edge.left],
2017
+ &blocks[edge.right],
2018
+ edge.cores,
2019
+ edge.alignment,
2020
+ &token_keys,
2021
+ );
2022
+ matched_runs_by_node[nodes[0]].extend(left_runs);
2023
+ matched_runs_by_node[nodes[1]].extend(right_runs);
2024
+ }
2025
+
1977
2026
  let mut members_by_root: IndexMap<usize, Vec<usize>> = IndexMap::new();
1978
2027
  for node in 0..node_blocks.len() {
1979
2028
  let root = find(&mut parent, node);
@@ -2001,8 +2050,14 @@ fn collect_near_miss_groups(
2001
2050
  segments.sort_unstable();
2002
2051
  let whole = spans.iter().any(Option::is_none);
2003
2052
  let (start, end) = (segments[0].0, segments[segments.len() - 1].1);
2053
+ let matched_runs: Vec<(usize, usize)> = nodes
2054
+ .iter()
2055
+ .filter(|&&node| node_blocks[node] == block)
2056
+ .flat_map(|&node| matched_runs_by_node[node].iter().copied())
2057
+ .collect();
2004
2058
  CountedOccurrence {
2005
2059
  shared_with_merged_group: false,
2060
+ matched_runs: Some(merge_overlapping_cores(&matched_runs)),
2006
2061
  token_count: segments.iter().map(|segment| segment.1 - segment.0).sum(),
2007
2062
  segments,
2008
2063
  start_token_index: start,
@@ -2022,6 +2077,13 @@ fn collect_near_miss_groups(
2022
2077
  .collect()
2023
2078
  };
2024
2079
  let touched_groups_of = |node: usize| &touched_groups_by_node[node];
2080
+ let to_anchor_occurrences = |nodes: &[usize]| -> Vec<CountedOccurrence> {
2081
+ let mut anchors = to_occurrences(nodes);
2082
+ for anchor in &mut anchors {
2083
+ anchor.shared_with_merged_group = true;
2084
+ }
2085
+ anchors
2086
+ };
2025
2087
  let mut groups: Vec<Vec<CountedOccurrence>> = Vec::new();
2026
2088
  for members in members_by_root.values() {
2027
2089
  if members.len() < 2 {
@@ -2074,6 +2136,7 @@ fn collect_near_miss_groups(
2074
2136
  let mut consumed: FxHashSet<(usize, usize)> = FxHashSet::default();
2075
2137
  let mut merged: Vec<CountedOccurrence> = Vec::new();
2076
2138
  let mut unanchored_nodes: Vec<usize> = Vec::new();
2139
+ let mut anchor_nodes: Vec<usize> = Vec::new();
2077
2140
  let mut nodes_by_block: IndexMap<usize, Vec<usize>> = IndexMap::new();
2078
2141
  for &member_index in members {
2079
2142
  nodes_by_block
@@ -2086,10 +2149,8 @@ fn collect_near_miss_groups(
2086
2149
  // groups belong to the same copy. Consecutive position-order slices keep the
2087
2150
  // coalesced spans disjoint.
2088
2151
  let mut fragments: Vec<(CountedOccurrence, usize)> = Vec::new();
2089
- let mut plain_nodes: Vec<usize> = Vec::new();
2090
2152
  for &node in block_nodes {
2091
2153
  let (range_start, range_end) = node_range(node);
2092
- let fragment_count = fragments.len();
2093
2154
  for &group_index in &fully_clustered {
2094
2155
  for (occurrence_index, occurrence) in
2095
2156
  reported_groups[group_index].iter().enumerate()
@@ -2103,18 +2164,20 @@ fn collect_near_miss_groups(
2103
2164
  }
2104
2165
  }
2105
2166
  }
2106
- if fragments.len() == fragment_count && touched_groups_of(node).is_empty() {
2107
- plain_nodes.push(node);
2108
- }
2109
2167
  }
2110
2168
  if fragments.is_empty() {
2111
- unanchored_nodes.extend(plain_nodes);
2112
- } else {
2113
- // An untouched core of a block that also holds fragments is part of the same
2114
- // copy; a group index no reported group uses keeps it in that copy.
2115
- for occurrence in to_occurrences(&plain_nodes) {
2116
- fragments.push((occurrence, usize::MAX));
2117
- }
2169
+ let (anchors, plain): (Vec<usize>, Vec<usize>) = block_nodes
2170
+ .iter()
2171
+ .partition(|&&node| !touched_groups_of(node).is_empty());
2172
+ anchor_nodes.extend(anchors);
2173
+ unanchored_nodes.extend(plain);
2174
+ continue;
2175
+ }
2176
+ // The block's nodes are part of the same copy as its fragments: a block matched
2177
+ // beyond the fragments it holds is a copy at that size. A group index no reported
2178
+ // group uses keeps them in that copy.
2179
+ for occurrence in to_occurrences(block_nodes) {
2180
+ fragments.push((occurrence, usize::MAX));
2118
2181
  }
2119
2182
  fragments.sort_by_key(|(occurrence, _)| {
2120
2183
  (occurrence.start_token_index, occurrence.end_token_index)
@@ -2134,21 +2197,26 @@ fn collect_near_miss_groups(
2134
2197
  }
2135
2198
  }
2136
2199
  merged.extend(to_occurrences(&unanchored_nodes));
2137
- merged.sort_by_key(|occurrence| {
2138
- (occurrence.start_token_index, occurrence.end_token_index)
2139
- });
2140
2200
  // The rebuild consumed every fully-clustered group, so shared-span marks from earlier
2141
2201
  // partial merges no longer point at a separate merged group.
2142
2202
  for occurrence in &mut merged {
2143
2203
  occurrence.shared_with_merged_group = false;
2144
2204
  }
2205
+ merged.extend(to_anchor_occurrences(&anchor_nodes));
2206
+ merged.sort_by_key(|occurrence| {
2207
+ (occurrence.start_token_index, occurrence.end_token_index)
2208
+ });
2145
2209
  reported_groups[target_index] = merged;
2146
2210
  for &source_index in source_indexes {
2147
2211
  reported_groups[source_index].clear();
2148
2212
  }
2149
2213
  } else {
2150
- let occurrences = to_occurrences(&uncovered);
2214
+ let mut occurrences = to_occurrences(&uncovered);
2215
+ occurrences.extend(to_anchor_occurrences(&covered));
2151
2216
  if occurrences.len() >= 2 {
2217
+ occurrences.sort_by_key(|occurrence| {
2218
+ (occurrence.start_token_index, occurrence.end_token_index)
2219
+ });
2152
2220
  groups.push(occurrences);
2153
2221
  }
2154
2222
  }
@@ -2263,8 +2331,27 @@ fn coalesce_occurrences(occurrences: Vec<CountedOccurrence>) -> CountedOccurrenc
2263
2331
  _ => segments.push(segment),
2264
2332
  }
2265
2333
  }
2334
+ // An exact fragment is matched throughout, a near-miss part where its partners match it.
2335
+ let matched_runs = occurrences
2336
+ .iter()
2337
+ .any(|occurrence| occurrence.matched_runs.is_some())
2338
+ .then(|| {
2339
+ let runs: Vec<(usize, usize)> = occurrences
2340
+ .iter()
2341
+ .flat_map(|occurrence| {
2342
+ occurrence
2343
+ .matched_runs
2344
+ .as_ref()
2345
+ .unwrap_or(&occurrence.segments)
2346
+ .iter()
2347
+ .copied()
2348
+ })
2349
+ .collect();
2350
+ merge_overlapping_cores(&runs)
2351
+ });
2266
2352
  CountedOccurrence {
2267
2353
  shared_with_merged_group: false,
2354
+ matched_runs,
2268
2355
  token_count: segments.iter().map(|segment| segment.1 - segment.0).sum(),
2269
2356
  start_token_index: occurrences
2270
2357
  .iter()
@@ -2300,32 +2387,45 @@ fn merge_overlapping_cores(cores: &[(usize, usize)]) -> Vec<(usize, usize)> {
2300
2387
  /// other token interned from its hash pairs (literal VALUES folded in, unlike the exact
2301
2388
  /// fingerprint's kind tags), plus which tokens are content-bearing (names and literal values).
2302
2389
  /// Interned per call so a file's symbol ids (and thus its n-gram hashes) never depend on which
2303
- /// other files the process measured before it.
2304
- fn to_symbol_stream(tokens: &[Token<'_>]) -> (Vec<i32>, Vec<bool>) {
2390
+ /// other files the process measured before it. A symbol id still depends on where in the file its
2391
+ /// token first occurs, so each non-identifier token also gets a key of its content alone.
2392
+ fn to_symbol_stream(tokens: &[Token<'_>]) -> (Vec<i32>, Vec<bool>, Vec<i64>) {
2305
2393
  let mut symbol_by_token_hashes: FxHashMap<(i32, i32, i32, i32), i32> = FxHashMap::default();
2306
2394
  let mut id_by_identifier: FxHashMap<&str, i32> = FxHashMap::default();
2307
- tokens
2308
- .iter()
2309
- .map(|token| {
2310
- if token.is_id {
2311
- let next_id = id_by_identifier.len() as i32;
2312
- let id = *id_by_identifier
2313
- .entry(token.text.as_ref())
2314
- .or_insert(next_id);
2315
- return (-(id + 1), false);
2316
- }
2317
- let next_symbol = symbol_by_token_hashes.len() as i32;
2318
- let symbol = *symbol_by_token_hashes
2319
- .entry((
2320
- token.text_hash,
2321
- token.text_hash2,
2322
- token.literal_hash.unwrap_or(0),
2323
- token.literal_hash2.unwrap_or(0),
2324
- ))
2325
- .or_insert(next_symbol);
2326
- (symbol, token.is_name || token.literal_hash.is_some())
2327
- })
2328
- .unzip()
2395
+ let mut symbols = Vec::with_capacity(tokens.len());
2396
+ let mut is_content = Vec::with_capacity(tokens.len());
2397
+ let mut token_keys = Vec::with_capacity(tokens.len());
2398
+ for token in tokens {
2399
+ if token.is_id {
2400
+ let next_id = id_by_identifier.len() as i32;
2401
+ let id = *id_by_identifier
2402
+ .entry(token.text.as_ref())
2403
+ .or_insert(next_id);
2404
+ symbols.push(-(id + 1));
2405
+ is_content.push(false);
2406
+ token_keys.push(0);
2407
+ continue;
2408
+ }
2409
+ let hashes = (
2410
+ token.text_hash,
2411
+ token.text_hash2,
2412
+ token.literal_hash.unwrap_or(0),
2413
+ token.literal_hash2.unwrap_or(0),
2414
+ );
2415
+ let next_symbol = symbol_by_token_hashes.len() as i32;
2416
+ symbols.push(*symbol_by_token_hashes.entry(hashes).or_insert(next_symbol));
2417
+ is_content.push(token.is_name || token.literal_hash.is_some());
2418
+ token_keys.push(token_content_key(hashes));
2419
+ }
2420
+ (symbols, is_content, token_keys)
2421
+ }
2422
+
2423
+ /// A 53-bit key of a token's two text hashes, each mixed with the matching literal value hash; the
2424
+ /// same key as `tokenKey` in crossFileNearMiss.ts, so that both detectors order content alike.
2425
+ fn token_content_key((text, text2, literal, literal2): (i32, i32, i32, i32)) -> i64 {
2426
+ let primary = (text ^ literal.wrapping_mul(0x9E37_79B1_u32 as i32)) as u32;
2427
+ let secondary = (text2 ^ literal2.wrapping_mul(0x85EB_CA6B_u32 as i32)) as u32;
2428
+ i64::from(primary) * 0x20_0000 + i64::from(secondary >> 11)
2329
2429
  }
2330
2430
 
2331
2431
  /// Returns a lookup of the outermost container statements inside a token range, excluding a
@@ -2429,23 +2529,36 @@ fn count_redundant_fragments(group: &[CountedOccurrence]) -> usize {
2429
2529
  }
2430
2530
 
2431
2531
  /// Only CODE lines carrying matched tokens count; the unmatched gap of a merged clone stays out
2432
- /// of line coverage.
2532
+ /// of line coverage. A line of a near-miss copy counts when its partners match more than half of
2533
+ /// its tokens: a line the copy adds or rewrites is not duplicated, while a longest common
2534
+ /// subsequence still pairs a stray token of it (a bracket, a separator) with one of the partner.
2433
2535
  fn collect_matched_lines(
2434
2536
  occurrence: &CountedOccurrence,
2435
2537
  code_line_numbers: &FxHashSet<usize>,
2436
2538
  tokens: &[Token<'_>],
2437
2539
  ) -> FxHashSet<usize> {
2438
- let mut lines: FxHashSet<usize> = FxHashSet::default();
2540
+ let mut token_counts_by_row: FxHashMap<usize, (usize, usize)> = FxHashMap::default();
2541
+ let mut runs = occurrence.matched_runs.iter().flatten().peekable();
2439
2542
  for &(segment_start, segment_end) in &occurrence.segments {
2440
- for token in &tokens[segment_start..segment_end.min(tokens.len())] {
2543
+ let segment = tokens.iter().enumerate().take(segment_end);
2544
+ for (index, token) in segment.skip(segment_start) {
2545
+ while runs.next_if(|run| run.1 <= index).is_some() {}
2546
+ let matched =
2547
+ occurrence.matched_runs.is_none() || runs.peek().is_some_and(|run| run.0 <= index);
2441
2548
  for row in token.start_row..=token.end_row {
2442
- if code_line_numbers.contains(&(row + 1)) {
2443
- lines.insert(row + 1);
2444
- }
2549
+ let (matched_count, token_count) = token_counts_by_row.entry(row + 1).or_default();
2550
+ *matched_count += usize::from(matched);
2551
+ *token_count += 1;
2445
2552
  }
2446
2553
  }
2447
2554
  }
2448
- lines
2555
+ token_counts_by_row
2556
+ .into_iter()
2557
+ .filter(|(line, (matched_count, token_count))| {
2558
+ code_line_numbers.contains(line) && matched_count * 2 > *token_count
2559
+ })
2560
+ .map(|(line, _)| line)
2561
+ .collect()
2449
2562
  }
2450
2563
 
2451
2564
  fn summarize_duplicates(