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.
- package/README.md +7 -3
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.d.ts +2 -2
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/crossFileNearMiss.cjs +1 -1
- package/dist/crossFileNearMiss.cjs.map +1 -1
- package/dist/crossFileNearMiss.d.ts +12 -9
- package/dist/crossFileNearMiss.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/dist/duplicateSelection.cjs +1 -1
- package/dist/duplicateSelection.cjs.map +1 -1
- package/dist/duplicateSelection.d.ts +6 -11
- package/dist/duplicateSelection.js +1 -1
- package/dist/duplicateSelection.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.d.ts +10 -4
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/types.d.ts +7 -2
- package/native/code-gauge.wasm +0 -0
- package/native/src/duplication.rs +438 -184
- package/package.json +8 -8
|
@@ -5,7 +5,7 @@ use std::sync::OnceLock;
|
|
|
5
5
|
use tree_sitter::Node;
|
|
6
6
|
|
|
7
7
|
use crate::near_miss::{
|
|
8
|
-
|
|
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;
|
|
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
|
|
1518
|
-
//
|
|
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
|
|
1522
|
-
let
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
{
|
|
1528
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
-
|
|
1559
|
-
|
|
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:
|
|
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 (
|
|
1650
|
-
(result.
|
|
1814
|
+
let (left, right) = if swapped {
|
|
1815
|
+
(result.second, result.first)
|
|
1651
1816
|
} else {
|
|
1652
|
-
(result.
|
|
1817
|
+
(result.first, result.second)
|
|
1653
1818
|
};
|
|
1654
|
-
// A
|
|
1655
|
-
//
|
|
1656
|
-
//
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
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
|
-
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
groups[
|
|
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
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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
|
-
///
|
|
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
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
|
1776
|
-
let
|
|
1777
|
-
if pairs.len() < 2 || (!
|
|
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
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
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
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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
|
-
|
|
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
|
|
1852
|
-
//
|
|
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
|
|
1859
|
-
|
|
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
|
-
//
|
|
1888
|
-
//
|
|
1889
|
-
//
|
|
1890
|
-
let
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2016
|
-
|
|
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
|
|
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
|
|
2113
|
-
//
|
|
2114
|
-
// reports content the cluster does not share
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
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
|
|
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
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
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.
|
|
2178
|
-
|
|
2179
|
-
|
|
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 =
|
|
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)
|
|
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()
|