jscpd-rs 0.1.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/CHANGELOG.md +69 -0
- package/Cargo.lock +1323 -0
- package/Cargo.toml +54 -0
- package/LICENSE +21 -0
- package/README.md +372 -0
- package/docs/api-parity.md +49 -0
- package/docs/cloning-plan.md +281 -0
- package/docs/compat-baseline.md +535 -0
- package/docs/format-porting.md +86 -0
- package/docs/junior-task-template.md +62 -0
- package/docs/junior-workflow.md +87 -0
- package/docs/migrating-from-jscpd.md +193 -0
- package/docs/npm-release.md +116 -0
- package/docs/public-benchmark-suite.md +81 -0
- package/docs/release-checklist.md +200 -0
- package/docs/release-decisions.md +103 -0
- package/docs/release-readiness.md +51 -0
- package/docs/upstream-bugs.md +501 -0
- package/docs/upstream-issue-drafts.md +393 -0
- package/docs/user-guide.md +309 -0
- package/examples/dump_oxc_tokens.rs +112 -0
- package/examples/library_api.rs +42 -0
- package/npm/bin/jscpd-rs.js +6 -0
- package/npm/bin/jscpd-server.js +6 -0
- package/npm/lib/run-binary.js +68 -0
- package/npm/scripts/postinstall.js +50 -0
- package/package.json +53 -0
- package/skills/dry-refactoring/SKILL.md +63 -0
- package/skills/jscpd/SKILL.md +85 -0
- package/src/app.rs +512 -0
- package/src/bin/jscpd-server.rs +429 -0
- package/src/blame.rs +130 -0
- package/src/cli/config.rs +543 -0
- package/src/cli/parsing.rs +301 -0
- package/src/cli/tests.rs +543 -0
- package/src/cli.rs +671 -0
- package/src/detector/matching/secondary.rs +387 -0
- package/src/detector/matching.rs +274 -0
- package/src/detector/model.rs +190 -0
- package/src/detector/prepare.rs +71 -0
- package/src/detector/skip_local.rs +40 -0
- package/src/detector/statistics.rs +138 -0
- package/src/detector/store.rs +96 -0
- package/src/detector/tests.rs +238 -0
- package/src/detector.rs +265 -0
- package/src/files/discovery.rs +508 -0
- package/src/files/gitignore.rs +203 -0
- package/src/files/paths.rs +68 -0
- package/src/files/shebang.rs +106 -0
- package/src/files/tests.rs +523 -0
- package/src/files.rs +25 -0
- package/src/formats.rs +570 -0
- package/src/lib.rs +433 -0
- package/src/main.rs +26 -0
- package/src/report/ai.rs +125 -0
- package/src/report/badge.rs +238 -0
- package/src/report/console.rs +180 -0
- package/src/report/console_common.rs +37 -0
- package/src/report/console_full.rs +139 -0
- package/src/report/csv.rs +65 -0
- package/src/report/escape.rs +8 -0
- package/src/report/file_output.rs +28 -0
- package/src/report/html/assets.rs +47 -0
- package/src/report/html.rs +336 -0
- package/src/report/json.rs +119 -0
- package/src/report/markdown.rs +125 -0
- package/src/report/sarif.rs +302 -0
- package/src/report/silent.rs +22 -0
- package/src/report/source.rs +38 -0
- package/src/report/summary.rs +50 -0
- package/src/report/test_support.rs +133 -0
- package/src/report/threshold.rs +76 -0
- package/src/report/xcode.rs +90 -0
- package/src/report/xml.rs +119 -0
- package/src/report.rs +250 -0
- package/src/server/mcp.rs +942 -0
- package/src/server.rs +1081 -0
- package/src/tokenizer/apex.rs +97 -0
- package/src/tokenizer/blocks.rs +532 -0
- package/src/tokenizer/embedded.rs +106 -0
- package/src/tokenizer/generic.rs +511 -0
- package/src/tokenizer/hash.rs +27 -0
- package/src/tokenizer/ignore.rs +33 -0
- package/src/tokenizer/line_index.rs +33 -0
- package/src/tokenizer/markdown.rs +289 -0
- package/src/tokenizer/markup_attrs.rs +289 -0
- package/src/tokenizer/oxc/fallback.rs +275 -0
- package/src/tokenizer/oxc/jsx.rs +168 -0
- package/src/tokenizer/oxc/kind.rs +177 -0
- package/src/tokenizer/oxc/lexical.rs +67 -0
- package/src/tokenizer/oxc.rs +659 -0
- package/src/tokenizer/scan.rs +88 -0
- package/src/tokenizer/tap.rs +150 -0
- package/src/tokenizer/tests.rs +915 -0
- package/src/tokenizer.rs +328 -0
- package/src/verbose.rs +195 -0
package/src/tokenizer.rs
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
mod apex;
|
|
2
|
+
mod blocks;
|
|
3
|
+
mod embedded;
|
|
4
|
+
mod generic;
|
|
5
|
+
mod hash;
|
|
6
|
+
mod ignore;
|
|
7
|
+
mod line_index;
|
|
8
|
+
mod markdown;
|
|
9
|
+
mod markup_attrs;
|
|
10
|
+
mod oxc;
|
|
11
|
+
mod scan;
|
|
12
|
+
mod tap;
|
|
13
|
+
|
|
14
|
+
use serde::Serialize;
|
|
15
|
+
|
|
16
|
+
use crate::cli::{Mode, Options};
|
|
17
|
+
|
|
18
|
+
use generic::tokenize_generic;
|
|
19
|
+
use hash::hash_token;
|
|
20
|
+
use ignore::find_ignore_regions;
|
|
21
|
+
use line_index::LineIndex;
|
|
22
|
+
use oxc::{is_oxc_format, tokenize_oxc_maps};
|
|
23
|
+
use scan::count_prism_whitespace_tokens;
|
|
24
|
+
|
|
25
|
+
/// One-based source location used in tokens, fragments, and reports.
|
|
26
|
+
#[derive(Clone, Debug, Serialize)]
|
|
27
|
+
pub struct Location {
|
|
28
|
+
/// One-based line number.
|
|
29
|
+
pub line: usize,
|
|
30
|
+
/// Zero-based column number.
|
|
31
|
+
pub column: usize,
|
|
32
|
+
/// Zero-based byte position in the original source text.
|
|
33
|
+
pub position: usize,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Detection token after mode filtering and jscpd-compatible hashing.
|
|
37
|
+
#[derive(Clone, Debug)]
|
|
38
|
+
pub struct DetectionToken {
|
|
39
|
+
/// Stable token hash used by the duplicate detector.
|
|
40
|
+
pub hash: u64,
|
|
41
|
+
/// Start location of the token.
|
|
42
|
+
pub start: Location,
|
|
43
|
+
/// End location of the token.
|
|
44
|
+
pub end: Location,
|
|
45
|
+
/// Byte range in the original source text.
|
|
46
|
+
pub range: [usize; 2],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Token map for a single detected format block.
|
|
50
|
+
///
|
|
51
|
+
/// Embedded formats can produce more than one map for one source document, for
|
|
52
|
+
/// example script/style blocks extracted from markup-like files.
|
|
53
|
+
#[derive(Clone, Debug)]
|
|
54
|
+
pub struct TokenMap {
|
|
55
|
+
/// Format name associated with this token map.
|
|
56
|
+
pub format: String,
|
|
57
|
+
/// Detection tokens in source order.
|
|
58
|
+
pub tokens: Vec<DetectionToken>,
|
|
59
|
+
positions_assigned: bool,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Token map associated with a source identifier and line count.
|
|
63
|
+
#[derive(Clone, Debug)]
|
|
64
|
+
pub struct SourceTokenMap {
|
|
65
|
+
/// Stable source identifier, usually a file path.
|
|
66
|
+
pub source_id: String,
|
|
67
|
+
/// Format name associated with this token map.
|
|
68
|
+
pub format: String,
|
|
69
|
+
/// Detection tokens in source order.
|
|
70
|
+
pub tokens: Vec<DetectionToken>,
|
|
71
|
+
/// Total source lines represented by this map.
|
|
72
|
+
pub lines: usize,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Native tokenizer used by the detector.
|
|
76
|
+
///
|
|
77
|
+
/// JS/TS/JSX/TSX formats use Oxc-backed tokenization. Long-tail formats use
|
|
78
|
+
/// the generic native tokenizer unless a format has a dedicated implementation.
|
|
79
|
+
#[derive(Clone, Debug)]
|
|
80
|
+
pub struct Tokenizer {
|
|
81
|
+
options: Options,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl Default for Tokenizer {
|
|
85
|
+
fn default() -> Self {
|
|
86
|
+
Self::new()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
impl Tokenizer {
|
|
91
|
+
/// Create a tokenizer with default detector options.
|
|
92
|
+
pub fn new() -> Self {
|
|
93
|
+
Self {
|
|
94
|
+
options: Options::default(),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Create a tokenizer with caller-provided options.
|
|
99
|
+
pub fn with_options(options: Options) -> Self {
|
|
100
|
+
Self { options }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Return the options used by this tokenizer.
|
|
104
|
+
pub fn options(&self) -> &Options {
|
|
105
|
+
&self.options
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Mutably access tokenizer options.
|
|
109
|
+
pub fn options_mut(&mut self) -> &mut Options {
|
|
110
|
+
&mut self.options
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Tokenize a source string and return the first token stream.
|
|
114
|
+
///
|
|
115
|
+
/// Use [`Tokenizer::tokenize_maps`] when a format can produce multiple
|
|
116
|
+
/// embedded token maps.
|
|
117
|
+
pub fn tokenize(&self, content: &str, format: &str) -> Vec<DetectionToken> {
|
|
118
|
+
self.tokenize_maps(content, format)
|
|
119
|
+
.into_iter()
|
|
120
|
+
.next()
|
|
121
|
+
.map(|map| map.tokens)
|
|
122
|
+
.unwrap_or_default()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Tokenize source text into one or more format-specific token maps.
|
|
126
|
+
pub fn tokenize_maps(&self, content: &str, format: &str) -> Vec<TokenMap> {
|
|
127
|
+
tokenize_maps_for_detection(content, format, &self.options)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Tokenize source text and attach a source identifier to each generated map.
|
|
131
|
+
pub fn generate_maps(
|
|
132
|
+
&self,
|
|
133
|
+
source_id: impl Into<String>,
|
|
134
|
+
content: &str,
|
|
135
|
+
format: &str,
|
|
136
|
+
) -> Vec<SourceTokenMap> {
|
|
137
|
+
let source_id = source_id.into();
|
|
138
|
+
self.tokenize_maps(content, format)
|
|
139
|
+
.into_iter()
|
|
140
|
+
.map(|map| SourceTokenMap {
|
|
141
|
+
source_id: source_id.clone(),
|
|
142
|
+
lines: token_map_line_count(&map.tokens),
|
|
143
|
+
format: map.format,
|
|
144
|
+
tokens: map.tokens,
|
|
145
|
+
})
|
|
146
|
+
.collect()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
151
|
+
enum TokenKind {
|
|
152
|
+
Comment,
|
|
153
|
+
Constant,
|
|
154
|
+
Empty,
|
|
155
|
+
Keyword,
|
|
156
|
+
NewLine,
|
|
157
|
+
Number,
|
|
158
|
+
Operator,
|
|
159
|
+
Punctuation,
|
|
160
|
+
String,
|
|
161
|
+
Default,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#[derive(Clone, Copy)]
|
|
165
|
+
struct ByteSpan {
|
|
166
|
+
start: usize,
|
|
167
|
+
end: usize,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
struct TokenContext<'a> {
|
|
171
|
+
content: &'a str,
|
|
172
|
+
options: &'a Options,
|
|
173
|
+
ignore_regions: &'a [[usize; 2]],
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
impl TokenContext<'_> {
|
|
177
|
+
fn slice(&self, span: ByteSpan) -> &str {
|
|
178
|
+
&self.content[span.start..span.end]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn overlaps_ignore_region(&self, span: ByteSpan) -> bool {
|
|
182
|
+
self.ignore_regions
|
|
183
|
+
.iter()
|
|
184
|
+
.any(|[region_start, region_end]| span.start < *region_end && span.end > *region_start)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[cfg(test)]
|
|
189
|
+
fn tokenize_for_detection(content: &str, format: &str, options: &Options) -> Vec<DetectionToken> {
|
|
190
|
+
tokenize_maps_for_detection(content, format, options)
|
|
191
|
+
.into_iter()
|
|
192
|
+
.next()
|
|
193
|
+
.map(|map| map.tokens)
|
|
194
|
+
.unwrap_or_default()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
pub fn tokenize_maps_for_detection(
|
|
198
|
+
content: &str,
|
|
199
|
+
format: &str,
|
|
200
|
+
options: &Options,
|
|
201
|
+
) -> Vec<TokenMap> {
|
|
202
|
+
let ignore_regions = find_ignore_regions(content, options);
|
|
203
|
+
let mut maps = if format == "markdown" {
|
|
204
|
+
markdown::tokenize_maps(content, options, &ignore_regions)
|
|
205
|
+
} else if format == "apex" {
|
|
206
|
+
apex::tokenize_maps(content, options, &ignore_regions)
|
|
207
|
+
} else if format == "tap" {
|
|
208
|
+
tap::tokenize_maps(content, options, &ignore_regions)
|
|
209
|
+
} else if matches!(format, "markup" | "vue" | "svelte" | "astro") {
|
|
210
|
+
blocks::tokenize_maps(content, format, options, &ignore_regions)
|
|
211
|
+
} else if is_oxc_format(format) {
|
|
212
|
+
tokenize_oxc_maps(content, format, options, &ignore_regions)
|
|
213
|
+
} else {
|
|
214
|
+
vec![TokenMap {
|
|
215
|
+
format: format.to_string(),
|
|
216
|
+
tokens: tokenize_generic(content, format, options, &ignore_regions),
|
|
217
|
+
positions_assigned: false,
|
|
218
|
+
}]
|
|
219
|
+
};
|
|
220
|
+
for map in &mut maps {
|
|
221
|
+
if !map.positions_assigned {
|
|
222
|
+
assign_token_positions(content, &map.format, options, &mut map.tokens);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
maps
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn token_map_line_count(tokens: &[DetectionToken]) -> usize {
|
|
229
|
+
match (tokens.first(), tokens.last()) {
|
|
230
|
+
(Some(first), Some(last)) => last.end.line.saturating_sub(first.start.line),
|
|
231
|
+
_ => 0,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn assign_token_positions(
|
|
236
|
+
content: &str,
|
|
237
|
+
format: &str,
|
|
238
|
+
options: &Options,
|
|
239
|
+
tokens: &mut [DetectionToken],
|
|
240
|
+
) {
|
|
241
|
+
let needs_report_positions =
|
|
242
|
+
options.reporters.iter().any(|reporter| reporter == "json") || !options.silent;
|
|
243
|
+
if !needs_report_positions || !matches!(format, "javascript" | "typescript" | "jsx" | "tsx") {
|
|
244
|
+
for (position, token) in tokens.iter_mut().enumerate() {
|
|
245
|
+
token.start.position = position;
|
|
246
|
+
token.end.position = position;
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let mut position = 0usize;
|
|
252
|
+
let mut previous_end = 0usize;
|
|
253
|
+
for token in tokens {
|
|
254
|
+
if token.range[0] > previous_end {
|
|
255
|
+
position += count_prism_whitespace_tokens(content, previous_end, token.range[0]);
|
|
256
|
+
}
|
|
257
|
+
token.start.position = position;
|
|
258
|
+
token.end.position = position;
|
|
259
|
+
position += 1;
|
|
260
|
+
previous_end = previous_end.max(token.range[1]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn push_token(
|
|
265
|
+
tokens: &mut Vec<DetectionToken>,
|
|
266
|
+
context: &TokenContext<'_>,
|
|
267
|
+
kind: TokenKind,
|
|
268
|
+
span: ByteSpan,
|
|
269
|
+
start: Location,
|
|
270
|
+
end: Location,
|
|
271
|
+
) {
|
|
272
|
+
if context.options.mode == Mode::Weak && kind == TokenKind::Comment {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if context.overlaps_ignore_region(span) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
tokens.push(DetectionToken {
|
|
279
|
+
hash: hash_token(kind, context.slice(span), context.options.ignore_case),
|
|
280
|
+
start,
|
|
281
|
+
end,
|
|
282
|
+
range: [span.start, span.end],
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fn push_strict_whitespace_tokens(
|
|
287
|
+
tokens: &mut Vec<DetectionToken>,
|
|
288
|
+
context: &TokenContext<'_>,
|
|
289
|
+
span: ByteSpan,
|
|
290
|
+
line_index: &LineIndex,
|
|
291
|
+
) {
|
|
292
|
+
if context.options.mode != Mode::Strict {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
let mut start = span.start;
|
|
296
|
+
while start < span.end {
|
|
297
|
+
let (end, kind) = scan_whitespace_token(context.content, start, span.end);
|
|
298
|
+
push_token(
|
|
299
|
+
tokens,
|
|
300
|
+
context,
|
|
301
|
+
kind,
|
|
302
|
+
ByteSpan { start, end },
|
|
303
|
+
line_index.location(start),
|
|
304
|
+
line_index.location(end),
|
|
305
|
+
);
|
|
306
|
+
start = end.max(start + 1);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fn scan_whitespace_token(content: &str, start: usize, limit: usize) -> (usize, TokenKind) {
|
|
311
|
+
let bytes = content.as_bytes();
|
|
312
|
+
if bytes[start] == b'\n' {
|
|
313
|
+
return (start + 1, TokenKind::NewLine);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let mut end = start;
|
|
317
|
+
while end < limit {
|
|
318
|
+
let ch = content[end..].chars().next().unwrap_or('\0');
|
|
319
|
+
if ch == '\n' || !ch.is_whitespace() {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
end += ch.len_utf8();
|
|
323
|
+
}
|
|
324
|
+
(end, TokenKind::Empty)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#[cfg(test)]
|
|
328
|
+
mod tests;
|
package/src/verbose.rs
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
2
|
+
|
|
3
|
+
use serde::Serialize;
|
|
4
|
+
|
|
5
|
+
use crate::detector::{CloneMatch, DetectionResult, Fragment, SkippedClone};
|
|
6
|
+
use crate::tokenizer::Location;
|
|
7
|
+
|
|
8
|
+
const GREY: &str = "\x1b[90m";
|
|
9
|
+
const YELLOW: &str = "\x1b[33m";
|
|
10
|
+
const RESET_COLOR: &str = "\x1b[39m";
|
|
11
|
+
|
|
12
|
+
pub fn write_detection_events(result: &DetectionResult) {
|
|
13
|
+
print!("{}", detection_events_output(result, current_time_millis()));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fn detection_events_output(result: &DetectionResult, found_date: u128) -> String {
|
|
17
|
+
let mut output = String::new();
|
|
18
|
+
let mut emitted = vec![false; result.clones.len()];
|
|
19
|
+
let mut emitted_skipped = vec![false; result.skipped_clones.len()];
|
|
20
|
+
for source in result.sources.iter().rev() {
|
|
21
|
+
output.push_str(&format!("{YELLOW}START_DETECTION{RESET_COLOR}\n"));
|
|
22
|
+
output.push_str(&format!(
|
|
23
|
+
"{GREY}Start detection for source id={} format={}{RESET_COLOR}\n",
|
|
24
|
+
source.path, source.format
|
|
25
|
+
));
|
|
26
|
+
for (idx, clone) in result.clones.iter().enumerate() {
|
|
27
|
+
if emitted[idx]
|
|
28
|
+
|| clone.format != source.format
|
|
29
|
+
|| clone.duplication_a.source_id != source.path
|
|
30
|
+
{
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
push_clone_found(&mut output, clone, found_date + idx as u128);
|
|
34
|
+
emitted[idx] = true;
|
|
35
|
+
}
|
|
36
|
+
for (idx, skipped) in result.skipped_clones.iter().enumerate() {
|
|
37
|
+
if emitted_skipped[idx]
|
|
38
|
+
|| skipped.clone.format != source.format
|
|
39
|
+
|| skipped.clone.duplication_a.source_id != source.path
|
|
40
|
+
{
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
push_clone_skipped(&mut output, skipped);
|
|
44
|
+
emitted_skipped[idx] = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (idx, clone) in result.clones.iter().enumerate() {
|
|
48
|
+
if !emitted[idx] {
|
|
49
|
+
push_clone_found(&mut output, clone, found_date + idx as u128);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (idx, skipped) in result.skipped_clones.iter().enumerate() {
|
|
53
|
+
if !emitted_skipped[idx] {
|
|
54
|
+
push_clone_skipped(&mut output, skipped);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
output
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn push_clone_found(output: &mut String, clone: &CloneMatch, found_date: u128) {
|
|
61
|
+
output.push_str(&format!("{YELLOW}CLONE_FOUND{RESET_COLOR}\n"));
|
|
62
|
+
if let Ok(json) = serde_json::to_string_pretty(&VerboseClone::new(clone, found_date)) {
|
|
63
|
+
for line in json.lines() {
|
|
64
|
+
output.push_str(GREY);
|
|
65
|
+
output.push_str(line);
|
|
66
|
+
output.push_str(RESET_COLOR);
|
|
67
|
+
output.push('\n');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn push_clone_skipped(output: &mut String, skipped: &SkippedClone) {
|
|
73
|
+
output.push_str(&format!("{YELLOW}CLONE_SKIPPED{RESET_COLOR}\n"));
|
|
74
|
+
output.push_str(&format!(
|
|
75
|
+
"{GREY}Clone skipped: {}{RESET_COLOR}\n",
|
|
76
|
+
skipped.message.join(" ")
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn current_time_millis() -> u128 {
|
|
81
|
+
SystemTime::now()
|
|
82
|
+
.duration_since(UNIX_EPOCH)
|
|
83
|
+
.map(|duration| duration.as_millis())
|
|
84
|
+
.unwrap_or_default()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[derive(Serialize)]
|
|
88
|
+
#[serde(rename_all = "camelCase")]
|
|
89
|
+
struct VerboseClone<'a> {
|
|
90
|
+
format: &'a str,
|
|
91
|
+
found_date: u128,
|
|
92
|
+
duplication_a: VerboseFragment<'a>,
|
|
93
|
+
duplication_b: VerboseFragment<'a>,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl<'a> VerboseClone<'a> {
|
|
97
|
+
fn new(clone: &'a CloneMatch, found_date: u128) -> Self {
|
|
98
|
+
Self {
|
|
99
|
+
format: &clone.format,
|
|
100
|
+
found_date,
|
|
101
|
+
duplication_a: VerboseFragment::new(&clone.duplication_a),
|
|
102
|
+
duplication_b: VerboseFragment::new(&clone.duplication_b),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[derive(Serialize)]
|
|
108
|
+
struct VerboseFragment<'a> {
|
|
109
|
+
#[serde(rename = "sourceId")]
|
|
110
|
+
source_id: &'a str,
|
|
111
|
+
start: &'a Location,
|
|
112
|
+
end: &'a Location,
|
|
113
|
+
range: [usize; 2],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
impl<'a> VerboseFragment<'a> {
|
|
117
|
+
fn new(fragment: &'a Fragment) -> Self {
|
|
118
|
+
Self {
|
|
119
|
+
source_id: &fragment.source_id,
|
|
120
|
+
start: &fragment.start,
|
|
121
|
+
end: &fragment.end,
|
|
122
|
+
range: fragment.range,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[cfg(test)]
|
|
128
|
+
mod tests {
|
|
129
|
+
use std::collections::HashMap;
|
|
130
|
+
|
|
131
|
+
use crate::detector::{
|
|
132
|
+
CloneMatch, DetectionResult, Fragment, SkippedClone, SourceSummary, Statistics,
|
|
133
|
+
};
|
|
134
|
+
use crate::tokenizer::Location;
|
|
135
|
+
|
|
136
|
+
use super::detection_events_output;
|
|
137
|
+
|
|
138
|
+
#[test]
|
|
139
|
+
fn verbose_events_match_upstream_event_shape() {
|
|
140
|
+
let result = DetectionResult {
|
|
141
|
+
clones: vec![CloneMatch {
|
|
142
|
+
format: "javascript".to_string(),
|
|
143
|
+
duplication_a: fragment("src/a.js", 2),
|
|
144
|
+
duplication_b: fragment("src/b.js", 8),
|
|
145
|
+
tokens: 6,
|
|
146
|
+
}],
|
|
147
|
+
skipped_clones: vec![SkippedClone {
|
|
148
|
+
clone: CloneMatch {
|
|
149
|
+
format: "javascript".to_string(),
|
|
150
|
+
duplication_a: fragment("src/a.js", 20),
|
|
151
|
+
duplication_b: fragment("src/b.js", 30),
|
|
152
|
+
tokens: 3,
|
|
153
|
+
},
|
|
154
|
+
message: vec!["Lines of code less than limit (2 < 5)".to_string()],
|
|
155
|
+
}],
|
|
156
|
+
statistics: Statistics::default(),
|
|
157
|
+
sources: vec![SourceSummary {
|
|
158
|
+
path: "src/a.js".to_string(),
|
|
159
|
+
format: "javascript".to_string(),
|
|
160
|
+
lines: 10,
|
|
161
|
+
tokens: 20,
|
|
162
|
+
}],
|
|
163
|
+
source_contents: HashMap::new(),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
let output = detection_events_output(&result, 123);
|
|
167
|
+
|
|
168
|
+
assert!(output.contains("START_DETECTION"));
|
|
169
|
+
assert!(output.contains("Start detection for source id=src/a.js format=javascript"));
|
|
170
|
+
assert!(output.contains("CLONE_FOUND"));
|
|
171
|
+
assert!(output.contains("CLONE_SKIPPED"));
|
|
172
|
+
assert!(output.contains("Clone skipped: Lines of code less than limit (2 < 5)"));
|
|
173
|
+
assert!(output.contains(r#""foundDate": 123"#));
|
|
174
|
+
assert!(output.contains(r#""sourceId": "src/a.js""#));
|
|
175
|
+
assert!(!output.contains(r#""tokens""#));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn fragment(source_id: &str, line: usize) -> Fragment {
|
|
179
|
+
Fragment {
|
|
180
|
+
source_id: source_id.to_string(),
|
|
181
|
+
start: location(line, 1, 0),
|
|
182
|
+
end: location(line + 3, 1, 6),
|
|
183
|
+
range: [0, 6],
|
|
184
|
+
blame: None,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn location(line: usize, column: usize, position: usize) -> Location {
|
|
189
|
+
Location {
|
|
190
|
+
line,
|
|
191
|
+
column,
|
|
192
|
+
position,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|