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.
Files changed (96) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/Cargo.lock +1323 -0
  3. package/Cargo.toml +54 -0
  4. package/LICENSE +21 -0
  5. package/README.md +372 -0
  6. package/docs/api-parity.md +49 -0
  7. package/docs/cloning-plan.md +281 -0
  8. package/docs/compat-baseline.md +535 -0
  9. package/docs/format-porting.md +86 -0
  10. package/docs/junior-task-template.md +62 -0
  11. package/docs/junior-workflow.md +87 -0
  12. package/docs/migrating-from-jscpd.md +193 -0
  13. package/docs/npm-release.md +116 -0
  14. package/docs/public-benchmark-suite.md +81 -0
  15. package/docs/release-checklist.md +200 -0
  16. package/docs/release-decisions.md +103 -0
  17. package/docs/release-readiness.md +51 -0
  18. package/docs/upstream-bugs.md +501 -0
  19. package/docs/upstream-issue-drafts.md +393 -0
  20. package/docs/user-guide.md +309 -0
  21. package/examples/dump_oxc_tokens.rs +112 -0
  22. package/examples/library_api.rs +42 -0
  23. package/npm/bin/jscpd-rs.js +6 -0
  24. package/npm/bin/jscpd-server.js +6 -0
  25. package/npm/lib/run-binary.js +68 -0
  26. package/npm/scripts/postinstall.js +50 -0
  27. package/package.json +53 -0
  28. package/skills/dry-refactoring/SKILL.md +63 -0
  29. package/skills/jscpd/SKILL.md +85 -0
  30. package/src/app.rs +512 -0
  31. package/src/bin/jscpd-server.rs +429 -0
  32. package/src/blame.rs +130 -0
  33. package/src/cli/config.rs +543 -0
  34. package/src/cli/parsing.rs +301 -0
  35. package/src/cli/tests.rs +543 -0
  36. package/src/cli.rs +671 -0
  37. package/src/detector/matching/secondary.rs +387 -0
  38. package/src/detector/matching.rs +274 -0
  39. package/src/detector/model.rs +190 -0
  40. package/src/detector/prepare.rs +71 -0
  41. package/src/detector/skip_local.rs +40 -0
  42. package/src/detector/statistics.rs +138 -0
  43. package/src/detector/store.rs +96 -0
  44. package/src/detector/tests.rs +238 -0
  45. package/src/detector.rs +265 -0
  46. package/src/files/discovery.rs +508 -0
  47. package/src/files/gitignore.rs +203 -0
  48. package/src/files/paths.rs +68 -0
  49. package/src/files/shebang.rs +106 -0
  50. package/src/files/tests.rs +523 -0
  51. package/src/files.rs +25 -0
  52. package/src/formats.rs +570 -0
  53. package/src/lib.rs +433 -0
  54. package/src/main.rs +26 -0
  55. package/src/report/ai.rs +125 -0
  56. package/src/report/badge.rs +238 -0
  57. package/src/report/console.rs +180 -0
  58. package/src/report/console_common.rs +37 -0
  59. package/src/report/console_full.rs +139 -0
  60. package/src/report/csv.rs +65 -0
  61. package/src/report/escape.rs +8 -0
  62. package/src/report/file_output.rs +28 -0
  63. package/src/report/html/assets.rs +47 -0
  64. package/src/report/html.rs +336 -0
  65. package/src/report/json.rs +119 -0
  66. package/src/report/markdown.rs +125 -0
  67. package/src/report/sarif.rs +302 -0
  68. package/src/report/silent.rs +22 -0
  69. package/src/report/source.rs +38 -0
  70. package/src/report/summary.rs +50 -0
  71. package/src/report/test_support.rs +133 -0
  72. package/src/report/threshold.rs +76 -0
  73. package/src/report/xcode.rs +90 -0
  74. package/src/report/xml.rs +119 -0
  75. package/src/report.rs +250 -0
  76. package/src/server/mcp.rs +942 -0
  77. package/src/server.rs +1081 -0
  78. package/src/tokenizer/apex.rs +97 -0
  79. package/src/tokenizer/blocks.rs +532 -0
  80. package/src/tokenizer/embedded.rs +106 -0
  81. package/src/tokenizer/generic.rs +511 -0
  82. package/src/tokenizer/hash.rs +27 -0
  83. package/src/tokenizer/ignore.rs +33 -0
  84. package/src/tokenizer/line_index.rs +33 -0
  85. package/src/tokenizer/markdown.rs +289 -0
  86. package/src/tokenizer/markup_attrs.rs +289 -0
  87. package/src/tokenizer/oxc/fallback.rs +275 -0
  88. package/src/tokenizer/oxc/jsx.rs +168 -0
  89. package/src/tokenizer/oxc/kind.rs +177 -0
  90. package/src/tokenizer/oxc/lexical.rs +67 -0
  91. package/src/tokenizer/oxc.rs +659 -0
  92. package/src/tokenizer/scan.rs +88 -0
  93. package/src/tokenizer/tap.rs +150 -0
  94. package/src/tokenizer/tests.rs +915 -0
  95. package/src/tokenizer.rs +328 -0
  96. 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
+ }