code-gauge 4.4.0 → 4.6.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 +16 -5
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.d.ts +11 -4
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/crossFileNearMiss.cjs +2 -0
- package/dist/crossFileNearMiss.cjs.map +1 -0
- package/dist/crossFileNearMiss.d.ts +33 -0
- package/dist/crossFileNearMiss.js +2 -0
- package/dist/crossFileNearMiss.js.map +1 -0
- package/dist/diffCommand.cjs +1 -1
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +3 -3
- package/dist/diffCommand.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.d.ts +11 -0
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/metrics.cjs +1 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.ts +10 -0
- package/dist/metrics.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +2 -2
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +7 -2
- package/dist/nativeMetrics.js +2 -2
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.d.ts +12 -1
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/types.d.ts +4 -2
- package/native/src/dep_degree.rs +2 -3
- package/native/src/duplication.rs +490 -337
- package/native/src/functions.rs +1 -1
- package/native/src/lib.rs +7 -2
- package/native/src/measure.rs +35 -11
- package/native/src/near_miss.rs +455 -0
- package/native/src/types.rs +5 -0
- package/package.json +10 -10
|
@@ -4,13 +4,14 @@ 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,
|
|
10
11
|
};
|
|
11
12
|
use crate::util::{all_children, is_identifier_leaf, named_children, node_text, to_int32, Source};
|
|
12
13
|
|
|
13
|
-
/// Block-like nodes considered as whole-subtree duplicate candidates
|
|
14
|
+
/// Block-like nodes considered as whole-subtree duplicate candidates.
|
|
14
15
|
const DUPLICATE_BLOCK_TYPES: &[&str] = &[
|
|
15
16
|
"statement_block",
|
|
16
17
|
"block",
|
|
@@ -314,13 +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); see duplication.ts.
|
|
318
|
-
const NEAR_MISS_NGRAM_SIZE: usize = 5;
|
|
319
|
-
/// Filtration threshold: shared distinct n-grams over the smaller set; see duplication.ts.
|
|
320
|
-
const NEAR_MISS_FILTRATION_PERCENT: usize = 10;
|
|
321
|
-
/// Exclusive bound on shared content-bearing tokens (names and literal values); see duplication.ts.
|
|
322
|
-
const MIN_CONTENT_SIMILARITY_PERCENT: usize = 50;
|
|
323
|
-
|
|
324
318
|
/// See isLiteralDense in duplication.ts: >= 20% literal values marks a region as data-like.
|
|
325
319
|
fn is_literal_dense(literal_count: usize, token_count: usize) -> bool {
|
|
326
320
|
literal_count * 5 >= token_count
|
|
@@ -377,15 +371,17 @@ struct DuplicateCandidate {
|
|
|
377
371
|
end_line: usize,
|
|
378
372
|
}
|
|
379
373
|
|
|
380
|
-
///
|
|
381
|
-
///
|
|
382
|
-
pub
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
374
|
+
/// A file's normalized token stream with the block and statement structure clone detection
|
|
375
|
+
/// matches over, built once per parse and shared by within-file and cross-file detection.
|
|
376
|
+
pub struct TokenizedSource<'a> {
|
|
377
|
+
tokens: Vec<Token<'a>>,
|
|
378
|
+
block_ranges: Vec<TokenRange>,
|
|
379
|
+
container_statement_ranges: Vec<Vec<TokenRange>>,
|
|
380
|
+
literal_count_prefix: Vec<usize>,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
pub fn tokenize<'a>(root: Node<'_>, code: &Source<'a>) -> TokenizedSource<'a> {
|
|
384
|
+
let mut tokens: Vec<Token<'a>> = Vec::new();
|
|
389
385
|
let mut block_ranges: Vec<TokenRange> = Vec::new();
|
|
390
386
|
let mut container_statement_ranges: Vec<Vec<TokenRange>> = Vec::new();
|
|
391
387
|
collect_tokens(
|
|
@@ -395,66 +391,71 @@ pub fn measure_duplication(
|
|
|
395
391
|
&mut block_ranges,
|
|
396
392
|
&mut container_statement_ranges,
|
|
397
393
|
);
|
|
398
|
-
|
|
399
394
|
let literal_count_prefix = build_literal_count_prefix(&tokens);
|
|
395
|
+
TokenizedSource {
|
|
396
|
+
tokens,
|
|
397
|
+
block_ranges,
|
|
398
|
+
container_statement_ranges,
|
|
399
|
+
literal_count_prefix,
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/// Detects copy-pasted regions within a file. Fingerprints replicate the JavaScript int32 hash
|
|
404
|
+
/// arithmetic of fingerprintKey in duplication.ts, so its candidates group with the window
|
|
405
|
+
/// candidates cross-file matching fingerprints in TypeScript.
|
|
406
|
+
pub fn measure_duplication(
|
|
407
|
+
source: &TokenizedSource<'_>,
|
|
408
|
+
code_line_numbers: &HashSet<usize>,
|
|
409
|
+
settings: &DuplicationSettings,
|
|
410
|
+
) -> DuplicationMetrics {
|
|
411
|
+
let tokens = &source.tokens;
|
|
412
|
+
let literal_count_prefix = &source.literal_count_prefix;
|
|
400
413
|
let mut candidates = collect_block_candidates(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
&block_ranges,
|
|
414
|
+
tokens,
|
|
415
|
+
literal_count_prefix,
|
|
416
|
+
&source.block_ranges,
|
|
404
417
|
settings.min_tokens,
|
|
405
418
|
);
|
|
406
419
|
candidates.extend(collect_sequence_candidates(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
&container_statement_ranges,
|
|
420
|
+
tokens,
|
|
421
|
+
literal_count_prefix,
|
|
422
|
+
&source.container_statement_ranges,
|
|
410
423
|
settings.min_tokens,
|
|
411
424
|
));
|
|
412
425
|
let counted = select_maximal_duplicates(candidates);
|
|
413
426
|
let mut groups = merge_adjacent_groups(to_counted_groups(&counted), settings.max_gap_tokens);
|
|
414
|
-
let near_miss = collect_near_miss_groups(
|
|
415
|
-
&tokens,
|
|
416
|
-
&literal_count_prefix,
|
|
417
|
-
&block_ranges,
|
|
418
|
-
settings,
|
|
419
|
-
&mut groups,
|
|
420
|
-
);
|
|
427
|
+
let near_miss = collect_near_miss_groups(source, settings, &mut groups);
|
|
421
428
|
// Near-miss clustering can merge exact groups away, leaving empty entries behind.
|
|
422
429
|
groups.retain(|group| !group.is_empty());
|
|
423
430
|
groups.extend(near_miss);
|
|
424
|
-
summarize_duplicates(&groups, code_line_numbers,
|
|
431
|
+
summarize_duplicates(&groups, code_line_numbers, tokens)
|
|
425
432
|
}
|
|
426
433
|
|
|
427
434
|
/// Collects one file's contribution to cross-file clone detection: catalogued candidates (whole
|
|
428
|
-
/// block subtrees plus each statement container's full run)
|
|
429
|
-
///
|
|
430
|
-
///
|
|
431
|
-
///
|
|
435
|
+
/// block subtrees plus each statement container's full run), the normalized token stream and
|
|
436
|
+
/// statement structure, and the blocks near-miss comparison considers. Source indexes are emitted
|
|
437
|
+
/// in UTF-16 code units (the tree is parsed from UTF-16, so node byte offsets are halved) to match
|
|
438
|
+
/// JavaScript string indexes.
|
|
432
439
|
pub fn collect_cross_file_file_data(
|
|
433
|
-
|
|
434
|
-
code: &Source<'_>,
|
|
440
|
+
source: &TokenizedSource<'_>,
|
|
435
441
|
min_tokens: usize,
|
|
436
442
|
) -> (
|
|
437
443
|
Vec<CrossFileCandidate>,
|
|
438
444
|
Vec<CrossFileToken>,
|
|
439
445
|
Vec<Vec<CrossFileTokenRange>>,
|
|
446
|
+
Vec<CrossFileTokenRange>,
|
|
440
447
|
) {
|
|
441
|
-
let
|
|
442
|
-
let
|
|
443
|
-
let mut
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
&mut block_ranges,
|
|
449
|
-
&mut container_statement_ranges,
|
|
448
|
+
let tokens = &source.tokens;
|
|
449
|
+
let literal_count_prefix = &source.literal_count_prefix;
|
|
450
|
+
let mut candidates = collect_block_candidates(
|
|
451
|
+
tokens,
|
|
452
|
+
literal_count_prefix,
|
|
453
|
+
&source.block_ranges,
|
|
454
|
+
min_tokens,
|
|
450
455
|
);
|
|
451
|
-
let literal_count_prefix = build_literal_count_prefix(&tokens);
|
|
452
|
-
|
|
453
|
-
let mut candidates =
|
|
454
|
-
collect_block_candidates(&tokens, &literal_count_prefix, &block_ranges, min_tokens);
|
|
455
456
|
// Single-statement containers are catalogued too: a file whose only top-level statement is not
|
|
456
457
|
// a block type (a lone exported table) must still be matchable when wholly copied.
|
|
457
|
-
for statements in &container_statement_ranges {
|
|
458
|
+
for statements in &source.container_statement_ranges {
|
|
458
459
|
let (Some(first), Some(last)) = (statements.first(), statements.last()) else {
|
|
459
460
|
continue;
|
|
460
461
|
};
|
|
@@ -465,8 +466,8 @@ pub fn collect_cross_file_file_data(
|
|
|
465
466
|
let fingerprint = format!(
|
|
466
467
|
"s:{}",
|
|
467
468
|
fingerprint_key(
|
|
468
|
-
|
|
469
|
-
|
|
469
|
+
tokens,
|
|
470
|
+
literal_count_prefix,
|
|
470
471
|
first.start_token_index,
|
|
471
472
|
last.end_token_index
|
|
472
473
|
)
|
|
@@ -507,29 +508,34 @@ pub fn collect_cross_file_file_data(
|
|
|
507
508
|
end_row: token.end_row,
|
|
508
509
|
})
|
|
509
510
|
.collect();
|
|
510
|
-
let container_statement_payloads =
|
|
511
|
+
let container_statement_payloads = source
|
|
512
|
+
.container_statement_ranges
|
|
511
513
|
.iter()
|
|
512
|
-
.map(|statements|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
end_token_index: range.end_token_index,
|
|
518
|
-
start_index: range.start_index / 2,
|
|
519
|
-
end_index: range.end_index / 2,
|
|
520
|
-
start_line: range.start_line,
|
|
521
|
-
end_line: range.end_line,
|
|
522
|
-
})
|
|
523
|
-
.collect()
|
|
524
|
-
})
|
|
514
|
+
.map(|statements| statements.iter().map(to_token_range_payload).collect())
|
|
515
|
+
.collect();
|
|
516
|
+
let near_miss_block_payloads = select_near_miss_blocks(source, min_tokens)
|
|
517
|
+
.into_iter()
|
|
518
|
+
.map(to_token_range_payload)
|
|
525
519
|
.collect();
|
|
526
520
|
(
|
|
527
521
|
candidate_payloads,
|
|
528
522
|
token_payloads,
|
|
529
523
|
container_statement_payloads,
|
|
524
|
+
near_miss_block_payloads,
|
|
530
525
|
)
|
|
531
526
|
}
|
|
532
527
|
|
|
528
|
+
fn to_token_range_payload(range: &TokenRange) -> CrossFileTokenRange {
|
|
529
|
+
CrossFileTokenRange {
|
|
530
|
+
start_token_index: range.start_token_index,
|
|
531
|
+
end_token_index: range.end_token_index,
|
|
532
|
+
start_index: range.start_index / 2,
|
|
533
|
+
end_index: range.end_index / 2,
|
|
534
|
+
start_line: range.start_line,
|
|
535
|
+
end_line: range.end_line,
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
533
539
|
fn collect_tokens<'a>(
|
|
534
540
|
root: Node<'_>,
|
|
535
541
|
code: &Source<'a>,
|
|
@@ -577,8 +583,9 @@ fn collect_tokens<'a>(
|
|
|
577
583
|
statement_ranges.push(child_range);
|
|
578
584
|
}
|
|
579
585
|
}
|
|
580
|
-
// Single-statement containers are recorded too
|
|
581
|
-
//
|
|
586
|
+
// Single-statement containers are recorded too: window enumeration needs two
|
|
587
|
+
// statements and yields nothing for them, but cross-file matching catalogues each
|
|
588
|
+
// container's full run.
|
|
582
589
|
if is_container && !statement_ranges.is_empty() {
|
|
583
590
|
container_statement_ranges.push(statement_ranges);
|
|
584
591
|
}
|
|
@@ -731,8 +738,8 @@ fn make_text_token<'a>(
|
|
|
731
738
|
}
|
|
732
739
|
}
|
|
733
740
|
|
|
734
|
-
/// The value of a literal as folded into literal-dense fingerprints
|
|
735
|
-
///
|
|
741
|
+
/// The value of a literal as folded into literal-dense fingerprints, independent of its delimiter
|
|
742
|
+
/// spelling (quote style, C# verbatim prefix) so equal values in differently quoted copies match.
|
|
736
743
|
fn literal_value_text<'a>(node: Node<'_>, kind: &str, code: &Source<'a>) -> Cow<'a, str> {
|
|
737
744
|
if kind != "#str" && kind != "#char" {
|
|
738
745
|
return Cow::Borrowed(node_text(node, code));
|
|
@@ -759,8 +766,8 @@ fn literal_value_text<'a>(node: Node<'_>, kind: &str, code: &Source<'a>) -> Cow<
|
|
|
759
766
|
Cow::Borrowed(strip_matching_quotes(text))
|
|
760
767
|
}
|
|
761
768
|
|
|
762
|
-
/// Strips one matching pair of surrounding ASCII quotes,
|
|
763
|
-
///
|
|
769
|
+
/// Strips one matching pair of surrounding ASCII quotes (quote characters are ASCII, so byte
|
|
770
|
+
/// indexing is UTF-8 safe).
|
|
764
771
|
fn strip_matching_quotes(text: &str) -> &str {
|
|
765
772
|
let bytes = text.as_bytes();
|
|
766
773
|
if bytes.len() >= 2 {
|
|
@@ -989,7 +996,7 @@ struct ContainerWindows {
|
|
|
989
996
|
statement_hashes: Vec<i32>,
|
|
990
997
|
}
|
|
991
998
|
|
|
992
|
-
/// Enumerates runs of consecutive sibling statements; see
|
|
999
|
+
/// Enumerates runs of consecutive sibling statements; see collectSequenceWindowCandidates in
|
|
993
1000
|
/// duplication.ts for the maximality and sub-window rules replicated here.
|
|
994
1001
|
fn collect_sequence_candidates(
|
|
995
1002
|
tokens: &[Token<'_>],
|
|
@@ -1319,7 +1326,7 @@ fn combine_hashes(hash: i64, value: i64) -> i64 {
|
|
|
1319
1326
|
(to_int32(hash).wrapping_mul(31)) as i64 + value
|
|
1320
1327
|
}
|
|
1321
1328
|
|
|
1322
|
-
/// Keeps only maximal, non-overlapping duplicates; see
|
|
1329
|
+
/// Keeps only maximal, non-overlapping duplicates; see selectMaximalGroups in duplicateSelection.ts.
|
|
1323
1330
|
fn select_maximal_duplicates(
|
|
1324
1331
|
candidates: Vec<DuplicateCandidate>,
|
|
1325
1332
|
) -> IndexMap<std::rc::Rc<str>, Vec<DuplicateCandidate>> {
|
|
@@ -1474,8 +1481,9 @@ fn merge_adjacent_groups(
|
|
|
1474
1481
|
let mut restart = true;
|
|
1475
1482
|
while restart {
|
|
1476
1483
|
restart = false;
|
|
1477
|
-
|
|
1478
|
-
|
|
1484
|
+
let partners_by_group = collect_gap_adjacent_partners(&groups, max_gap_tokens);
|
|
1485
|
+
'outer: for (left_index, partners) in partners_by_group.iter().enumerate() {
|
|
1486
|
+
for &right_index in partners {
|
|
1479
1487
|
let forward =
|
|
1480
1488
|
merge_groups(&groups[left_index], &groups[right_index], max_gap_tokens);
|
|
1481
1489
|
let swapped = forward.is_none();
|
|
@@ -1516,6 +1524,43 @@ fn merge_adjacent_groups(
|
|
|
1516
1524
|
groups
|
|
1517
1525
|
}
|
|
1518
1526
|
|
|
1527
|
+
/// Per group index, the ascending indexes of later groups that merge_groups can pair with it;
|
|
1528
|
+
/// see collectGapAdjacentPartners in duplication.ts.
|
|
1529
|
+
fn collect_gap_adjacent_partners(
|
|
1530
|
+
groups: &[Vec<CountedOccurrence>],
|
|
1531
|
+
max_gap_tokens: usize,
|
|
1532
|
+
) -> Vec<Vec<usize>> {
|
|
1533
|
+
let mut starts: Vec<(usize, usize)> = groups
|
|
1534
|
+
.iter()
|
|
1535
|
+
.enumerate()
|
|
1536
|
+
.flat_map(|(group_index, group)| {
|
|
1537
|
+
group
|
|
1538
|
+
.iter()
|
|
1539
|
+
.map(move |occurrence| (occurrence.start_token_index, group_index))
|
|
1540
|
+
})
|
|
1541
|
+
.collect();
|
|
1542
|
+
starts.sort_unstable();
|
|
1543
|
+
let mut partners: Vec<std::collections::BTreeSet<usize>> =
|
|
1544
|
+
vec![std::collections::BTreeSet::new(); groups.len()];
|
|
1545
|
+
for (group_index, group) in groups.iter().enumerate() {
|
|
1546
|
+
for occurrence in group {
|
|
1547
|
+
let first = starts.partition_point(|&(start, _)| start < occurrence.end_token_index);
|
|
1548
|
+
for &(start, other) in &starts[first..] {
|
|
1549
|
+
if start > occurrence.end_token_index + max_gap_tokens {
|
|
1550
|
+
break;
|
|
1551
|
+
}
|
|
1552
|
+
if other != group_index {
|
|
1553
|
+
partners[other.min(group_index)].insert(other.max(group_index));
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
partners
|
|
1559
|
+
.into_iter()
|
|
1560
|
+
.map(|set| set.into_iter().collect())
|
|
1561
|
+
.collect()
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1519
1564
|
fn group_sort_key(group: &[CountedOccurrence]) -> (usize, usize) {
|
|
1520
1565
|
group
|
|
1521
1566
|
.first()
|
|
@@ -1611,79 +1656,136 @@ fn merge_groups(
|
|
|
1611
1656
|
})
|
|
1612
1657
|
}
|
|
1613
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
|
+
|
|
1614
1662
|
/// Detects near-miss (Type-3) clone groups among block candidates the exact pipeline left
|
|
1615
|
-
/// unreported
|
|
1616
|
-
///
|
|
1617
|
-
///
|
|
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.
|
|
1618
1668
|
fn collect_near_miss_groups(
|
|
1619
|
-
|
|
1620
|
-
literal_count_prefix: &[usize],
|
|
1621
|
-
block_ranges: &[TokenRange],
|
|
1669
|
+
source: &TokenizedSource<'_>,
|
|
1622
1670
|
settings: &DuplicationSettings,
|
|
1623
1671
|
reported_groups: &mut [Vec<CountedOccurrence>],
|
|
1624
1672
|
) -> Vec<Vec<CountedOccurrence>> {
|
|
1625
1673
|
if settings.min_similarity_percent >= 100 {
|
|
1626
1674
|
return Vec::new();
|
|
1627
1675
|
}
|
|
1628
|
-
let
|
|
1629
|
-
|
|
1630
|
-
.filter(|range| {
|
|
1631
|
-
let token_count = range.end_token_index - range.start_token_index;
|
|
1632
|
-
let literal_count = literal_count_prefix
|
|
1633
|
-
.get(range.end_token_index)
|
|
1634
|
-
.copied()
|
|
1635
|
-
.unwrap_or(0)
|
|
1636
|
-
- literal_count_prefix
|
|
1637
|
-
.get(range.start_token_index)
|
|
1638
|
-
.copied()
|
|
1639
|
-
.unwrap_or(0);
|
|
1640
|
-
token_count >= settings.min_tokens && !is_literal_dense(literal_count, token_count)
|
|
1641
|
-
})
|
|
1642
|
-
.collect();
|
|
1643
|
-
eligible.sort_by_key(|range| {
|
|
1644
|
-
(
|
|
1645
|
-
range.start_token_index,
|
|
1646
|
-
std::cmp::Reverse(range.end_token_index),
|
|
1647
|
-
)
|
|
1648
|
-
});
|
|
1649
|
-
let comparable = select_comparable_blocks(&eligible);
|
|
1676
|
+
let tokens = &source.tokens;
|
|
1677
|
+
let comparable = select_near_miss_blocks(source, settings.min_tokens);
|
|
1650
1678
|
if comparable.len() < 2 {
|
|
1651
1679
|
return Vec::new();
|
|
1652
1680
|
}
|
|
1653
1681
|
|
|
1654
|
-
// Reported-group indices whose occurrences overlap
|
|
1655
|
-
//
|
|
1656
|
-
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
|
|
1657
1700
|
.iter()
|
|
1658
1701
|
.map(|range| {
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
.
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
})
|
|
1667
|
-
})
|
|
1668
|
-
.map(|(group_index, _)| group_index)
|
|
1669
|
-
.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
|
+
)
|
|
1670
1709
|
})
|
|
1671
1710
|
.collect();
|
|
1711
|
+
let matcher = Matcher::new(
|
|
1712
|
+
&mut blocks,
|
|
1713
|
+
settings.min_tokens,
|
|
1714
|
+
settings.min_similarity_percent,
|
|
1715
|
+
);
|
|
1672
1716
|
|
|
1673
|
-
|
|
1674
|
-
// other files the process measured before it.
|
|
1675
|
-
let mut symbol_id_by_token_hashes: HashMap<(i32, i32, i32, i32), i32> = HashMap::new();
|
|
1676
|
-
let sequences: Vec<NormalizedBlock> = comparable
|
|
1677
|
-
.iter()
|
|
1678
|
-
.map(|range| normalize_block_sequence(tokens, range, &mut symbol_id_by_token_hashes))
|
|
1679
|
-
.collect();
|
|
1680
|
-
let ngram_sets: Vec<HashSet<i32>> = sequences
|
|
1717
|
+
let block_touched: Vec<bool> = comparable
|
|
1681
1718
|
.iter()
|
|
1682
|
-
.map(|
|
|
1719
|
+
.map(|range| !touched_groups_in(range.start_token_index, range.end_token_index).is_empty())
|
|
1683
1720
|
.collect();
|
|
1684
|
-
let
|
|
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
|
+
};
|
|
1685
1787
|
|
|
1686
|
-
let mut parent: Vec<usize> = (0..
|
|
1788
|
+
let mut parent: Vec<usize> = (0..node_blocks.len()).collect();
|
|
1687
1789
|
fn find(parent: &mut [usize], mut index: usize) -> usize {
|
|
1688
1790
|
let mut root = index;
|
|
1689
1791
|
while parent[root] != root {
|
|
@@ -1696,52 +1798,84 @@ fn collect_near_miss_groups(
|
|
|
1696
1798
|
}
|
|
1697
1799
|
root
|
|
1698
1800
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
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()
|
|
1718
1823
|
{
|
|
1719
1824
|
continue;
|
|
1720
1825
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
{
|
|
1725
|
-
let left_root = find(&mut parent, left_index);
|
|
1726
|
-
let right_root = find(&mut parent, right_index);
|
|
1727
|
-
parent[left_root.max(right_root)] = left_root.min(right_root);
|
|
1728
|
-
}
|
|
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);
|
|
1729
1829
|
}
|
|
1730
1830
|
|
|
1731
1831
|
let mut members_by_root: IndexMap<usize, Vec<usize>> = IndexMap::new();
|
|
1732
|
-
for
|
|
1733
|
-
let root = find(&mut parent,
|
|
1734
|
-
members_by_root.entry(root).or_default().push(
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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()
|
|
1744
1877
|
};
|
|
1878
|
+
let touched_groups_of = |node: usize| &touched_groups_by_node[node];
|
|
1745
1879
|
let mut groups: Vec<Vec<CountedOccurrence>> = Vec::new();
|
|
1746
1880
|
for members in members_by_root.values() {
|
|
1747
1881
|
if members.len() < 2 {
|
|
@@ -1750,38 +1884,36 @@ fn collect_near_miss_groups(
|
|
|
1750
1884
|
let uncovered: Vec<usize> = members
|
|
1751
1885
|
.iter()
|
|
1752
1886
|
.copied()
|
|
1753
|
-
.filter(|&index|
|
|
1887
|
+
.filter(|&index| touched_groups_of(index).is_empty())
|
|
1754
1888
|
.collect();
|
|
1755
1889
|
let covered: Vec<usize> = members
|
|
1756
1890
|
.iter()
|
|
1757
1891
|
.copied()
|
|
1758
|
-
.filter(|&index| !
|
|
1892
|
+
.filter(|&index| !touched_groups_of(index).is_empty())
|
|
1759
1893
|
.collect();
|
|
1760
1894
|
if covered.is_empty() {
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
.collect(),
|
|
1766
|
-
);
|
|
1895
|
+
let occurrences = to_occurrences(members);
|
|
1896
|
+
if occurrences.len() >= 2 {
|
|
1897
|
+
groups.push(occurrences);
|
|
1898
|
+
}
|
|
1767
1899
|
continue;
|
|
1768
1900
|
}
|
|
1769
1901
|
if uncovered.is_empty() {
|
|
1770
1902
|
continue;
|
|
1771
1903
|
}
|
|
1772
|
-
// An anchored cluster extends a reported group only when
|
|
1773
|
-
// the cluster
|
|
1904
|
+
// An anchored cluster extends a reported group only when every occurrence of that group
|
|
1905
|
+
// overlaps one of the cluster's member nodes: an occurrence disjoint from all members
|
|
1906
|
+
// reports content the cluster does not share.
|
|
1774
1907
|
let overlaps_member = |occurrence: &CountedOccurrence| {
|
|
1775
1908
|
members.iter().any(|&index| {
|
|
1776
|
-
let
|
|
1777
|
-
occurrence.start_token_index <
|
|
1778
|
-
&& 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
|
|
1779
1911
|
})
|
|
1780
1912
|
};
|
|
1781
1913
|
// Ascending by construction: BTreeSet iteration is sorted and filter preserves order.
|
|
1782
1914
|
let fully_clustered: Vec<usize> = covered
|
|
1783
1915
|
.iter()
|
|
1784
|
-
.flat_map(|&index|
|
|
1916
|
+
.flat_map(|&index| touched_groups_of(index).iter().copied())
|
|
1785
1917
|
.collect::<std::collections::BTreeSet<usize>>()
|
|
1786
1918
|
.into_iter()
|
|
1787
1919
|
.filter(|&group_index| {
|
|
@@ -1790,33 +1922,57 @@ fn collect_near_miss_groups(
|
|
|
1790
1922
|
})
|
|
1791
1923
|
.collect();
|
|
1792
1924
|
if let Some((&target_index, source_indexes)) = fully_clustered.split_first() {
|
|
1793
|
-
// Rebuild the component as ONE group with one coalesced occurrence per member block
|
|
1794
|
-
//
|
|
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.
|
|
1795
1928
|
let mut consumed: HashSet<(usize, usize)> = HashSet::new();
|
|
1796
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();
|
|
1797
1932
|
for &member_index in members {
|
|
1798
|
-
|
|
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() {
|
|
1799
1939
|
// Occurrences of ONE group are distinct copies; only fragments from DIFFERENT
|
|
1800
1940
|
// groups belong to the same copy. Consecutive position-order slices keep the
|
|
1801
|
-
// coalesced spans disjoint
|
|
1941
|
+
// coalesced spans disjoint.
|
|
1802
1942
|
let mut fragments: Vec<(CountedOccurrence, usize)> = Vec::new();
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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()
|
|
1810
1950
|
{
|
|
1811
|
-
consumed.
|
|
1812
|
-
|
|
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
|
+
}
|
|
1813
1958
|
}
|
|
1814
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
|
+
}
|
|
1815
1972
|
}
|
|
1816
1973
|
fragments.sort_by_key(|(occurrence, _)| {
|
|
1817
1974
|
(occurrence.start_token_index, occurrence.end_token_index)
|
|
1818
1975
|
});
|
|
1819
|
-
let had_fragments = !fragments.is_empty();
|
|
1820
1976
|
let mut copy_parts: Vec<CountedOccurrence> = Vec::new();
|
|
1821
1977
|
let mut copy_groups: HashSet<usize> = HashSet::new();
|
|
1822
1978
|
for (occurrence, group_index) in fragments {
|
|
@@ -1830,10 +1986,8 @@ fn collect_near_miss_groups(
|
|
|
1830
1986
|
if !copy_parts.is_empty() {
|
|
1831
1987
|
merged.push(coalesce_occurrences(copy_parts));
|
|
1832
1988
|
}
|
|
1833
|
-
if !had_fragments && touched_groups_by_block[member_index].is_empty() {
|
|
1834
|
-
merged.push(to_occurrence(comparable[member_index]));
|
|
1835
|
-
}
|
|
1836
1989
|
}
|
|
1990
|
+
merged.extend(to_occurrences(&unanchored_nodes));
|
|
1837
1991
|
merged.sort_by_key(|occurrence| {
|
|
1838
1992
|
(occurrence.start_token_index, occurrence.end_token_index)
|
|
1839
1993
|
});
|
|
@@ -1846,22 +2000,46 @@ fn collect_near_miss_groups(
|
|
|
1846
2000
|
for &source_index in source_indexes {
|
|
1847
2001
|
reported_groups[source_index].clear();
|
|
1848
2002
|
}
|
|
1849
|
-
} else
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
.collect(),
|
|
1855
|
-
);
|
|
2003
|
+
} else {
|
|
2004
|
+
let occurrences = to_occurrences(&uncovered);
|
|
2005
|
+
if occurrences.len() >= 2 {
|
|
2006
|
+
groups.push(occurrences);
|
|
2007
|
+
}
|
|
1856
2008
|
}
|
|
1857
2009
|
}
|
|
1858
2010
|
groups.sort_by_key(|group| group_sort_key(group));
|
|
1859
2011
|
groups
|
|
1860
2012
|
}
|
|
1861
2013
|
|
|
1862
|
-
///
|
|
1863
|
-
///
|
|
1864
|
-
///
|
|
2014
|
+
/// The mutually disjoint blocks near-miss comparison considers: at least `min_tokens` long, not
|
|
2015
|
+
/// literal-dense (data tables are compared by value, not shape), and selected by
|
|
2016
|
+
/// select_comparable_blocks.
|
|
2017
|
+
fn select_near_miss_blocks<'s>(
|
|
2018
|
+
source: &'s TokenizedSource<'_>,
|
|
2019
|
+
min_tokens: usize,
|
|
2020
|
+
) -> Vec<&'s TokenRange> {
|
|
2021
|
+
let literal_count_prefix = &source.literal_count_prefix;
|
|
2022
|
+
let mut eligible: Vec<&TokenRange> = source
|
|
2023
|
+
.block_ranges
|
|
2024
|
+
.iter()
|
|
2025
|
+
.filter(|range| {
|
|
2026
|
+
let token_count = range.end_token_index - range.start_token_index;
|
|
2027
|
+
let literal_count = literal_count_prefix[range.end_token_index]
|
|
2028
|
+
- literal_count_prefix[range.start_token_index];
|
|
2029
|
+
token_count >= min_tokens && !is_literal_dense(literal_count, token_count)
|
|
2030
|
+
})
|
|
2031
|
+
.collect();
|
|
2032
|
+
eligible.sort_by_key(|range| {
|
|
2033
|
+
(
|
|
2034
|
+
range.start_token_index,
|
|
2035
|
+
std::cmp::Reverse(range.end_token_index),
|
|
2036
|
+
)
|
|
2037
|
+
});
|
|
2038
|
+
select_comparable_blocks(&eligible)
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
/// Keeps the block ranges the near-miss phase compares: wrappers whose subtree branches into two
|
|
2042
|
+
/// or more disjoint eligible sub-blocks are descended through; linear chains keep their top.
|
|
1865
2043
|
fn select_comparable_blocks<'a>(eligible: &[&'a TokenRange]) -> Vec<&'a TokenRange> {
|
|
1866
2044
|
struct ForestNode<'a> {
|
|
1867
2045
|
range: &'a TokenRange,
|
|
@@ -1919,8 +2097,7 @@ fn select_comparable_blocks<'a>(eligible: &[&'a TokenRange]) -> Vec<&'a TokenRan
|
|
|
1919
2097
|
kept
|
|
1920
2098
|
}
|
|
1921
2099
|
|
|
1922
|
-
/// One copy's fragments (an exact prefix and suffix split by a large edit) as one occurrence
|
|
1923
|
-
/// mirrors coalesceOccurrences in duplication.ts.
|
|
2100
|
+
/// One copy's fragments (an exact prefix and suffix split by a large edit) as one occurrence.
|
|
1924
2101
|
fn coalesce_occurrences(occurrences: Vec<CountedOccurrence>) -> CountedOccurrence {
|
|
1925
2102
|
if occurrences.len() == 1 {
|
|
1926
2103
|
return occurrences.into_iter().next().expect("non-empty");
|
|
@@ -1959,149 +2136,125 @@ fn coalesce_occurrences(occurrences: Vec<CountedOccurrence>) -> CountedOccurrenc
|
|
|
1959
2136
|
}
|
|
1960
2137
|
}
|
|
1961
2138
|
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
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
|
|
1967
2151
|
}
|
|
1968
2152
|
|
|
1969
|
-
///
|
|
1970
|
-
/// (literal VALUES
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
)
|
|
1976
|
-
let mut
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
let
|
|
1989
|
-
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
|
|
1990
2173
|
.entry((
|
|
1991
2174
|
token.text_hash,
|
|
1992
2175
|
token.text_hash2,
|
|
1993
2176
|
token.literal_hash.unwrap_or(0),
|
|
1994
2177
|
token.literal_hash2.unwrap_or(0),
|
|
1995
2178
|
))
|
|
1996
|
-
.or_insert(
|
|
1997
|
-
|
|
1998
|
-
*content_count_by_symbol.entry(id).or_insert(0) += 1;
|
|
1999
|
-
content_total += 1;
|
|
2000
|
-
}
|
|
2001
|
-
id
|
|
2002
|
-
};
|
|
2003
|
-
sequence.push(value);
|
|
2004
|
-
}
|
|
2005
|
-
NormalizedBlock {
|
|
2006
|
-
sequence,
|
|
2007
|
-
content_count_by_symbol,
|
|
2008
|
-
content_total,
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
/// Multiset overlap of two blocks' content-bearing symbols, for the content gate.
|
|
2013
|
-
fn content_overlap(left: &NormalizedBlock, right: &NormalizedBlock) -> usize {
|
|
2014
|
-
let (smaller, larger) =
|
|
2015
|
-
if left.content_count_by_symbol.len() <= right.content_count_by_symbol.len() {
|
|
2016
|
-
(left, right)
|
|
2017
|
-
} else {
|
|
2018
|
-
(right, left)
|
|
2019
|
-
};
|
|
2020
|
-
smaller
|
|
2021
|
-
.content_count_by_symbol
|
|
2022
|
-
.iter()
|
|
2023
|
-
.map(|(symbol, count)| {
|
|
2024
|
-
(*count).min(
|
|
2025
|
-
larger
|
|
2026
|
-
.content_count_by_symbol
|
|
2027
|
-
.get(symbol)
|
|
2028
|
-
.copied()
|
|
2029
|
-
.unwrap_or(0),
|
|
2030
|
-
)
|
|
2179
|
+
.or_insert(next_symbol);
|
|
2180
|
+
(symbol, token.is_name || token.literal_hash.is_some())
|
|
2031
2181
|
})
|
|
2032
|
-
.
|
|
2182
|
+
.unzip()
|
|
2033
2183
|
}
|
|
2034
2184
|
|
|
2035
|
-
///
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
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
|
+
}
|
|
2046
2208
|
}
|
|
2047
|
-
|
|
2209
|
+
top_level
|
|
2048
2210
|
}
|
|
2049
|
-
ngrams
|
|
2050
2211
|
}
|
|
2051
2212
|
|
|
2052
|
-
///
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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;
|
|
2066
2243
|
}
|
|
2244
|
+
posting.push(right);
|
|
2067
2245
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
return 0;
|
|
2078
|
-
}
|
|
2079
|
-
let word_count = a.len().div_ceil(64);
|
|
2080
|
-
let mut position_masks: HashMap<i32, Vec<u64>> = HashMap::new();
|
|
2081
|
-
for (index, &symbol) in a.iter().enumerate() {
|
|
2082
|
-
position_masks
|
|
2083
|
-
.entry(symbol)
|
|
2084
|
-
.or_insert_with(|| vec![0; word_count])[index / 64] |= 1u64 << (index % 64);
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
let mut v = vec![0u64; word_count];
|
|
2088
|
-
for symbol in b {
|
|
2089
|
-
let match_mask = position_masks.get(symbol);
|
|
2090
|
-
// `(v << 1) | 1` shifts a carry bit across words; subtraction borrows across words.
|
|
2091
|
-
let mut shift_carry = 1u64;
|
|
2092
|
-
let mut borrow = 0u64;
|
|
2093
|
-
for (word, slot) in v.iter_mut().enumerate() {
|
|
2094
|
-
let previous = *slot;
|
|
2095
|
-
let x = match_mask.map_or(0, |mask| mask[word]) | previous;
|
|
2096
|
-
let shifted = (previous << 1) | shift_carry;
|
|
2097
|
-
shift_carry = previous >> 63;
|
|
2098
|
-
let (partial, underflow1) = x.overflowing_sub(shifted);
|
|
2099
|
-
let (difference, underflow2) = partial.overflowing_sub(borrow);
|
|
2100
|
-
borrow = u64::from(underflow1 || underflow2);
|
|
2101
|
-
*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
|
+
}
|
|
2102
2255
|
}
|
|
2256
|
+
touched.clear();
|
|
2103
2257
|
}
|
|
2104
|
-
v.iter().map(|word| word.count_ones() as usize).sum()
|
|
2105
2258
|
}
|
|
2106
2259
|
|
|
2107
2260
|
/// Redundant copies one group adds to duplicate_block_count; a faithful port of
|