code-gauge 4.5.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -6
- package/THIRD-PARTY-NOTICES.txt +8911 -0
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.js +1 -1
- 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 +18 -12
- package/dist/crossFileNearMiss.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/dist/diffCommand.cjs +1 -1
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +1 -1
- package/dist/languages.cjs +1 -1
- package/dist/languages.cjs.map +1 -1
- package/dist/languages.js +1 -1
- package/dist/languages.js.map +1 -1
- package/dist/nativeMetrics.cjs +3 -3
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +14 -0
- package/dist/nativeMetrics.js +3 -3
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.js +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/wasmBinding.cjs +2 -0
- package/dist/wasmBinding.cjs.map +1 -0
- package/dist/wasmBinding.d.ts +9 -0
- package/dist/wasmBinding.js +2 -0
- package/dist/wasmBinding.js.map +1 -0
- package/dist/worker.cjs +2 -0
- package/dist/worker.cjs.map +1 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +2 -0
- package/dist/worker.js.map +1 -0
- package/native/Cargo.toml +5 -2
- package/native/build.rs +4 -1
- package/native/code-gauge.wasm +0 -0
- package/native/src/duplication.rs +332 -234
- package/native/src/lib.rs +32 -37
- package/native/src/napi.rs +45 -0
- package/native/src/near_miss.rs +455 -0
- package/native/src/wasm.rs +128 -0
- package/package.json +18 -13
|
@@ -4,6 +4,7 @@ use std::collections::{HashMap, HashSet};
|
|
|
4
4
|
use std::sync::OnceLock;
|
|
5
5
|
use tree_sitter::Node;
|
|
6
6
|
|
|
7
|
+
use crate::near_miss::{Block, Matcher, PairMatch, FILTRATION_PERCENT, MAX_LENGTH_RATIO};
|
|
7
8
|
use crate::types::{
|
|
8
9
|
CrossFileCandidate, CrossFileToken, CrossFileTokenRange, DuplicateBlockOccurrence,
|
|
9
10
|
DuplicationMetrics,
|
|
@@ -314,14 +315,6 @@ impl Default for DuplicationSettings {
|
|
|
314
315
|
}
|
|
315
316
|
}
|
|
316
317
|
}
|
|
317
|
-
/// N-gram size for the near-miss candidate index (NIL's default); shared with crossFileNearMiss.ts.
|
|
318
|
-
const NEAR_MISS_NGRAM_SIZE: usize = 5;
|
|
319
|
-
/// Filtration threshold: shared distinct n-grams over the smaller set; shared with crossFileNearMiss.ts.
|
|
320
|
-
const NEAR_MISS_FILTRATION_PERCENT: usize = 10;
|
|
321
|
-
/// Exclusive bound on shared content-bearing tokens (names and literal values); shared with
|
|
322
|
-
/// crossFileNearMiss.ts.
|
|
323
|
-
const MIN_CONTENT_SIMILARITY_PERCENT: usize = 50;
|
|
324
|
-
|
|
325
318
|
/// See isLiteralDense in duplication.ts: >= 20% literal values marks a region as data-like.
|
|
326
319
|
fn is_literal_dense(literal_count: usize, token_count: usize) -> bool {
|
|
327
320
|
literal_count * 5 >= token_count
|
|
@@ -1663,10 +1656,15 @@ fn merge_groups(
|
|
|
1663
1656
|
})
|
|
1664
1657
|
}
|
|
1665
1658
|
|
|
1659
|
+
/// A verified near-miss pair as (block, core, block, core); a `None` core is a whole-block match.
|
|
1660
|
+
type MatchEdge = (usize, Option<(usize, usize)>, usize, Option<(usize, usize)>);
|
|
1661
|
+
|
|
1666
1662
|
/// Detects near-miss (Type-3) clone groups among block candidates the exact pipeline left
|
|
1667
|
-
/// unreported: NIL-style n-gram filtration, then
|
|
1668
|
-
///
|
|
1669
|
-
///
|
|
1663
|
+
/// unreported: NIL-style n-gram filtration, then pair verification (near_miss::Matcher), then
|
|
1664
|
+
/// transitive clustering of verified pairs (crossFileNearMiss.ts applies the same model across
|
|
1665
|
+
/// files). A block that matched only locally is reported as its matched cores (overlapping cores
|
|
1666
|
+
/// merged), each clustered with its own partners, so code no verified pair matched never counts
|
|
1667
|
+
/// as duplicated.
|
|
1670
1668
|
fn collect_near_miss_groups(
|
|
1671
1669
|
source: &TokenizedSource<'_>,
|
|
1672
1670
|
settings: &DuplicationSettings,
|
|
@@ -1681,39 +1679,113 @@ fn collect_near_miss_groups(
|
|
|
1681
1679
|
return Vec::new();
|
|
1682
1680
|
}
|
|
1683
1681
|
|
|
1684
|
-
// Reported-group indices whose occurrences overlap
|
|
1685
|
-
//
|
|
1686
|
-
let
|
|
1682
|
+
// Reported-group indices whose occurrences overlap a token range: near-miss nodes covering
|
|
1683
|
+
// such content anchor comparisons but are never re-reported.
|
|
1684
|
+
let touched_groups_in = |start: usize, end: usize| -> Vec<usize> {
|
|
1685
|
+
reported_groups
|
|
1686
|
+
.iter()
|
|
1687
|
+
.enumerate()
|
|
1688
|
+
.filter(|(_, group)| {
|
|
1689
|
+
group.iter().any(|occurrence| {
|
|
1690
|
+
occurrence.start_token_index < end && start < occurrence.end_token_index
|
|
1691
|
+
})
|
|
1692
|
+
})
|
|
1693
|
+
.map(|(group_index, _)| group_index)
|
|
1694
|
+
.collect()
|
|
1695
|
+
};
|
|
1696
|
+
|
|
1697
|
+
let (symbols, is_content) = to_symbol_stream(tokens);
|
|
1698
|
+
let statements = top_level_statement_finder(&source.container_statement_ranges);
|
|
1699
|
+
let mut blocks: Vec<Block> = comparable
|
|
1687
1700
|
.iter()
|
|
1688
1701
|
.map(|range| {
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
.
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
})
|
|
1697
|
-
})
|
|
1698
|
-
.map(|(group_index, _)| group_index)
|
|
1699
|
-
.collect()
|
|
1702
|
+
Block::new(
|
|
1703
|
+
&symbols,
|
|
1704
|
+
&is_content,
|
|
1705
|
+
range.start_token_index,
|
|
1706
|
+
range.end_token_index,
|
|
1707
|
+
statements(range.start_token_index, range.end_token_index),
|
|
1708
|
+
)
|
|
1700
1709
|
})
|
|
1701
1710
|
.collect();
|
|
1711
|
+
let matcher = Matcher::new(
|
|
1712
|
+
&mut blocks,
|
|
1713
|
+
settings.min_tokens,
|
|
1714
|
+
settings.min_similarity_percent,
|
|
1715
|
+
);
|
|
1702
1716
|
|
|
1703
|
-
|
|
1704
|
-
// other files the process measured before it.
|
|
1705
|
-
let mut symbol_id_by_token_hashes: HashMap<(i32, i32, i32, i32), i32> = HashMap::new();
|
|
1706
|
-
let sequences: Vec<NormalizedBlock> = comparable
|
|
1717
|
+
let block_touched: Vec<bool> = comparable
|
|
1707
1718
|
.iter()
|
|
1708
|
-
.map(|range|
|
|
1719
|
+
.map(|range| !touched_groups_in(range.start_token_index, range.end_token_index).is_empty())
|
|
1709
1720
|
.collect();
|
|
1710
|
-
let
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
.
|
|
1714
|
-
|
|
1721
|
+
let mut edges: Vec<MatchEdge> = Vec::new();
|
|
1722
|
+
for_each_candidate_pair(
|
|
1723
|
+
&blocks,
|
|
1724
|
+
settings.min_similarity_percent,
|
|
1725
|
+
|left_index, right_index| match matcher.verify(&blocks[left_index], &blocks[right_index]) {
|
|
1726
|
+
None => {}
|
|
1727
|
+
// A whole match between two blocks that both overlap reported content could never join
|
|
1728
|
+
// a group, and recording it would collapse the blocks' core nodes.
|
|
1729
|
+
Some(PairMatch::Whole)
|
|
1730
|
+
if !(block_touched[left_index] && block_touched[right_index]) =>
|
|
1731
|
+
{
|
|
1732
|
+
edges.push((left_index, None, right_index, None))
|
|
1733
|
+
}
|
|
1734
|
+
Some(PairMatch::Whole) => {}
|
|
1735
|
+
Some(PairMatch::Local(cores)) => {
|
|
1736
|
+
for (left_core, right_core) in cores {
|
|
1737
|
+
edges.push((left_index, Some(left_core), right_index, Some(right_core)));
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
},
|
|
1741
|
+
);
|
|
1742
|
+
|
|
1743
|
+
// Clustering runs over (block, core) nodes: a block with a recorded whole match is one
|
|
1744
|
+
// node, and otherwise each union of its overlapping local cores is its own node, so disjoint
|
|
1745
|
+
// cores matched with different partners fall into separate groups.
|
|
1746
|
+
let mut matched_whole = vec![false; comparable.len()];
|
|
1747
|
+
let mut local_cores: Vec<Vec<(usize, usize)>> = vec![Vec::new(); comparable.len()];
|
|
1748
|
+
for &(left_index, left_core, right_index, right_core) in &edges {
|
|
1749
|
+
for (index, core) in [(left_index, left_core), (right_index, right_core)] {
|
|
1750
|
+
match core {
|
|
1751
|
+
Some(core) => local_cores[index].push(core),
|
|
1752
|
+
None => matched_whole[index] = true,
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
let mut node_blocks: Vec<usize> = Vec::new();
|
|
1757
|
+
let mut node_spans: Vec<Option<(usize, usize)>> = Vec::new();
|
|
1758
|
+
let mut first_node_by_block: Vec<usize> = Vec::with_capacity(comparable.len());
|
|
1759
|
+
for index in 0..comparable.len() {
|
|
1760
|
+
first_node_by_block.push(node_blocks.len());
|
|
1761
|
+
let cores = if matched_whole[index] {
|
|
1762
|
+
Vec::new()
|
|
1763
|
+
} else {
|
|
1764
|
+
merge_overlapping_cores(&local_cores[index])
|
|
1765
|
+
};
|
|
1766
|
+
if cores.is_empty() {
|
|
1767
|
+
node_blocks.push(index);
|
|
1768
|
+
node_spans.push(None);
|
|
1769
|
+
}
|
|
1770
|
+
for core in cores {
|
|
1771
|
+
node_blocks.push(index);
|
|
1772
|
+
node_spans.push(Some(core));
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
let node_of = |index: usize, core: Option<(usize, usize)>| {
|
|
1776
|
+
let first = first_node_by_block[index];
|
|
1777
|
+
match core {
|
|
1778
|
+
Some(core) if !matched_whole[index] => (first..node_blocks.len())
|
|
1779
|
+
.take_while(|&node| node_blocks[node] == index)
|
|
1780
|
+
.find(|&node| {
|
|
1781
|
+
node_spans[node].is_some_and(|span| span.0 <= core.0 && core.1 <= span.1)
|
|
1782
|
+
})
|
|
1783
|
+
.expect("every local core lies in one of its block's merged cores"),
|
|
1784
|
+
_ => first,
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1715
1787
|
|
|
1716
|
-
let mut parent: Vec<usize> = (0..
|
|
1788
|
+
let mut parent: Vec<usize> = (0..node_blocks.len()).collect();
|
|
1717
1789
|
fn find(parent: &mut [usize], mut index: usize) -> usize {
|
|
1718
1790
|
let mut root = index;
|
|
1719
1791
|
while parent[root] != root {
|
|
@@ -1726,52 +1798,84 @@ fn collect_near_miss_groups(
|
|
|
1726
1798
|
}
|
|
1727
1799
|
root
|
|
1728
1800
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1801
|
+
// Coverage is judged per node: a core is covered only when a reported occurrence overlaps
|
|
1802
|
+
// the core itself, not merely elsewhere in its block.
|
|
1803
|
+
let node_range = |node: usize| {
|
|
1804
|
+
node_spans[node].unwrap_or((
|
|
1805
|
+
comparable[node_blocks[node]].start_token_index,
|
|
1806
|
+
comparable[node_blocks[node]].end_token_index,
|
|
1807
|
+
))
|
|
1808
|
+
};
|
|
1809
|
+
let touched_groups_by_node: Vec<Vec<usize>> = (0..node_blocks.len())
|
|
1810
|
+
.map(|node| {
|
|
1811
|
+
let (start, end) = node_range(node);
|
|
1812
|
+
touched_groups_in(start, end)
|
|
1813
|
+
})
|
|
1814
|
+
.collect();
|
|
1815
|
+
for &(left_index, left_core, right_index, right_core) in &edges {
|
|
1816
|
+
let (left_node, right_node) = (
|
|
1817
|
+
node_of(left_index, left_core),
|
|
1818
|
+
node_of(right_index, right_core),
|
|
1819
|
+
);
|
|
1820
|
+
// Two already-reported nodes have nothing new to contribute to each other.
|
|
1821
|
+
if !touched_groups_by_node[left_node].is_empty()
|
|
1822
|
+
&& !touched_groups_by_node[right_node].is_empty()
|
|
1748
1823
|
{
|
|
1749
1824
|
continue;
|
|
1750
1825
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
{
|
|
1755
|
-
let left_root = find(&mut parent, left_index);
|
|
1756
|
-
let right_root = find(&mut parent, right_index);
|
|
1757
|
-
parent[left_root.max(right_root)] = left_root.min(right_root);
|
|
1758
|
-
}
|
|
1826
|
+
let left_root = find(&mut parent, left_node);
|
|
1827
|
+
let right_root = find(&mut parent, right_node);
|
|
1828
|
+
parent[left_root.max(right_root)] = left_root.min(right_root);
|
|
1759
1829
|
}
|
|
1760
1830
|
|
|
1761
1831
|
let mut members_by_root: IndexMap<usize, Vec<usize>> = IndexMap::new();
|
|
1762
|
-
for
|
|
1763
|
-
let root = find(&mut parent,
|
|
1764
|
-
members_by_root.entry(root).or_default().push(
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1832
|
+
for node in 0..node_blocks.len() {
|
|
1833
|
+
let root = find(&mut parent, node);
|
|
1834
|
+
members_by_root.entry(root).or_default().push(node);
|
|
1835
|
+
}
|
|
1836
|
+
// A group's nodes from one block become ONE occurrence whose segments are its cores, so the
|
|
1837
|
+
// fragment-weighted count charges the block as one copy (as for gapped clones), not once per
|
|
1838
|
+
// core.
|
|
1839
|
+
let to_occurrences = |nodes: &[usize]| -> Vec<CountedOccurrence> {
|
|
1840
|
+
let mut spans_by_block: IndexMap<usize, Vec<Option<(usize, usize)>>> = IndexMap::new();
|
|
1841
|
+
for &node in nodes {
|
|
1842
|
+
spans_by_block
|
|
1843
|
+
.entry(node_blocks[node])
|
|
1844
|
+
.or_default()
|
|
1845
|
+
.push(node_spans[node]);
|
|
1846
|
+
}
|
|
1847
|
+
spans_by_block
|
|
1848
|
+
.into_iter()
|
|
1849
|
+
.map(|(block, spans)| {
|
|
1850
|
+
let range = comparable[block];
|
|
1851
|
+
let mut segments: Vec<(usize, usize)> = spans
|
|
1852
|
+
.iter()
|
|
1853
|
+
.map(|span| span.unwrap_or((range.start_token_index, range.end_token_index)))
|
|
1854
|
+
.collect();
|
|
1855
|
+
segments.sort_unstable();
|
|
1856
|
+
let whole = spans.iter().any(Option::is_none);
|
|
1857
|
+
let (start, end) = (segments[0].0, segments[segments.len() - 1].1);
|
|
1858
|
+
CountedOccurrence {
|
|
1859
|
+
shared_with_merged_group: false,
|
|
1860
|
+
token_count: segments.iter().map(|segment| segment.1 - segment.0).sum(),
|
|
1861
|
+
segments,
|
|
1862
|
+
start_token_index: start,
|
|
1863
|
+
end_token_index: end,
|
|
1864
|
+
start_line: if whole {
|
|
1865
|
+
range.start_line
|
|
1866
|
+
} else {
|
|
1867
|
+
tokens[start].start_row + 1
|
|
1868
|
+
},
|
|
1869
|
+
end_line: if whole {
|
|
1870
|
+
range.end_line
|
|
1871
|
+
} else {
|
|
1872
|
+
tokens[end - 1].end_row + 1
|
|
1873
|
+
},
|
|
1874
|
+
}
|
|
1875
|
+
})
|
|
1876
|
+
.collect()
|
|
1774
1877
|
};
|
|
1878
|
+
let touched_groups_of = |node: usize| &touched_groups_by_node[node];
|
|
1775
1879
|
let mut groups: Vec<Vec<CountedOccurrence>> = Vec::new();
|
|
1776
1880
|
for members in members_by_root.values() {
|
|
1777
1881
|
if members.len() < 2 {
|
|
@@ -1780,39 +1884,36 @@ fn collect_near_miss_groups(
|
|
|
1780
1884
|
let uncovered: Vec<usize> = members
|
|
1781
1885
|
.iter()
|
|
1782
1886
|
.copied()
|
|
1783
|
-
.filter(|&index|
|
|
1887
|
+
.filter(|&index| touched_groups_of(index).is_empty())
|
|
1784
1888
|
.collect();
|
|
1785
1889
|
let covered: Vec<usize> = members
|
|
1786
1890
|
.iter()
|
|
1787
1891
|
.copied()
|
|
1788
|
-
.filter(|&index| !
|
|
1892
|
+
.filter(|&index| !touched_groups_of(index).is_empty())
|
|
1789
1893
|
.collect();
|
|
1790
1894
|
if covered.is_empty() {
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
.collect(),
|
|
1796
|
-
);
|
|
1895
|
+
let occurrences = to_occurrences(members);
|
|
1896
|
+
if occurrences.len() >= 2 {
|
|
1897
|
+
groups.push(occurrences);
|
|
1898
|
+
}
|
|
1797
1899
|
continue;
|
|
1798
1900
|
}
|
|
1799
1901
|
if uncovered.is_empty() {
|
|
1800
1902
|
continue;
|
|
1801
1903
|
}
|
|
1802
1904
|
// An anchored cluster extends a reported group only when every occurrence of that group
|
|
1803
|
-
// overlaps one of the cluster's member
|
|
1905
|
+
// overlaps one of the cluster's member nodes: an occurrence disjoint from all members
|
|
1804
1906
|
// reports content the cluster does not share.
|
|
1805
1907
|
let overlaps_member = |occurrence: &CountedOccurrence| {
|
|
1806
1908
|
members.iter().any(|&index| {
|
|
1807
|
-
let
|
|
1808
|
-
occurrence.start_token_index <
|
|
1809
|
-
&& range.start_token_index < occurrence.end_token_index
|
|
1909
|
+
let (start, end) = node_range(index);
|
|
1910
|
+
occurrence.start_token_index < end && start < occurrence.end_token_index
|
|
1810
1911
|
})
|
|
1811
1912
|
};
|
|
1812
1913
|
// Ascending by construction: BTreeSet iteration is sorted and filter preserves order.
|
|
1813
1914
|
let fully_clustered: Vec<usize> = covered
|
|
1814
1915
|
.iter()
|
|
1815
|
-
.flat_map(|&index|
|
|
1916
|
+
.flat_map(|&index| touched_groups_of(index).iter().copied())
|
|
1816
1917
|
.collect::<std::collections::BTreeSet<usize>>()
|
|
1817
1918
|
.into_iter()
|
|
1818
1919
|
.filter(|&group_index| {
|
|
@@ -1821,32 +1922,57 @@ fn collect_near_miss_groups(
|
|
|
1821
1922
|
})
|
|
1822
1923
|
.collect();
|
|
1823
1924
|
if let Some((&target_index, source_indexes)) = fully_clustered.split_first() {
|
|
1824
|
-
// Rebuild the component as ONE group with one coalesced occurrence per member block
|
|
1925
|
+
// Rebuild the component as ONE group with one coalesced occurrence per member block:
|
|
1926
|
+
// the fragments every node of a block overlaps are collected together, since a block's
|
|
1927
|
+
// cores are parts of one copy.
|
|
1825
1928
|
let mut consumed: HashSet<(usize, usize)> = HashSet::new();
|
|
1826
1929
|
let mut merged: Vec<CountedOccurrence> = Vec::new();
|
|
1930
|
+
let mut unanchored_nodes: Vec<usize> = Vec::new();
|
|
1931
|
+
let mut nodes_by_block: IndexMap<usize, Vec<usize>> = IndexMap::new();
|
|
1827
1932
|
for &member_index in members {
|
|
1828
|
-
|
|
1933
|
+
nodes_by_block
|
|
1934
|
+
.entry(node_blocks[member_index])
|
|
1935
|
+
.or_default()
|
|
1936
|
+
.push(member_index);
|
|
1937
|
+
}
|
|
1938
|
+
for block_nodes in nodes_by_block.values() {
|
|
1829
1939
|
// Occurrences of ONE group are distinct copies; only fragments from DIFFERENT
|
|
1830
1940
|
// groups belong to the same copy. Consecutive position-order slices keep the
|
|
1831
1941
|
// coalesced spans disjoint.
|
|
1832
1942
|
let mut fragments: Vec<(CountedOccurrence, usize)> = Vec::new();
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1943
|
+
let mut plain_nodes: Vec<usize> = Vec::new();
|
|
1944
|
+
for &node in block_nodes {
|
|
1945
|
+
let (range_start, range_end) = node_range(node);
|
|
1946
|
+
let fragment_count = fragments.len();
|
|
1947
|
+
for &group_index in &fully_clustered {
|
|
1948
|
+
for (occurrence_index, occurrence) in
|
|
1949
|
+
reported_groups[group_index].iter().enumerate()
|
|
1840
1950
|
{
|
|
1841
|
-
consumed.
|
|
1842
|
-
|
|
1951
|
+
if !consumed.contains(&(group_index, occurrence_index))
|
|
1952
|
+
&& occurrence.start_token_index < range_end
|
|
1953
|
+
&& range_start < occurrence.end_token_index
|
|
1954
|
+
{
|
|
1955
|
+
consumed.insert((group_index, occurrence_index));
|
|
1956
|
+
fragments.push((occurrence.clone(), group_index));
|
|
1957
|
+
}
|
|
1843
1958
|
}
|
|
1844
1959
|
}
|
|
1960
|
+
if fragments.len() == fragment_count && touched_groups_of(node).is_empty() {
|
|
1961
|
+
plain_nodes.push(node);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
if fragments.is_empty() {
|
|
1965
|
+
unanchored_nodes.extend(plain_nodes);
|
|
1966
|
+
} else {
|
|
1967
|
+
// An untouched core of a block that also holds fragments is part of the same
|
|
1968
|
+
// copy; a group index no reported group uses keeps it in that copy.
|
|
1969
|
+
for occurrence in to_occurrences(&plain_nodes) {
|
|
1970
|
+
fragments.push((occurrence, usize::MAX));
|
|
1971
|
+
}
|
|
1845
1972
|
}
|
|
1846
1973
|
fragments.sort_by_key(|(occurrence, _)| {
|
|
1847
1974
|
(occurrence.start_token_index, occurrence.end_token_index)
|
|
1848
1975
|
});
|
|
1849
|
-
let had_fragments = !fragments.is_empty();
|
|
1850
1976
|
let mut copy_parts: Vec<CountedOccurrence> = Vec::new();
|
|
1851
1977
|
let mut copy_groups: HashSet<usize> = HashSet::new();
|
|
1852
1978
|
for (occurrence, group_index) in fragments {
|
|
@@ -1860,10 +1986,8 @@ fn collect_near_miss_groups(
|
|
|
1860
1986
|
if !copy_parts.is_empty() {
|
|
1861
1987
|
merged.push(coalesce_occurrences(copy_parts));
|
|
1862
1988
|
}
|
|
1863
|
-
if !had_fragments && touched_groups_by_block[member_index].is_empty() {
|
|
1864
|
-
merged.push(to_occurrence(comparable[member_index]));
|
|
1865
|
-
}
|
|
1866
1989
|
}
|
|
1990
|
+
merged.extend(to_occurrences(&unanchored_nodes));
|
|
1867
1991
|
merged.sort_by_key(|occurrence| {
|
|
1868
1992
|
(occurrence.start_token_index, occurrence.end_token_index)
|
|
1869
1993
|
});
|
|
@@ -1876,13 +2000,11 @@ fn collect_near_miss_groups(
|
|
|
1876
2000
|
for &source_index in source_indexes {
|
|
1877
2001
|
reported_groups[source_index].clear();
|
|
1878
2002
|
}
|
|
1879
|
-
} else
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
.collect(),
|
|
1885
|
-
);
|
|
2003
|
+
} else {
|
|
2004
|
+
let occurrences = to_occurrences(&uncovered);
|
|
2005
|
+
if occurrences.len() >= 2 {
|
|
2006
|
+
groups.push(occurrences);
|
|
2007
|
+
}
|
|
1886
2008
|
}
|
|
1887
2009
|
}
|
|
1888
2010
|
groups.sort_by_key(|group| group_sort_key(group));
|
|
@@ -2014,149 +2136,125 @@ fn coalesce_occurrences(occurrences: Vec<CountedOccurrence>) -> CountedOccurrenc
|
|
|
2014
2136
|
}
|
|
2015
2137
|
}
|
|
2016
2138
|
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2139
|
+
/// The unions of overlapping cores, in position order.
|
|
2140
|
+
fn merge_overlapping_cores(cores: &[(usize, usize)]) -> Vec<(usize, usize)> {
|
|
2141
|
+
let mut sorted = cores.to_vec();
|
|
2142
|
+
sorted.sort_unstable();
|
|
2143
|
+
let mut merged: Vec<(usize, usize)> = Vec::new();
|
|
2144
|
+
for (start, end) in sorted {
|
|
2145
|
+
match merged.last_mut() {
|
|
2146
|
+
Some(last) if start < last.1 => last.1 = last.1.max(end),
|
|
2147
|
+
_ => merged.push((start, end)),
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
merged
|
|
2022
2151
|
}
|
|
2023
2152
|
|
|
2024
|
-
///
|
|
2025
|
-
///
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
)
|
|
2031
|
-
let mut
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
let
|
|
2044
|
-
let id = *symbol_id_by_token_hashes
|
|
2153
|
+
/// The file's tokens as a near-miss symbol stream: identifiers as -(file-level id + 1), every
|
|
2154
|
+
/// other token interned from its hash pairs (literal VALUES folded in, unlike the exact
|
|
2155
|
+
/// fingerprint's kind tags), plus which tokens are content-bearing (names and literal values).
|
|
2156
|
+
/// Interned per call so a file's symbol ids (and thus its n-gram hashes) never depend on which
|
|
2157
|
+
/// other files the process measured before it.
|
|
2158
|
+
fn to_symbol_stream(tokens: &[Token<'_>]) -> (Vec<i32>, Vec<bool>) {
|
|
2159
|
+
let mut symbol_by_token_hashes: HashMap<(i32, i32, i32, i32), i32> = HashMap::new();
|
|
2160
|
+
let mut id_by_identifier: HashMap<&str, i32> = HashMap::new();
|
|
2161
|
+
tokens
|
|
2162
|
+
.iter()
|
|
2163
|
+
.map(|token| {
|
|
2164
|
+
if token.is_id {
|
|
2165
|
+
let next_id = id_by_identifier.len() as i32;
|
|
2166
|
+
let id = *id_by_identifier
|
|
2167
|
+
.entry(token.text.as_ref())
|
|
2168
|
+
.or_insert(next_id);
|
|
2169
|
+
return (-(id + 1), false);
|
|
2170
|
+
}
|
|
2171
|
+
let next_symbol = symbol_by_token_hashes.len() as i32;
|
|
2172
|
+
let symbol = *symbol_by_token_hashes
|
|
2045
2173
|
.entry((
|
|
2046
2174
|
token.text_hash,
|
|
2047
2175
|
token.text_hash2,
|
|
2048
2176
|
token.literal_hash.unwrap_or(0),
|
|
2049
2177
|
token.literal_hash2.unwrap_or(0),
|
|
2050
2178
|
))
|
|
2051
|
-
.or_insert(
|
|
2052
|
-
|
|
2053
|
-
*content_count_by_symbol.entry(id).or_insert(0) += 1;
|
|
2054
|
-
content_total += 1;
|
|
2055
|
-
}
|
|
2056
|
-
id
|
|
2057
|
-
};
|
|
2058
|
-
sequence.push(value);
|
|
2059
|
-
}
|
|
2060
|
-
NormalizedBlock {
|
|
2061
|
-
sequence,
|
|
2062
|
-
content_count_by_symbol,
|
|
2063
|
-
content_total,
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
/// Multiset overlap of two blocks' content-bearing symbols, for the content gate.
|
|
2068
|
-
fn content_overlap(left: &NormalizedBlock, right: &NormalizedBlock) -> usize {
|
|
2069
|
-
let (smaller, larger) =
|
|
2070
|
-
if left.content_count_by_symbol.len() <= right.content_count_by_symbol.len() {
|
|
2071
|
-
(left, right)
|
|
2072
|
-
} else {
|
|
2073
|
-
(right, left)
|
|
2074
|
-
};
|
|
2075
|
-
smaller
|
|
2076
|
-
.content_count_by_symbol
|
|
2077
|
-
.iter()
|
|
2078
|
-
.map(|(symbol, count)| {
|
|
2079
|
-
(*count).min(
|
|
2080
|
-
larger
|
|
2081
|
-
.content_count_by_symbol
|
|
2082
|
-
.get(symbol)
|
|
2083
|
-
.copied()
|
|
2084
|
-
.unwrap_or(0),
|
|
2085
|
-
)
|
|
2179
|
+
.or_insert(next_symbol);
|
|
2180
|
+
(symbol, token.is_name || token.literal_hash.is_some())
|
|
2086
2181
|
})
|
|
2087
|
-
.
|
|
2182
|
+
.unzip()
|
|
2088
2183
|
}
|
|
2089
2184
|
|
|
2090
|
-
///
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2185
|
+
/// Returns a lookup of the outermost container statements inside a token range, excluding a
|
|
2186
|
+
/// statement spanning the whole range (the block itself).
|
|
2187
|
+
fn top_level_statement_finder(
|
|
2188
|
+
container_statement_ranges: &[Vec<TokenRange>],
|
|
2189
|
+
) -> impl Fn(usize, usize) -> Vec<(usize, usize)> {
|
|
2190
|
+
let mut statements: Vec<(usize, usize)> = container_statement_ranges
|
|
2191
|
+
.iter()
|
|
2192
|
+
.flatten()
|
|
2193
|
+
.map(|range| (range.start_token_index, range.end_token_index))
|
|
2194
|
+
.filter(|(start, end)| start < end)
|
|
2195
|
+
.collect();
|
|
2196
|
+
statements.sort_by_key(|&(start, end)| (start, std::cmp::Reverse(end)));
|
|
2197
|
+
move |start, end| {
|
|
2198
|
+
let mut top_level: Vec<(usize, usize)> = Vec::new();
|
|
2199
|
+
let first = statements.partition_point(|statement| statement.0 < start);
|
|
2200
|
+
for &statement in statements[first..]
|
|
2201
|
+
.iter()
|
|
2202
|
+
.take_while(|statement| statement.0 < end)
|
|
2203
|
+
{
|
|
2204
|
+
let nested = top_level.last().is_some_and(|last| statement.0 < last.1);
|
|
2205
|
+
if statement.1 <= end && statement != (start, end) && !nested {
|
|
2206
|
+
top_level.push(statement);
|
|
2207
|
+
}
|
|
2101
2208
|
}
|
|
2102
|
-
|
|
2209
|
+
top_level
|
|
2103
2210
|
}
|
|
2104
|
-
ngrams
|
|
2105
2211
|
}
|
|
2106
2212
|
|
|
2107
|
-
///
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2213
|
+
/// Visits every block pair sharing at least FILTRATION_PERCENT of the smaller block's distinct
|
|
2214
|
+
/// n-grams, except pairs whose length ratio rules out both whole-block similarity and
|
|
2215
|
+
/// MAX_LENGTH_RATIO; the same scan as forEachCandidatePair in crossFileNearMiss.ts. Blocks are
|
|
2216
|
+
/// visited in ascending length, so each posting list is scanned backwards only while its blocks are
|
|
2217
|
+
/// long enough, and shared counts accumulate in a dense counter instead of a pair map.
|
|
2218
|
+
fn for_each_candidate_pair(
|
|
2219
|
+
blocks: &[Block],
|
|
2220
|
+
min_similarity_percent: usize,
|
|
2221
|
+
mut visit: impl FnMut(usize, usize),
|
|
2222
|
+
) {
|
|
2223
|
+
let mut order: Vec<usize> = (0..blocks.len()).collect();
|
|
2224
|
+
order.sort_by_key(|&index| blocks[index].len());
|
|
2225
|
+
let mut postings: HashMap<i32, Vec<usize>> = HashMap::new();
|
|
2226
|
+
let mut shared_counts = vec![0usize; blocks.len()];
|
|
2227
|
+
let mut touched: Vec<usize> = Vec::new();
|
|
2228
|
+
for right in order {
|
|
2229
|
+
let length = blocks[right].len();
|
|
2230
|
+
let min_left_length = length
|
|
2231
|
+
.div_ceil(MAX_LENGTH_RATIO)
|
|
2232
|
+
.min((min_similarity_percent * length).div_ceil(100));
|
|
2233
|
+
for &ngram in &blocks[right].ngrams {
|
|
2234
|
+
let posting = postings.entry(ngram).or_default();
|
|
2235
|
+
for &left in posting.iter().rev() {
|
|
2236
|
+
if blocks[left].len() < min_left_length {
|
|
2237
|
+
break;
|
|
2238
|
+
}
|
|
2239
|
+
if shared_counts[left] == 0 {
|
|
2240
|
+
touched.push(left);
|
|
2241
|
+
}
|
|
2242
|
+
shared_counts[left] += 1;
|
|
2121
2243
|
}
|
|
2244
|
+
posting.push(right);
|
|
2122
2245
|
}
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
return 0;
|
|
2133
|
-
}
|
|
2134
|
-
let word_count = a.len().div_ceil(64);
|
|
2135
|
-
let mut position_masks: HashMap<i32, Vec<u64>> = HashMap::new();
|
|
2136
|
-
for (index, &symbol) in a.iter().enumerate() {
|
|
2137
|
-
position_masks
|
|
2138
|
-
.entry(symbol)
|
|
2139
|
-
.or_insert_with(|| vec![0; word_count])[index / 64] |= 1u64 << (index % 64);
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
let mut v = vec![0u64; word_count];
|
|
2143
|
-
for symbol in b {
|
|
2144
|
-
let match_mask = position_masks.get(symbol);
|
|
2145
|
-
// `(v << 1) | 1` shifts a carry bit across words; subtraction borrows across words.
|
|
2146
|
-
let mut shift_carry = 1u64;
|
|
2147
|
-
let mut borrow = 0u64;
|
|
2148
|
-
for (word, slot) in v.iter_mut().enumerate() {
|
|
2149
|
-
let previous = *slot;
|
|
2150
|
-
let x = match_mask.map_or(0, |mask| mask[word]) | previous;
|
|
2151
|
-
let shifted = (previous << 1) | shift_carry;
|
|
2152
|
-
shift_carry = previous >> 63;
|
|
2153
|
-
let (partial, underflow1) = x.overflowing_sub(shifted);
|
|
2154
|
-
let (difference, underflow2) = partial.overflowing_sub(borrow);
|
|
2155
|
-
borrow = u64::from(underflow1 || underflow2);
|
|
2156
|
-
*slot = x & !difference;
|
|
2246
|
+
// Ascending so the visit order does not depend on the n-gram set's iteration order.
|
|
2247
|
+
touched.sort_unstable();
|
|
2248
|
+
for &left in &touched {
|
|
2249
|
+
let shared = std::mem::take(&mut shared_counts[left]);
|
|
2250
|
+
if shared * 100
|
|
2251
|
+
>= FILTRATION_PERCENT * blocks[left].ngrams.len().min(blocks[right].ngrams.len())
|
|
2252
|
+
{
|
|
2253
|
+
visit(left, right);
|
|
2254
|
+
}
|
|
2157
2255
|
}
|
|
2256
|
+
touched.clear();
|
|
2158
2257
|
}
|
|
2159
|
-
v.iter().map(|word| word.count_ones() as usize).sum()
|
|
2160
2258
|
}
|
|
2161
2259
|
|
|
2162
2260
|
/// Redundant copies one group adds to duplicate_block_count; a faithful port of
|