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
package/native/src/near_miss.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
use rustc_hash::{FxHashMap, FxHashSet};
|
|
2
|
+
use std::borrow::Cow;
|
|
2
3
|
|
|
3
4
|
/// N-gram size for the candidate index and local-match anchors (NIL's default); shared with
|
|
4
5
|
/// crossFileNearMiss.ts.
|
|
@@ -40,6 +41,11 @@ pub(crate) struct Block {
|
|
|
40
41
|
is_content: Vec<bool>,
|
|
41
42
|
/// Identifiers anonymized by first occurrence within the block.
|
|
42
43
|
sequence: Vec<i32>,
|
|
44
|
+
/// The content of the block as written: per token its content key, or the number of its
|
|
45
|
+
/// identifier. Equal for equal content wherever it lies, which symbols, numbered by first
|
|
46
|
+
/// occurrence in the file, are not; ordered like `writtenContent` in crossFileNearMiss.ts, so
|
|
47
|
+
/// that both detectors take the sides of a pair in the same order.
|
|
48
|
+
written_content: Vec<i64>,
|
|
43
49
|
pub ngrams: FxHashSet<i32>,
|
|
44
50
|
/// The n-grams occurring exactly once in the block with their offsets, sorted by hash so two
|
|
45
51
|
/// blocks' local-match anchors intersect by merging.
|
|
@@ -48,15 +54,18 @@ pub(crate) struct Block {
|
|
|
48
54
|
/// Matcher::new turns the counts into information-weighted counts.
|
|
49
55
|
content: Vec<(i32, u64)>,
|
|
50
56
|
content_total: u64,
|
|
51
|
-
/// The sequence with its top-level statements in canonical order
|
|
52
|
-
/// for statement-order-insensitive
|
|
53
|
-
|
|
57
|
+
/// The sequence with its top-level statements in canonical order and the block offset each of
|
|
58
|
+
/// its tokens comes from, when the block has enough statements for statement-order-insensitive
|
|
59
|
+
/// comparison.
|
|
60
|
+
canonical_sequence: Option<(Vec<i32>, Vec<usize>)>,
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
impl Block {
|
|
64
|
+
/// `token_keys` holds the content key of each non-identifier token of the file.
|
|
57
65
|
pub fn new(
|
|
58
66
|
symbols: &[i32],
|
|
59
67
|
is_content: &[bool],
|
|
68
|
+
token_keys: &[i64],
|
|
60
69
|
start: usize,
|
|
61
70
|
end: usize,
|
|
62
71
|
statements: Vec<(usize, usize)>,
|
|
@@ -64,16 +73,25 @@ impl Block {
|
|
|
64
73
|
let symbols = symbols[start..end].to_vec();
|
|
65
74
|
let is_content = is_content[start..end].to_vec();
|
|
66
75
|
// Identifier-blind, so a block copied into different surroundings (renumbering its
|
|
67
|
-
// identifiers) or with reordered statements still shares its n-grams.
|
|
68
|
-
|
|
76
|
+
// identifiers) or with reordered statements still shares its n-grams. Hashed from the
|
|
77
|
+
// content of the tokens: with symbols, numbered by first occurrence in the file, which
|
|
78
|
+
// n-grams collide, and so which anchor a match, would depend on where other code stands.
|
|
79
|
+
let hashed: Vec<i32> = symbols
|
|
80
|
+
.iter()
|
|
81
|
+
.zip(&token_keys[start..end])
|
|
82
|
+
.map(|(&symbol, &token_key)| {
|
|
83
|
+
if symbol < 0 {
|
|
84
|
+
BLIND_IDENTIFIER
|
|
85
|
+
} else {
|
|
86
|
+
(token_key ^ (token_key >> 32)) as i32
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
.collect();
|
|
90
|
+
let ngram_hashes: Vec<i32> = hashed
|
|
69
91
|
.windows(NGRAM_SIZE)
|
|
70
92
|
.map(|window| {
|
|
71
|
-
window.iter().fold(5381i32, |hash, &
|
|
72
|
-
hash.wrapping_mul(31).wrapping_add(
|
|
73
|
-
BLIND_IDENTIFIER
|
|
74
|
-
} else {
|
|
75
|
-
symbol
|
|
76
|
-
})
|
|
93
|
+
window.iter().fold(5381i32, |hash, &value| {
|
|
94
|
+
hash.wrapping_mul(31).wrapping_add(value)
|
|
77
95
|
})
|
|
78
96
|
})
|
|
79
97
|
.collect();
|
|
@@ -91,14 +109,21 @@ impl Block {
|
|
|
91
109
|
let canonical_sequence = (statements.len() >= MIN_REORDER_STATEMENT_COUNT).then(|| {
|
|
92
110
|
canonical_sequence(
|
|
93
111
|
&symbols,
|
|
112
|
+
&token_keys[start..end],
|
|
94
113
|
statements.iter().map(|&(statement_start, statement_end)| {
|
|
95
114
|
(statement_start - start, statement_end - start)
|
|
96
115
|
}),
|
|
97
116
|
)
|
|
98
117
|
});
|
|
118
|
+
let sequence = anonymize(&symbols);
|
|
99
119
|
Block {
|
|
100
120
|
start,
|
|
101
|
-
|
|
121
|
+
written_content: sequence
|
|
122
|
+
.iter()
|
|
123
|
+
.zip(&token_keys[start..end])
|
|
124
|
+
.map(|(&symbol, &token_key)| content_value(symbol, token_key))
|
|
125
|
+
.collect(),
|
|
126
|
+
sequence,
|
|
102
127
|
ngrams: occurrence_counts.into_keys().collect(),
|
|
103
128
|
unique_ngrams,
|
|
104
129
|
content: count_content(&symbols, &is_content),
|
|
@@ -112,16 +137,65 @@ impl Block {
|
|
|
112
137
|
pub fn len(&self) -> usize {
|
|
113
138
|
self.symbols.len()
|
|
114
139
|
}
|
|
140
|
+
|
|
141
|
+
/// The content of the block or of a core of it, as written (see `written_content`), with the
|
|
142
|
+
/// identifiers of a core numbered within it.
|
|
143
|
+
fn content(&self, core: Option<(usize, usize)>, token_keys: &[i64]) -> Cow<'_, [i64]> {
|
|
144
|
+
if core.is_none() {
|
|
145
|
+
return Cow::Borrowed(&self.written_content);
|
|
146
|
+
}
|
|
147
|
+
let (sequence, positions) = self.compared(core, false);
|
|
148
|
+
sequence
|
|
149
|
+
.iter()
|
|
150
|
+
.zip(&positions)
|
|
151
|
+
.map(|(&symbol, &position)| content_value(symbol, token_keys[position]))
|
|
152
|
+
.collect()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// The sequence a match compared, with the absolute token index of each of its symbols: a core,
|
|
156
|
+
/// the block with its statements in canonical order, or the block as written.
|
|
157
|
+
fn compared(
|
|
158
|
+
&self,
|
|
159
|
+
core: Option<(usize, usize)>,
|
|
160
|
+
reordered: bool,
|
|
161
|
+
) -> (Cow<'_, [i32]>, Vec<usize>) {
|
|
162
|
+
match (core, &self.canonical_sequence) {
|
|
163
|
+
(Some((start, end)), _) => (
|
|
164
|
+
Cow::Owned(anonymize(
|
|
165
|
+
&self.symbols[start - self.start..end - self.start],
|
|
166
|
+
)),
|
|
167
|
+
(start..end).collect(),
|
|
168
|
+
),
|
|
169
|
+
(None, Some((sequence, offsets))) if reordered => (
|
|
170
|
+
Cow::Borrowed(sequence),
|
|
171
|
+
offsets.iter().map(|offset| self.start + offset).collect(),
|
|
172
|
+
),
|
|
173
|
+
_ => (
|
|
174
|
+
Cow::Borrowed(&self.sequence),
|
|
175
|
+
(self.start..self.start + self.len()).collect(),
|
|
176
|
+
),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
115
179
|
}
|
|
116
180
|
|
|
181
|
+
/// Runs of tokens, as half-open absolute token ranges in position order.
|
|
182
|
+
pub(crate) type TokenRuns = Vec<(usize, usize)>;
|
|
183
|
+
|
|
117
184
|
/// A verified core in each block of a pair, as absolute token ranges.
|
|
118
185
|
pub(crate) type CorePair = ((usize, usize), (usize, usize));
|
|
119
186
|
|
|
120
187
|
/// How a verified pair matched: whole blocks, or every anchored core pair (one per gap-split
|
|
121
188
|
/// chain segment) where the blocks share a copy embedded in different code.
|
|
122
189
|
pub(crate) enum PairMatch {
|
|
123
|
-
Whole,
|
|
124
|
-
Local(Vec<CorePair>),
|
|
190
|
+
Whole(Alignment),
|
|
191
|
+
Local(Vec<(CorePair, Alignment)>),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// What Matcher::align needs to recover the tokens a verified match pairs.
|
|
195
|
+
#[derive(Clone, Copy)]
|
|
196
|
+
pub(crate) struct Alignment {
|
|
197
|
+
/// Whether the blocks matched only with their statements in canonical order.
|
|
198
|
+
reordered: bool,
|
|
125
199
|
}
|
|
126
200
|
|
|
127
201
|
/// Verifies near-miss block pairs: token-level LCS against the larger side (NiCad's per-fragment
|
|
@@ -182,21 +256,69 @@ impl Matcher {
|
|
|
182
256
|
&right.content,
|
|
183
257
|
right.content_total,
|
|
184
258
|
)
|
|
185
|
-
&& (lcs_length(&left.sequence, &right.sequence) * 100 >= required
|
|
186
|
-
|| self.matches_reordered(left, right, required))
|
|
187
259
|
{
|
|
188
|
-
|
|
260
|
+
let in_order = lcs_length(&left.sequence, &right.sequence);
|
|
261
|
+
if in_order * 100 >= required {
|
|
262
|
+
return Some(PairMatch::Whole(Alignment { reordered: false }));
|
|
263
|
+
}
|
|
264
|
+
// With their top-level statements (each anonymized on its own) in a canonical order,
|
|
265
|
+
// a copy whose independent statements were swapped still matches.
|
|
266
|
+
if let (Some((left, _)), Some((right, _))) =
|
|
267
|
+
(&left.canonical_sequence, &right.canonical_sequence)
|
|
268
|
+
{
|
|
269
|
+
let reordered = lcs_length(left, right);
|
|
270
|
+
if reordered * 100 >= required {
|
|
271
|
+
return Some(PairMatch::Whole(Alignment { reordered: true }));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Of the equally long chains of anchors, the one kept depends on which block comes first,
|
|
276
|
+
// so the blocks are taken in the order of their content rather than of their positions.
|
|
277
|
+
if right.written_content < left.written_content {
|
|
278
|
+
return self
|
|
279
|
+
.match_locally(right, left)
|
|
280
|
+
.map(|matched| match matched {
|
|
281
|
+
PairMatch::Local(cores) => PairMatch::Local(
|
|
282
|
+
cores
|
|
283
|
+
.into_iter()
|
|
284
|
+
.map(|((right_core, left_core), alignment)| {
|
|
285
|
+
((left_core, right_core), alignment)
|
|
286
|
+
})
|
|
287
|
+
.collect(),
|
|
288
|
+
),
|
|
289
|
+
whole => whole,
|
|
290
|
+
});
|
|
189
291
|
}
|
|
190
292
|
self.match_locally(left, right)
|
|
191
293
|
}
|
|
192
294
|
|
|
193
|
-
///
|
|
194
|
-
///
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
295
|
+
/// The token runs a verified match pairs, per side as absolute token ranges: those of a longest
|
|
296
|
+
/// common subsequence of what the match compared, `cores` or else the whole blocks. Of the
|
|
297
|
+
/// equally long subsequences, which one is marked depends on which sequence comes first, so
|
|
298
|
+
/// the sides are taken in the order of their content rather than of their positions.
|
|
299
|
+
/// `token_keys` holds the content key of each non-identifier token of the file.
|
|
300
|
+
pub fn align(
|
|
301
|
+
&self,
|
|
302
|
+
left: &Block,
|
|
303
|
+
right: &Block,
|
|
304
|
+
cores: Option<CorePair>,
|
|
305
|
+
alignment: Alignment,
|
|
306
|
+
token_keys: &[i64],
|
|
307
|
+
) -> (TokenRuns, TokenRuns) {
|
|
308
|
+
let (left_core, right_core) = cores.unzip();
|
|
309
|
+
let (left_sequence, left_positions) = left.compared(left_core, alignment.reordered);
|
|
310
|
+
let (right_sequence, right_positions) = right.compared(right_core, alignment.reordered);
|
|
311
|
+
let (left_matched, right_matched) =
|
|
312
|
+
if left.content(left_core, token_keys) <= right.content(right_core, token_keys) {
|
|
313
|
+
mark_lcs(&left_sequence, &right_sequence)
|
|
314
|
+
} else {
|
|
315
|
+
let (right_matched, left_matched) = mark_lcs(&right_sequence, &left_sequence);
|
|
316
|
+
(left_matched, right_matched)
|
|
317
|
+
};
|
|
318
|
+
(
|
|
319
|
+
to_runs(left_positions, left_matched),
|
|
320
|
+
to_runs(right_positions, right_matched),
|
|
321
|
+
)
|
|
200
322
|
}
|
|
201
323
|
|
|
202
324
|
/// Matches the cores two blocks share inside different surroundings (a copy wrapped in added
|
|
@@ -242,7 +364,7 @@ impl Matcher {
|
|
|
242
364
|
.map(|index| anchors[index])
|
|
243
365
|
.collect();
|
|
244
366
|
let chain = longest_increasing_chain(&run_anchors);
|
|
245
|
-
let cores: Vec<CorePair> = chain_segments(&chain)
|
|
367
|
+
let cores: Vec<(CorePair, Alignment)> = chain_segments(&chain)
|
|
246
368
|
.filter_map(|segment| {
|
|
247
369
|
let (first, last) = (segment[0], segment[segment.len() - 1]);
|
|
248
370
|
let (left_start, left_end) = (first.0, last.0 + NGRAM_SIZE);
|
|
@@ -250,23 +372,28 @@ impl Matcher {
|
|
|
250
372
|
let (left_length, right_length) = (left_end - left_start, right_end - right_start);
|
|
251
373
|
let shorter = left_length.min(right_length);
|
|
252
374
|
let required = self.min_similarity_percent * left_length.max(right_length);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
375
|
+
if shorter < self.min_tokens
|
|
376
|
+
|| shorter * 100 < required
|
|
377
|
+
|| anchored_token_count(segment) * 100 < MIN_ANCHOR_COVERAGE_PERCENT * shorter
|
|
378
|
+
|| !self.spans_share_content(
|
|
257
379
|
left,
|
|
258
380
|
(left_start, left_end),
|
|
259
381
|
right,
|
|
260
382
|
(right_start, right_end),
|
|
261
383
|
)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
384
|
+
{
|
|
385
|
+
return None;
|
|
386
|
+
}
|
|
387
|
+
let lcs_length = lcs_length(
|
|
388
|
+
&anonymize(&left.symbols[left_start..left_end]),
|
|
389
|
+
&anonymize(&right.symbols[right_start..right_end]),
|
|
390
|
+
);
|
|
391
|
+
(lcs_length * 100 >= required).then_some((
|
|
392
|
+
(
|
|
393
|
+
(left.start + left_start, left.start + left_end),
|
|
394
|
+
(right.start + right_start, right.start + right_end),
|
|
395
|
+
),
|
|
396
|
+
Alignment { reordered: false },
|
|
270
397
|
))
|
|
271
398
|
})
|
|
272
399
|
.collect();
|
|
@@ -361,25 +488,43 @@ fn anonymize(symbols: &[i32]) -> Vec<i32> {
|
|
|
361
488
|
}
|
|
362
489
|
|
|
363
490
|
/// The block's units (its top-level statements, as block-relative offsets, and the token runs
|
|
364
|
-
/// between them), each anonymized on its own and sorted, concatenated
|
|
491
|
+
/// between them), each anonymized on its own and sorted, concatenated, with the block offset of
|
|
492
|
+
/// every token. The units are ordered by the content of their tokens (`token_keys`, per token of
|
|
493
|
+
/// the block): symbols, numbered by first occurrence in the file, would order them by where
|
|
494
|
+
/// other code stands.
|
|
365
495
|
fn canonical_sequence(
|
|
366
496
|
symbols: &[i32],
|
|
497
|
+
token_keys: &[i64],
|
|
367
498
|
statements: impl Iterator<Item = (usize, usize)>,
|
|
368
|
-
) -> Vec<i32> {
|
|
369
|
-
let mut units: Vec<Vec<i32
|
|
499
|
+
) -> (Vec<i32>, Vec<usize>) {
|
|
500
|
+
let mut units: Vec<(Vec<i64>, usize, Vec<i32>)> = Vec::new();
|
|
501
|
+
let mut push_unit = |start: usize, end: usize| {
|
|
502
|
+
let unit = anonymize(&symbols[start..end]);
|
|
503
|
+
let content = unit
|
|
504
|
+
.iter()
|
|
505
|
+
.zip(&token_keys[start..end])
|
|
506
|
+
.map(|(&symbol, &token_key)| content_value(symbol, token_key))
|
|
507
|
+
.collect();
|
|
508
|
+
units.push((content, start, unit));
|
|
509
|
+
};
|
|
370
510
|
let mut cursor = 0;
|
|
371
511
|
for (start, end) in statements {
|
|
372
512
|
if cursor < start {
|
|
373
|
-
|
|
513
|
+
push_unit(cursor, start);
|
|
374
514
|
}
|
|
375
|
-
|
|
515
|
+
push_unit(start, end);
|
|
376
516
|
cursor = end;
|
|
377
517
|
}
|
|
378
518
|
if cursor < symbols.len() {
|
|
379
|
-
|
|
519
|
+
push_unit(cursor, symbols.len());
|
|
380
520
|
}
|
|
381
521
|
units.sort_unstable();
|
|
382
|
-
units
|
|
522
|
+
let offsets = units
|
|
523
|
+
.iter()
|
|
524
|
+
.flat_map(|(_, start, unit)| *start..*start + unit.len())
|
|
525
|
+
.collect();
|
|
526
|
+
let sequence = units.into_iter().flat_map(|(_, _, unit)| unit).collect();
|
|
527
|
+
(sequence, offsets)
|
|
383
528
|
}
|
|
384
529
|
|
|
385
530
|
/// The longest chain of anchors increasing in both blocks (anchors arrive sorted by left offset),
|
|
@@ -419,13 +564,20 @@ fn chain_segments(chain: &[(usize, usize)]) -> impl Iterator<Item = &[(usize, us
|
|
|
419
564
|
})
|
|
420
565
|
}
|
|
421
566
|
|
|
422
|
-
/// Longest-common-subsequence LENGTH
|
|
423
|
-
///
|
|
424
|
-
///
|
|
567
|
+
/// Longest-common-subsequence LENGTH. Only the length is needed and LCS length is
|
|
568
|
+
/// algorithm-independent, so u64 words are safe even though the TypeScript port in
|
|
569
|
+
/// src/duplication.ts uses 32-bit words.
|
|
425
570
|
fn lcs_length(a: &[i32], b: &[i32]) -> usize {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
571
|
+
lcs_growth_bits(a, b.iter())
|
|
572
|
+
.iter()
|
|
573
|
+
.map(|word| word.count_ones() as usize)
|
|
574
|
+
.sum()
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/// One bit per position of `a`, set where the longest common subsequence of `symbols` with the
|
|
578
|
+
/// prefix of `a` ending there is longer than with the prefix before it, so the set bits up to a
|
|
579
|
+
/// position count that length; via the Allison–Dix bit-parallel recurrence.
|
|
580
|
+
fn lcs_growth_bits<'a>(a: &[i32], symbols: impl Iterator<Item = &'a i32>) -> Vec<u64> {
|
|
429
581
|
let word_count = a.len().div_ceil(64);
|
|
430
582
|
let mut position_masks: FxHashMap<i32, Vec<u64>> = FxHashMap::default();
|
|
431
583
|
for (index, &symbol) in a.iter().enumerate() {
|
|
@@ -435,7 +587,7 @@ fn lcs_length(a: &[i32], b: &[i32]) -> usize {
|
|
|
435
587
|
}
|
|
436
588
|
|
|
437
589
|
let mut v = vec![0u64; word_count];
|
|
438
|
-
for symbol in
|
|
590
|
+
for symbol in symbols {
|
|
439
591
|
let match_mask = position_masks.get(symbol);
|
|
440
592
|
// `(v << 1) | 1` shifts a carry bit across words; subtraction borrows across words.
|
|
441
593
|
let mut shift_carry = 1u64;
|
|
@@ -451,5 +603,104 @@ fn lcs_length(a: &[i32], b: &[i32]) -> usize {
|
|
|
451
603
|
*slot = x & !difference;
|
|
452
604
|
}
|
|
453
605
|
}
|
|
454
|
-
v
|
|
606
|
+
v
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/// Which tokens of each sequence a longest common subsequence pairs, by Hirschberg's divide and
|
|
610
|
+
/// conquer over the bit-parallel recurrence, which alone yields lengths but no pairing.
|
|
611
|
+
fn mark_lcs(a: &[i32], b: &[i32]) -> (Vec<bool>, Vec<bool>) {
|
|
612
|
+
let mut a_matched = vec![false; a.len()];
|
|
613
|
+
let mut b_matched = vec![false; b.len()];
|
|
614
|
+
mark_lcs_into(a, b, &mut a_matched, &mut b_matched);
|
|
615
|
+
(a_matched, b_matched)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
fn mark_lcs_into(a: &[i32], b: &[i32], a_matched: &mut [bool], b_matched: &mut [bool]) {
|
|
619
|
+
// A common prefix or suffix belongs to some longest common subsequence.
|
|
620
|
+
let prefix = a
|
|
621
|
+
.iter()
|
|
622
|
+
.zip(b)
|
|
623
|
+
.take_while(|(left, right)| left == right)
|
|
624
|
+
.count();
|
|
625
|
+
let suffix = a[prefix..]
|
|
626
|
+
.iter()
|
|
627
|
+
.rev()
|
|
628
|
+
.zip(b[prefix..].iter().rev())
|
|
629
|
+
.take_while(|(left, right)| left == right)
|
|
630
|
+
.count();
|
|
631
|
+
let (a_end, b_end) = (a.len() - suffix, b.len() - suffix);
|
|
632
|
+
a_matched[..prefix].fill(true);
|
|
633
|
+
b_matched[..prefix].fill(true);
|
|
634
|
+
a_matched[a_end..].fill(true);
|
|
635
|
+
b_matched[b_end..].fill(true);
|
|
636
|
+
let (a, b) = (&a[prefix..a_end], &b[prefix..b_end]);
|
|
637
|
+
let (a_matched, b_matched) = (&mut a_matched[prefix..a_end], &mut b_matched[prefix..b_end]);
|
|
638
|
+
if a.is_empty() || b.is_empty() {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if a.len() == 1 {
|
|
642
|
+
if let Some(position) = b.iter().position(|symbol| *symbol == a[0]) {
|
|
643
|
+
a_matched[0] = true;
|
|
644
|
+
b_matched[position] = true;
|
|
645
|
+
}
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
let middle = a.len() / 2;
|
|
649
|
+
let reversed_b: Vec<i32> = b.iter().rev().copied().collect();
|
|
650
|
+
let forward = lcs_prefix_lengths(a[..middle].iter(), b);
|
|
651
|
+
let backward = lcs_prefix_lengths(a[middle..].iter().rev(), &reversed_b);
|
|
652
|
+
let mut split = 0;
|
|
653
|
+
for candidate in 1..=b.len() {
|
|
654
|
+
if forward[candidate] + backward[b.len() - candidate]
|
|
655
|
+
> forward[split] + backward[b.len() - split]
|
|
656
|
+
{
|
|
657
|
+
split = candidate;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
let (a_head, a_tail) = a_matched.split_at_mut(middle);
|
|
661
|
+
let (b_head, b_tail) = b_matched.split_at_mut(split);
|
|
662
|
+
mark_lcs_into(&a[..middle], &b[..split], a_head, b_head);
|
|
663
|
+
mark_lcs_into(&a[middle..], &b[split..], a_tail, b_tail);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/// The longest-common-subsequence length of `a` with every prefix of `b`.
|
|
667
|
+
fn lcs_prefix_lengths<'a>(a: impl Iterator<Item = &'a i32>, b: &[i32]) -> Vec<u32> {
|
|
668
|
+
let bits = lcs_growth_bits(b, a);
|
|
669
|
+
let mut lengths = Vec::with_capacity(b.len() + 1);
|
|
670
|
+
let mut length = 0;
|
|
671
|
+
lengths.push(length);
|
|
672
|
+
for index in 0..b.len() {
|
|
673
|
+
length += (bits[index / 64] >> (index % 64)) as u32 & 1;
|
|
674
|
+
lengths.push(length);
|
|
675
|
+
}
|
|
676
|
+
lengths
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/// The maximal runs of the matched positions, as half-open ranges.
|
|
680
|
+
fn to_runs(positions: Vec<usize>, matched: Vec<bool>) -> TokenRuns {
|
|
681
|
+
let mut matched_positions: Vec<usize> = positions
|
|
682
|
+
.into_iter()
|
|
683
|
+
.zip(matched)
|
|
684
|
+
.filter(|&(_, matched)| matched)
|
|
685
|
+
.map(|(position, _)| position)
|
|
686
|
+
.collect();
|
|
687
|
+
matched_positions.sort_unstable();
|
|
688
|
+
let mut runs = TokenRuns::new();
|
|
689
|
+
for position in matched_positions {
|
|
690
|
+
match runs.last_mut() {
|
|
691
|
+
Some(last) if last.1 == position => last.1 = position + 1,
|
|
692
|
+
_ => runs.push((position, position + 1)),
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
runs
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/// What a token contributes to the content of a range: its content key, or the range-local number
|
|
699
|
+
/// of its identifier.
|
|
700
|
+
fn content_value(symbol: i32, token_key: i64) -> i64 {
|
|
701
|
+
if symbol < 0 {
|
|
702
|
+
i64::from(symbol)
|
|
703
|
+
} else {
|
|
704
|
+
token_key
|
|
705
|
+
}
|
|
455
706
|
}
|
package/native/src/types.rs
CHANGED
|
@@ -80,7 +80,10 @@ pub struct CognitiveBlock {
|
|
|
80
80
|
#[serde(rename_all = "camelCase")]
|
|
81
81
|
pub struct DuplicateBlockOccurrence {
|
|
82
82
|
pub end_line: usize,
|
|
83
|
+
/// The 1-based code lines carrying the occurrence's matched tokens, sorted ascending.
|
|
84
|
+
pub line_numbers: Vec<usize>,
|
|
83
85
|
pub start_line: usize,
|
|
86
|
+
pub token_count: usize,
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
#[derive(Serialize)]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-gauge",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.0",
|
|
4
4
|
"description": "Measure code metrics with tree-sitter.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -93,12 +93,12 @@
|
|
|
93
93
|
"registry": "https://registry.npmjs.org/"
|
|
94
94
|
},
|
|
95
95
|
"optionalDependencies": {
|
|
96
|
-
"code-gauge-linux-x64-gnu": "10.
|
|
97
|
-
"code-gauge-linux-arm64-gnu": "10.
|
|
98
|
-
"code-gauge-linux-x64-musl": "10.
|
|
99
|
-
"code-gauge-linux-arm64-musl": "10.
|
|
100
|
-
"code-gauge-darwin-x64": "10.
|
|
101
|
-
"code-gauge-darwin-arm64": "10.
|
|
102
|
-
"code-gauge-win32-x64-msvc": "10.
|
|
96
|
+
"code-gauge-linux-x64-gnu": "10.2.0",
|
|
97
|
+
"code-gauge-linux-arm64-gnu": "10.2.0",
|
|
98
|
+
"code-gauge-linux-x64-musl": "10.2.0",
|
|
99
|
+
"code-gauge-linux-arm64-musl": "10.2.0",
|
|
100
|
+
"code-gauge-darwin-x64": "10.2.0",
|
|
101
|
+
"code-gauge-darwin-arm64": "10.2.0",
|
|
102
|
+
"code-gauge-win32-x64-msvc": "10.2.0"
|
|
103
103
|
}
|
|
104
104
|
}
|