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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, HashMap};
|
|
2
|
+
|
|
3
|
+
use serde::Serialize;
|
|
4
|
+
|
|
5
|
+
use crate::tokenizer::Location;
|
|
6
|
+
|
|
7
|
+
/// Git blame lines keyed by line number.
|
|
8
|
+
pub type BlamedLines = BTreeMap<String, BlamedLine>;
|
|
9
|
+
|
|
10
|
+
/// Git blame information for one duplicated source line.
|
|
11
|
+
#[derive(Clone, Debug, Serialize)]
|
|
12
|
+
pub struct BlamedLine {
|
|
13
|
+
/// Commit revision.
|
|
14
|
+
pub rev: String,
|
|
15
|
+
/// Author name reported by Git.
|
|
16
|
+
pub author: String,
|
|
17
|
+
/// Author or commit date reported by Git.
|
|
18
|
+
pub date: String,
|
|
19
|
+
/// Source line text.
|
|
20
|
+
pub line: String,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
24
|
+
pub(super) struct SourceId(pub(super) usize);
|
|
25
|
+
|
|
26
|
+
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
27
|
+
pub(super) struct FormatId(pub(super) usize);
|
|
28
|
+
|
|
29
|
+
/// One duplicated fragment in a source file.
|
|
30
|
+
#[derive(Clone, Debug, Serialize)]
|
|
31
|
+
pub struct Fragment {
|
|
32
|
+
#[serde(rename = "sourceId")]
|
|
33
|
+
/// Source identifier, usually a path.
|
|
34
|
+
pub source_id: String,
|
|
35
|
+
/// Start location of the duplicated fragment.
|
|
36
|
+
pub start: Location,
|
|
37
|
+
/// End location of the duplicated fragment.
|
|
38
|
+
pub end: Location,
|
|
39
|
+
/// Byte range of the duplicated fragment.
|
|
40
|
+
pub range: [usize; 2],
|
|
41
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
42
|
+
/// Optional Git blame information keyed by line number.
|
|
43
|
+
pub blame: Option<BlamedLines>,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Pair of duplicated fragments reported as one clone.
|
|
47
|
+
#[derive(Clone, Debug, Serialize)]
|
|
48
|
+
pub struct CloneMatch {
|
|
49
|
+
/// Format name shared by both fragments.
|
|
50
|
+
pub format: String,
|
|
51
|
+
#[serde(rename = "duplicationA")]
|
|
52
|
+
/// First duplicated fragment.
|
|
53
|
+
pub duplication_a: Fragment,
|
|
54
|
+
#[serde(rename = "duplicationB")]
|
|
55
|
+
/// Second duplicated fragment.
|
|
56
|
+
pub duplication_b: Fragment,
|
|
57
|
+
/// Number of detection tokens in the clone.
|
|
58
|
+
pub tokens: usize,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Clone skipped from final output with compatibility/debug messages.
|
|
62
|
+
#[derive(Clone, Debug)]
|
|
63
|
+
pub struct SkippedClone {
|
|
64
|
+
/// Skipped clone candidate.
|
|
65
|
+
pub clone: CloneMatch,
|
|
66
|
+
/// Reason messages explaining why the clone was skipped.
|
|
67
|
+
pub message: Vec<String>,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Aggregated duplication counters for a source, format, or whole run.
|
|
71
|
+
#[derive(Clone, Debug, Default, Serialize)]
|
|
72
|
+
pub struct StatisticRow {
|
|
73
|
+
/// Total line count.
|
|
74
|
+
pub lines: usize,
|
|
75
|
+
/// Total token count.
|
|
76
|
+
pub tokens: usize,
|
|
77
|
+
/// Number of sources included in the row.
|
|
78
|
+
pub sources: usize,
|
|
79
|
+
/// Number of clone pairs.
|
|
80
|
+
pub clones: usize,
|
|
81
|
+
#[serde(rename = "duplicatedLines")]
|
|
82
|
+
/// Number of lines covered by at least one clone.
|
|
83
|
+
pub duplicated_lines: usize,
|
|
84
|
+
#[serde(rename = "duplicatedTokens")]
|
|
85
|
+
/// Number of duplicated tokens.
|
|
86
|
+
pub duplicated_tokens: usize,
|
|
87
|
+
/// Duplicated line percentage.
|
|
88
|
+
pub percentage: f64,
|
|
89
|
+
#[serde(rename = "percentageTokens")]
|
|
90
|
+
/// Duplicated token percentage.
|
|
91
|
+
pub percentage_tokens: f64,
|
|
92
|
+
#[serde(rename = "newDuplicatedLines")]
|
|
93
|
+
/// New duplicated line count, kept for upstream report shape.
|
|
94
|
+
pub new_duplicated_lines: usize,
|
|
95
|
+
#[serde(rename = "newClones")]
|
|
96
|
+
/// New clone count, kept for upstream report shape.
|
|
97
|
+
pub new_clones: usize,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Duplication statistics grouped by format.
|
|
101
|
+
#[derive(Clone, Debug, Default, Serialize)]
|
|
102
|
+
pub struct FormatStatistic {
|
|
103
|
+
/// Per-source statistics for this format.
|
|
104
|
+
pub sources: HashMap<String, StatisticRow>,
|
|
105
|
+
/// Total statistics for this format.
|
|
106
|
+
pub total: StatisticRow,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Duplication statistics for a full detection run.
|
|
110
|
+
#[derive(Clone, Debug, Default, Serialize)]
|
|
111
|
+
pub struct Statistics {
|
|
112
|
+
/// Total statistics across all formats.
|
|
113
|
+
pub total: StatisticRow,
|
|
114
|
+
/// Statistics grouped by format name.
|
|
115
|
+
pub formats: HashMap<String, FormatStatistic>,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Summary of one analyzed source.
|
|
119
|
+
#[derive(Clone, Debug, Serialize)]
|
|
120
|
+
pub struct SourceSummary {
|
|
121
|
+
/// Source path or identifier.
|
|
122
|
+
pub path: String,
|
|
123
|
+
/// Detected or assigned format.
|
|
124
|
+
pub format: String,
|
|
125
|
+
/// Source line count.
|
|
126
|
+
pub lines: usize,
|
|
127
|
+
/// Detection token count.
|
|
128
|
+
pub tokens: usize,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Complete detector output.
|
|
132
|
+
#[derive(Clone, Debug, Serialize)]
|
|
133
|
+
pub struct DetectionResult {
|
|
134
|
+
/// Reported clone pairs.
|
|
135
|
+
pub clones: Vec<CloneMatch>,
|
|
136
|
+
#[serde(skip)]
|
|
137
|
+
/// Clone candidates skipped from final reports.
|
|
138
|
+
pub skipped_clones: Vec<SkippedClone>,
|
|
139
|
+
/// Aggregate statistics.
|
|
140
|
+
pub statistics: Statistics,
|
|
141
|
+
/// Analyzed source summaries.
|
|
142
|
+
pub sources: Vec<SourceSummary>,
|
|
143
|
+
#[serde(skip)]
|
|
144
|
+
/// Source contents keyed by source identifier for reporters that need
|
|
145
|
+
/// fragments.
|
|
146
|
+
pub source_contents: HashMap<String, String>,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[derive(Clone, Debug)]
|
|
150
|
+
pub(super) struct TokenSpan {
|
|
151
|
+
pub(super) start: Location,
|
|
152
|
+
pub(super) end: Location,
|
|
153
|
+
pub(super) range: [usize; 2],
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[derive(Clone, Debug)]
|
|
157
|
+
pub(super) struct SourceMeta {
|
|
158
|
+
pub(super) source_id: String,
|
|
159
|
+
pub(super) format: String,
|
|
160
|
+
pub(super) content: String,
|
|
161
|
+
pub(super) lines: usize,
|
|
162
|
+
pub(super) tokens: usize,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[derive(Clone, Debug)]
|
|
166
|
+
pub(super) struct TokenStream {
|
|
167
|
+
pub(super) source_id: SourceId,
|
|
168
|
+
pub(super) format_id: FormatId,
|
|
169
|
+
pub(super) hashes: Vec<u64>,
|
|
170
|
+
pub(super) spans: Vec<TokenSpan>,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[derive(Clone, Copy, Debug)]
|
|
174
|
+
pub(super) struct Occurrence {
|
|
175
|
+
pub(super) source_id: SourceId,
|
|
176
|
+
pub(super) token_start: usize,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[derive(Clone, Debug)]
|
|
180
|
+
pub(super) struct PreparedSource {
|
|
181
|
+
pub(super) meta: SourceMeta,
|
|
182
|
+
pub(super) stream: TokenStream,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[derive(Clone, Debug)]
|
|
186
|
+
pub(crate) struct PreparedSourceDraft {
|
|
187
|
+
pub(super) meta: SourceMeta,
|
|
188
|
+
pub(super) hashes: Vec<u64>,
|
|
189
|
+
pub(super) spans: Vec<TokenSpan>,
|
|
190
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
use rustc_hash::FxHashMap;
|
|
2
|
+
|
|
3
|
+
use crate::cli::Options;
|
|
4
|
+
use crate::files::SourceFile;
|
|
5
|
+
use crate::tokenizer::{DetectionToken, tokenize_maps_for_detection};
|
|
6
|
+
|
|
7
|
+
use super::model::{FormatId, PreparedSourceDraft, SourceMeta, TokenSpan};
|
|
8
|
+
|
|
9
|
+
pub(super) fn assign_formats(files: &[PreparedSourceDraft]) -> (Vec<FormatId>, Vec<String>) {
|
|
10
|
+
let mut by_name = FxHashMap::default();
|
|
11
|
+
let mut names = Vec::new();
|
|
12
|
+
let ids = files
|
|
13
|
+
.iter()
|
|
14
|
+
.map(|file| {
|
|
15
|
+
if let Some(id) = by_name.get(&file.meta.format) {
|
|
16
|
+
*id
|
|
17
|
+
} else {
|
|
18
|
+
let id = FormatId(names.len());
|
|
19
|
+
by_name.insert(file.meta.format.clone(), id);
|
|
20
|
+
names.push(file.meta.format.clone());
|
|
21
|
+
id
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.collect();
|
|
25
|
+
(ids, names)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub(super) fn prepare_file_maps(file: SourceFile, options: &Options) -> Vec<PreparedSourceDraft> {
|
|
29
|
+
tokenize_maps_for_detection(&file.content, &file.format, options)
|
|
30
|
+
.into_iter()
|
|
31
|
+
.map(|map| {
|
|
32
|
+
let (hashes, spans) = split_tokens(map.tokens);
|
|
33
|
+
let (stat_lines, stat_tokens) = token_stream_statistics(&spans);
|
|
34
|
+
PreparedSourceDraft {
|
|
35
|
+
meta: SourceMeta {
|
|
36
|
+
source_id: file.source_id.clone(),
|
|
37
|
+
format: map.format,
|
|
38
|
+
content: file.content.clone(),
|
|
39
|
+
lines: stat_lines,
|
|
40
|
+
tokens: stat_tokens,
|
|
41
|
+
},
|
|
42
|
+
hashes,
|
|
43
|
+
spans,
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.collect()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn split_tokens(tokens: Vec<DetectionToken>) -> (Vec<u64>, Vec<TokenSpan>) {
|
|
50
|
+
let mut hashes = Vec::with_capacity(tokens.len());
|
|
51
|
+
let mut spans = Vec::with_capacity(tokens.len());
|
|
52
|
+
for token in tokens {
|
|
53
|
+
hashes.push(token.hash);
|
|
54
|
+
spans.push(TokenSpan {
|
|
55
|
+
start: token.start,
|
|
56
|
+
end: token.end,
|
|
57
|
+
range: token.range,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
(hashes, spans)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn token_stream_statistics(spans: &[TokenSpan]) -> (usize, usize) {
|
|
64
|
+
match (spans.first(), spans.last()) {
|
|
65
|
+
(Some(first), Some(last)) => (
|
|
66
|
+
last.end.line.saturating_sub(first.start.line),
|
|
67
|
+
last.end.position.saturating_sub(first.start.position),
|
|
68
|
+
),
|
|
69
|
+
_ => (0, 0),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
|
|
3
|
+
use crate::cli::Options;
|
|
4
|
+
|
|
5
|
+
pub(super) fn same_configured_root(a: &str, b: &str, options: &Options) -> bool {
|
|
6
|
+
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
|
7
|
+
let a = normalize_for_prefix(Path::new(a), &cwd);
|
|
8
|
+
let b = normalize_for_prefix(Path::new(b), &cwd);
|
|
9
|
+
|
|
10
|
+
options.paths.iter().any(|root| {
|
|
11
|
+
let root = normalize_for_prefix(root, &cwd);
|
|
12
|
+
is_under_root(&a, &root) && is_under_root(&b, &root)
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fn is_under_root(path: &[PathBuf], root: &[PathBuf]) -> bool {
|
|
17
|
+
path.len() > root.len() && path.starts_with(root)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn normalize_for_prefix(path: &Path, cwd: &Path) -> Vec<PathBuf> {
|
|
21
|
+
let full_path = if path.is_absolute() {
|
|
22
|
+
path.to_path_buf()
|
|
23
|
+
} else {
|
|
24
|
+
cwd.join(path)
|
|
25
|
+
};
|
|
26
|
+
let mut normalized = Vec::new();
|
|
27
|
+
|
|
28
|
+
for component in full_path.components() {
|
|
29
|
+
match component {
|
|
30
|
+
std::path::Component::CurDir => {}
|
|
31
|
+
std::path::Component::ParentDir => {
|
|
32
|
+
normalized.pop();
|
|
33
|
+
}
|
|
34
|
+
std::path::Component::Normal(value) => normalized.push(PathBuf::from(value)),
|
|
35
|
+
std::path::Component::RootDir | std::path::Component::Prefix(_) => {}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
normalized
|
|
40
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
use super::model::{CloneMatch, StatisticRow, Statistics};
|
|
2
|
+
|
|
3
|
+
#[derive(Clone, Debug, Default)]
|
|
4
|
+
pub struct Statistic {
|
|
5
|
+
statistics: Statistics,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
impl Statistic {
|
|
9
|
+
pub fn new() -> Self {
|
|
10
|
+
Self::default()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub fn get_statistic(&self) -> &Statistics {
|
|
14
|
+
&self.statistics
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn into_statistics(self) -> Statistics {
|
|
18
|
+
self.statistics
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub fn match_source(
|
|
22
|
+
&mut self,
|
|
23
|
+
source_id: impl AsRef<str>,
|
|
24
|
+
format_name: impl AsRef<str>,
|
|
25
|
+
lines: usize,
|
|
26
|
+
tokens: usize,
|
|
27
|
+
) {
|
|
28
|
+
update_source_statistics(
|
|
29
|
+
&mut self.statistics,
|
|
30
|
+
source_id.as_ref(),
|
|
31
|
+
format_name.as_ref(),
|
|
32
|
+
lines,
|
|
33
|
+
tokens,
|
|
34
|
+
);
|
|
35
|
+
finalize_percentages(&mut self.statistics);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn clone_found(&mut self, clone: &CloneMatch) {
|
|
39
|
+
update_clone_statistics(&mut self.statistics, clone);
|
|
40
|
+
finalize_percentages(&mut self.statistics);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn clone_lines(clone: &CloneMatch) -> usize {
|
|
45
|
+
clone
|
|
46
|
+
.duplication_a
|
|
47
|
+
.end
|
|
48
|
+
.line
|
|
49
|
+
.saturating_sub(clone.duplication_a.start.line)
|
|
50
|
+
+ 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub(super) fn clone_stat_lines(clone: &CloneMatch) -> usize {
|
|
54
|
+
clone
|
|
55
|
+
.duplication_a
|
|
56
|
+
.end
|
|
57
|
+
.line
|
|
58
|
+
.saturating_sub(clone.duplication_a.start.line)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn clone_stat_tokens(clone: &CloneMatch) -> usize {
|
|
62
|
+
clone
|
|
63
|
+
.duplication_a
|
|
64
|
+
.end
|
|
65
|
+
.position
|
|
66
|
+
.saturating_sub(clone.duplication_a.start.position)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pub(super) fn update_source_statistics(
|
|
70
|
+
statistics: &mut Statistics,
|
|
71
|
+
source_id: &str,
|
|
72
|
+
format_name: &str,
|
|
73
|
+
lines: usize,
|
|
74
|
+
tokens: usize,
|
|
75
|
+
) {
|
|
76
|
+
statistics.total.sources += 1;
|
|
77
|
+
statistics.total.lines += lines;
|
|
78
|
+
statistics.total.tokens += tokens;
|
|
79
|
+
|
|
80
|
+
let format = statistics
|
|
81
|
+
.formats
|
|
82
|
+
.entry(format_name.to_string())
|
|
83
|
+
.or_default();
|
|
84
|
+
format.total.sources += 1;
|
|
85
|
+
format.total.lines += lines;
|
|
86
|
+
format.total.tokens += tokens;
|
|
87
|
+
|
|
88
|
+
let source = format.sources.entry(source_id.to_string()).or_default();
|
|
89
|
+
source.sources = 1;
|
|
90
|
+
source.lines += lines;
|
|
91
|
+
source.tokens += tokens;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pub(super) fn update_clone_statistics(statistics: &mut Statistics, clone: &CloneMatch) {
|
|
95
|
+
let lines = clone_stat_lines(clone);
|
|
96
|
+
let tokens = clone_stat_tokens(clone);
|
|
97
|
+
statistics.total.clones += 1;
|
|
98
|
+
statistics.total.duplicated_lines += lines;
|
|
99
|
+
statistics.total.duplicated_tokens += tokens;
|
|
100
|
+
|
|
101
|
+
let format = statistics.formats.entry(clone.format.clone()).or_default();
|
|
102
|
+
format.total.clones += 1;
|
|
103
|
+
format.total.duplicated_lines += lines;
|
|
104
|
+
format.total.duplicated_tokens += tokens;
|
|
105
|
+
|
|
106
|
+
for source_id in [
|
|
107
|
+
&clone.duplication_a.source_id,
|
|
108
|
+
&clone.duplication_b.source_id,
|
|
109
|
+
] {
|
|
110
|
+
let source = format.sources.entry(source_id.clone()).or_default();
|
|
111
|
+
source.clones += 1;
|
|
112
|
+
source.duplicated_lines += lines;
|
|
113
|
+
source.duplicated_tokens += tokens;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pub(super) fn finalize_percentages(statistics: &mut Statistics) {
|
|
118
|
+
update_row_percentages(&mut statistics.total);
|
|
119
|
+
for format in statistics.formats.values_mut() {
|
|
120
|
+
update_row_percentages(&mut format.total);
|
|
121
|
+
for source in format.sources.values_mut() {
|
|
122
|
+
update_row_percentages(source);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fn update_row_percentages(row: &mut StatisticRow) {
|
|
128
|
+
row.percentage = percentage(row.lines, row.duplicated_lines);
|
|
129
|
+
row.percentage_tokens = percentage(row.tokens, row.duplicated_tokens);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn percentage(total: usize, duplicated: usize) -> f64 {
|
|
133
|
+
if total == 0 {
|
|
134
|
+
0.0
|
|
135
|
+
} else {
|
|
136
|
+
((duplicated as f64 * 10000.0) / total as f64).round() / 100.0
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
use std::{collections::HashMap, error::Error, fmt};
|
|
2
|
+
|
|
3
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
4
|
+
pub struct MemoryStoreError {
|
|
5
|
+
namespace: String,
|
|
6
|
+
key: String,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
impl MemoryStoreError {
|
|
10
|
+
pub fn new(namespace: impl Into<String>, key: impl Into<String>) -> Self {
|
|
11
|
+
Self {
|
|
12
|
+
namespace: namespace.into(),
|
|
13
|
+
key: key.into(),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn namespace(&self) -> &str {
|
|
18
|
+
&self.namespace
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub fn key(&self) -> &str {
|
|
22
|
+
&self.key
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl fmt::Display for MemoryStoreError {
|
|
27
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
28
|
+
write!(
|
|
29
|
+
formatter,
|
|
30
|
+
"key '{}' not found in namespace '{}'",
|
|
31
|
+
self.key, self.namespace
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl Error for MemoryStoreError {}
|
|
37
|
+
|
|
38
|
+
#[derive(Clone, Debug)]
|
|
39
|
+
pub struct MemoryStore<T> {
|
|
40
|
+
namespace: String,
|
|
41
|
+
values: HashMap<String, HashMap<String, T>>,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl<T> Default for MemoryStore<T> {
|
|
45
|
+
fn default() -> Self {
|
|
46
|
+
Self::new()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
impl<T> MemoryStore<T> {
|
|
51
|
+
pub fn new() -> Self {
|
|
52
|
+
Self {
|
|
53
|
+
namespace: String::new(),
|
|
54
|
+
values: HashMap::new(),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub fn namespace(&mut self, namespace: impl Into<String>) {
|
|
59
|
+
self.namespace = namespace.into();
|
|
60
|
+
self.values.entry(self.namespace.clone()).or_default();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub fn current_namespace(&self) -> &str {
|
|
64
|
+
&self.namespace
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub fn get(&self, key: impl AsRef<str>) -> Result<&T, MemoryStoreError> {
|
|
68
|
+
let key = key.as_ref();
|
|
69
|
+
self.values
|
|
70
|
+
.get(&self.namespace)
|
|
71
|
+
.and_then(|namespace| namespace.get(key))
|
|
72
|
+
.ok_or_else(|| MemoryStoreError::new(self.namespace.clone(), key))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pub fn set(&mut self, key: impl Into<String>, value: T) -> &T {
|
|
76
|
+
let key = key.into();
|
|
77
|
+
self.values
|
|
78
|
+
.entry(self.namespace.clone())
|
|
79
|
+
.or_default()
|
|
80
|
+
.entry(key)
|
|
81
|
+
.insert_entry(value)
|
|
82
|
+
.into_mut()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pub fn close(&mut self) {
|
|
86
|
+
self.values.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub fn is_empty(&self) -> bool {
|
|
90
|
+
self.values.values().all(HashMap::is_empty)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pub fn len(&self) -> usize {
|
|
94
|
+
self.values.values().map(HashMap::len).sum()
|
|
95
|
+
}
|
|
96
|
+
}
|