code-gauge 10.1.0 → 10.2.1
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 +37 -15
- package/THIRD-PARTY-NOTICES.txt +8 -8
- package/dist/checkCommand.cjs +1 -1
- package/dist/checkCommand.cjs.map +1 -1
- package/dist/checkCommand.js +1 -1
- package/dist/checkCommand.js.map +1 -1
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.d.ts +3 -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 +9 -4
- package/dist/crossFileNearMiss.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.d.ts +28 -7
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/thresholdCheck.cjs +1 -1
- package/dist/thresholdCheck.cjs.map +1 -1
- package/dist/thresholdCheck.d.ts +2 -0
- package/dist/thresholdCheck.js +1 -1
- package/dist/thresholdCheck.js.map +1 -1
- package/dist/thresholds.cjs.map +1 -1
- package/dist/thresholds.d.ts +2 -3
- package/dist/thresholds.js.map +1 -1
- package/dist/types.d.ts +20 -8
- package/native/Cargo.lock +16 -16
- package/native/Cargo.toml +8 -8
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +10 -1
- package/native/src/container.rs +158 -7
- package/native/src/dep_degree.rs +13 -14
- package/native/src/duplication.rs +302 -137
- package/native/src/functions.rs +112 -24
- package/native/src/languages.rs +5 -0
- package/native/src/measure.rs +9 -4
- package/native/src/near_miss.rs +303 -52
- package/package.json +8 -8
|
@@ -4,7 +4,9 @@ use std::borrow::Cow;
|
|
|
4
4
|
use std::sync::OnceLock;
|
|
5
5
|
use tree_sitter::Node;
|
|
6
6
|
|
|
7
|
-
use crate::near_miss::{
|
|
7
|
+
use crate::near_miss::{
|
|
8
|
+
Block, CorePair, Matcher, PairMatch, TokenRuns, FILTRATION_PERCENT, MAX_LENGTH_RATIO,
|
|
9
|
+
};
|
|
8
10
|
use crate::tree_index::NodeExt;
|
|
9
11
|
use crate::types::{
|
|
10
12
|
CrossFileCandidate, CrossFileToken, CrossFileTokenRange, DuplicateBlockOccurrence,
|
|
@@ -1577,9 +1579,16 @@ fn dedupe_by_region(group: Vec<DuplicateCandidate>) -> Vec<DuplicateCandidate> {
|
|
|
1577
1579
|
#[derive(Clone)]
|
|
1578
1580
|
struct CountedOccurrence {
|
|
1579
1581
|
segments: Vec<(usize, usize)>,
|
|
1580
|
-
/// Set on
|
|
1581
|
-
///
|
|
1582
|
+
/// Set on an occurrence whose span another group counts, so block counting must not count it
|
|
1583
|
+
/// again: a retained group's occurrence that a partial gapped merge also paired into a merged
|
|
1584
|
+
/// group, and a near-miss anchor, which overlaps the reported occurrence it was matched through.
|
|
1582
1585
|
shared_with_merged_group: bool,
|
|
1586
|
+
/// The token runs of a near-miss copy that its partners match, within its segments;
|
|
1587
|
+
/// `None` for an exact or gapped copy, whose segments are matched throughout.
|
|
1588
|
+
matched_runs: Option<Vec<(usize, usize)>>,
|
|
1589
|
+
/// The exact fragments coalesced into a near-miss copy: each line they reach is duplicated,
|
|
1590
|
+
/// whatever else of the copy shares the line.
|
|
1591
|
+
exact_runs: Vec<(usize, usize)>,
|
|
1583
1592
|
/// Sum of segment token counts (the gap tokens are not matched content).
|
|
1584
1593
|
token_count: usize,
|
|
1585
1594
|
start_token_index: usize,
|
|
@@ -1588,6 +1597,16 @@ struct CountedOccurrence {
|
|
|
1588
1597
|
end_line: usize,
|
|
1589
1598
|
}
|
|
1590
1599
|
|
|
1600
|
+
impl CountedOccurrence {
|
|
1601
|
+
/// Whether a matched segment overlaps the token range: the gap of a gapped clone is not
|
|
1602
|
+
/// matched content.
|
|
1603
|
+
fn matches_within(&self, (start, end): (usize, usize)) -> bool {
|
|
1604
|
+
self.segments
|
|
1605
|
+
.iter()
|
|
1606
|
+
.any(|&(segment_start, segment_end)| segment_start < end && start < segment_end)
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1591
1610
|
fn to_counted_groups(
|
|
1592
1611
|
counted: &IndexMap<std::rc::Rc<str>, Vec<DuplicateCandidate>>,
|
|
1593
1612
|
) -> Vec<Vec<CountedOccurrence>> {
|
|
@@ -1597,6 +1616,8 @@ fn to_counted_groups(
|
|
|
1597
1616
|
.iter()
|
|
1598
1617
|
.map(|candidate| CountedOccurrence {
|
|
1599
1618
|
shared_with_merged_group: false,
|
|
1619
|
+
matched_runs: None,
|
|
1620
|
+
exact_runs: Vec::new(),
|
|
1600
1621
|
segments: vec![(candidate.start_token_index, candidate.end_token_index)],
|
|
1601
1622
|
token_count: candidate.token_count,
|
|
1602
1623
|
start_token_index: candidate.start_token_index,
|
|
@@ -1785,6 +1806,8 @@ fn merge_groups(
|
|
|
1785
1806
|
CountedOccurrence {
|
|
1786
1807
|
// A merged occurrence is a fresh span combination; it inherits no shared marks.
|
|
1787
1808
|
shared_with_merged_group: false,
|
|
1809
|
+
matched_runs: None,
|
|
1810
|
+
exact_runs: Vec::new(),
|
|
1788
1811
|
segments: [leading.segments.clone(), trailing.segments.clone()].concat(),
|
|
1789
1812
|
token_count: leading.token_count + trailing.token_count,
|
|
1790
1813
|
start_token_index: leading.start_token_index,
|
|
@@ -1802,15 +1825,32 @@ fn merge_groups(
|
|
|
1802
1825
|
})
|
|
1803
1826
|
}
|
|
1804
1827
|
|
|
1805
|
-
/// A verified near-miss pair
|
|
1806
|
-
|
|
1828
|
+
/// A verified near-miss pair of blocks; without `cores`, the blocks match as wholes.
|
|
1829
|
+
struct MatchEdge {
|
|
1830
|
+
left: usize,
|
|
1831
|
+
right: usize,
|
|
1832
|
+
cores: Option<CorePair>,
|
|
1833
|
+
/// The token runs the pair matches, on the left and on the right.
|
|
1834
|
+
matched_runs: (TokenRuns, TokenRuns),
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
impl MatchEdge {
|
|
1838
|
+
/// The (block, core) on each side.
|
|
1839
|
+
fn sides(&self) -> [(usize, Option<(usize, usize)>); 2] {
|
|
1840
|
+
let (left_core, right_core) = self.cores.unzip();
|
|
1841
|
+
[(self.left, left_core), (self.right, right_core)]
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1807
1844
|
|
|
1808
1845
|
/// Detects near-miss (Type-3) clone groups among block candidates the exact pipeline left
|
|
1809
1846
|
/// unreported: NIL-style n-gram filtration, then pair verification (near_miss::Matcher), then
|
|
1810
1847
|
/// transitive clustering of verified pairs (crossFileNearMiss.ts applies the same model across
|
|
1811
1848
|
/// files). A block that matched only locally is reported as its matched cores (overlapping cores
|
|
1812
1849
|
/// merged), each clustered with its own partners, so code no verified pair matched never counts
|
|
1813
|
-
/// as duplicated.
|
|
1850
|
+
/// as duplicated. A node overlapping a reported occurrence is an anchor: it joins its group at its
|
|
1851
|
+
/// own size, so the copies of a block are listed at the size they matched it, and is marked as
|
|
1852
|
+
/// counted elsewhere unless the group takes over the reported occurrences it overlaps. An anchor
|
|
1853
|
+
/// still counts the lines its partners match outside those occurrences.
|
|
1814
1854
|
fn collect_near_miss_groups(
|
|
1815
1855
|
source: &TokenizedSource<'_>,
|
|
1816
1856
|
settings: &DuplicationSettings,
|
|
@@ -1825,22 +1865,23 @@ fn collect_near_miss_groups(
|
|
|
1825
1865
|
return Vec::new();
|
|
1826
1866
|
}
|
|
1827
1867
|
|
|
1828
|
-
// Reported-group indices whose occurrences overlap a token range
|
|
1829
|
-
//
|
|
1868
|
+
// Reported-group indices whose occurrences' matched segments overlap a token range (the gap
|
|
1869
|
+
// of a gapped clone is not reported content): near-miss nodes covering such content anchor
|
|
1870
|
+
// comparisons but are never re-reported.
|
|
1830
1871
|
let touched_groups_in = |start: usize, end: usize| -> Vec<usize> {
|
|
1831
1872
|
reported_groups
|
|
1832
1873
|
.iter()
|
|
1833
1874
|
.enumerate()
|
|
1834
1875
|
.filter(|(_, group)| {
|
|
1835
|
-
group
|
|
1836
|
-
|
|
1837
|
-
|
|
1876
|
+
group
|
|
1877
|
+
.iter()
|
|
1878
|
+
.any(|occurrence| occurrence.matches_within((start, end)))
|
|
1838
1879
|
})
|
|
1839
1880
|
.map(|(group_index, _)| group_index)
|
|
1840
1881
|
.collect()
|
|
1841
1882
|
};
|
|
1842
1883
|
|
|
1843
|
-
let (symbols, is_content) = to_symbol_stream(tokens);
|
|
1884
|
+
let (symbols, is_content, token_keys) = to_symbol_stream(tokens);
|
|
1844
1885
|
let statements = top_level_statement_finder(&source.container_statement_ranges);
|
|
1845
1886
|
let mut blocks: Vec<Block> = comparable
|
|
1846
1887
|
.iter()
|
|
@@ -1848,6 +1889,7 @@ fn collect_near_miss_groups(
|
|
|
1848
1889
|
Block::new(
|
|
1849
1890
|
&symbols,
|
|
1850
1891
|
&is_content,
|
|
1892
|
+
&token_keys,
|
|
1851
1893
|
range.start_token_index,
|
|
1852
1894
|
range.end_token_index,
|
|
1853
1895
|
statements(range.start_token_index, range.end_token_index),
|
|
@@ -1860,27 +1902,69 @@ fn collect_near_miss_groups(
|
|
|
1860
1902
|
settings.min_similarity_percent,
|
|
1861
1903
|
);
|
|
1862
1904
|
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1905
|
+
// The lines of a near-miss copy that are duplicated are those a verified partner matches,
|
|
1906
|
+
// which takes the pairing itself. Every pair kept is aligned: verifying it already cost a
|
|
1907
|
+
// longest common subsequence of the same sequences, and any choice among a copy's partners
|
|
1908
|
+
// would let their number or their order decide what the copy counts.
|
|
1909
|
+
//
|
|
1910
|
+
// A pair matching nothing outside reported content has nothing to add to it and is dropped
|
|
1911
|
+
// before clustering, so that it neither collapses the blocks' core nodes nor widens a node
|
|
1912
|
+
// beyond what the pairs clustering it matched. Any other pair is kept however much of it is
|
|
1913
|
+
// reported: what the exact pipeline reports of a copy depends on where its copies stand, and
|
|
1914
|
+
// the lines the copy counts must not.
|
|
1915
|
+
let mut reported_prefix = vec![0usize; tokens.len() + 1];
|
|
1916
|
+
{
|
|
1917
|
+
let mut reported = vec![false; tokens.len()];
|
|
1918
|
+
for occurrence in reported_groups.iter().flatten() {
|
|
1919
|
+
for &(start, end) in &occurrence.segments {
|
|
1920
|
+
reported[start..end].fill(true);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
for (index, &flag) in reported.iter().enumerate() {
|
|
1924
|
+
reported_prefix[index + 1] = reported_prefix[index] + usize::from(flag);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
let reported_count =
|
|
1928
|
+
|(start, end): (usize, usize)| reported_prefix[end] - reported_prefix[start];
|
|
1929
|
+
let unreported_count = |runs: &[(usize, usize)]| -> usize {
|
|
1930
|
+
runs.iter()
|
|
1931
|
+
.map(|&run| run.1 - run.0 - reported_count(run))
|
|
1932
|
+
.sum()
|
|
1933
|
+
};
|
|
1934
|
+
let block_span = |index: usize| {
|
|
1935
|
+
(
|
|
1936
|
+
comparable[index].start_token_index,
|
|
1937
|
+
comparable[index].end_token_index,
|
|
1938
|
+
)
|
|
1939
|
+
};
|
|
1867
1940
|
let mut edges: Vec<MatchEdge> = Vec::new();
|
|
1941
|
+
let mut add_edge = |left: usize, right: usize, cores: Option<CorePair>, alignment| {
|
|
1942
|
+
let (left_span, right_span) = cores.unwrap_or((block_span(left), block_span(right)));
|
|
1943
|
+
// Fully reported spans need no alignment to be dropped.
|
|
1944
|
+
if unreported_count(&[left_span, right_span]) == 0 {
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
let matched_runs =
|
|
1948
|
+
matcher.align(&blocks[left], &blocks[right], cores, alignment, &token_keys);
|
|
1949
|
+
if unreported_count(&matched_runs.0) + unreported_count(&matched_runs.1) == 0 {
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
edges.push(MatchEdge {
|
|
1953
|
+
left,
|
|
1954
|
+
right,
|
|
1955
|
+
cores,
|
|
1956
|
+
matched_runs,
|
|
1957
|
+
});
|
|
1958
|
+
};
|
|
1868
1959
|
for_each_candidate_pair(
|
|
1869
1960
|
&blocks,
|
|
1870
1961
|
settings.min_similarity_percent,
|
|
1871
1962
|
|left_index, right_index| match matcher.verify(&blocks[left_index], &blocks[right_index]) {
|
|
1872
1963
|
None => {}
|
|
1873
|
-
|
|
1874
|
-
// a group, and recording it would collapse the blocks' core nodes.
|
|
1875
|
-
Some(PairMatch::Whole)
|
|
1876
|
-
if !(block_touched[left_index] && block_touched[right_index]) =>
|
|
1877
|
-
{
|
|
1878
|
-
edges.push((left_index, None, right_index, None))
|
|
1879
|
-
}
|
|
1880
|
-
Some(PairMatch::Whole) => {}
|
|
1964
|
+
Some(PairMatch::Whole(alignment)) => add_edge(left_index, right_index, None, alignment),
|
|
1881
1965
|
Some(PairMatch::Local(cores)) => {
|
|
1882
|
-
for (
|
|
1883
|
-
|
|
1966
|
+
for (core_pair, alignment) in cores {
|
|
1967
|
+
add_edge(left_index, right_index, Some(core_pair), alignment);
|
|
1884
1968
|
}
|
|
1885
1969
|
}
|
|
1886
1970
|
},
|
|
@@ -1891,8 +1975,8 @@ fn collect_near_miss_groups(
|
|
|
1891
1975
|
// cores matched with different partners fall into separate groups.
|
|
1892
1976
|
let mut matched_whole = vec![false; comparable.len()];
|
|
1893
1977
|
let mut local_cores: Vec<Vec<(usize, usize)>> = vec![Vec::new(); comparable.len()];
|
|
1894
|
-
for
|
|
1895
|
-
for (index, core) in
|
|
1978
|
+
for edge in &edges {
|
|
1979
|
+
for (index, core) in edge.sides() {
|
|
1896
1980
|
match core {
|
|
1897
1981
|
Some(core) => local_cores[index].push(core),
|
|
1898
1982
|
None => matched_whole[index] = true,
|
|
@@ -1958,22 +2042,22 @@ fn collect_near_miss_groups(
|
|
|
1958
2042
|
touched_groups_in(start, end)
|
|
1959
2043
|
})
|
|
1960
2044
|
.collect();
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
// Two already-reported nodes have nothing new to contribute to each other.
|
|
1967
|
-
if !touched_groups_by_node[left_node].is_empty()
|
|
1968
|
-
&& !touched_groups_by_node[right_node].is_empty()
|
|
1969
|
-
{
|
|
1970
|
-
continue;
|
|
1971
|
-
}
|
|
2045
|
+
let edge_nodes: Vec<[usize; 2]> = edges
|
|
2046
|
+
.iter()
|
|
2047
|
+
.map(|edge| edge.sides().map(|(index, core)| node_of(index, core)))
|
|
2048
|
+
.collect();
|
|
2049
|
+
for &[left_node, right_node] in &edge_nodes {
|
|
1972
2050
|
let left_root = find(&mut parent, left_node);
|
|
1973
2051
|
let right_root = find(&mut parent, right_node);
|
|
1974
2052
|
parent[left_root.max(right_root)] = left_root.min(right_root);
|
|
1975
2053
|
}
|
|
1976
2054
|
|
|
2055
|
+
let mut matched_runs_by_node: Vec<Vec<(usize, usize)>> = vec![Vec::new(); node_blocks.len()];
|
|
2056
|
+
for (edge, nodes) in edges.iter().zip(&edge_nodes) {
|
|
2057
|
+
matched_runs_by_node[nodes[0]].extend(&edge.matched_runs.0);
|
|
2058
|
+
matched_runs_by_node[nodes[1]].extend(&edge.matched_runs.1);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1977
2061
|
let mut members_by_root: IndexMap<usize, Vec<usize>> = IndexMap::new();
|
|
1978
2062
|
for node in 0..node_blocks.len() {
|
|
1979
2063
|
let root = find(&mut parent, node);
|
|
@@ -2001,8 +2085,15 @@ fn collect_near_miss_groups(
|
|
|
2001
2085
|
segments.sort_unstable();
|
|
2002
2086
|
let whole = spans.iter().any(Option::is_none);
|
|
2003
2087
|
let (start, end) = (segments[0].0, segments[segments.len() - 1].1);
|
|
2088
|
+
let matched_runs: Vec<(usize, usize)> = nodes
|
|
2089
|
+
.iter()
|
|
2090
|
+
.filter(|&&node| node_blocks[node] == block)
|
|
2091
|
+
.flat_map(|&node| matched_runs_by_node[node].iter().copied())
|
|
2092
|
+
.collect();
|
|
2004
2093
|
CountedOccurrence {
|
|
2005
2094
|
shared_with_merged_group: false,
|
|
2095
|
+
matched_runs: Some(merge_overlapping_cores(&matched_runs)),
|
|
2096
|
+
exact_runs: Vec::new(),
|
|
2006
2097
|
token_count: segments.iter().map(|segment| segment.1 - segment.0).sum(),
|
|
2007
2098
|
segments,
|
|
2008
2099
|
start_token_index: start,
|
|
@@ -2022,6 +2113,13 @@ fn collect_near_miss_groups(
|
|
|
2022
2113
|
.collect()
|
|
2023
2114
|
};
|
|
2024
2115
|
let touched_groups_of = |node: usize| &touched_groups_by_node[node];
|
|
2116
|
+
let to_anchor_occurrences = |nodes: &[usize]| -> Vec<CountedOccurrence> {
|
|
2117
|
+
let mut anchors = to_occurrences(nodes);
|
|
2118
|
+
for anchor in &mut anchors {
|
|
2119
|
+
anchor.shared_with_merged_group = true;
|
|
2120
|
+
}
|
|
2121
|
+
anchors
|
|
2122
|
+
};
|
|
2025
2123
|
let mut groups: Vec<Vec<CountedOccurrence>> = Vec::new();
|
|
2026
2124
|
for members in members_by_root.values() {
|
|
2027
2125
|
if members.len() < 2 {
|
|
@@ -2044,17 +2142,42 @@ fn collect_near_miss_groups(
|
|
|
2044
2142
|
}
|
|
2045
2143
|
continue;
|
|
2046
2144
|
}
|
|
2047
|
-
if
|
|
2145
|
+
if members.iter().all(|&node| {
|
|
2146
|
+
let (start, end) = node_range(node);
|
|
2147
|
+
reported_count((start, end)) == end - start
|
|
2148
|
+
}) {
|
|
2048
2149
|
continue;
|
|
2049
2150
|
}
|
|
2050
|
-
// An anchored cluster extends a reported group only when
|
|
2051
|
-
//
|
|
2052
|
-
// reports content the cluster does not share
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2151
|
+
// An anchored cluster extends a reported group only when it stands for the same copies:
|
|
2152
|
+
// every occurrence of the group overlaps the member nodes of exactly one block (one
|
|
2153
|
+
// disjoint from all members reports content the cluster does not share, and one reaching
|
|
2154
|
+
// into two members would make one copy take over lines of the next), exceeds the span of
|
|
2155
|
+
// those nodes by less than `min_tokens` (a copy is listed at the size it matched, not at
|
|
2156
|
+
// that of a clone larger by a reportable part, which may run into code no member
|
|
2157
|
+
// matched), and shares that block with no other occurrence of the group (those repeat
|
|
2158
|
+
// within the copy, not between the copies).
|
|
2159
|
+
let member_block_of = |occurrence: &CountedOccurrence| {
|
|
2160
|
+
let mut blocks = members
|
|
2161
|
+
.iter()
|
|
2162
|
+
.filter(|&&index| occurrence.matches_within(node_range(index)))
|
|
2163
|
+
.map(|&index| node_blocks[index]);
|
|
2164
|
+
let block = blocks.next()?;
|
|
2165
|
+
let mut spans = members
|
|
2166
|
+
.iter()
|
|
2167
|
+
.filter(|&&index| node_blocks[index] == block)
|
|
2168
|
+
.map(|&index| node_range(index));
|
|
2169
|
+
let first = spans.next()?;
|
|
2170
|
+
let span = spans.fold(first, |span, node| (span.0.min(node.0), span.1.max(node.1)));
|
|
2171
|
+
let excess = span.0.saturating_sub(occurrence.start_token_index)
|
|
2172
|
+
+ occurrence.end_token_index.saturating_sub(span.1);
|
|
2173
|
+
(blocks.all(|other| other == block) && excess < settings.min_tokens).then_some(block)
|
|
2174
|
+
};
|
|
2175
|
+
let stands_for_members = |group: &[CountedOccurrence]| {
|
|
2176
|
+
let mut blocks: FxHashSet<usize> = FxHashSet::default();
|
|
2177
|
+
!group.is_empty()
|
|
2178
|
+
&& group.iter().all(|occurrence| {
|
|
2179
|
+
member_block_of(occurrence).is_some_and(|block| blocks.insert(block))
|
|
2180
|
+
})
|
|
2058
2181
|
};
|
|
2059
2182
|
// Ascending by construction: BTreeSet iteration is sorted and filter preserves order.
|
|
2060
2183
|
let fully_clustered: Vec<usize> = covered
|
|
@@ -2062,18 +2185,15 @@ fn collect_near_miss_groups(
|
|
|
2062
2185
|
.flat_map(|&index| touched_groups_of(index).iter().copied())
|
|
2063
2186
|
.collect::<std::collections::BTreeSet<usize>>()
|
|
2064
2187
|
.into_iter()
|
|
2065
|
-
.filter(|&group_index|
|
|
2066
|
-
let group = &reported_groups[group_index];
|
|
2067
|
-
!group.is_empty() && group.iter().all(&overlaps_member)
|
|
2068
|
-
})
|
|
2188
|
+
.filter(|&group_index| stands_for_members(&reported_groups[group_index]))
|
|
2069
2189
|
.collect();
|
|
2070
2190
|
if let Some((&target_index, source_indexes)) = fully_clustered.split_first() {
|
|
2071
2191
|
// Rebuild the component as ONE group with one coalesced occurrence per member block:
|
|
2072
|
-
// the fragments
|
|
2192
|
+
// the fragments the nodes of a block overlap are collected together, since a block's
|
|
2073
2193
|
// cores are parts of one copy.
|
|
2074
|
-
let mut consumed: FxHashSet<(usize, usize)> = FxHashSet::default();
|
|
2075
2194
|
let mut merged: Vec<CountedOccurrence> = Vec::new();
|
|
2076
2195
|
let mut unanchored_nodes: Vec<usize> = Vec::new();
|
|
2196
|
+
let mut anchor_nodes: Vec<usize> = Vec::new();
|
|
2077
2197
|
let mut nodes_by_block: IndexMap<usize, Vec<usize>> = IndexMap::new();
|
|
2078
2198
|
for &member_index in members {
|
|
2079
2199
|
nodes_by_block
|
|
@@ -2082,73 +2202,51 @@ fn collect_near_miss_groups(
|
|
|
2082
2202
|
.push(member_index);
|
|
2083
2203
|
}
|
|
2084
2204
|
for block_nodes in nodes_by_block.values() {
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
{
|
|
2097
|
-
if !consumed.contains(&(group_index, occurrence_index))
|
|
2098
|
-
&& occurrence.start_token_index < range_end
|
|
2099
|
-
&& range_start < occurrence.end_token_index
|
|
2100
|
-
{
|
|
2101
|
-
consumed.insert((group_index, occurrence_index));
|
|
2102
|
-
fragments.push((occurrence.clone(), group_index));
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
if fragments.len() == fragment_count && touched_groups_of(node).is_empty() {
|
|
2107
|
-
plain_nodes.push(node);
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2205
|
+
let overlaps_block_node = |occurrence: &&CountedOccurrence| {
|
|
2206
|
+
block_nodes
|
|
2207
|
+
.iter()
|
|
2208
|
+
.any(|&node| occurrence.matches_within(node_range(node)))
|
|
2209
|
+
};
|
|
2210
|
+
let mut fragments: Vec<CountedOccurrence> = fully_clustered
|
|
2211
|
+
.iter()
|
|
2212
|
+
.flat_map(|&group_index| &reported_groups[group_index])
|
|
2213
|
+
.filter(overlaps_block_node)
|
|
2214
|
+
.cloned()
|
|
2215
|
+
.collect();
|
|
2110
2216
|
if fragments.is_empty() {
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
fragments.sort_by_key(|(occurrence, _)| {
|
|
2120
|
-
(occurrence.start_token_index, occurrence.end_token_index)
|
|
2121
|
-
});
|
|
2122
|
-
let mut copy_parts: Vec<CountedOccurrence> = Vec::new();
|
|
2123
|
-
let mut copy_groups: FxHashSet<usize> = FxHashSet::default();
|
|
2124
|
-
for (occurrence, group_index) in fragments {
|
|
2125
|
-
if copy_groups.contains(&group_index) {
|
|
2126
|
-
merged.push(coalesce_occurrences(std::mem::take(&mut copy_parts)));
|
|
2127
|
-
copy_groups.clear();
|
|
2128
|
-
}
|
|
2129
|
-
copy_parts.push(occurrence);
|
|
2130
|
-
copy_groups.insert(group_index);
|
|
2131
|
-
}
|
|
2132
|
-
if !copy_parts.is_empty() {
|
|
2133
|
-
merged.push(coalesce_occurrences(copy_parts));
|
|
2217
|
+
let (anchors, plain): (Vec<usize>, Vec<usize>) = block_nodes
|
|
2218
|
+
.iter()
|
|
2219
|
+
.partition(|&&node| !touched_groups_of(node).is_empty());
|
|
2220
|
+
anchor_nodes.extend(anchors);
|
|
2221
|
+
unanchored_nodes.extend(plain);
|
|
2222
|
+
continue;
|
|
2134
2223
|
}
|
|
2224
|
+
// The block's nodes are part of the same copy as its fragments: a block matched
|
|
2225
|
+
// beyond the fragments it holds is a copy at that size.
|
|
2226
|
+
fragments.extend(to_occurrences(block_nodes));
|
|
2227
|
+
merged.push(coalesce_occurrences(fragments));
|
|
2135
2228
|
}
|
|
2136
2229
|
merged.extend(to_occurrences(&unanchored_nodes));
|
|
2137
|
-
merged.sort_by_key(|occurrence| {
|
|
2138
|
-
(occurrence.start_token_index, occurrence.end_token_index)
|
|
2139
|
-
});
|
|
2140
2230
|
// The rebuild consumed every fully-clustered group, so shared-span marks from earlier
|
|
2141
2231
|
// partial merges no longer point at a separate merged group.
|
|
2142
2232
|
for occurrence in &mut merged {
|
|
2143
2233
|
occurrence.shared_with_merged_group = false;
|
|
2144
2234
|
}
|
|
2235
|
+
merged.extend(to_anchor_occurrences(&anchor_nodes));
|
|
2236
|
+
merged.sort_by_key(|occurrence| {
|
|
2237
|
+
(occurrence.start_token_index, occurrence.end_token_index)
|
|
2238
|
+
});
|
|
2145
2239
|
reported_groups[target_index] = merged;
|
|
2146
2240
|
for &source_index in source_indexes {
|
|
2147
2241
|
reported_groups[source_index].clear();
|
|
2148
2242
|
}
|
|
2149
2243
|
} else {
|
|
2150
|
-
let occurrences = to_occurrences(&uncovered);
|
|
2244
|
+
let mut occurrences = to_occurrences(&uncovered);
|
|
2245
|
+
occurrences.extend(to_anchor_occurrences(&covered));
|
|
2151
2246
|
if occurrences.len() >= 2 {
|
|
2247
|
+
occurrences.sort_by_key(|occurrence| {
|
|
2248
|
+
(occurrence.start_token_index, occurrence.end_token_index)
|
|
2249
|
+
});
|
|
2152
2250
|
groups.push(occurrences);
|
|
2153
2251
|
}
|
|
2154
2252
|
}
|
|
@@ -2263,8 +2361,41 @@ fn coalesce_occurrences(occurrences: Vec<CountedOccurrence>) -> CountedOccurrenc
|
|
|
2263
2361
|
_ => segments.push(segment),
|
|
2264
2362
|
}
|
|
2265
2363
|
}
|
|
2364
|
+
// An exact fragment is matched throughout, a near-miss part where its partners match it.
|
|
2365
|
+
let matched_runs = occurrences
|
|
2366
|
+
.iter()
|
|
2367
|
+
.any(|occurrence| occurrence.matched_runs.is_some())
|
|
2368
|
+
.then(|| {
|
|
2369
|
+
let runs: Vec<(usize, usize)> = occurrences
|
|
2370
|
+
.iter()
|
|
2371
|
+
.flat_map(|occurrence| {
|
|
2372
|
+
occurrence
|
|
2373
|
+
.matched_runs
|
|
2374
|
+
.as_ref()
|
|
2375
|
+
.unwrap_or(&occurrence.segments)
|
|
2376
|
+
.iter()
|
|
2377
|
+
.copied()
|
|
2378
|
+
})
|
|
2379
|
+
.collect();
|
|
2380
|
+
merge_overlapping_cores(&runs)
|
|
2381
|
+
});
|
|
2382
|
+
let exact_runs: Vec<(usize, usize)> = occurrences
|
|
2383
|
+
.iter()
|
|
2384
|
+
.flat_map(|occurrence| match occurrence.matched_runs {
|
|
2385
|
+
Some(_) => &occurrence.exact_runs,
|
|
2386
|
+
None => &occurrence.segments,
|
|
2387
|
+
})
|
|
2388
|
+
.copied()
|
|
2389
|
+
.collect();
|
|
2266
2390
|
CountedOccurrence {
|
|
2267
2391
|
shared_with_merged_group: false,
|
|
2392
|
+
// Without a near-miss part, the segments are matched throughout and say it all.
|
|
2393
|
+
exact_runs: if matched_runs.is_some() {
|
|
2394
|
+
merge_overlapping_cores(&exact_runs)
|
|
2395
|
+
} else {
|
|
2396
|
+
Vec::new()
|
|
2397
|
+
},
|
|
2398
|
+
matched_runs,
|
|
2268
2399
|
token_count: segments.iter().map(|segment| segment.1 - segment.0).sum(),
|
|
2269
2400
|
start_token_index: occurrences
|
|
2270
2401
|
.iter()
|
|
@@ -2300,32 +2431,45 @@ fn merge_overlapping_cores(cores: &[(usize, usize)]) -> Vec<(usize, usize)> {
|
|
|
2300
2431
|
/// other token interned from its hash pairs (literal VALUES folded in, unlike the exact
|
|
2301
2432
|
/// fingerprint's kind tags), plus which tokens are content-bearing (names and literal values).
|
|
2302
2433
|
/// Interned per call so a file's symbol ids (and thus its n-gram hashes) never depend on which
|
|
2303
|
-
/// other files the process measured before it.
|
|
2304
|
-
|
|
2434
|
+
/// other files the process measured before it. A symbol id still depends on where in the file its
|
|
2435
|
+
/// token first occurs, so each non-identifier token also gets a key of its content alone.
|
|
2436
|
+
fn to_symbol_stream(tokens: &[Token<'_>]) -> (Vec<i32>, Vec<bool>, Vec<i64>) {
|
|
2305
2437
|
let mut symbol_by_token_hashes: FxHashMap<(i32, i32, i32, i32), i32> = FxHashMap::default();
|
|
2306
2438
|
let mut id_by_identifier: FxHashMap<&str, i32> = FxHashMap::default();
|
|
2307
|
-
tokens
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
.
|
|
2439
|
+
let mut symbols = Vec::with_capacity(tokens.len());
|
|
2440
|
+
let mut is_content = Vec::with_capacity(tokens.len());
|
|
2441
|
+
let mut token_keys = Vec::with_capacity(tokens.len());
|
|
2442
|
+
for token in tokens {
|
|
2443
|
+
if token.is_id {
|
|
2444
|
+
let next_id = id_by_identifier.len() as i32;
|
|
2445
|
+
let id = *id_by_identifier
|
|
2446
|
+
.entry(token.text.as_ref())
|
|
2447
|
+
.or_insert(next_id);
|
|
2448
|
+
symbols.push(-(id + 1));
|
|
2449
|
+
is_content.push(false);
|
|
2450
|
+
token_keys.push(0);
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
let hashes = (
|
|
2454
|
+
token.text_hash,
|
|
2455
|
+
token.text_hash2,
|
|
2456
|
+
token.literal_hash.unwrap_or(0),
|
|
2457
|
+
token.literal_hash2.unwrap_or(0),
|
|
2458
|
+
);
|
|
2459
|
+
let next_symbol = symbol_by_token_hashes.len() as i32;
|
|
2460
|
+
symbols.push(*symbol_by_token_hashes.entry(hashes).or_insert(next_symbol));
|
|
2461
|
+
is_content.push(token.is_name || token.literal_hash.is_some());
|
|
2462
|
+
token_keys.push(token_content_key(hashes));
|
|
2463
|
+
}
|
|
2464
|
+
(symbols, is_content, token_keys)
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
/// A 53-bit key of a token's two text hashes, each mixed with the matching literal value hash; the
|
|
2468
|
+
/// same key as `tokenKey` in crossFileNearMiss.ts, so that both detectors order content alike.
|
|
2469
|
+
fn token_content_key((text, text2, literal, literal2): (i32, i32, i32, i32)) -> i64 {
|
|
2470
|
+
let primary = (text ^ literal.wrapping_mul(0x9E37_79B1_u32 as i32)) as u32;
|
|
2471
|
+
let secondary = (text2 ^ literal2.wrapping_mul(0x85EB_CA6B_u32 as i32)) as u32;
|
|
2472
|
+
i64::from(primary) * 0x20_0000 + i64::from(secondary >> 11)
|
|
2329
2473
|
}
|
|
2330
2474
|
|
|
2331
2475
|
/// Returns a lookup of the outermost container statements inside a token range, excluding a
|
|
@@ -2429,23 +2573,44 @@ fn count_redundant_fragments(group: &[CountedOccurrence]) -> usize {
|
|
|
2429
2573
|
}
|
|
2430
2574
|
|
|
2431
2575
|
/// Only CODE lines carrying matched tokens count; the unmatched gap of a merged clone stays out
|
|
2432
|
-
/// of line coverage.
|
|
2576
|
+
/// of line coverage. A line of a near-miss copy counts when its partners match more than half of
|
|
2577
|
+
/// its tokens: a line the copy adds or rewrites is not duplicated, while a longest common
|
|
2578
|
+
/// subsequence still pairs a stray token of it (a bracket, a separator) with one of the partner.
|
|
2579
|
+
/// A line an exact fragment of the copy reaches counts in any case.
|
|
2433
2580
|
fn collect_matched_lines(
|
|
2434
2581
|
occurrence: &CountedOccurrence,
|
|
2435
2582
|
code_line_numbers: &FxHashSet<usize>,
|
|
2436
2583
|
tokens: &[Token<'_>],
|
|
2437
2584
|
) -> FxHashSet<usize> {
|
|
2438
|
-
let mut
|
|
2585
|
+
let mut token_counts_by_row: FxHashMap<usize, (usize, usize)> = FxHashMap::default();
|
|
2586
|
+
let mut runs = occurrence.matched_runs.iter().flatten().peekable();
|
|
2439
2587
|
for &(segment_start, segment_end) in &occurrence.segments {
|
|
2440
|
-
|
|
2588
|
+
let segment = tokens.iter().enumerate().take(segment_end);
|
|
2589
|
+
for (index, token) in segment.skip(segment_start) {
|
|
2590
|
+
while runs.next_if(|run| run.1 <= index).is_some() {}
|
|
2591
|
+
let matched =
|
|
2592
|
+
occurrence.matched_runs.is_none() || runs.peek().is_some_and(|run| run.0 <= index);
|
|
2441
2593
|
for row in token.start_row..=token.end_row {
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2594
|
+
let (matched_count, token_count) = token_counts_by_row.entry(row + 1).or_default();
|
|
2595
|
+
*matched_count += usize::from(matched);
|
|
2596
|
+
*token_count += 1;
|
|
2445
2597
|
}
|
|
2446
2598
|
}
|
|
2447
2599
|
}
|
|
2448
|
-
|
|
2600
|
+
let exact_rows: FxHashSet<usize> = occurrence
|
|
2601
|
+
.exact_runs
|
|
2602
|
+
.iter()
|
|
2603
|
+
.flat_map(|&(start, end)| &tokens[start..end])
|
|
2604
|
+
.flat_map(|token| token.start_row + 1..=token.end_row + 1)
|
|
2605
|
+
.collect();
|
|
2606
|
+
token_counts_by_row
|
|
2607
|
+
.into_iter()
|
|
2608
|
+
.filter(|(line, (matched_count, token_count))| {
|
|
2609
|
+
code_line_numbers.contains(line)
|
|
2610
|
+
&& (matched_count * 2 > *token_count || exact_rows.contains(line))
|
|
2611
|
+
})
|
|
2612
|
+
.map(|(line, _)| line)
|
|
2613
|
+
.collect()
|
|
2449
2614
|
}
|
|
2450
2615
|
|
|
2451
2616
|
fn summarize_duplicates(
|