1use std::path::{Path, PathBuf};
2
3use serde::Serialize;
4
5use crate::duplicates::{CloneInstance, DuplicationReport};
6use crate::graph::{ModuleGraph, ReferenceKind};
7
8fn path_matches(module_path: &Path, root: &Path, user_path: &str) -> bool {
13 let rel = module_path.strip_prefix(root).unwrap_or(module_path);
14 let rel_str = rel.to_string_lossy();
15 if rel_str == user_path || module_path.to_string_lossy() == user_path {
16 return true;
17 }
18 if root.canonicalize().is_ok_and(|canonical_root| {
19 module_path
20 .strip_prefix(&canonical_root)
21 .is_ok_and(|rel| rel.to_string_lossy() == user_path)
22 }) {
23 return true;
24 }
25 let module_str = module_path.to_string_lossy();
26 module_str.ends_with(&format!("/{user_path}"))
27}
28
29#[derive(Debug, Serialize)]
31pub struct ExportTrace {
32 pub file: PathBuf,
34 pub export_name: String,
36 pub file_reachable: bool,
38 pub is_entry_point: bool,
40 pub is_used: bool,
42 pub direct_references: Vec<ExportReference>,
44 pub re_export_chains: Vec<ReExportChain>,
46 pub reason: String,
48}
49
50#[derive(Debug, Serialize)]
52pub struct ExportReference {
53 pub from_file: PathBuf,
54 pub kind: String,
55}
56
57#[derive(Debug, Serialize)]
59pub struct ReExportChain {
60 pub barrel_file: PathBuf,
62 pub exported_as: String,
64 pub reference_count: usize,
66}
67
68#[derive(Debug, Serialize)]
70pub struct FileTrace {
71 pub file: PathBuf,
73 pub is_reachable: bool,
75 pub is_entry_point: bool,
77 pub exports: Vec<TracedExport>,
79 pub imports_from: Vec<PathBuf>,
81 pub imported_by: Vec<PathBuf>,
83 pub re_exports: Vec<TracedReExport>,
85}
86
87#[derive(Debug, Serialize)]
89pub struct TracedExport {
90 pub name: String,
91 pub is_type_only: bool,
92 pub reference_count: usize,
93 pub referenced_by: Vec<ExportReference>,
94}
95
96#[derive(Debug, Serialize)]
98pub struct TracedReExport {
99 pub source_file: PathBuf,
100 pub imported_name: String,
101 pub exported_name: String,
102}
103
104#[derive(Debug, Serialize)]
106pub struct DependencyTrace {
107 pub package_name: String,
109 pub imported_by: Vec<PathBuf>,
111 pub type_only_imported_by: Vec<PathBuf>,
113 pub is_used: bool,
115 pub import_count: usize,
117}
118
119#[derive(Debug, Clone, Serialize)]
121pub struct PipelineTimings {
122 pub discover_files_ms: f64,
123 pub file_count: usize,
124 pub workspaces_ms: f64,
125 pub workspace_count: usize,
126 pub plugins_ms: f64,
127 pub script_analysis_ms: f64,
128 pub parse_extract_ms: f64,
129 pub module_count: usize,
130 pub cache_hits: usize,
132 pub cache_misses: usize,
134 pub cache_update_ms: f64,
135 pub entry_points_ms: f64,
136 pub entry_point_count: usize,
137 pub resolve_imports_ms: f64,
138 pub build_graph_ms: f64,
139 pub analyze_ms: f64,
140 pub total_ms: f64,
141}
142
143#[must_use]
145pub fn trace_export(
146 graph: &ModuleGraph,
147 root: &Path,
148 file_path: &str,
149 export_name: &str,
150) -> Option<ExportTrace> {
151 let module = graph
153 .modules
154 .iter()
155 .find(|m| path_matches(&m.path, root, file_path))?;
156
157 let export = module.exports.iter().find(|e| {
159 let name_str = e.name.to_string();
160 name_str == export_name || (export_name == "default" && name_str == "default")
161 })?;
162
163 let direct_references: Vec<ExportReference> = export
164 .references
165 .iter()
166 .map(|r| {
167 let from_path = graph.modules.get(r.from_file.0 as usize).map_or_else(
168 || PathBuf::from(format!("<unknown:{}>", r.from_file.0)),
169 |m| m.path.strip_prefix(root).unwrap_or(&m.path).to_path_buf(),
170 );
171 ExportReference {
172 from_file: from_path,
173 kind: format_reference_kind(&r.kind),
174 }
175 })
176 .collect();
177
178 let re_export_chains: Vec<ReExportChain> = graph
180 .modules
181 .iter()
182 .flat_map(|m| {
183 m.re_exports
184 .iter()
185 .filter(|re| {
186 re.source_file == module.file_id
187 && (re.imported_name == export_name || re.imported_name == "*")
188 })
189 .map(|re| {
190 let barrel_export = m.exports.iter().find(|e| {
191 if re.exported_name == "*" {
192 e.name.to_string() == export_name
193 } else {
194 e.name.to_string() == re.exported_name
195 }
196 });
197 ReExportChain {
198 barrel_file: m.path.strip_prefix(root).unwrap_or(&m.path).to_path_buf(),
199 exported_as: re.exported_name.clone(),
200 reference_count: barrel_export.map_or(0, |e| e.references.len()),
201 }
202 })
203 })
204 .collect();
205
206 let is_used = !export.references.is_empty();
207 let reason = if !module.is_reachable {
208 "File is unreachable from any entry point".to_string()
209 } else if is_used {
210 format!(
211 "Used by {} file(s){}",
212 export.references.len(),
213 if re_export_chains.is_empty() {
214 String::new()
215 } else {
216 format!(", re-exported through {} barrel(s)", re_export_chains.len())
217 }
218 )
219 } else if module.is_entry_point {
220 "No internal references, but file is an entry point (export is externally accessible)"
221 .to_string()
222 } else if !re_export_chains.is_empty() {
223 format!(
224 "Re-exported through {} barrel(s) but no consumer imports it through the barrel",
225 re_export_chains.len()
226 )
227 } else {
228 "No references found — export is unused".to_string()
229 };
230
231 Some(ExportTrace {
232 file: module
233 .path
234 .strip_prefix(root)
235 .unwrap_or(&module.path)
236 .to_path_buf(),
237 export_name: export_name.to_string(),
238 file_reachable: module.is_reachable,
239 is_entry_point: module.is_entry_point,
240 is_used,
241 direct_references,
242 re_export_chains,
243 reason,
244 })
245}
246
247#[must_use]
249pub fn trace_file(graph: &ModuleGraph, root: &Path, file_path: &str) -> Option<FileTrace> {
250 let module = graph
251 .modules
252 .iter()
253 .find(|m| path_matches(&m.path, root, file_path))?;
254
255 let exports: Vec<TracedExport> = module
256 .exports
257 .iter()
258 .map(|e| TracedExport {
259 name: e.name.to_string(),
260 is_type_only: e.is_type_only,
261 reference_count: e.references.len(),
262 referenced_by: e
263 .references
264 .iter()
265 .map(|r| {
266 let from_path = graph.modules.get(r.from_file.0 as usize).map_or_else(
267 || PathBuf::from(format!("<unknown:{}>", r.from_file.0)),
268 |m| m.path.strip_prefix(root).unwrap_or(&m.path).to_path_buf(),
269 );
270 ExportReference {
271 from_file: from_path,
272 kind: format_reference_kind(&r.kind),
273 }
274 })
275 .collect(),
276 })
277 .collect();
278
279 let imports_from: Vec<PathBuf> = graph
281 .edges_for(module.file_id)
282 .iter()
283 .filter_map(|target_id| {
284 graph
285 .modules
286 .get(target_id.0 as usize)
287 .map(|m| m.path.strip_prefix(root).unwrap_or(&m.path).to_path_buf())
288 })
289 .collect();
290
291 let imported_by: Vec<PathBuf> = graph
293 .reverse_deps
294 .get(module.file_id.0 as usize)
295 .map(|deps| {
296 deps.iter()
297 .filter_map(|fid| {
298 graph
299 .modules
300 .get(fid.0 as usize)
301 .map(|m| m.path.strip_prefix(root).unwrap_or(&m.path).to_path_buf())
302 })
303 .collect()
304 })
305 .unwrap_or_default();
306
307 let re_exports: Vec<TracedReExport> = module
308 .re_exports
309 .iter()
310 .map(|re| {
311 let source_path = graph.modules.get(re.source_file.0 as usize).map_or_else(
312 || PathBuf::from(format!("<unknown:{}>", re.source_file.0)),
313 |m| m.path.strip_prefix(root).unwrap_or(&m.path).to_path_buf(),
314 );
315 TracedReExport {
316 source_file: source_path,
317 imported_name: re.imported_name.clone(),
318 exported_name: re.exported_name.clone(),
319 }
320 })
321 .collect();
322
323 Some(FileTrace {
324 file: module
325 .path
326 .strip_prefix(root)
327 .unwrap_or(&module.path)
328 .to_path_buf(),
329 is_reachable: module.is_reachable,
330 is_entry_point: module.is_entry_point,
331 exports,
332 imports_from,
333 imported_by,
334 re_exports,
335 })
336}
337
338#[must_use]
340pub fn trace_dependency(graph: &ModuleGraph, root: &Path, package_name: &str) -> DependencyTrace {
341 let imported_by: Vec<PathBuf> = graph
342 .package_usage
343 .get(package_name)
344 .map(|ids| {
345 ids.iter()
346 .filter_map(|fid| {
347 graph
348 .modules
349 .get(fid.0 as usize)
350 .map(|m| m.path.strip_prefix(root).unwrap_or(&m.path).to_path_buf())
351 })
352 .collect()
353 })
354 .unwrap_or_default();
355
356 let type_only_imported_by: Vec<PathBuf> = graph
357 .type_only_package_usage
358 .get(package_name)
359 .map(|ids| {
360 ids.iter()
361 .filter_map(|fid| {
362 graph
363 .modules
364 .get(fid.0 as usize)
365 .map(|m| m.path.strip_prefix(root).unwrap_or(&m.path).to_path_buf())
366 })
367 .collect()
368 })
369 .unwrap_or_default();
370
371 let import_count = imported_by.len();
372 DependencyTrace {
373 package_name: package_name.to_string(),
374 imported_by,
375 type_only_imported_by,
376 is_used: import_count > 0,
377 import_count,
378 }
379}
380
381fn format_reference_kind(kind: &ReferenceKind) -> String {
382 match kind {
383 ReferenceKind::NamedImport => "named import".to_string(),
384 ReferenceKind::DefaultImport => "default import".to_string(),
385 ReferenceKind::NamespaceImport => "namespace import".to_string(),
386 ReferenceKind::ReExport => "re-export".to_string(),
387 ReferenceKind::DynamicImport => "dynamic import".to_string(),
388 ReferenceKind::SideEffectImport => "side-effect import".to_string(),
389 }
390}
391
392#[derive(Debug, Serialize)]
394pub struct CloneTrace {
395 pub file: PathBuf,
396 pub line: usize,
397 pub matched_instance: Option<CloneInstance>,
398 pub clone_groups: Vec<TracedCloneGroup>,
399}
400
401#[derive(Debug, Serialize)]
402pub struct TracedCloneGroup {
403 pub token_count: usize,
404 pub line_count: usize,
405 pub instances: Vec<CloneInstance>,
406}
407
408#[must_use]
409pub fn trace_clone(
410 report: &DuplicationReport,
411 root: &Path,
412 file_path: &str,
413 line: usize,
414) -> CloneTrace {
415 let resolved = root.join(file_path);
416 let mut matched_instance = None;
417 let mut clone_groups = Vec::new();
418
419 for group in &report.clone_groups {
420 let matching = group.instances.iter().find(|inst| {
421 let inst_matches = inst.file == resolved
422 || inst.file.strip_prefix(root).unwrap_or(&inst.file) == Path::new(file_path);
423 inst_matches && inst.start_line <= line && line <= inst.end_line
424 });
425
426 if let Some(matched) = matching {
427 if matched_instance.is_none() {
428 matched_instance = Some(matched.clone());
429 }
430 clone_groups.push(TracedCloneGroup {
431 token_count: group.token_count,
432 line_count: group.line_count,
433 instances: group.instances.clone(),
434 });
435 }
436 }
437
438 CloneTrace {
439 file: PathBuf::from(file_path),
440 line,
441 matched_instance,
442 clone_groups,
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449 use rustc_hash::FxHashSet;
450
451 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
452 use crate::extract::{ExportInfo, ExportName, ImportInfo, ImportedName};
453 use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule};
454
455 fn build_test_graph() -> ModuleGraph {
456 let files = vec![
457 DiscoveredFile {
458 id: FileId(0),
459 path: PathBuf::from("/project/src/entry.ts"),
460 size_bytes: 100,
461 },
462 DiscoveredFile {
463 id: FileId(1),
464 path: PathBuf::from("/project/src/utils.ts"),
465 size_bytes: 50,
466 },
467 DiscoveredFile {
468 id: FileId(2),
469 path: PathBuf::from("/project/src/unused.ts"),
470 size_bytes: 30,
471 },
472 ];
473
474 let entry_points = vec![EntryPoint {
475 path: PathBuf::from("/project/src/entry.ts"),
476 source: EntryPointSource::PackageJsonMain,
477 }];
478
479 let resolved_modules = vec![
480 ResolvedModule {
481 file_id: FileId(0),
482 path: PathBuf::from("/project/src/entry.ts"),
483 exports: vec![],
484 re_exports: vec![],
485 resolved_imports: vec![ResolvedImport {
486 info: ImportInfo {
487 source: "./utils".to_string(),
488 imported_name: ImportedName::Named("foo".to_string()),
489 local_name: "foo".to_string(),
490 is_type_only: false,
491 span: oxc_span::Span::new(0, 10),
492 source_span: oxc_span::Span::default(),
493 },
494 target: ResolveResult::InternalModule(FileId(1)),
495 }],
496 resolved_dynamic_imports: vec![],
497 resolved_dynamic_patterns: vec![],
498 member_accesses: vec![],
499 whole_object_uses: vec![],
500 has_cjs_exports: false,
501 unused_import_bindings: FxHashSet::default(),
502 },
503 ResolvedModule {
504 file_id: FileId(1),
505 path: PathBuf::from("/project/src/utils.ts"),
506 exports: vec![
507 ExportInfo {
508 name: ExportName::Named("foo".to_string()),
509 local_name: Some("foo".to_string()),
510 is_type_only: false,
511 is_public: false,
512 span: oxc_span::Span::new(0, 20),
513 members: vec![],
514 },
515 ExportInfo {
516 name: ExportName::Named("bar".to_string()),
517 local_name: Some("bar".to_string()),
518 is_type_only: false,
519 is_public: false,
520 span: oxc_span::Span::new(21, 40),
521 members: vec![],
522 },
523 ],
524 re_exports: vec![],
525 resolved_imports: vec![],
526 resolved_dynamic_imports: vec![],
527 resolved_dynamic_patterns: vec![],
528 member_accesses: vec![],
529 whole_object_uses: vec![],
530 has_cjs_exports: false,
531 unused_import_bindings: FxHashSet::default(),
532 },
533 ResolvedModule {
534 file_id: FileId(2),
535 path: PathBuf::from("/project/src/unused.ts"),
536 exports: vec![ExportInfo {
537 name: ExportName::Named("baz".to_string()),
538 local_name: Some("baz".to_string()),
539 is_type_only: false,
540 is_public: false,
541 span: oxc_span::Span::new(0, 15),
542 members: vec![],
543 }],
544 re_exports: vec![],
545 resolved_imports: vec![],
546 resolved_dynamic_imports: vec![],
547 resolved_dynamic_patterns: vec![],
548 member_accesses: vec![],
549 whole_object_uses: vec![],
550 has_cjs_exports: false,
551 unused_import_bindings: FxHashSet::default(),
552 },
553 ];
554
555 ModuleGraph::build(&resolved_modules, &entry_points, &files)
556 }
557
558 #[test]
559 fn trace_used_export() {
560 let graph = build_test_graph();
561 let root = Path::new("/project");
562
563 let trace = trace_export(&graph, root, "src/utils.ts", "foo").unwrap();
564 assert!(trace.is_used);
565 assert!(trace.file_reachable);
566 assert_eq!(trace.direct_references.len(), 1);
567 assert_eq!(
568 trace.direct_references[0].from_file,
569 PathBuf::from("src/entry.ts")
570 );
571 assert_eq!(trace.direct_references[0].kind, "named import");
572 }
573
574 #[test]
575 fn trace_unused_export() {
576 let graph = build_test_graph();
577 let root = Path::new("/project");
578
579 let trace = trace_export(&graph, root, "src/utils.ts", "bar").unwrap();
580 assert!(!trace.is_used);
581 assert!(trace.file_reachable);
582 assert!(trace.direct_references.is_empty());
583 }
584
585 #[test]
586 fn trace_unreachable_file_export() {
587 let graph = build_test_graph();
588 let root = Path::new("/project");
589
590 let trace = trace_export(&graph, root, "src/unused.ts", "baz").unwrap();
591 assert!(!trace.is_used);
592 assert!(!trace.file_reachable);
593 assert!(trace.reason.contains("unreachable"));
594 }
595
596 #[test]
597 fn trace_nonexistent_export() {
598 let graph = build_test_graph();
599 let root = Path::new("/project");
600
601 let trace = trace_export(&graph, root, "src/utils.ts", "nonexistent");
602 assert!(trace.is_none());
603 }
604
605 #[test]
606 fn trace_nonexistent_file() {
607 let graph = build_test_graph();
608 let root = Path::new("/project");
609
610 let trace = trace_export(&graph, root, "src/nope.ts", "foo");
611 assert!(trace.is_none());
612 }
613
614 #[test]
615 fn trace_file_edges() {
616 let graph = build_test_graph();
617 let root = Path::new("/project");
618
619 let trace = trace_file(&graph, root, "src/entry.ts").unwrap();
620 assert!(trace.is_entry_point);
621 assert!(trace.is_reachable);
622 assert_eq!(trace.imports_from.len(), 1);
623 assert_eq!(trace.imports_from[0], PathBuf::from("src/utils.ts"));
624 assert!(trace.imported_by.is_empty());
625 }
626
627 #[test]
628 fn trace_file_imported_by() {
629 let graph = build_test_graph();
630 let root = Path::new("/project");
631
632 let trace = trace_file(&graph, root, "src/utils.ts").unwrap();
633 assert!(!trace.is_entry_point);
634 assert!(trace.is_reachable);
635 assert_eq!(trace.exports.len(), 2);
636 assert_eq!(trace.imported_by.len(), 1);
637 assert_eq!(trace.imported_by[0], PathBuf::from("src/entry.ts"));
638 }
639
640 #[test]
641 fn trace_unreachable_file() {
642 let graph = build_test_graph();
643 let root = Path::new("/project");
644
645 let trace = trace_file(&graph, root, "src/unused.ts").unwrap();
646 assert!(!trace.is_reachable);
647 assert!(!trace.is_entry_point);
648 assert!(trace.imported_by.is_empty());
649 }
650
651 #[test]
652 fn trace_dependency_used() {
653 let files = vec![DiscoveredFile {
655 id: FileId(0),
656 path: PathBuf::from("/project/src/app.ts"),
657 size_bytes: 100,
658 }];
659 let entry_points = vec![EntryPoint {
660 path: PathBuf::from("/project/src/app.ts"),
661 source: EntryPointSource::PackageJsonMain,
662 }];
663 let resolved_modules = vec![ResolvedModule {
664 file_id: FileId(0),
665 path: PathBuf::from("/project/src/app.ts"),
666 exports: vec![],
667 re_exports: vec![],
668 resolved_imports: vec![ResolvedImport {
669 info: ImportInfo {
670 source: "lodash".to_string(),
671 imported_name: ImportedName::Named("get".to_string()),
672 local_name: "get".to_string(),
673 is_type_only: false,
674 span: oxc_span::Span::new(0, 10),
675 source_span: oxc_span::Span::default(),
676 },
677 target: ResolveResult::NpmPackage("lodash".to_string()),
678 }],
679 resolved_dynamic_imports: vec![],
680 resolved_dynamic_patterns: vec![],
681 member_accesses: vec![],
682 whole_object_uses: vec![],
683 has_cjs_exports: false,
684 unused_import_bindings: FxHashSet::default(),
685 }];
686
687 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
688 let root = Path::new("/project");
689
690 let trace = trace_dependency(&graph, root, "lodash");
691 assert!(trace.is_used);
692 assert_eq!(trace.import_count, 1);
693 assert_eq!(trace.imported_by[0], PathBuf::from("src/app.ts"));
694 }
695
696 #[test]
697 fn trace_dependency_unused() {
698 let files = vec![DiscoveredFile {
699 id: FileId(0),
700 path: PathBuf::from("/project/src/app.ts"),
701 size_bytes: 100,
702 }];
703 let entry_points = vec![EntryPoint {
704 path: PathBuf::from("/project/src/app.ts"),
705 source: EntryPointSource::PackageJsonMain,
706 }];
707 let resolved_modules = vec![ResolvedModule {
708 file_id: FileId(0),
709 path: PathBuf::from("/project/src/app.ts"),
710 exports: vec![],
711 re_exports: vec![],
712 resolved_imports: vec![],
713 resolved_dynamic_imports: vec![],
714 resolved_dynamic_patterns: vec![],
715 member_accesses: vec![],
716 whole_object_uses: vec![],
717 has_cjs_exports: false,
718 unused_import_bindings: FxHashSet::default(),
719 }];
720
721 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
722 let root = Path::new("/project");
723
724 let trace = trace_dependency(&graph, root, "nonexistent-pkg");
725 assert!(!trace.is_used);
726 assert_eq!(trace.import_count, 0);
727 assert!(trace.imported_by.is_empty());
728 }
729
730 #[test]
731 fn trace_clone_finds_matching_group() {
732 use crate::duplicates::{CloneGroup, CloneInstance, DuplicationReport, DuplicationStats};
733 let report = DuplicationReport {
734 clone_groups: vec![CloneGroup {
735 instances: vec![
736 CloneInstance {
737 file: PathBuf::from("/project/src/a.ts"),
738 start_line: 10,
739 end_line: 20,
740 start_col: 0,
741 end_col: 0,
742 fragment: "fn foo() {}".to_string(),
743 },
744 CloneInstance {
745 file: PathBuf::from("/project/src/b.ts"),
746 start_line: 5,
747 end_line: 15,
748 start_col: 0,
749 end_col: 0,
750 fragment: "fn foo() {}".to_string(),
751 },
752 ],
753 token_count: 60,
754 line_count: 11,
755 }],
756 clone_families: vec![],
757 stats: DuplicationStats {
758 total_files: 2,
759 files_with_clones: 2,
760 total_lines: 100,
761 duplicated_lines: 22,
762 total_tokens: 200,
763 duplicated_tokens: 120,
764 clone_groups: 1,
765 clone_instances: 2,
766 duplication_percentage: 22.0,
767 },
768 };
769 let trace = trace_clone(&report, Path::new("/project"), "src/a.ts", 15);
770 assert!(trace.matched_instance.is_some());
771 assert_eq!(trace.clone_groups.len(), 1);
772 assert_eq!(trace.clone_groups[0].instances.len(), 2);
773 }
774
775 #[test]
776 fn trace_clone_no_match() {
777 use crate::duplicates::{CloneGroup, CloneInstance, DuplicationReport, DuplicationStats};
778 let report = DuplicationReport {
779 clone_groups: vec![CloneGroup {
780 instances: vec![CloneInstance {
781 file: PathBuf::from("/project/src/a.ts"),
782 start_line: 10,
783 end_line: 20,
784 start_col: 0,
785 end_col: 0,
786 fragment: "fn foo() {}".to_string(),
787 }],
788 token_count: 60,
789 line_count: 11,
790 }],
791 clone_families: vec![],
792 stats: DuplicationStats {
793 total_files: 1,
794 files_with_clones: 1,
795 total_lines: 50,
796 duplicated_lines: 11,
797 total_tokens: 100,
798 duplicated_tokens: 60,
799 clone_groups: 1,
800 clone_instances: 1,
801 duplication_percentage: 22.0,
802 },
803 };
804 let trace = trace_clone(&report, Path::new("/project"), "src/a.ts", 25);
805 assert!(trace.matched_instance.is_none());
806 assert!(trace.clone_groups.is_empty());
807 }
808
809 #[test]
810 fn trace_clone_line_boundary() {
811 use crate::duplicates::{CloneGroup, CloneInstance, DuplicationReport, DuplicationStats};
812 let report = DuplicationReport {
813 clone_groups: vec![CloneGroup {
814 instances: vec![
815 CloneInstance {
816 file: PathBuf::from("/project/src/a.ts"),
817 start_line: 10,
818 end_line: 20,
819 start_col: 0,
820 end_col: 0,
821 fragment: "code".to_string(),
822 },
823 CloneInstance {
824 file: PathBuf::from("/project/src/b.ts"),
825 start_line: 1,
826 end_line: 11,
827 start_col: 0,
828 end_col: 0,
829 fragment: "code".to_string(),
830 },
831 ],
832 token_count: 50,
833 line_count: 11,
834 }],
835 clone_families: vec![],
836 stats: DuplicationStats {
837 total_files: 2,
838 files_with_clones: 2,
839 total_lines: 100,
840 duplicated_lines: 22,
841 total_tokens: 200,
842 duplicated_tokens: 100,
843 clone_groups: 1,
844 clone_instances: 2,
845 duplication_percentage: 22.0,
846 },
847 };
848 let root = Path::new("/project");
849 assert!(
850 trace_clone(&report, root, "src/a.ts", 10)
851 .matched_instance
852 .is_some()
853 );
854 assert!(
855 trace_clone(&report, root, "src/a.ts", 20)
856 .matched_instance
857 .is_some()
858 );
859 assert!(
860 trace_clone(&report, root, "src/a.ts", 21)
861 .matched_instance
862 .is_none()
863 );
864 }
865}