code-gauge 10.2.1 → 10.2.3

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.
@@ -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
+ }
1630
+ }
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
+ }
1529
1640
  }
1530
- kept_regions.push((candidate.start_index, candidate.end_index));
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,6 +1730,9 @@ 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)>>,
@@ -1615,7 +1765,8 @@ fn to_counted_groups(
1615
1765
  let mut occurrences: Vec<CountedOccurrence> = group
1616
1766
  .iter()
1617
1767
  .map(|candidate| CountedOccurrence {
1618
- 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,
1619
1770
  matched_runs: None,
1620
1771
  exact_runs: Vec::new(),
1621
1772
  segments: vec![(candidate.start_token_index, candidate.end_token_index)],
@@ -1636,7 +1787,7 @@ fn to_counted_groups(
1636
1787
  /// Merges duplicate groups separated by a small token gap into one gapped (Type-3) clone group;
1637
1788
  /// see mergeAdjacentGroups in duplication.ts for the pairing, partial-merge (unequal
1638
1789
  /// cardinalities: the fully-paired group is subsumed, the other is retained with all its
1639
- /// occurrences), and fixpoint/termination rules replicated here.
1790
+ /// occurrences), nested-copy, and fixpoint/termination rules replicated here.
1640
1791
  fn merge_adjacent_groups(
1641
1792
  mut groups: Vec<Vec<CountedOccurrence>>,
1642
1793
  max_gap_tokens: usize,
@@ -1660,27 +1811,29 @@ fn merge_adjacent_groups(
1660
1811
  let Some(result) = result else {
1661
1812
  continue;
1662
1813
  };
1663
- let (left_consumed, right_consumed) = if swapped {
1664
- (result.second_consumed, result.first_consumed)
1814
+ let (left, right) = if swapped {
1815
+ (result.second, result.first)
1665
1816
  } else {
1666
- (result.first_consumed, result.second_consumed)
1817
+ (result.first, result.second)
1667
1818
  };
1668
- // A partial merge retains the not-fully-consumed group with ALL its occurrences,
1669
- // so its paired occurrences now also live inside the merged group's occurrences:
1670
- // mark them so duplicate_block_count counts each token span once.
1671
- if left_consumed && right_consumed {
1672
- groups[left_index] = result.merged;
1673
- groups.remove(right_index);
1674
- } else if right_consumed {
1675
- groups[right_index] = result.merged;
1676
- for &occurrence_index in &result.paired_retained_indexes {
1677
- 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
+ }
1678
1827
  }
1679
- } else {
1680
- groups[left_index] = result.merged;
1681
- for &occurrence_index in &result.paired_retained_indexes {
1682
- 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);
1683
1833
  }
1834
+ (false, true) => groups[right_index] = result.merged,
1835
+ (true, false) => groups[left_index] = result.merged,
1836
+ (false, false) => groups.push(result.merged),
1684
1837
  }
1685
1838
  groups.sort_by_key(|group| group_sort_key(group));
1686
1839
  restart = true;
@@ -1737,16 +1890,21 @@ fn group_sort_key(group: &[CountedOccurrence]) -> (usize, usize) {
1737
1890
 
1738
1891
  struct MergeResult {
1739
1892
  merged: Vec<CountedOccurrence>,
1740
- /// Whether every occurrence of the respective input group was paired into the merge.
1741
- first_consumed: bool,
1742
- second_consumed: bool,
1743
- /// Indexes (into the retained, not fully consumed group) of the occurrences that were paired.
1744
- 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>,
1745
1903
  }
1746
1904
 
1747
1905
  /// Pairs `second` occurrences with gap-preceding `first` occurrences, greedily in source order;
1748
1906
  /// a faithful port of mergeGroups in duplication.ts (at least two pairs, at least one group fully
1749
- /// consumed, merged spans never overlap).
1907
+ /// paired, merged spans never overlap).
1750
1908
  fn merge_groups(
1751
1909
  first: &[CountedOccurrence],
1752
1910
  second: &[CountedOccurrence],
@@ -1756,13 +1914,21 @@ fn merge_groups(
1756
1914
  // again: their spans already live inside that merged group, so re-pairing them would assemble
1757
1915
  // a second, competing merged group instead of letting the existing merged group extend (and
1758
1916
  // would count the same span twice). Consumption is still judged against the FULL group, so a
1759
- // group holding shared occurrences is never subsumed away.
1760
- let leadings: Vec<usize> = (0..first.len())
1761
- .filter(|&index| !first[index].shared_with_merged_group)
1762
- .collect();
1763
- let trailings: Vec<usize> = (0..second.len())
1764
- .filter(|&index| !second[index].shared_with_merged_group)
1765
- .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));
1766
1932
  let mut pairs: Vec<(usize, usize)> = Vec::new();
1767
1933
  let mut leading_position = 0usize;
1768
1934
  let mut previous_trailing_end: Option<usize> = None;
@@ -1786,17 +1952,18 @@ fn merge_groups(
1786
1952
  }
1787
1953
  }
1788
1954
  }
1789
- let first_consumed = pairs.len() == first.len();
1790
- let second_consumed = pairs.len() == second.len();
1791
- 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) {
1792
1958
  return None;
1793
1959
  }
1794
- let paired_retained_indexes: Vec<usize> = if first_consumed == second_consumed {
1795
- Vec::new()
1796
- } else if first_consumed {
1797
- pairs.iter().map(|&(_, trailing)| trailing).collect()
1798
- } else {
1799
- 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)
1800
1967
  };
1801
1968
  let merged = pairs
1802
1969
  .iter()
@@ -1806,6 +1973,7 @@ fn merge_groups(
1806
1973
  CountedOccurrence {
1807
1974
  // A merged occurrence is a fresh span combination; it inherits no shared marks.
1808
1975
  shared_with_merged_group: false,
1976
+ nested_in_larger_group: false,
1809
1977
  matched_runs: None,
1810
1978
  exact_runs: Vec::new(),
1811
1979
  segments: [leading.segments.clone(), trailing.segments.clone()].concat(),
@@ -1819,9 +1987,14 @@ fn merge_groups(
1819
1987
  .collect();
1820
1988
  Some(MergeResult {
1821
1989
  merged,
1822
- first_consumed,
1823
- second_consumed,
1824
- 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
+ },
1825
1998
  })
1826
1999
  }
1827
2000
 
@@ -1907,28 +2080,39 @@ fn collect_near_miss_groups(
1907
2080
  // longest common subsequence of the same sequences, and any choice among a copy's partners
1908
2081
  // would let their number or their order decide what the copy counts.
1909
2082
  //
1910
- // A pair matching nothing outside reported content has nothing to add to it and is dropped
2083
+ // A pair matching nothing outside listed content has nothing to add to it and is dropped
1911
2084
  // before clustering, so that it neither collapses the blocks' core nodes nor widens a node
1912
2085
  // beyond what the pairs clustering it matched. Any other pair is kept however much of it is
1913
2086
  // reported: what the exact pipeline reports of a copy depends on where its copies stand, and
1914
2087
  // the lines the copy counts must not.
1915
- let mut reported_prefix = vec![0usize; tokens.len() + 1];
1916
- {
2088
+ let reported_prefix_of = |groups: &mut dyn Iterator<Item = &Vec<CountedOccurrence>>| {
1917
2089
  let mut reported = vec![false; tokens.len()];
1918
- for occurrence in reported_groups.iter().flatten() {
2090
+ for occurrence in groups.flatten() {
1919
2091
  for &(start, end) in &occurrence.segments {
1920
2092
  reported[start..end].fill(true);
1921
2093
  }
1922
2094
  }
2095
+ let mut prefix = vec![0usize; tokens.len() + 1];
1923
2096
  for (index, &flag) in reported.iter().enumerate() {
1924
- reported_prefix[index + 1] = reported_prefix[index] + usize::from(flag);
2097
+ prefix[index + 1] = prefix[index] + usize::from(flag);
1925
2098
  }
1926
- }
2099
+ prefix
2100
+ };
2101
+ let reported_prefix = reported_prefix_of(&mut reported_groups.iter());
1927
2102
  let reported_count =
1928
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
+ }));
1929
2113
  let unreported_count = |runs: &[(usize, usize)]| -> usize {
1930
2114
  runs.iter()
1931
- .map(|&run| run.1 - run.0 - reported_count(run))
2115
+ .map(|&(start, end)| end - start - (listed_prefix[end] - listed_prefix[start]))
1932
2116
  .sum()
1933
2117
  };
1934
2118
  let block_span = |index: usize| {
@@ -1940,7 +2124,7 @@ fn collect_near_miss_groups(
1940
2124
  let mut edges: Vec<MatchEdge> = Vec::new();
1941
2125
  let mut add_edge = |left: usize, right: usize, cores: Option<CorePair>, alignment| {
1942
2126
  let (left_span, right_span) = cores.unwrap_or((block_span(left), block_span(right)));
1943
- // Fully reported spans need no alignment to be dropped.
2127
+ // Fully listed spans need no alignment to be dropped.
1944
2128
  if unreported_count(&[left_span, right_span]) == 0 {
1945
2129
  return;
1946
2130
  }
@@ -2092,6 +2276,7 @@ fn collect_near_miss_groups(
2092
2276
  .collect();
2093
2277
  CountedOccurrence {
2094
2278
  shared_with_merged_group: false,
2279
+ nested_in_larger_group: false,
2095
2280
  matched_runs: Some(merge_overlapping_cores(&matched_runs)),
2096
2281
  exact_runs: Vec::new(),
2097
2282
  token_count: segments.iter().map(|segment| segment.1 - segment.0).sum(),
@@ -2228,9 +2413,25 @@ fn collect_near_miss_groups(
2228
2413
  }
2229
2414
  merged.extend(to_occurrences(&unanchored_nodes));
2230
2415
  // The rebuild consumed every fully-clustered group, so shared-span marks from earlier
2231
- // 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.
2232
2420
  for occurrence in &mut merged {
2233
- 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
+ });
2234
2435
  }
2235
2436
  merged.extend(to_anchor_occurrences(&anchor_nodes));
2236
2437
  merged.sort_by_key(|occurrence| {
@@ -2389,6 +2590,7 @@ fn coalesce_occurrences(occurrences: Vec<CountedOccurrence>) -> CountedOccurrenc
2389
2590
  .collect();
2390
2591
  CountedOccurrence {
2391
2592
  shared_with_merged_group: false,
2593
+ nested_in_larger_group: false,
2392
2594
  // Without a near-miss part, the segments are matched throughout and say it all.
2393
2595
  exact_runs: if matched_runs.is_some() {
2394
2596
  merge_overlapping_cores(&exact_runs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-gauge",
3
- "version": "10.2.1",
3
+ "version": "10.2.3",
4
4
  "description": "Measure code metrics with tree-sitter.",
5
5
  "keywords": [
6
6
  "cli",
@@ -93,12 +93,12 @@
93
93
  "registry": "https://registry.npmjs.org/"
94
94
  },
95
95
  "optionalDependencies": {
96
- "code-gauge-linux-x64-gnu": "10.2.1",
97
- "code-gauge-linux-arm64-gnu": "10.2.1",
98
- "code-gauge-linux-x64-musl": "10.2.1",
99
- "code-gauge-linux-arm64-musl": "10.2.1",
100
- "code-gauge-darwin-x64": "10.2.1",
101
- "code-gauge-darwin-arm64": "10.2.1",
102
- "code-gauge-win32-x64-msvc": "10.2.1"
96
+ "code-gauge-linux-x64-gnu": "10.2.3",
97
+ "code-gauge-linux-arm64-gnu": "10.2.3",
98
+ "code-gauge-linux-x64-musl": "10.2.3",
99
+ "code-gauge-linux-arm64-musl": "10.2.3",
100
+ "code-gauge-darwin-x64": "10.2.3",
101
+ "code-gauge-darwin-arm64": "10.2.3",
102
+ "code-gauge-win32-x64-msvc": "10.2.3"
103
103
  }
104
104
  }