code-gauge 10.0.0 → 10.2.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 +56 -34
- package/THIRD-PARTY-NOTICES.txt +8 -8
- package/dist/checkCommand.cjs +2 -2
- package/dist/checkCommand.cjs.map +1 -1
- package/dist/checkCommand.d.ts +2 -5
- package/dist/checkCommand.js +2 -2
- 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 +14 -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 +6 -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 +22 -7
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/nativeMetrics.cjs +1 -1
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +1 -1
- package/dist/nativeMetrics.js +1 -1
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/thresholdCheck.cjs +1 -1
- package/dist/thresholdCheck.cjs.map +1 -1
- package/dist/thresholdCheck.d.ts +10 -8
- package/dist/thresholdCheck.js +1 -1
- package/dist/thresholdCheck.js.map +1 -1
- package/dist/thresholds.cjs +1 -1
- package/dist/thresholds.cjs.map +1 -1
- package/dist/thresholds.d.ts +5 -3
- package/dist/thresholds.js +1 -1
- package/dist/thresholds.js.map +1 -1
- package/dist/types.d.ts +29 -7
- package/dist/wasmBinding.cjs +1 -1
- package/dist/wasmBinding.js +1 -1
- 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 +213 -85
- package/native/src/functions.rs +112 -24
- package/native/src/languages.rs +5 -0
- package/native/src/lib.rs +1 -1
- package/native/src/measure.rs +9 -4
- package/native/src/near_miss.rs +303 -52
- package/native/src/types.rs +3 -0
- 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
|
+
Alignment, Block, CorePair, Matcher, PairMatch, 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,13 @@ 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)>>,
|
|
1583
1589
|
/// Sum of segment token counts (the gap tokens are not matched content).
|
|
1584
1590
|
token_count: usize,
|
|
1585
1591
|
start_token_index: usize,
|
|
@@ -1597,6 +1603,7 @@ fn to_counted_groups(
|
|
|
1597
1603
|
.iter()
|
|
1598
1604
|
.map(|candidate| CountedOccurrence {
|
|
1599
1605
|
shared_with_merged_group: false,
|
|
1606
|
+
matched_runs: None,
|
|
1600
1607
|
segments: vec![(candidate.start_token_index, candidate.end_token_index)],
|
|
1601
1608
|
token_count: candidate.token_count,
|
|
1602
1609
|
start_token_index: candidate.start_token_index,
|
|
@@ -1785,6 +1792,7 @@ fn merge_groups(
|
|
|
1785
1792
|
CountedOccurrence {
|
|
1786
1793
|
// A merged occurrence is a fresh span combination; it inherits no shared marks.
|
|
1787
1794
|
shared_with_merged_group: false,
|
|
1795
|
+
matched_runs: None,
|
|
1788
1796
|
segments: [leading.segments.clone(), trailing.segments.clone()].concat(),
|
|
1789
1797
|
token_count: leading.token_count + trailing.token_count,
|
|
1790
1798
|
start_token_index: leading.start_token_index,
|
|
@@ -1802,15 +1810,30 @@ fn merge_groups(
|
|
|
1802
1810
|
})
|
|
1803
1811
|
}
|
|
1804
1812
|
|
|
1805
|
-
/// A verified near-miss pair
|
|
1806
|
-
|
|
1813
|
+
/// A verified near-miss pair of blocks; without `cores`, the blocks match as wholes.
|
|
1814
|
+
struct MatchEdge {
|
|
1815
|
+
left: usize,
|
|
1816
|
+
right: usize,
|
|
1817
|
+
cores: Option<CorePair>,
|
|
1818
|
+
alignment: Alignment,
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
impl MatchEdge {
|
|
1822
|
+
/// The (block, core) on each side.
|
|
1823
|
+
fn sides(&self) -> [(usize, Option<(usize, usize)>); 2] {
|
|
1824
|
+
let (left_core, right_core) = self.cores.unzip();
|
|
1825
|
+
[(self.left, left_core), (self.right, right_core)]
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1807
1828
|
|
|
1808
1829
|
/// Detects near-miss (Type-3) clone groups among block candidates the exact pipeline left
|
|
1809
1830
|
/// unreported: NIL-style n-gram filtration, then pair verification (near_miss::Matcher), then
|
|
1810
1831
|
/// transitive clustering of verified pairs (crossFileNearMiss.ts applies the same model across
|
|
1811
1832
|
/// files). A block that matched only locally is reported as its matched cores (overlapping cores
|
|
1812
1833
|
/// merged), each clustered with its own partners, so code no verified pair matched never counts
|
|
1813
|
-
/// as duplicated.
|
|
1834
|
+
/// as duplicated. A node overlapping a reported occurrence is an anchor: it joins its group at its
|
|
1835
|
+
/// 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.
|
|
1814
1837
|
fn collect_near_miss_groups(
|
|
1815
1838
|
source: &TokenizedSource<'_>,
|
|
1816
1839
|
settings: &DuplicationSettings,
|
|
@@ -1840,7 +1863,7 @@ fn collect_near_miss_groups(
|
|
|
1840
1863
|
.collect()
|
|
1841
1864
|
};
|
|
1842
1865
|
|
|
1843
|
-
let (symbols, is_content) = to_symbol_stream(tokens);
|
|
1866
|
+
let (symbols, is_content, token_keys) = to_symbol_stream(tokens);
|
|
1844
1867
|
let statements = top_level_statement_finder(&source.container_statement_ranges);
|
|
1845
1868
|
let mut blocks: Vec<Block> = comparable
|
|
1846
1869
|
.iter()
|
|
@@ -1848,6 +1871,7 @@ fn collect_near_miss_groups(
|
|
|
1848
1871
|
Block::new(
|
|
1849
1872
|
&symbols,
|
|
1850
1873
|
&is_content,
|
|
1874
|
+
&token_keys,
|
|
1851
1875
|
range.start_token_index,
|
|
1852
1876
|
range.end_token_index,
|
|
1853
1877
|
statements(range.start_token_index, range.end_token_index),
|
|
@@ -1860,9 +1884,13 @@ fn collect_near_miss_groups(
|
|
|
1860
1884
|
settings.min_similarity_percent,
|
|
1861
1885
|
);
|
|
1862
1886
|
|
|
1887
|
+
// Two spans that both overlap reported content have nothing new to contribute to each other.
|
|
1888
|
+
// Their pair is dropped before clustering, so that it neither collapses the blocks' core nodes
|
|
1889
|
+
// nor widens a node beyond what the pairs clustering it matched.
|
|
1890
|
+
let is_touched = |(start, end): (usize, usize)| !touched_groups_in(start, end).is_empty();
|
|
1863
1891
|
let block_touched: Vec<bool> = comparable
|
|
1864
1892
|
.iter()
|
|
1865
|
-
.map(|range|
|
|
1893
|
+
.map(|range| is_touched((range.start_token_index, range.end_token_index)))
|
|
1866
1894
|
.collect();
|
|
1867
1895
|
let mut edges: Vec<MatchEdge> = Vec::new();
|
|
1868
1896
|
for_each_candidate_pair(
|
|
@@ -1870,17 +1898,27 @@ fn collect_near_miss_groups(
|
|
|
1870
1898
|
settings.min_similarity_percent,
|
|
1871
1899
|
|left_index, right_index| match matcher.verify(&blocks[left_index], &blocks[right_index]) {
|
|
1872
1900
|
None => {}
|
|
1873
|
-
|
|
1874
|
-
// a group, and recording it would collapse the blocks' core nodes.
|
|
1875
|
-
Some(PairMatch::Whole)
|
|
1901
|
+
Some(PairMatch::Whole(alignment))
|
|
1876
1902
|
if !(block_touched[left_index] && block_touched[right_index]) =>
|
|
1877
1903
|
{
|
|
1878
|
-
edges.push(
|
|
1904
|
+
edges.push(MatchEdge {
|
|
1905
|
+
left: left_index,
|
|
1906
|
+
right: right_index,
|
|
1907
|
+
cores: None,
|
|
1908
|
+
alignment,
|
|
1909
|
+
})
|
|
1879
1910
|
}
|
|
1880
|
-
Some(PairMatch::Whole) => {}
|
|
1911
|
+
Some(PairMatch::Whole(_)) => {}
|
|
1881
1912
|
Some(PairMatch::Local(cores)) => {
|
|
1882
|
-
for (
|
|
1883
|
-
|
|
1913
|
+
for (core_pair, alignment) in cores {
|
|
1914
|
+
if !(is_touched(core_pair.0) && is_touched(core_pair.1)) {
|
|
1915
|
+
edges.push(MatchEdge {
|
|
1916
|
+
left: left_index,
|
|
1917
|
+
right: right_index,
|
|
1918
|
+
cores: Some(core_pair),
|
|
1919
|
+
alignment,
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1884
1922
|
}
|
|
1885
1923
|
}
|
|
1886
1924
|
},
|
|
@@ -1891,8 +1929,8 @@ fn collect_near_miss_groups(
|
|
|
1891
1929
|
// cores matched with different partners fall into separate groups.
|
|
1892
1930
|
let mut matched_whole = vec![false; comparable.len()];
|
|
1893
1931
|
let mut local_cores: Vec<Vec<(usize, usize)>> = vec![Vec::new(); comparable.len()];
|
|
1894
|
-
for
|
|
1895
|
-
for (index, core) in
|
|
1932
|
+
for edge in &edges {
|
|
1933
|
+
for (index, core) in edge.sides() {
|
|
1896
1934
|
match core {
|
|
1897
1935
|
Some(core) => local_cores[index].push(core),
|
|
1898
1936
|
None => matched_whole[index] = true,
|
|
@@ -1958,22 +1996,33 @@ fn collect_near_miss_groups(
|
|
|
1958
1996
|
touched_groups_in(start, end)
|
|
1959
1997
|
})
|
|
1960
1998
|
.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
|
-
}
|
|
1999
|
+
let edge_nodes: Vec<[usize; 2]> = edges
|
|
2000
|
+
.iter()
|
|
2001
|
+
.map(|edge| edge.sides().map(|(index, core)| node_of(index, core)))
|
|
2002
|
+
.collect();
|
|
2003
|
+
for &[left_node, right_node] in &edge_nodes {
|
|
1972
2004
|
let left_root = find(&mut parent, left_node);
|
|
1973
2005
|
let right_root = find(&mut parent, right_node);
|
|
1974
2006
|
parent[left_root.max(right_root)] = left_root.min(right_root);
|
|
1975
2007
|
}
|
|
1976
2008
|
|
|
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
|
+
let mut matched_runs_by_node: Vec<Vec<(usize, usize)>> = vec![Vec::new(); node_blocks.len()];
|
|
2014
|
+
for (edge, nodes) in edges.iter().zip(&edge_nodes) {
|
|
2015
|
+
let (left_runs, right_runs) = matcher.align(
|
|
2016
|
+
&blocks[edge.left],
|
|
2017
|
+
&blocks[edge.right],
|
|
2018
|
+
edge.cores,
|
|
2019
|
+
edge.alignment,
|
|
2020
|
+
&token_keys,
|
|
2021
|
+
);
|
|
2022
|
+
matched_runs_by_node[nodes[0]].extend(left_runs);
|
|
2023
|
+
matched_runs_by_node[nodes[1]].extend(right_runs);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1977
2026
|
let mut members_by_root: IndexMap<usize, Vec<usize>> = IndexMap::new();
|
|
1978
2027
|
for node in 0..node_blocks.len() {
|
|
1979
2028
|
let root = find(&mut parent, node);
|
|
@@ -2001,8 +2050,14 @@ fn collect_near_miss_groups(
|
|
|
2001
2050
|
segments.sort_unstable();
|
|
2002
2051
|
let whole = spans.iter().any(Option::is_none);
|
|
2003
2052
|
let (start, end) = (segments[0].0, segments[segments.len() - 1].1);
|
|
2053
|
+
let matched_runs: Vec<(usize, usize)> = nodes
|
|
2054
|
+
.iter()
|
|
2055
|
+
.filter(|&&node| node_blocks[node] == block)
|
|
2056
|
+
.flat_map(|&node| matched_runs_by_node[node].iter().copied())
|
|
2057
|
+
.collect();
|
|
2004
2058
|
CountedOccurrence {
|
|
2005
2059
|
shared_with_merged_group: false,
|
|
2060
|
+
matched_runs: Some(merge_overlapping_cores(&matched_runs)),
|
|
2006
2061
|
token_count: segments.iter().map(|segment| segment.1 - segment.0).sum(),
|
|
2007
2062
|
segments,
|
|
2008
2063
|
start_token_index: start,
|
|
@@ -2022,6 +2077,13 @@ fn collect_near_miss_groups(
|
|
|
2022
2077
|
.collect()
|
|
2023
2078
|
};
|
|
2024
2079
|
let touched_groups_of = |node: usize| &touched_groups_by_node[node];
|
|
2080
|
+
let to_anchor_occurrences = |nodes: &[usize]| -> Vec<CountedOccurrence> {
|
|
2081
|
+
let mut anchors = to_occurrences(nodes);
|
|
2082
|
+
for anchor in &mut anchors {
|
|
2083
|
+
anchor.shared_with_merged_group = true;
|
|
2084
|
+
}
|
|
2085
|
+
anchors
|
|
2086
|
+
};
|
|
2025
2087
|
let mut groups: Vec<Vec<CountedOccurrence>> = Vec::new();
|
|
2026
2088
|
for members in members_by_root.values() {
|
|
2027
2089
|
if members.len() < 2 {
|
|
@@ -2074,6 +2136,7 @@ fn collect_near_miss_groups(
|
|
|
2074
2136
|
let mut consumed: FxHashSet<(usize, usize)> = FxHashSet::default();
|
|
2075
2137
|
let mut merged: Vec<CountedOccurrence> = Vec::new();
|
|
2076
2138
|
let mut unanchored_nodes: Vec<usize> = Vec::new();
|
|
2139
|
+
let mut anchor_nodes: Vec<usize> = Vec::new();
|
|
2077
2140
|
let mut nodes_by_block: IndexMap<usize, Vec<usize>> = IndexMap::new();
|
|
2078
2141
|
for &member_index in members {
|
|
2079
2142
|
nodes_by_block
|
|
@@ -2086,10 +2149,8 @@ fn collect_near_miss_groups(
|
|
|
2086
2149
|
// groups belong to the same copy. Consecutive position-order slices keep the
|
|
2087
2150
|
// coalesced spans disjoint.
|
|
2088
2151
|
let mut fragments: Vec<(CountedOccurrence, usize)> = Vec::new();
|
|
2089
|
-
let mut plain_nodes: Vec<usize> = Vec::new();
|
|
2090
2152
|
for &node in block_nodes {
|
|
2091
2153
|
let (range_start, range_end) = node_range(node);
|
|
2092
|
-
let fragment_count = fragments.len();
|
|
2093
2154
|
for &group_index in &fully_clustered {
|
|
2094
2155
|
for (occurrence_index, occurrence) in
|
|
2095
2156
|
reported_groups[group_index].iter().enumerate()
|
|
@@ -2103,18 +2164,20 @@ fn collect_near_miss_groups(
|
|
|
2103
2164
|
}
|
|
2104
2165
|
}
|
|
2105
2166
|
}
|
|
2106
|
-
if fragments.len() == fragment_count && touched_groups_of(node).is_empty() {
|
|
2107
|
-
plain_nodes.push(node);
|
|
2108
|
-
}
|
|
2109
2167
|
}
|
|
2110
2168
|
if fragments.is_empty() {
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2169
|
+
let (anchors, plain): (Vec<usize>, Vec<usize>) = block_nodes
|
|
2170
|
+
.iter()
|
|
2171
|
+
.partition(|&&node| !touched_groups_of(node).is_empty());
|
|
2172
|
+
anchor_nodes.extend(anchors);
|
|
2173
|
+
unanchored_nodes.extend(plain);
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
// The block's nodes are part of the same copy as its fragments: a block matched
|
|
2177
|
+
// beyond the fragments it holds is a copy at that size. A group index no reported
|
|
2178
|
+
// group uses keeps them in that copy.
|
|
2179
|
+
for occurrence in to_occurrences(block_nodes) {
|
|
2180
|
+
fragments.push((occurrence, usize::MAX));
|
|
2118
2181
|
}
|
|
2119
2182
|
fragments.sort_by_key(|(occurrence, _)| {
|
|
2120
2183
|
(occurrence.start_token_index, occurrence.end_token_index)
|
|
@@ -2134,21 +2197,26 @@ fn collect_near_miss_groups(
|
|
|
2134
2197
|
}
|
|
2135
2198
|
}
|
|
2136
2199
|
merged.extend(to_occurrences(&unanchored_nodes));
|
|
2137
|
-
merged.sort_by_key(|occurrence| {
|
|
2138
|
-
(occurrence.start_token_index, occurrence.end_token_index)
|
|
2139
|
-
});
|
|
2140
2200
|
// The rebuild consumed every fully-clustered group, so shared-span marks from earlier
|
|
2141
2201
|
// partial merges no longer point at a separate merged group.
|
|
2142
2202
|
for occurrence in &mut merged {
|
|
2143
2203
|
occurrence.shared_with_merged_group = false;
|
|
2144
2204
|
}
|
|
2205
|
+
merged.extend(to_anchor_occurrences(&anchor_nodes));
|
|
2206
|
+
merged.sort_by_key(|occurrence| {
|
|
2207
|
+
(occurrence.start_token_index, occurrence.end_token_index)
|
|
2208
|
+
});
|
|
2145
2209
|
reported_groups[target_index] = merged;
|
|
2146
2210
|
for &source_index in source_indexes {
|
|
2147
2211
|
reported_groups[source_index].clear();
|
|
2148
2212
|
}
|
|
2149
2213
|
} else {
|
|
2150
|
-
let occurrences = to_occurrences(&uncovered);
|
|
2214
|
+
let mut occurrences = to_occurrences(&uncovered);
|
|
2215
|
+
occurrences.extend(to_anchor_occurrences(&covered));
|
|
2151
2216
|
if occurrences.len() >= 2 {
|
|
2217
|
+
occurrences.sort_by_key(|occurrence| {
|
|
2218
|
+
(occurrence.start_token_index, occurrence.end_token_index)
|
|
2219
|
+
});
|
|
2152
2220
|
groups.push(occurrences);
|
|
2153
2221
|
}
|
|
2154
2222
|
}
|
|
@@ -2263,8 +2331,27 @@ fn coalesce_occurrences(occurrences: Vec<CountedOccurrence>) -> CountedOccurrenc
|
|
|
2263
2331
|
_ => segments.push(segment),
|
|
2264
2332
|
}
|
|
2265
2333
|
}
|
|
2334
|
+
// An exact fragment is matched throughout, a near-miss part where its partners match it.
|
|
2335
|
+
let matched_runs = occurrences
|
|
2336
|
+
.iter()
|
|
2337
|
+
.any(|occurrence| occurrence.matched_runs.is_some())
|
|
2338
|
+
.then(|| {
|
|
2339
|
+
let runs: Vec<(usize, usize)> = occurrences
|
|
2340
|
+
.iter()
|
|
2341
|
+
.flat_map(|occurrence| {
|
|
2342
|
+
occurrence
|
|
2343
|
+
.matched_runs
|
|
2344
|
+
.as_ref()
|
|
2345
|
+
.unwrap_or(&occurrence.segments)
|
|
2346
|
+
.iter()
|
|
2347
|
+
.copied()
|
|
2348
|
+
})
|
|
2349
|
+
.collect();
|
|
2350
|
+
merge_overlapping_cores(&runs)
|
|
2351
|
+
});
|
|
2266
2352
|
CountedOccurrence {
|
|
2267
2353
|
shared_with_merged_group: false,
|
|
2354
|
+
matched_runs,
|
|
2268
2355
|
token_count: segments.iter().map(|segment| segment.1 - segment.0).sum(),
|
|
2269
2356
|
start_token_index: occurrences
|
|
2270
2357
|
.iter()
|
|
@@ -2300,32 +2387,45 @@ fn merge_overlapping_cores(cores: &[(usize, usize)]) -> Vec<(usize, usize)> {
|
|
|
2300
2387
|
/// other token interned from its hash pairs (literal VALUES folded in, unlike the exact
|
|
2301
2388
|
/// fingerprint's kind tags), plus which tokens are content-bearing (names and literal values).
|
|
2302
2389
|
/// 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
|
-
|
|
2390
|
+
/// other files the process measured before it. A symbol id still depends on where in the file its
|
|
2391
|
+
/// token first occurs, so each non-identifier token also gets a key of its content alone.
|
|
2392
|
+
fn to_symbol_stream(tokens: &[Token<'_>]) -> (Vec<i32>, Vec<bool>, Vec<i64>) {
|
|
2305
2393
|
let mut symbol_by_token_hashes: FxHashMap<(i32, i32, i32, i32), i32> = FxHashMap::default();
|
|
2306
2394
|
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
|
-
.
|
|
2395
|
+
let mut symbols = Vec::with_capacity(tokens.len());
|
|
2396
|
+
let mut is_content = Vec::with_capacity(tokens.len());
|
|
2397
|
+
let mut token_keys = Vec::with_capacity(tokens.len());
|
|
2398
|
+
for token in tokens {
|
|
2399
|
+
if token.is_id {
|
|
2400
|
+
let next_id = id_by_identifier.len() as i32;
|
|
2401
|
+
let id = *id_by_identifier
|
|
2402
|
+
.entry(token.text.as_ref())
|
|
2403
|
+
.or_insert(next_id);
|
|
2404
|
+
symbols.push(-(id + 1));
|
|
2405
|
+
is_content.push(false);
|
|
2406
|
+
token_keys.push(0);
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
let hashes = (
|
|
2410
|
+
token.text_hash,
|
|
2411
|
+
token.text_hash2,
|
|
2412
|
+
token.literal_hash.unwrap_or(0),
|
|
2413
|
+
token.literal_hash2.unwrap_or(0),
|
|
2414
|
+
);
|
|
2415
|
+
let next_symbol = symbol_by_token_hashes.len() as i32;
|
|
2416
|
+
symbols.push(*symbol_by_token_hashes.entry(hashes).or_insert(next_symbol));
|
|
2417
|
+
is_content.push(token.is_name || token.literal_hash.is_some());
|
|
2418
|
+
token_keys.push(token_content_key(hashes));
|
|
2419
|
+
}
|
|
2420
|
+
(symbols, is_content, token_keys)
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
/// A 53-bit key of a token's two text hashes, each mixed with the matching literal value hash; the
|
|
2424
|
+
/// same key as `tokenKey` in crossFileNearMiss.ts, so that both detectors order content alike.
|
|
2425
|
+
fn token_content_key((text, text2, literal, literal2): (i32, i32, i32, i32)) -> i64 {
|
|
2426
|
+
let primary = (text ^ literal.wrapping_mul(0x9E37_79B1_u32 as i32)) as u32;
|
|
2427
|
+
let secondary = (text2 ^ literal2.wrapping_mul(0x85EB_CA6B_u32 as i32)) as u32;
|
|
2428
|
+
i64::from(primary) * 0x20_0000 + i64::from(secondary >> 11)
|
|
2329
2429
|
}
|
|
2330
2430
|
|
|
2331
2431
|
/// Returns a lookup of the outermost container statements inside a token range, excluding a
|
|
@@ -2428,6 +2528,39 @@ fn count_redundant_fragments(group: &[CountedOccurrence]) -> usize {
|
|
|
2428
2528
|
}
|
|
2429
2529
|
}
|
|
2430
2530
|
|
|
2531
|
+
/// Only CODE lines carrying matched tokens count; the unmatched gap of a merged clone stays out
|
|
2532
|
+
/// of line coverage. A line of a near-miss copy counts when its partners match more than half of
|
|
2533
|
+
/// its tokens: a line the copy adds or rewrites is not duplicated, while a longest common
|
|
2534
|
+
/// subsequence still pairs a stray token of it (a bracket, a separator) with one of the partner.
|
|
2535
|
+
fn collect_matched_lines(
|
|
2536
|
+
occurrence: &CountedOccurrence,
|
|
2537
|
+
code_line_numbers: &FxHashSet<usize>,
|
|
2538
|
+
tokens: &[Token<'_>],
|
|
2539
|
+
) -> FxHashSet<usize> {
|
|
2540
|
+
let mut token_counts_by_row: FxHashMap<usize, (usize, usize)> = FxHashMap::default();
|
|
2541
|
+
let mut runs = occurrence.matched_runs.iter().flatten().peekable();
|
|
2542
|
+
for &(segment_start, segment_end) in &occurrence.segments {
|
|
2543
|
+
let segment = tokens.iter().enumerate().take(segment_end);
|
|
2544
|
+
for (index, token) in segment.skip(segment_start) {
|
|
2545
|
+
while runs.next_if(|run| run.1 <= index).is_some() {}
|
|
2546
|
+
let matched =
|
|
2547
|
+
occurrence.matched_runs.is_none() || runs.peek().is_some_and(|run| run.0 <= index);
|
|
2548
|
+
for row in token.start_row..=token.end_row {
|
|
2549
|
+
let (matched_count, token_count) = token_counts_by_row.entry(row + 1).or_default();
|
|
2550
|
+
*matched_count += usize::from(matched);
|
|
2551
|
+
*token_count += 1;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
token_counts_by_row
|
|
2556
|
+
.into_iter()
|
|
2557
|
+
.filter(|(line, (matched_count, token_count))| {
|
|
2558
|
+
code_line_numbers.contains(line) && matched_count * 2 > *token_count
|
|
2559
|
+
})
|
|
2560
|
+
.map(|(line, _)| line)
|
|
2561
|
+
.collect()
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2431
2564
|
fn summarize_duplicates(
|
|
2432
2565
|
groups: &[Vec<CountedOccurrence>],
|
|
2433
2566
|
code_line_numbers: &FxHashSet<usize>,
|
|
@@ -2439,25 +2572,20 @@ fn summarize_duplicates(
|
|
|
2439
2572
|
let mut duplicated_lines: FxHashSet<usize> = FxHashSet::default();
|
|
2440
2573
|
for group in groups {
|
|
2441
2574
|
duplicate_block_count += count_redundant_fragments(group);
|
|
2442
|
-
for occurrence in group {
|
|
2443
|
-
max_duplicate_block_size = max_duplicate_block_size.max(occurrence.token_count);
|
|
2444
|
-
// Only CODE lines carrying matched tokens count; the unmatched gap of a merged clone
|
|
2445
|
-
// stays out of line coverage.
|
|
2446
|
-
for &(segment_start, segment_end) in &occurrence.segments {
|
|
2447
|
-
for token in &tokens[segment_start..segment_end.min(tokens.len())] {
|
|
2448
|
-
for row in token.start_row..=token.end_row {
|
|
2449
|
-
if code_line_numbers.contains(&(row + 1)) {
|
|
2450
|
-
duplicated_lines.insert(row + 1);
|
|
2451
|
-
}
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
}
|
|
2456
2575
|
let mut occurrences: Vec<DuplicateBlockOccurrence> = group
|
|
2457
2576
|
.iter()
|
|
2458
|
-
.map(|occurrence|
|
|
2459
|
-
|
|
2460
|
-
|
|
2577
|
+
.map(|occurrence| {
|
|
2578
|
+
max_duplicate_block_size = max_duplicate_block_size.max(occurrence.token_count);
|
|
2579
|
+
let lines = collect_matched_lines(occurrence, code_line_numbers, tokens);
|
|
2580
|
+
duplicated_lines.extend(&lines);
|
|
2581
|
+
let mut line_numbers: Vec<usize> = lines.into_iter().collect();
|
|
2582
|
+
line_numbers.sort_unstable();
|
|
2583
|
+
DuplicateBlockOccurrence {
|
|
2584
|
+
start_line: occurrence.start_line,
|
|
2585
|
+
end_line: occurrence.end_line,
|
|
2586
|
+
line_numbers,
|
|
2587
|
+
token_count: occurrence.token_count,
|
|
2588
|
+
}
|
|
2461
2589
|
})
|
|
2462
2590
|
.collect();
|
|
2463
2591
|
occurrences.sort_by_key(|occurrence| occurrence.start_line);
|