code-gauge 10.0.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 (54) hide show
  1. package/README.md +56 -34
  2. package/THIRD-PARTY-NOTICES.txt +8 -8
  3. package/dist/checkCommand.cjs +2 -2
  4. package/dist/checkCommand.cjs.map +1 -1
  5. package/dist/checkCommand.d.ts +2 -5
  6. package/dist/checkCommand.js +2 -2
  7. package/dist/checkCommand.js.map +1 -1
  8. package/dist/crossFileDuplication.cjs +1 -1
  9. package/dist/crossFileDuplication.cjs.map +1 -1
  10. package/dist/crossFileDuplication.d.ts +14 -2
  11. package/dist/crossFileDuplication.js +1 -1
  12. package/dist/crossFileDuplication.js.map +1 -1
  13. package/dist/crossFileNearMiss.cjs +1 -1
  14. package/dist/crossFileNearMiss.cjs.map +1 -1
  15. package/dist/crossFileNearMiss.d.ts +6 -4
  16. package/dist/crossFileNearMiss.js +1 -1
  17. package/dist/crossFileNearMiss.js.map +1 -1
  18. package/dist/duplication.cjs +1 -1
  19. package/dist/duplication.cjs.map +1 -1
  20. package/dist/duplication.d.ts +22 -7
  21. package/dist/duplication.js +1 -1
  22. package/dist/duplication.js.map +1 -1
  23. package/dist/nativeMetrics.cjs +1 -1
  24. package/dist/nativeMetrics.cjs.map +1 -1
  25. package/dist/nativeMetrics.d.ts +1 -1
  26. package/dist/nativeMetrics.js +1 -1
  27. package/dist/nativeMetrics.js.map +1 -1
  28. package/dist/thresholdCheck.cjs +1 -1
  29. package/dist/thresholdCheck.cjs.map +1 -1
  30. package/dist/thresholdCheck.d.ts +10 -8
  31. package/dist/thresholdCheck.js +1 -1
  32. package/dist/thresholdCheck.js.map +1 -1
  33. package/dist/thresholds.cjs +1 -1
  34. package/dist/thresholds.cjs.map +1 -1
  35. package/dist/thresholds.d.ts +5 -3
  36. package/dist/thresholds.js +1 -1
  37. package/dist/thresholds.js.map +1 -1
  38. package/dist/types.d.ts +29 -7
  39. package/dist/wasmBinding.cjs +1 -1
  40. package/dist/wasmBinding.js +1 -1
  41. package/native/Cargo.lock +16 -16
  42. package/native/Cargo.toml +8 -8
  43. package/native/code-gauge.wasm +0 -0
  44. package/native/src/complexity.rs +10 -1
  45. package/native/src/container.rs +158 -7
  46. package/native/src/dep_degree.rs +13 -14
  47. package/native/src/duplication.rs +213 -85
  48. package/native/src/functions.rs +112 -24
  49. package/native/src/languages.rs +5 -0
  50. package/native/src/lib.rs +1 -1
  51. package/native/src/measure.rs +9 -4
  52. package/native/src/near_miss.rs +303 -52
  53. package/native/src/types.rs +3 -0
  54. package/package.json +8 -8
@@ -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
@@ -2428,6 +2528,39 @@ fn count_redundant_fragments(group: &[CountedOccurrence]) -> usize {
2428
2528
  }
2429
2529
  }
2430
2530
 
2531
+ /// Only CODE lines carrying matched tokens count; the unmatched gap of a merged clone stays out
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.
2535
+ fn collect_matched_lines(
2536
+ occurrence: &CountedOccurrence,
2537
+ code_line_numbers: &FxHashSet<usize>,
2538
+ tokens: &[Token<'_>],
2539
+ ) -> FxHashSet<usize> {
2540
+ let mut token_counts_by_row: FxHashMap<usize, (usize, usize)> = FxHashMap::default();
2541
+ let mut runs = occurrence.matched_runs.iter().flatten().peekable();
2542
+ for &(segment_start, segment_end) in &occurrence.segments {
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);
2548
+ for row in token.start_row..=token.end_row {
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;
2552
+ }
2553
+ }
2554
+ }
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()
2562
+ }
2563
+
2431
2564
  fn summarize_duplicates(
2432
2565
  groups: &[Vec<CountedOccurrence>],
2433
2566
  code_line_numbers: &FxHashSet<usize>,
@@ -2439,25 +2572,20 @@ fn summarize_duplicates(
2439
2572
  let mut duplicated_lines: FxHashSet<usize> = FxHashSet::default();
2440
2573
  for group in groups {
2441
2574
  duplicate_block_count += count_redundant_fragments(group);
2442
- for occurrence in group {
2443
- max_duplicate_block_size = max_duplicate_block_size.max(occurrence.token_count);
2444
- // Only CODE lines carrying matched tokens count; the unmatched gap of a merged clone
2445
- // stays out of line coverage.
2446
- for &(segment_start, segment_end) in &occurrence.segments {
2447
- for token in &tokens[segment_start..segment_end.min(tokens.len())] {
2448
- for row in token.start_row..=token.end_row {
2449
- if code_line_numbers.contains(&(row + 1)) {
2450
- duplicated_lines.insert(row + 1);
2451
- }
2452
- }
2453
- }
2454
- }
2455
- }
2456
2575
  let mut occurrences: Vec<DuplicateBlockOccurrence> = group
2457
2576
  .iter()
2458
- .map(|occurrence| DuplicateBlockOccurrence {
2459
- start_line: occurrence.start_line,
2460
- end_line: occurrence.end_line,
2577
+ .map(|occurrence| {
2578
+ max_duplicate_block_size = max_duplicate_block_size.max(occurrence.token_count);
2579
+ let lines = collect_matched_lines(occurrence, code_line_numbers, tokens);
2580
+ duplicated_lines.extend(&lines);
2581
+ let mut line_numbers: Vec<usize> = lines.into_iter().collect();
2582
+ line_numbers.sort_unstable();
2583
+ DuplicateBlockOccurrence {
2584
+ start_line: occurrence.start_line,
2585
+ end_line: occurrence.end_line,
2586
+ line_numbers,
2587
+ token_count: occurrence.token_count,
2588
+ }
2461
2589
  })
2462
2590
  .collect();
2463
2591
  occurrences.sort_by_key(|occurrence| occurrence.start_line);