code-gauge 10.2.0 → 10.2.2

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.
@@ -5,7 +5,7 @@ use std::sync::OnceLock;
5
5
  use tree_sitter::Node;
6
6
 
7
7
  use crate::near_miss::{
8
- Alignment, Block, CorePair, Matcher, PairMatch, FILTRATION_PERCENT, MAX_LENGTH_RATIO,
8
+ Block, CorePair, Matcher, PairMatch, TokenRuns, FILTRATION_PERCENT, MAX_LENGTH_RATIO,
9
9
  };
10
10
  use crate::tree_index::NodeExt;
11
11
  use crate::types::{
@@ -381,6 +381,9 @@ struct DuplicateCandidate {
381
381
  end_index: usize,
382
382
  start_line: usize,
383
383
  end_line: usize,
384
+ /// Set by select_maximal_duplicates on a copy lying inside a larger group's region: it is
385
+ /// reported with its group, but its span is already counted by that larger group.
386
+ nested_in_larger_group: bool,
384
387
  }
385
388
 
386
389
  /// A file's normalized token stream with the block and statement structure clone detection
@@ -1375,6 +1378,7 @@ fn to_candidate(
1375
1378
  end_index: last.end_index,
1376
1379
  start_line: first.start_line,
1377
1380
  end_line: last.end_line,
1381
+ nested_in_larger_group: false,
1378
1382
  }
1379
1383
  }
1380
1384
 
@@ -1474,7 +1478,8 @@ fn combine_hashes(hash: i64, value: i64) -> i64 {
1474
1478
  (to_int32(hash).wrapping_mul(31)) as i64 + value
1475
1479
  }
1476
1480
 
1477
- /// Keeps only maximal, non-overlapping duplicates; see selectMaximalGroups in duplicateSelection.ts.
1481
+ /// Keeps only maximal, non-overlapping duplicates; a port of selectMaximalGroups in
1482
+ /// duplicateSelection.ts, which states the ranking, shedding, and nested-copy rules.
1478
1483
  fn select_maximal_duplicates(
1479
1484
  candidates: Vec<DuplicateCandidate>,
1480
1485
  ) -> IndexMap<std::rc::Rc<str>, Vec<DuplicateCandidate>> {
@@ -1514,49 +1519,191 @@ fn select_maximal_duplicates(
1514
1519
  let mut duplicates: Vec<DuplicateCandidate> = groups.into_iter().flatten().collect();
1515
1520
  duplicates.sort_by_key(|candidate| std::cmp::Reverse(coverage(candidate)));
1516
1521
 
1517
- // Greedy selection can keep a candidate whose group ends up below two survivors; the largest
1518
- // failed group is removed and the selection reruns, one group at a time.
1522
+ // Greedy selection can keep a candidate whose group ends up below two survivors; the failed
1523
+ // groups are removed and the selection reruns: the largest one at a time, then all at once.
1519
1524
  let mut rerun = 0;
1520
1525
  loop {
1521
- let mut kept_regions: Vec<(usize, usize)> = Vec::new();
1522
- let mut counted: IndexMap<std::rc::Rc<str>, Vec<DuplicateCandidate>> = IndexMap::new();
1523
- for candidate in &duplicates {
1524
- if kept_regions
1525
- .iter()
1526
- .any(|region| region.0 < candidate.end_index && candidate.start_index < region.1)
1527
- {
1528
- continue;
1526
+ let (mut counted, blockers) = select_greedily(&duplicates);
1527
+ let failed = |group: &[DuplicateCandidate]| group.len() < 2;
1528
+ // A failed group that another failed group kept a copy from is not shed before it:
1529
+ // shedding the blocking group can rescue it, while shedding it frees nothing the other
1530
+ // waits for.
1531
+ let blocked_by_failed = |fingerprint: &std::rc::Rc<str>| {
1532
+ blockers.get(fingerprint).is_some_and(|blocking| {
1533
+ blocking.iter().any(|other| {
1534
+ other != fingerprint && counted.get(other).is_some_and(|group| failed(group))
1535
+ })
1536
+ })
1537
+ };
1538
+ let largest_failed = counted
1539
+ .iter()
1540
+ .filter(|(_, group)| failed(group))
1541
+ // The first of the largest, as in selectMaximalGroups.
1542
+ .rev()
1543
+ .max_by_key(|(fingerprint, group)| {
1544
+ (
1545
+ !blocked_by_failed(fingerprint),
1546
+ group.first().map_or(0, |first| first.token_count),
1547
+ )
1548
+ })
1549
+ .map(|(fingerprint, _)| fingerprint.clone());
1550
+ // No failed fingerprint means every counted group kept at least two survivors.
1551
+ let Some(largest_failed) = largest_failed else {
1552
+ return counted;
1553
+ };
1554
+ if rerun >= 2 * MAX_SELECTION_RERUN_COUNT {
1555
+ drop_failed_groups(&mut counted);
1556
+ return counted;
1557
+ }
1558
+ if rerun < MAX_SELECTION_RERUN_COUNT {
1559
+ duplicates.retain(|candidate| candidate.fingerprint != largest_failed);
1560
+ } else {
1561
+ duplicates.retain(|candidate| {
1562
+ counted
1563
+ .get(&candidate.fingerprint)
1564
+ .is_none_or(|group| !failed(group))
1565
+ });
1566
+ }
1567
+ rerun += 1;
1568
+ }
1569
+ }
1570
+
1571
+ /// Per fingerprint, the fingerprints of the kept regions that kept a candidate of it from being
1572
+ /// selected.
1573
+ type BlockersByFingerprint = FxHashMap<std::rc::Rc<str>, Vec<std::rc::Rc<str>>>;
1574
+
1575
+ /// One greedy selection in ranking order, in two passes. The first keeps each candidate that
1576
+ /// overlaps no kept region and lets a candidate a kept region contains join its group as a nested
1577
+ /// copy. The second lets a candidate left over take the place of the kept copies it encloses,
1578
+ /// which become nested copies of their groups, unless it partially overlaps a kept region: a
1579
+ /// larger clone must not displace a clone of the first pass it merely overlaps. A candidate
1580
+ /// lying inside a kept region in the end is a nested copy whenever its turn came. Groups left
1581
+ /// without a standalone copy are dropped.
1582
+ fn select_greedily(
1583
+ duplicates: &[DuplicateCandidate],
1584
+ ) -> (
1585
+ IndexMap<std::rc::Rc<str>, Vec<DuplicateCandidate>>,
1586
+ BlockersByFingerprint,
1587
+ ) {
1588
+ let is_region = |candidate: &DuplicateCandidate, region: (usize, usize)| {
1589
+ (candidate.start_index, candidate.end_index) == region
1590
+ };
1591
+ // Mutually non-overlapping, each with the fingerprint of the candidate occupying it.
1592
+ let mut kept_regions: Vec<((usize, usize), std::rc::Rc<str>)> = Vec::new();
1593
+ let mut counted: IndexMap<std::rc::Rc<str>, Vec<DuplicateCandidate>> = IndexMap::new();
1594
+ let mut nested: Vec<DuplicateCandidate> = Vec::new();
1595
+ let mut left_over: Vec<&DuplicateCandidate> = Vec::new();
1596
+ let mut blockers = BlockersByFingerprint::default();
1597
+ for encloses in [false, true] {
1598
+ let candidates: Vec<&DuplicateCandidate> = if encloses {
1599
+ std::mem::take(&mut left_over)
1600
+ } else {
1601
+ duplicates.iter().collect()
1602
+ };
1603
+ 'candidates: for candidate in candidates {
1604
+ let (start, end) = (candidate.start_index, candidate.end_index);
1605
+ let mut enclosed: Vec<usize> = Vec::new();
1606
+ for (index, ((region_start, region_end), blocker)) in kept_regions.iter().enumerate() {
1607
+ let (region_start, region_end) = (*region_start, *region_end);
1608
+ if region_start >= end || start >= region_end {
1609
+ continue;
1610
+ }
1611
+ if region_start <= start && end <= region_end {
1612
+ nested.push(DuplicateCandidate {
1613
+ nested_in_larger_group: true,
1614
+ ..candidate.clone()
1615
+ });
1616
+ continue 'candidates;
1617
+ }
1618
+ if encloses && start <= region_start && region_end <= end {
1619
+ enclosed.push(index);
1620
+ } else {
1621
+ left_over.push(candidate);
1622
+ if encloses {
1623
+ blockers
1624
+ .entry(candidate.fingerprint.clone())
1625
+ .or_default()
1626
+ .push(blocker.clone());
1627
+ }
1628
+ continue 'candidates;
1629
+ }
1529
1630
  }
1530
- kept_regions.push((candidate.start_index, candidate.end_index));
1631
+ // Descending, so that removing one keeps the indexes of the others.
1632
+ for &index in enclosed.iter().rev() {
1633
+ let (region, fingerprint) = kept_regions.remove(index);
1634
+ if let Some(inner) = counted
1635
+ .get_mut(&fingerprint)
1636
+ .and_then(|group| group.iter_mut().find(|inner| is_region(inner, region)))
1637
+ {
1638
+ inner.nested_in_larger_group = true;
1639
+ }
1640
+ }
1641
+ kept_regions.push(((start, end), candidate.fingerprint.clone()));
1531
1642
  counted
1532
1643
  .entry(candidate.fingerprint.clone())
1533
1644
  .or_default()
1534
1645
  .push(candidate.clone());
1535
1646
  }
1536
-
1537
- let mut failed_fingerprint: Option<std::rc::Rc<str>> = None;
1538
- let mut failed_token_count: i64 = -1;
1539
- for (fingerprint, group) in &counted {
1540
- let token_count = group
1541
- .first()
1542
- .map(|first| first.token_count as i64)
1543
- .unwrap_or(0);
1544
- if group.len() < 2 && token_count > failed_token_count {
1545
- failed_fingerprint = Some(fingerprint.clone());
1546
- failed_token_count = token_count;
1547
- }
1548
- }
1549
- // No failed fingerprint means every counted group kept at least two survivors.
1550
- let Some(failed_fingerprint) = failed_fingerprint else {
1551
- return counted;
1647
+ }
1648
+ // A candidate still left over overlapped a kept region partially when its turn came. Where
1649
+ // an enclosing candidate took that region since, the candidate lies inside a kept region now
1650
+ // and is a nested copy like one visited later.
1651
+ for candidate in left_over {
1652
+ let lies_in = |&((start, end), _): &((usize, usize), std::rc::Rc<str>)| {
1653
+ start <= candidate.start_index && candidate.end_index <= end
1552
1654
  };
1553
- if rerun >= MAX_SELECTION_RERUN_COUNT {
1554
- counted.retain(|_, group| group.len() >= 2);
1555
- return counted;
1655
+ if kept_regions.iter().any(lies_in) {
1656
+ nested.push(DuplicateCandidate {
1657
+ nested_in_larger_group: true,
1658
+ ..candidate.clone()
1659
+ });
1660
+ }
1661
+ }
1662
+ for candidate in nested {
1663
+ if let Some(group) = counted.get_mut(&candidate.fingerprint) {
1664
+ group.push(candidate);
1556
1665
  }
1666
+ }
1667
+ counted.retain(|_, group| {
1668
+ group
1669
+ .iter()
1670
+ .any(|candidate| !candidate.nested_in_larger_group)
1671
+ });
1672
+ (counted, blockers)
1673
+ }
1557
1674
 
1558
- duplicates.retain(|candidate| candidate.fingerprint != failed_fingerprint);
1559
- rerun += 1;
1675
+ /// Past the rerun caps: drops the groups below two copies without another selection pass, and
1676
+ /// with them the nested copies left inside no standalone copy, until nothing changes; see
1677
+ /// dropFailedGroups in duplicateSelection.ts.
1678
+ fn drop_failed_groups(counted: &mut IndexMap<std::rc::Rc<str>, Vec<DuplicateCandidate>>) {
1679
+ loop {
1680
+ let group_count = counted.len();
1681
+ counted.retain(|_, group| {
1682
+ group.len() >= 2
1683
+ && group
1684
+ .iter()
1685
+ .any(|candidate| !candidate.nested_in_larger_group)
1686
+ });
1687
+ let standalone: Vec<(usize, usize)> = counted
1688
+ .values()
1689
+ .flatten()
1690
+ .filter(|candidate| !candidate.nested_in_larger_group)
1691
+ .map(|candidate| (candidate.start_index, candidate.end_index))
1692
+ .collect();
1693
+ let mut changed = counted.len() != group_count;
1694
+ for group in counted.values_mut() {
1695
+ let copy_count = group.len();
1696
+ group.retain(|candidate| {
1697
+ !candidate.nested_in_larger_group
1698
+ || standalone.iter().any(|&(start, end)| {
1699
+ start <= candidate.start_index && candidate.end_index <= end
1700
+ })
1701
+ });
1702
+ changed |= group.len() != copy_count;
1703
+ }
1704
+ if !changed {
1705
+ return;
1706
+ }
1560
1707
  }
1561
1708
  }
1562
1709
 
@@ -1583,9 +1730,15 @@ struct CountedOccurrence {
1583
1730
  /// again: a retained group's occurrence that a partial gapped merge also paired into a merged
1584
1731
  /// group, and a near-miss anchor, which overlaps the reported occurrence it was matched through.
1585
1732
  shared_with_merged_group: bool,
1733
+ /// Set on a copy nested inside a larger group's region (it also sets
1734
+ /// `shared_with_merged_group`); see `nestedInLargerGroup` in duplication.ts.
1735
+ nested_in_larger_group: bool,
1586
1736
  /// The token runs of a near-miss copy that its partners match, within its segments;
1587
1737
  /// `None` for an exact or gapped copy, whose segments are matched throughout.
1588
1738
  matched_runs: Option<Vec<(usize, usize)>>,
1739
+ /// The exact fragments coalesced into a near-miss copy: each line they reach is duplicated,
1740
+ /// whatever else of the copy shares the line.
1741
+ exact_runs: Vec<(usize, usize)>,
1589
1742
  /// Sum of segment token counts (the gap tokens are not matched content).
1590
1743
  token_count: usize,
1591
1744
  start_token_index: usize,
@@ -1594,6 +1747,16 @@ struct CountedOccurrence {
1594
1747
  end_line: usize,
1595
1748
  }
1596
1749
 
1750
+ impl CountedOccurrence {
1751
+ /// Whether a matched segment overlaps the token range: the gap of a gapped clone is not
1752
+ /// matched content.
1753
+ fn matches_within(&self, (start, end): (usize, usize)) -> bool {
1754
+ self.segments
1755
+ .iter()
1756
+ .any(|&(segment_start, segment_end)| segment_start < end && start < segment_end)
1757
+ }
1758
+ }
1759
+
1597
1760
  fn to_counted_groups(
1598
1761
  counted: &IndexMap<std::rc::Rc<str>, Vec<DuplicateCandidate>>,
1599
1762
  ) -> Vec<Vec<CountedOccurrence>> {
@@ -1602,8 +1765,10 @@ fn to_counted_groups(
1602
1765
  let mut occurrences: Vec<CountedOccurrence> = group
1603
1766
  .iter()
1604
1767
  .map(|candidate| CountedOccurrence {
1605
- shared_with_merged_group: false,
1768
+ shared_with_merged_group: candidate.nested_in_larger_group,
1769
+ nested_in_larger_group: candidate.nested_in_larger_group,
1606
1770
  matched_runs: None,
1771
+ exact_runs: Vec::new(),
1607
1772
  segments: vec![(candidate.start_token_index, candidate.end_token_index)],
1608
1773
  token_count: candidate.token_count,
1609
1774
  start_token_index: candidate.start_token_index,
@@ -1622,7 +1787,7 @@ fn to_counted_groups(
1622
1787
  /// Merges duplicate groups separated by a small token gap into one gapped (Type-3) clone group;
1623
1788
  /// see mergeAdjacentGroups in duplication.ts for the pairing, partial-merge (unequal
1624
1789
  /// cardinalities: the fully-paired group is subsumed, the other is retained with all its
1625
- /// occurrences), and fixpoint/termination rules replicated here.
1790
+ /// occurrences), nested-copy, and fixpoint/termination rules replicated here.
1626
1791
  fn merge_adjacent_groups(
1627
1792
  mut groups: Vec<Vec<CountedOccurrence>>,
1628
1793
  max_gap_tokens: usize,
@@ -1646,27 +1811,29 @@ fn merge_adjacent_groups(
1646
1811
  let Some(result) = result else {
1647
1812
  continue;
1648
1813
  };
1649
- let (left_consumed, right_consumed) = if swapped {
1650
- (result.second_consumed, result.first_consumed)
1814
+ let (left, right) = if swapped {
1815
+ (result.second, result.first)
1651
1816
  } else {
1652
- (result.first_consumed, result.second_consumed)
1817
+ (result.first, result.second)
1653
1818
  };
1654
- // A partial merge retains the not-fully-consumed group with ALL its occurrences,
1655
- // so its paired occurrences now also live inside the merged group's occurrences:
1656
- // mark them so duplicate_block_count counts each token span once.
1657
- if left_consumed && right_consumed {
1658
- groups[left_index] = result.merged;
1659
- groups.remove(right_index);
1660
- } else if right_consumed {
1661
- groups[right_index] = result.merged;
1662
- for &occurrence_index in &result.paired_retained_indexes {
1663
- groups[left_index][occurrence_index].shared_with_merged_group = true;
1819
+ // A group that stays keeps ALL its occurrences, so its paired occurrences now
1820
+ // also live inside the merged group's occurrences: mark them so
1821
+ // duplicate_block_count counts each token span once.
1822
+ for (group_index, side) in [(left_index, &left), (right_index, &right)] {
1823
+ if !side.replaced {
1824
+ for &occurrence_index in &side.paired_indexes {
1825
+ groups[group_index][occurrence_index].shared_with_merged_group = true;
1826
+ }
1664
1827
  }
1665
- } else {
1666
- groups[left_index] = result.merged;
1667
- for &occurrence_index in &result.paired_retained_indexes {
1668
- groups[right_index][occurrence_index].shared_with_merged_group = true;
1828
+ }
1829
+ match (left.replaced, right.replaced) {
1830
+ (true, true) => {
1831
+ groups[left_index] = result.merged;
1832
+ groups.remove(right_index);
1669
1833
  }
1834
+ (false, true) => groups[right_index] = result.merged,
1835
+ (true, false) => groups[left_index] = result.merged,
1836
+ (false, false) => groups.push(result.merged),
1670
1837
  }
1671
1838
  groups.sort_by_key(|group| group_sort_key(group));
1672
1839
  restart = true;
@@ -1723,16 +1890,21 @@ fn group_sort_key(group: &[CountedOccurrence]) -> (usize, usize) {
1723
1890
 
1724
1891
  struct MergeResult {
1725
1892
  merged: Vec<CountedOccurrence>,
1726
- /// Whether every occurrence of the respective input group was paired into the merge.
1727
- first_consumed: bool,
1728
- second_consumed: bool,
1729
- /// Indexes (into the retained, not fully consumed group) of the occurrences that were paired.
1730
- paired_retained_indexes: Vec<usize>,
1893
+ first: MergedSide,
1894
+ second: MergedSide,
1895
+ }
1896
+
1897
+ struct MergedSide {
1898
+ /// Whether the merged group takes the input group's place: its standalone occurrences were
1899
+ /// all paired and it holds no nested copies that only it can report.
1900
+ replaced: bool,
1901
+ /// Indexes into the input group of the occurrences that were paired.
1902
+ paired_indexes: Vec<usize>,
1731
1903
  }
1732
1904
 
1733
1905
  /// Pairs `second` occurrences with gap-preceding `first` occurrences, greedily in source order;
1734
1906
  /// a faithful port of mergeGroups in duplication.ts (at least two pairs, at least one group fully
1735
- /// consumed, merged spans never overlap).
1907
+ /// paired, merged spans never overlap).
1736
1908
  fn merge_groups(
1737
1909
  first: &[CountedOccurrence],
1738
1910
  second: &[CountedOccurrence],
@@ -1742,13 +1914,21 @@ fn merge_groups(
1742
1914
  // again: their spans already live inside that merged group, so re-pairing them would assemble
1743
1915
  // a second, competing merged group instead of letting the existing merged group extend (and
1744
1916
  // would count the same span twice). Consumption is still judged against the FULL group, so a
1745
- // group holding shared occurrences is never subsumed away.
1746
- let leadings: Vec<usize> = (0..first.len())
1747
- .filter(|&index| !first[index].shared_with_merged_group)
1748
- .collect();
1749
- let trailings: Vec<usize> = (0..second.len())
1750
- .filter(|&index| !second[index].shared_with_merged_group)
1751
- .collect();
1917
+ // group holding shared occurrences is never subsumed away; nested copies, which the merged
1918
+ // span would not cover anyway, are left out of that judgment so they cannot veto a merge of
1919
+ // the standalone copies.
1920
+ let pairable = |group: &[CountedOccurrence]| -> Vec<usize> {
1921
+ (0..group.len())
1922
+ .filter(|&index| !group[index].shared_with_merged_group)
1923
+ .collect()
1924
+ };
1925
+ let standalone_count = |group: &[CountedOccurrence]| {
1926
+ group
1927
+ .iter()
1928
+ .filter(|occurrence| !occurrence.nested_in_larger_group)
1929
+ .count()
1930
+ };
1931
+ let (leadings, trailings) = (pairable(first), pairable(second));
1752
1932
  let mut pairs: Vec<(usize, usize)> = Vec::new();
1753
1933
  let mut leading_position = 0usize;
1754
1934
  let mut previous_trailing_end: Option<usize> = None;
@@ -1772,17 +1952,18 @@ fn merge_groups(
1772
1952
  }
1773
1953
  }
1774
1954
  }
1775
- let first_consumed = pairs.len() == first.len();
1776
- let second_consumed = pairs.len() == second.len();
1777
- if pairs.len() < 2 || (!first_consumed && !second_consumed) {
1955
+ let first_fully_paired = pairs.len() == standalone_count(first);
1956
+ let second_fully_paired = pairs.len() == standalone_count(second);
1957
+ if pairs.len() < 2 || (!first_fully_paired && !second_fully_paired) {
1778
1958
  return None;
1779
1959
  }
1780
- let paired_retained_indexes: Vec<usize> = if first_consumed == second_consumed {
1781
- Vec::new()
1782
- } else if first_consumed {
1783
- pairs.iter().map(|&(_, trailing)| trailing).collect()
1784
- } else {
1785
- pairs.iter().map(|&(leading, _)| leading).collect()
1960
+ // A group holding nested copies stays even when all its standalone copies pair: the merged
1961
+ // group's content is larger than what those copies matched, so only the original group can
1962
+ // report the nested copies.
1963
+ let holds_no_nested_copy = |group: &[CountedOccurrence]| {
1964
+ group
1965
+ .iter()
1966
+ .all(|occurrence| !occurrence.nested_in_larger_group)
1786
1967
  };
1787
1968
  let merged = pairs
1788
1969
  .iter()
@@ -1792,7 +1973,9 @@ fn merge_groups(
1792
1973
  CountedOccurrence {
1793
1974
  // A merged occurrence is a fresh span combination; it inherits no shared marks.
1794
1975
  shared_with_merged_group: false,
1976
+ nested_in_larger_group: false,
1795
1977
  matched_runs: None,
1978
+ exact_runs: Vec::new(),
1796
1979
  segments: [leading.segments.clone(), trailing.segments.clone()].concat(),
1797
1980
  token_count: leading.token_count + trailing.token_count,
1798
1981
  start_token_index: leading.start_token_index,
@@ -1804,9 +1987,14 @@ fn merge_groups(
1804
1987
  .collect();
1805
1988
  Some(MergeResult {
1806
1989
  merged,
1807
- first_consumed,
1808
- second_consumed,
1809
- paired_retained_indexes,
1990
+ first: MergedSide {
1991
+ replaced: first_fully_paired && holds_no_nested_copy(first),
1992
+ paired_indexes: pairs.iter().map(|&(leading, _)| leading).collect(),
1993
+ },
1994
+ second: MergedSide {
1995
+ replaced: second_fully_paired && holds_no_nested_copy(second),
1996
+ paired_indexes: pairs.iter().map(|&(_, trailing)| trailing).collect(),
1997
+ },
1810
1998
  })
1811
1999
  }
1812
2000
 
@@ -1815,7 +2003,8 @@ struct MatchEdge {
1815
2003
  left: usize,
1816
2004
  right: usize,
1817
2005
  cores: Option<CorePair>,
1818
- alignment: Alignment,
2006
+ /// The token runs the pair matches, on the left and on the right.
2007
+ matched_runs: (TokenRuns, TokenRuns),
1819
2008
  }
1820
2009
 
1821
2010
  impl MatchEdge {
@@ -1833,7 +2022,8 @@ impl MatchEdge {
1833
2022
  /// merged), each clustered with its own partners, so code no verified pair matched never counts
1834
2023
  /// as duplicated. A node overlapping a reported occurrence is an anchor: it joins its group at its
1835
2024
  /// 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.
2025
+ /// counted elsewhere unless the group takes over the reported occurrences it overlaps. An anchor
2026
+ /// still counts the lines its partners match outside those occurrences.
1837
2027
  fn collect_near_miss_groups(
1838
2028
  source: &TokenizedSource<'_>,
1839
2029
  settings: &DuplicationSettings,
@@ -1848,16 +2038,17 @@ fn collect_near_miss_groups(
1848
2038
  return Vec::new();
1849
2039
  }
1850
2040
 
1851
- // Reported-group indices whose occurrences overlap a token range: near-miss nodes covering
1852
- // such content anchor comparisons but are never re-reported.
2041
+ // Reported-group indices whose occurrences' matched segments overlap a token range (the gap
2042
+ // of a gapped clone is not reported content): near-miss nodes covering such content anchor
2043
+ // comparisons but are never re-reported.
1853
2044
  let touched_groups_in = |start: usize, end: usize| -> Vec<usize> {
1854
2045
  reported_groups
1855
2046
  .iter()
1856
2047
  .enumerate()
1857
2048
  .filter(|(_, group)| {
1858
- group.iter().any(|occurrence| {
1859
- occurrence.start_token_index < end && start < occurrence.end_token_index
1860
- })
2049
+ group
2050
+ .iter()
2051
+ .any(|occurrence| occurrence.matches_within((start, end)))
1861
2052
  })
1862
2053
  .map(|(group_index, _)| group_index)
1863
2054
  .collect()
@@ -1884,41 +2075,80 @@ fn collect_near_miss_groups(
1884
2075
  settings.min_similarity_percent,
1885
2076
  );
1886
2077
 
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();
1891
- let block_touched: Vec<bool> = comparable
1892
- .iter()
1893
- .map(|range| is_touched((range.start_token_index, range.end_token_index)))
1894
- .collect();
2078
+ // The lines of a near-miss copy that are duplicated are those a verified partner matches,
2079
+ // which takes the pairing itself. Every pair kept is aligned: verifying it already cost a
2080
+ // longest common subsequence of the same sequences, and any choice among a copy's partners
2081
+ // would let their number or their order decide what the copy counts.
2082
+ //
2083
+ // A pair matching nothing outside listed content has nothing to add to it and is dropped
2084
+ // before clustering, so that it neither collapses the blocks' core nodes nor widens a node
2085
+ // beyond what the pairs clustering it matched. Any other pair is kept however much of it is
2086
+ // reported: what the exact pipeline reports of a copy depends on where its copies stand, and
2087
+ // the lines the copy counts must not.
2088
+ let reported_prefix_of = |groups: &mut dyn Iterator<Item = &Vec<CountedOccurrence>>| {
2089
+ let mut reported = vec![false; tokens.len()];
2090
+ for occurrence in groups.flatten() {
2091
+ for &(start, end) in &occurrence.segments {
2092
+ reported[start..end].fill(true);
2093
+ }
2094
+ }
2095
+ let mut prefix = vec![0usize; tokens.len() + 1];
2096
+ for (index, &flag) in reported.iter().enumerate() {
2097
+ prefix[index + 1] = prefix[index] + usize::from(flag);
2098
+ }
2099
+ prefix
2100
+ };
2101
+ let reported_prefix = reported_prefix_of(&mut reported_groups.iter());
2102
+ let reported_count =
2103
+ |(start, end): (usize, usize)| reported_prefix[end] - reported_prefix[start];
2104
+ // Listed content is the reported content without the groups holding nested copies. Such a
2105
+ // group lists a part of larger copies with a copy standing elsewhere, which makes no pair
2106
+ // redundant: the blocks around its copies may be copies as wholes, which only their pair can
2107
+ // list at that size, taking the group over.
2108
+ let listed_prefix = reported_prefix_of(&mut reported_groups.iter().filter(|group| {
2109
+ group
2110
+ .iter()
2111
+ .all(|occurrence| !occurrence.nested_in_larger_group)
2112
+ }));
2113
+ let unreported_count = |runs: &[(usize, usize)]| -> usize {
2114
+ runs.iter()
2115
+ .map(|&(start, end)| end - start - (listed_prefix[end] - listed_prefix[start]))
2116
+ .sum()
2117
+ };
2118
+ let block_span = |index: usize| {
2119
+ (
2120
+ comparable[index].start_token_index,
2121
+ comparable[index].end_token_index,
2122
+ )
2123
+ };
1895
2124
  let mut edges: Vec<MatchEdge> = Vec::new();
2125
+ let mut add_edge = |left: usize, right: usize, cores: Option<CorePair>, alignment| {
2126
+ let (left_span, right_span) = cores.unwrap_or((block_span(left), block_span(right)));
2127
+ // Fully listed spans need no alignment to be dropped.
2128
+ if unreported_count(&[left_span, right_span]) == 0 {
2129
+ return;
2130
+ }
2131
+ let matched_runs =
2132
+ matcher.align(&blocks[left], &blocks[right], cores, alignment, &token_keys);
2133
+ if unreported_count(&matched_runs.0) + unreported_count(&matched_runs.1) == 0 {
2134
+ return;
2135
+ }
2136
+ edges.push(MatchEdge {
2137
+ left,
2138
+ right,
2139
+ cores,
2140
+ matched_runs,
2141
+ });
2142
+ };
1896
2143
  for_each_candidate_pair(
1897
2144
  &blocks,
1898
2145
  settings.min_similarity_percent,
1899
2146
  |left_index, right_index| match matcher.verify(&blocks[left_index], &blocks[right_index]) {
1900
2147
  None => {}
1901
- Some(PairMatch::Whole(alignment))
1902
- if !(block_touched[left_index] && block_touched[right_index]) =>
1903
- {
1904
- edges.push(MatchEdge {
1905
- left: left_index,
1906
- right: right_index,
1907
- cores: None,
1908
- alignment,
1909
- })
1910
- }
1911
- Some(PairMatch::Whole(_)) => {}
2148
+ Some(PairMatch::Whole(alignment)) => add_edge(left_index, right_index, None, alignment),
1912
2149
  Some(PairMatch::Local(cores)) => {
1913
2150
  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
- }
2151
+ add_edge(left_index, right_index, Some(core_pair), alignment);
1922
2152
  }
1923
2153
  }
1924
2154
  },
@@ -2006,21 +2236,10 @@ fn collect_near_miss_groups(
2006
2236
  parent[left_root.max(right_root)] = left_root.min(right_root);
2007
2237
  }
2008
2238
 
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
2239
  let mut matched_runs_by_node: Vec<Vec<(usize, usize)>> = vec![Vec::new(); node_blocks.len()];
2014
2240
  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);
2241
+ matched_runs_by_node[nodes[0]].extend(&edge.matched_runs.0);
2242
+ matched_runs_by_node[nodes[1]].extend(&edge.matched_runs.1);
2024
2243
  }
2025
2244
 
2026
2245
  let mut members_by_root: IndexMap<usize, Vec<usize>> = IndexMap::new();
@@ -2057,7 +2276,9 @@ fn collect_near_miss_groups(
2057
2276
  .collect();
2058
2277
  CountedOccurrence {
2059
2278
  shared_with_merged_group: false,
2279
+ nested_in_larger_group: false,
2060
2280
  matched_runs: Some(merge_overlapping_cores(&matched_runs)),
2281
+ exact_runs: Vec::new(),
2061
2282
  token_count: segments.iter().map(|segment| segment.1 - segment.0).sum(),
2062
2283
  segments,
2063
2284
  start_token_index: start,
@@ -2106,17 +2327,42 @@ fn collect_near_miss_groups(
2106
2327
  }
2107
2328
  continue;
2108
2329
  }
2109
- if uncovered.is_empty() {
2330
+ if members.iter().all(|&node| {
2331
+ let (start, end) = node_range(node);
2332
+ reported_count((start, end)) == end - start
2333
+ }) {
2110
2334
  continue;
2111
2335
  }
2112
- // An anchored cluster extends a reported group only when every occurrence of that group
2113
- // overlaps one of the cluster's member nodes: an occurrence disjoint from all members
2114
- // reports content the cluster does not share.
2115
- let overlaps_member = |occurrence: &CountedOccurrence| {
2116
- members.iter().any(|&index| {
2117
- let (start, end) = node_range(index);
2118
- occurrence.start_token_index < end && start < occurrence.end_token_index
2119
- })
2336
+ // An anchored cluster extends a reported group only when it stands for the same copies:
2337
+ // every occurrence of the group overlaps the member nodes of exactly one block (one
2338
+ // disjoint from all members reports content the cluster does not share, and one reaching
2339
+ // into two members would make one copy take over lines of the next), exceeds the span of
2340
+ // those nodes by less than `min_tokens` (a copy is listed at the size it matched, not at
2341
+ // that of a clone larger by a reportable part, which may run into code no member
2342
+ // matched), and shares that block with no other occurrence of the group (those repeat
2343
+ // within the copy, not between the copies).
2344
+ let member_block_of = |occurrence: &CountedOccurrence| {
2345
+ let mut blocks = members
2346
+ .iter()
2347
+ .filter(|&&index| occurrence.matches_within(node_range(index)))
2348
+ .map(|&index| node_blocks[index]);
2349
+ let block = blocks.next()?;
2350
+ let mut spans = members
2351
+ .iter()
2352
+ .filter(|&&index| node_blocks[index] == block)
2353
+ .map(|&index| node_range(index));
2354
+ let first = spans.next()?;
2355
+ let span = spans.fold(first, |span, node| (span.0.min(node.0), span.1.max(node.1)));
2356
+ let excess = span.0.saturating_sub(occurrence.start_token_index)
2357
+ + occurrence.end_token_index.saturating_sub(span.1);
2358
+ (blocks.all(|other| other == block) && excess < settings.min_tokens).then_some(block)
2359
+ };
2360
+ let stands_for_members = |group: &[CountedOccurrence]| {
2361
+ let mut blocks: FxHashSet<usize> = FxHashSet::default();
2362
+ !group.is_empty()
2363
+ && group.iter().all(|occurrence| {
2364
+ member_block_of(occurrence).is_some_and(|block| blocks.insert(block))
2365
+ })
2120
2366
  };
2121
2367
  // Ascending by construction: BTreeSet iteration is sorted and filter preserves order.
2122
2368
  let fully_clustered: Vec<usize> = covered
@@ -2124,16 +2370,12 @@ fn collect_near_miss_groups(
2124
2370
  .flat_map(|&index| touched_groups_of(index).iter().copied())
2125
2371
  .collect::<std::collections::BTreeSet<usize>>()
2126
2372
  .into_iter()
2127
- .filter(|&group_index| {
2128
- let group = &reported_groups[group_index];
2129
- !group.is_empty() && group.iter().all(&overlaps_member)
2130
- })
2373
+ .filter(|&group_index| stands_for_members(&reported_groups[group_index]))
2131
2374
  .collect();
2132
2375
  if let Some((&target_index, source_indexes)) = fully_clustered.split_first() {
2133
2376
  // Rebuild the component as ONE group with one coalesced occurrence per member block:
2134
- // the fragments every node of a block overlaps are collected together, since a block's
2377
+ // the fragments the nodes of a block overlap are collected together, since a block's
2135
2378
  // cores are parts of one copy.
2136
- let mut consumed: FxHashSet<(usize, usize)> = FxHashSet::default();
2137
2379
  let mut merged: Vec<CountedOccurrence> = Vec::new();
2138
2380
  let mut unanchored_nodes: Vec<usize> = Vec::new();
2139
2381
  let mut anchor_nodes: Vec<usize> = Vec::new();
@@ -2145,26 +2387,17 @@ fn collect_near_miss_groups(
2145
2387
  .push(member_index);
2146
2388
  }
2147
2389
  for block_nodes in nodes_by_block.values() {
2148
- // Occurrences of ONE group are distinct copies; only fragments from DIFFERENT
2149
- // groups belong to the same copy. Consecutive position-order slices keep the
2150
- // coalesced spans disjoint.
2151
- let mut fragments: Vec<(CountedOccurrence, usize)> = Vec::new();
2152
- for &node in block_nodes {
2153
- let (range_start, range_end) = node_range(node);
2154
- for &group_index in &fully_clustered {
2155
- for (occurrence_index, occurrence) in
2156
- reported_groups[group_index].iter().enumerate()
2157
- {
2158
- if !consumed.contains(&(group_index, occurrence_index))
2159
- && occurrence.start_token_index < range_end
2160
- && range_start < occurrence.end_token_index
2161
- {
2162
- consumed.insert((group_index, occurrence_index));
2163
- fragments.push((occurrence.clone(), group_index));
2164
- }
2165
- }
2166
- }
2167
- }
2390
+ let overlaps_block_node = |occurrence: &&CountedOccurrence| {
2391
+ block_nodes
2392
+ .iter()
2393
+ .any(|&node| occurrence.matches_within(node_range(node)))
2394
+ };
2395
+ let mut fragments: Vec<CountedOccurrence> = fully_clustered
2396
+ .iter()
2397
+ .flat_map(|&group_index| &reported_groups[group_index])
2398
+ .filter(overlaps_block_node)
2399
+ .cloned()
2400
+ .collect();
2168
2401
  if fragments.is_empty() {
2169
2402
  let (anchors, plain): (Vec<usize>, Vec<usize>) = block_nodes
2170
2403
  .iter()
@@ -2174,33 +2407,31 @@ fn collect_near_miss_groups(
2174
2407
  continue;
2175
2408
  }
2176
2409
  // 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));
2181
- }
2182
- fragments.sort_by_key(|(occurrence, _)| {
2183
- (occurrence.start_token_index, occurrence.end_token_index)
2184
- });
2185
- let mut copy_parts: Vec<CountedOccurrence> = Vec::new();
2186
- let mut copy_groups: FxHashSet<usize> = FxHashSet::default();
2187
- for (occurrence, group_index) in fragments {
2188
- if copy_groups.contains(&group_index) {
2189
- merged.push(coalesce_occurrences(std::mem::take(&mut copy_parts)));
2190
- copy_groups.clear();
2191
- }
2192
- copy_parts.push(occurrence);
2193
- copy_groups.insert(group_index);
2194
- }
2195
- if !copy_parts.is_empty() {
2196
- merged.push(coalesce_occurrences(copy_parts));
2197
- }
2410
+ // beyond the fragments it holds is a copy at that size.
2411
+ fragments.extend(to_occurrences(block_nodes));
2412
+ merged.push(coalesce_occurrences(fragments));
2198
2413
  }
2199
2414
  merged.extend(to_occurrences(&unanchored_nodes));
2200
2415
  // The rebuild consumed every fully-clustered group, so shared-span marks from earlier
2201
- // partial merges no longer point at a separate merged group.
2416
+ // partial merges no longer point at a separate merged group. A copy counts the
2417
+ // fragments it took over itself, unless a group left in place still counts all of it:
2418
+ // a larger clone whose matched segments enclose those of the copy, which its
2419
+ // fragments were nested in.
2202
2420
  for occurrence in &mut merged {
2203
- occurrence.shared_with_merged_group = false;
2421
+ occurrence.shared_with_merged_group = reported_groups
2422
+ .iter()
2423
+ .enumerate()
2424
+ .filter(|(group_index, _)| !fully_clustered.contains(group_index))
2425
+ .flat_map(|(_, group)| group)
2426
+ .any(|counting| {
2427
+ !counting.shared_with_merged_group
2428
+ && occurrence.segments.iter().all(|segment| {
2429
+ counting
2430
+ .segments
2431
+ .iter()
2432
+ .any(|matched| matched.0 <= segment.0 && segment.1 <= matched.1)
2433
+ })
2434
+ });
2204
2435
  }
2205
2436
  merged.extend(to_anchor_occurrences(&anchor_nodes));
2206
2437
  merged.sort_by_key(|occurrence| {
@@ -2349,8 +2580,23 @@ fn coalesce_occurrences(occurrences: Vec<CountedOccurrence>) -> CountedOccurrenc
2349
2580
  .collect();
2350
2581
  merge_overlapping_cores(&runs)
2351
2582
  });
2583
+ let exact_runs: Vec<(usize, usize)> = occurrences
2584
+ .iter()
2585
+ .flat_map(|occurrence| match occurrence.matched_runs {
2586
+ Some(_) => &occurrence.exact_runs,
2587
+ None => &occurrence.segments,
2588
+ })
2589
+ .copied()
2590
+ .collect();
2352
2591
  CountedOccurrence {
2353
2592
  shared_with_merged_group: false,
2593
+ nested_in_larger_group: false,
2594
+ // Without a near-miss part, the segments are matched throughout and say it all.
2595
+ exact_runs: if matched_runs.is_some() {
2596
+ merge_overlapping_cores(&exact_runs)
2597
+ } else {
2598
+ Vec::new()
2599
+ },
2354
2600
  matched_runs,
2355
2601
  token_count: segments.iter().map(|segment| segment.1 - segment.0).sum(),
2356
2602
  start_token_index: occurrences
@@ -2532,6 +2778,7 @@ fn count_redundant_fragments(group: &[CountedOccurrence]) -> usize {
2532
2778
  /// of line coverage. A line of a near-miss copy counts when its partners match more than half of
2533
2779
  /// its tokens: a line the copy adds or rewrites is not duplicated, while a longest common
2534
2780
  /// subsequence still pairs a stray token of it (a bracket, a separator) with one of the partner.
2781
+ /// A line an exact fragment of the copy reaches counts in any case.
2535
2782
  fn collect_matched_lines(
2536
2783
  occurrence: &CountedOccurrence,
2537
2784
  code_line_numbers: &FxHashSet<usize>,
@@ -2552,10 +2799,17 @@ fn collect_matched_lines(
2552
2799
  }
2553
2800
  }
2554
2801
  }
2802
+ let exact_rows: FxHashSet<usize> = occurrence
2803
+ .exact_runs
2804
+ .iter()
2805
+ .flat_map(|&(start, end)| &tokens[start..end])
2806
+ .flat_map(|token| token.start_row + 1..=token.end_row + 1)
2807
+ .collect();
2555
2808
  token_counts_by_row
2556
2809
  .into_iter()
2557
2810
  .filter(|(line, (matched_count, token_count))| {
2558
- code_line_numbers.contains(line) && matched_count * 2 > *token_count
2811
+ code_line_numbers.contains(line)
2812
+ && (matched_count * 2 > *token_count || exact_rows.contains(line))
2559
2813
  })
2560
2814
  .map(|(line, _)| line)
2561
2815
  .collect()