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