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 crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
446 use crate::extract::{ExportInfo, ExportName, ImportInfo, ImportedName};
447 use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule};
448
449 fn build_test_graph() -> ModuleGraph {
450 let files = vec![
451 DiscoveredFile {
452 id: FileId(0),
453 path: PathBuf::from("/project/src/entry.ts"),
454 size_bytes: 100,
455 },
456 DiscoveredFile {
457 id: FileId(1),
458 path: PathBuf::from("/project/src/utils.ts"),
459 size_bytes: 50,
460 },
461 DiscoveredFile {
462 id: FileId(2),
463 path: PathBuf::from("/project/src/unused.ts"),
464 size_bytes: 30,
465 },
466 ];
467
468 let entry_points = vec![EntryPoint {
469 path: PathBuf::from("/project/src/entry.ts"),
470 source: EntryPointSource::PackageJsonMain,
471 }];
472
473 let resolved_modules = vec![
474 ResolvedModule {
475 file_id: FileId(0),
476 path: PathBuf::from("/project/src/entry.ts"),
477 exports: vec![],
478 re_exports: vec![],
479 resolved_imports: vec![ResolvedImport {
480 info: ImportInfo {
481 source: "./utils".to_string(),
482 imported_name: ImportedName::Named("foo".to_string()),
483 local_name: "foo".to_string(),
484 is_type_only: false,
485 span: oxc_span::Span::new(0, 10),
486 },
487 target: ResolveResult::InternalModule(FileId(1)),
488 }],
489 resolved_dynamic_imports: vec![],
490 resolved_dynamic_patterns: vec![],
491 member_accesses: vec![],
492 whole_object_uses: vec![],
493 has_cjs_exports: false,
494 unused_import_bindings: vec![],
495 },
496 ResolvedModule {
497 file_id: FileId(1),
498 path: PathBuf::from("/project/src/utils.ts"),
499 exports: vec![
500 ExportInfo {
501 name: ExportName::Named("foo".to_string()),
502 local_name: Some("foo".to_string()),
503 is_type_only: false,
504 span: oxc_span::Span::new(0, 20),
505 members: vec![],
506 },
507 ExportInfo {
508 name: ExportName::Named("bar".to_string()),
509 local_name: Some("bar".to_string()),
510 is_type_only: false,
511 span: oxc_span::Span::new(21, 40),
512 members: vec![],
513 },
514 ],
515 re_exports: vec![],
516 resolved_imports: vec![],
517 resolved_dynamic_imports: vec![],
518 resolved_dynamic_patterns: vec![],
519 member_accesses: vec![],
520 whole_object_uses: vec![],
521 has_cjs_exports: false,
522 unused_import_bindings: vec![],
523 },
524 ResolvedModule {
525 file_id: FileId(2),
526 path: PathBuf::from("/project/src/unused.ts"),
527 exports: vec![ExportInfo {
528 name: ExportName::Named("baz".to_string()),
529 local_name: Some("baz".to_string()),
530 is_type_only: false,
531 span: oxc_span::Span::new(0, 15),
532 members: vec![],
533 }],
534 re_exports: vec![],
535 resolved_imports: vec![],
536 resolved_dynamic_imports: vec![],
537 resolved_dynamic_patterns: vec![],
538 member_accesses: vec![],
539 whole_object_uses: vec![],
540 has_cjs_exports: false,
541 unused_import_bindings: vec![],
542 },
543 ];
544
545 ModuleGraph::build(&resolved_modules, &entry_points, &files)
546 }
547
548 #[test]
549 fn trace_used_export() {
550 let graph = build_test_graph();
551 let root = Path::new("/project");
552
553 let trace = trace_export(&graph, root, "src/utils.ts", "foo").unwrap();
554 assert!(trace.is_used);
555 assert!(trace.file_reachable);
556 assert_eq!(trace.direct_references.len(), 1);
557 assert_eq!(
558 trace.direct_references[0].from_file,
559 PathBuf::from("src/entry.ts")
560 );
561 assert_eq!(trace.direct_references[0].kind, "named import");
562 }
563
564 #[test]
565 fn trace_unused_export() {
566 let graph = build_test_graph();
567 let root = Path::new("/project");
568
569 let trace = trace_export(&graph, root, "src/utils.ts", "bar").unwrap();
570 assert!(!trace.is_used);
571 assert!(trace.file_reachable);
572 assert!(trace.direct_references.is_empty());
573 }
574
575 #[test]
576 fn trace_unreachable_file_export() {
577 let graph = build_test_graph();
578 let root = Path::new("/project");
579
580 let trace = trace_export(&graph, root, "src/unused.ts", "baz").unwrap();
581 assert!(!trace.is_used);
582 assert!(!trace.file_reachable);
583 assert!(trace.reason.contains("unreachable"));
584 }
585
586 #[test]
587 fn trace_nonexistent_export() {
588 let graph = build_test_graph();
589 let root = Path::new("/project");
590
591 let trace = trace_export(&graph, root, "src/utils.ts", "nonexistent");
592 assert!(trace.is_none());
593 }
594
595 #[test]
596 fn trace_nonexistent_file() {
597 let graph = build_test_graph();
598 let root = Path::new("/project");
599
600 let trace = trace_export(&graph, root, "src/nope.ts", "foo");
601 assert!(trace.is_none());
602 }
603
604 #[test]
605 fn trace_file_edges() {
606 let graph = build_test_graph();
607 let root = Path::new("/project");
608
609 let trace = trace_file(&graph, root, "src/entry.ts").unwrap();
610 assert!(trace.is_entry_point);
611 assert!(trace.is_reachable);
612 assert_eq!(trace.imports_from.len(), 1);
613 assert_eq!(trace.imports_from[0], PathBuf::from("src/utils.ts"));
614 assert!(trace.imported_by.is_empty());
615 }
616
617 #[test]
618 fn trace_file_imported_by() {
619 let graph = build_test_graph();
620 let root = Path::new("/project");
621
622 let trace = trace_file(&graph, root, "src/utils.ts").unwrap();
623 assert!(!trace.is_entry_point);
624 assert!(trace.is_reachable);
625 assert_eq!(trace.exports.len(), 2);
626 assert_eq!(trace.imported_by.len(), 1);
627 assert_eq!(trace.imported_by[0], PathBuf::from("src/entry.ts"));
628 }
629
630 #[test]
631 fn trace_unreachable_file() {
632 let graph = build_test_graph();
633 let root = Path::new("/project");
634
635 let trace = trace_file(&graph, root, "src/unused.ts").unwrap();
636 assert!(!trace.is_reachable);
637 assert!(!trace.is_entry_point);
638 assert!(trace.imported_by.is_empty());
639 }
640
641 #[test]
642 fn trace_dependency_used() {
643 let files = vec![DiscoveredFile {
645 id: FileId(0),
646 path: PathBuf::from("/project/src/app.ts"),
647 size_bytes: 100,
648 }];
649 let entry_points = vec![EntryPoint {
650 path: PathBuf::from("/project/src/app.ts"),
651 source: EntryPointSource::PackageJsonMain,
652 }];
653 let resolved_modules = vec![ResolvedModule {
654 file_id: FileId(0),
655 path: PathBuf::from("/project/src/app.ts"),
656 exports: vec![],
657 re_exports: vec![],
658 resolved_imports: vec![ResolvedImport {
659 info: ImportInfo {
660 source: "lodash".to_string(),
661 imported_name: ImportedName::Named("get".to_string()),
662 local_name: "get".to_string(),
663 is_type_only: false,
664 span: oxc_span::Span::new(0, 10),
665 },
666 target: ResolveResult::NpmPackage("lodash".to_string()),
667 }],
668 resolved_dynamic_imports: vec![],
669 resolved_dynamic_patterns: vec![],
670 member_accesses: vec![],
671 whole_object_uses: vec![],
672 has_cjs_exports: false,
673 unused_import_bindings: vec![],
674 }];
675
676 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
677 let root = Path::new("/project");
678
679 let trace = trace_dependency(&graph, root, "lodash");
680 assert!(trace.is_used);
681 assert_eq!(trace.import_count, 1);
682 assert_eq!(trace.imported_by[0], PathBuf::from("src/app.ts"));
683 }
684
685 #[test]
686 fn trace_dependency_unused() {
687 let files = vec![DiscoveredFile {
688 id: FileId(0),
689 path: PathBuf::from("/project/src/app.ts"),
690 size_bytes: 100,
691 }];
692 let entry_points = vec![EntryPoint {
693 path: PathBuf::from("/project/src/app.ts"),
694 source: EntryPointSource::PackageJsonMain,
695 }];
696 let resolved_modules = vec![ResolvedModule {
697 file_id: FileId(0),
698 path: PathBuf::from("/project/src/app.ts"),
699 exports: vec![],
700 re_exports: vec![],
701 resolved_imports: vec![],
702 resolved_dynamic_imports: vec![],
703 resolved_dynamic_patterns: vec![],
704 member_accesses: vec![],
705 whole_object_uses: vec![],
706 has_cjs_exports: false,
707 unused_import_bindings: vec![],
708 }];
709
710 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
711 let root = Path::new("/project");
712
713 let trace = trace_dependency(&graph, root, "nonexistent-pkg");
714 assert!(!trace.is_used);
715 assert_eq!(trace.import_count, 0);
716 assert!(trace.imported_by.is_empty());
717 }
718
719 #[test]
720 fn trace_clone_finds_matching_group() {
721 use crate::duplicates::{CloneGroup, CloneInstance, DuplicationReport, DuplicationStats};
722 let report = DuplicationReport {
723 clone_groups: vec![CloneGroup {
724 instances: vec![
725 CloneInstance {
726 file: PathBuf::from("/project/src/a.ts"),
727 start_line: 10,
728 end_line: 20,
729 start_col: 0,
730 end_col: 0,
731 fragment: "fn foo() {}".to_string(),
732 },
733 CloneInstance {
734 file: PathBuf::from("/project/src/b.ts"),
735 start_line: 5,
736 end_line: 15,
737 start_col: 0,
738 end_col: 0,
739 fragment: "fn foo() {}".to_string(),
740 },
741 ],
742 token_count: 60,
743 line_count: 11,
744 }],
745 clone_families: vec![],
746 stats: DuplicationStats {
747 total_files: 2,
748 files_with_clones: 2,
749 total_lines: 100,
750 duplicated_lines: 22,
751 total_tokens: 200,
752 duplicated_tokens: 120,
753 clone_groups: 1,
754 clone_instances: 2,
755 duplication_percentage: 22.0,
756 },
757 };
758 let trace = trace_clone(&report, Path::new("/project"), "src/a.ts", 15);
759 assert!(trace.matched_instance.is_some());
760 assert_eq!(trace.clone_groups.len(), 1);
761 assert_eq!(trace.clone_groups[0].instances.len(), 2);
762 }
763
764 #[test]
765 fn trace_clone_no_match() {
766 use crate::duplicates::{CloneGroup, CloneInstance, DuplicationReport, DuplicationStats};
767 let report = DuplicationReport {
768 clone_groups: vec![CloneGroup {
769 instances: vec![CloneInstance {
770 file: PathBuf::from("/project/src/a.ts"),
771 start_line: 10,
772 end_line: 20,
773 start_col: 0,
774 end_col: 0,
775 fragment: "fn foo() {}".to_string(),
776 }],
777 token_count: 60,
778 line_count: 11,
779 }],
780 clone_families: vec![],
781 stats: DuplicationStats {
782 total_files: 1,
783 files_with_clones: 1,
784 total_lines: 50,
785 duplicated_lines: 11,
786 total_tokens: 100,
787 duplicated_tokens: 60,
788 clone_groups: 1,
789 clone_instances: 1,
790 duplication_percentage: 22.0,
791 },
792 };
793 let trace = trace_clone(&report, Path::new("/project"), "src/a.ts", 25);
794 assert!(trace.matched_instance.is_none());
795 assert!(trace.clone_groups.is_empty());
796 }
797
798 #[test]
799 fn trace_clone_line_boundary() {
800 use crate::duplicates::{CloneGroup, CloneInstance, DuplicationReport, DuplicationStats};
801 let report = DuplicationReport {
802 clone_groups: vec![CloneGroup {
803 instances: vec![
804 CloneInstance {
805 file: PathBuf::from("/project/src/a.ts"),
806 start_line: 10,
807 end_line: 20,
808 start_col: 0,
809 end_col: 0,
810 fragment: "code".to_string(),
811 },
812 CloneInstance {
813 file: PathBuf::from("/project/src/b.ts"),
814 start_line: 1,
815 end_line: 11,
816 start_col: 0,
817 end_col: 0,
818 fragment: "code".to_string(),
819 },
820 ],
821 token_count: 50,
822 line_count: 11,
823 }],
824 clone_families: vec![],
825 stats: DuplicationStats {
826 total_files: 2,
827 files_with_clones: 2,
828 total_lines: 100,
829 duplicated_lines: 22,
830 total_tokens: 200,
831 duplicated_tokens: 100,
832 clone_groups: 1,
833 clone_instances: 2,
834 duplication_percentage: 22.0,
835 },
836 };
837 let root = Path::new("/project");
838 assert!(
839 trace_clone(&report, root, "src/a.ts", 10)
840 .matched_instance
841 .is_some()
842 );
843 assert!(
844 trace_clone(&report, root, "src/a.ts", 20)
845 .matched_instance
846 .is_some()
847 );
848 assert!(
849 trace_clone(&report, root, "src/a.ts", 21)
850 .matched_instance
851 .is_none()
852 );
853 }
854}