1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7use crate::extract::MemberKind;
8use crate::output::IssueAction;
9use crate::output_dead_code::{
10 BoundaryViolationFinding, CircularDependencyFinding, DuplicateExportFinding,
11 EmptyCatalogGroupFinding, MisconfiguredDependencyOverrideFinding, PrivateTypeLeakFinding,
12 ReExportCycleFinding, TestOnlyDependencyFinding, TypeOnlyDependencyFinding,
13 UnlistedDependencyFinding, UnresolvedCatalogReferenceFinding, UnresolvedImportFinding,
14 UnusedCatalogEntryFinding, UnusedClassMemberFinding, UnusedDependencyFinding,
15 UnusedDependencyOverrideFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding,
16 UnusedExportFinding, UnusedFileFinding, UnusedOptionalDependencyFinding, UnusedTypeFinding,
17};
18use crate::serde_path;
19use crate::suppress::{IssueKind, closest_known_kind_name};
20
21#[derive(Debug, Clone, Default)]
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
27pub struct EntryPointSummary {
28 pub total: usize,
30 pub by_source: Vec<(String, usize)>,
33}
34
35#[derive(Debug, Default, Clone, Serialize)]
57#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
58pub struct AnalysisResults {
59 pub unused_files: Vec<UnusedFileFinding>,
63 pub unused_exports: Vec<UnusedExportFinding>,
67 pub unused_types: Vec<UnusedTypeFinding>,
72 pub private_type_leaks: Vec<PrivateTypeLeakFinding>,
76 pub unused_dependencies: Vec<UnusedDependencyFinding>,
81 pub unused_dev_dependencies: Vec<UnusedDevDependencyFinding>,
86 pub unused_optional_dependencies: Vec<UnusedOptionalDependencyFinding>,
90 pub unused_enum_members: Vec<UnusedEnumMemberFinding>,
94 pub unused_class_members: Vec<UnusedClassMemberFinding>,
100 pub unresolved_imports: Vec<UnresolvedImportFinding>,
104 pub unlisted_dependencies: Vec<UnlistedDependencyFinding>,
107 pub duplicate_exports: Vec<DuplicateExportFinding>,
112 pub type_only_dependencies: Vec<TypeOnlyDependencyFinding>,
116 #[serde(default)]
119 pub test_only_dependencies: Vec<TestOnlyDependencyFinding>,
120 pub circular_dependencies: Vec<CircularDependencyFinding>,
124 #[serde(default)]
131 pub re_export_cycles: Vec<ReExportCycleFinding>,
132 #[serde(default)]
136 pub boundary_violations: Vec<BoundaryViolationFinding>,
137 #[serde(default)]
139 pub stale_suppressions: Vec<StaleSuppression>,
140 #[serde(default)]
146 pub unused_catalog_entries: Vec<UnusedCatalogEntryFinding>,
147 #[serde(default)]
151 pub empty_catalog_groups: Vec<EmptyCatalogGroupFinding>,
152 #[serde(default)]
159 pub unresolved_catalog_references: Vec<UnresolvedCatalogReferenceFinding>,
160 #[serde(default)]
167 pub unused_dependency_overrides: Vec<UnusedDependencyOverrideFinding>,
168 #[serde(default)]
173 pub misconfigured_dependency_overrides: Vec<MisconfiguredDependencyOverrideFinding>,
174 #[serde(skip)]
178 pub suppression_count: usize,
179 #[serde(skip)]
186 pub active_suppressions: Vec<ActiveSuppression>,
187 #[serde(skip)]
190 pub feature_flags: Vec<FeatureFlag>,
191 #[serde(skip)]
199 pub security_findings: Vec<SecurityFinding>,
200 #[serde(skip)]
206 pub security_unresolved_edge_files: usize,
207 #[serde(skip)]
213 pub security_unresolved_callee_sites: usize,
214 #[serde(skip)]
218 pub export_usages: Vec<ExportUsage>,
219 #[serde(skip)]
223 pub entry_point_summary: Option<EntryPointSummary>,
224}
225
226impl AnalysisResults {
227 #[must_use]
258 pub const fn total_issues(&self) -> usize {
259 self.unused_files.len()
260 + self.unused_exports.len()
261 + self.unused_types.len()
262 + self.private_type_leaks.len()
263 + self.unused_dependencies.len()
264 + self.unused_dev_dependencies.len()
265 + self.unused_optional_dependencies.len()
266 + self.unused_enum_members.len()
267 + self.unused_class_members.len()
268 + self.unresolved_imports.len()
269 + self.unlisted_dependencies.len()
270 + self.duplicate_exports.len()
271 + self.type_only_dependencies.len()
272 + self.test_only_dependencies.len()
273 + self.circular_dependencies.len()
274 + self.re_export_cycles.len()
275 + self.boundary_violations.len()
276 + self.stale_suppressions.len()
277 + self.unused_catalog_entries.len()
278 + self.empty_catalog_groups.len()
279 + self.unresolved_catalog_references.len()
280 + self.unused_dependency_overrides.len()
281 + self.misconfigured_dependency_overrides.len()
282 }
283
284 #[must_use]
286 pub const fn has_issues(&self) -> bool {
287 self.total_issues() > 0
288 }
289
290 pub fn merge_into(&mut self, other: Self) {
303 let Self {
304 unused_files,
305 unused_exports,
306 unused_types,
307 private_type_leaks,
308 unused_dependencies,
309 unused_dev_dependencies,
310 unused_optional_dependencies,
311 unused_enum_members,
312 unused_class_members,
313 unresolved_imports,
314 unlisted_dependencies,
315 duplicate_exports,
316 type_only_dependencies,
317 test_only_dependencies,
318 circular_dependencies,
319 re_export_cycles,
320 boundary_violations,
321 stale_suppressions,
322 unused_catalog_entries,
323 empty_catalog_groups,
324 unresolved_catalog_references,
325 unused_dependency_overrides,
326 misconfigured_dependency_overrides,
327 suppression_count,
328 active_suppressions,
329 feature_flags,
330 security_findings,
331 security_unresolved_edge_files,
332 security_unresolved_callee_sites,
333 export_usages,
334 entry_point_summary,
335 } = other;
336
337 self.unused_files.extend(unused_files);
338 self.unused_exports.extend(unused_exports);
339 self.unused_types.extend(unused_types);
340 self.private_type_leaks.extend(private_type_leaks);
341 self.unused_dependencies.extend(unused_dependencies);
342 self.unused_dev_dependencies.extend(unused_dev_dependencies);
343 self.unused_optional_dependencies
344 .extend(unused_optional_dependencies);
345 self.unused_enum_members.extend(unused_enum_members);
346 self.unused_class_members.extend(unused_class_members);
347 self.unresolved_imports.extend(unresolved_imports);
348 self.unlisted_dependencies.extend(unlisted_dependencies);
349 self.duplicate_exports.extend(duplicate_exports);
350 self.type_only_dependencies.extend(type_only_dependencies);
351 self.test_only_dependencies.extend(test_only_dependencies);
352 self.circular_dependencies.extend(circular_dependencies);
353 self.re_export_cycles.extend(re_export_cycles);
354 self.boundary_violations.extend(boundary_violations);
355 self.stale_suppressions.extend(stale_suppressions);
356 self.unused_catalog_entries.extend(unused_catalog_entries);
357 self.empty_catalog_groups.extend(empty_catalog_groups);
358 self.unresolved_catalog_references
359 .extend(unresolved_catalog_references);
360 self.unused_dependency_overrides
361 .extend(unused_dependency_overrides);
362 self.misconfigured_dependency_overrides
363 .extend(misconfigured_dependency_overrides);
364 self.feature_flags.extend(feature_flags);
365 self.security_findings.extend(security_findings);
366 self.security_unresolved_edge_files += security_unresolved_edge_files;
367 self.security_unresolved_callee_sites += security_unresolved_callee_sites;
368 self.export_usages.extend(export_usages);
369 self.active_suppressions.extend(active_suppressions);
370 self.suppression_count += suppression_count;
371 if self.entry_point_summary.is_none() {
372 self.entry_point_summary = entry_point_summary;
373 }
374 }
375
376 #[expect(
383 clippy::too_many_lines,
384 reason = "one short sort_by per result array; splitting would add indirection without clarity"
385 )]
386 pub fn sort(&mut self) {
387 self.unused_files
388 .sort_by(|a, b| a.file.path.cmp(&b.file.path));
389
390 self.unused_exports.sort_by(|a, b| {
391 a.export
392 .path
393 .cmp(&b.export.path)
394 .then(a.export.line.cmp(&b.export.line))
395 .then(a.export.export_name.cmp(&b.export.export_name))
396 });
397
398 self.unused_types.sort_by(|a, b| {
399 a.export
400 .path
401 .cmp(&b.export.path)
402 .then(a.export.line.cmp(&b.export.line))
403 .then(a.export.export_name.cmp(&b.export.export_name))
404 });
405
406 self.private_type_leaks.sort_by(|a, b| {
407 a.leak
408 .path
409 .cmp(&b.leak.path)
410 .then(a.leak.line.cmp(&b.leak.line))
411 .then(a.leak.export_name.cmp(&b.leak.export_name))
412 .then(a.leak.type_name.cmp(&b.leak.type_name))
413 });
414
415 self.unused_dependencies.sort_by(|a, b| {
416 a.dep
417 .path
418 .cmp(&b.dep.path)
419 .then(a.dep.line.cmp(&b.dep.line))
420 .then(a.dep.package_name.cmp(&b.dep.package_name))
421 });
422
423 self.unused_dev_dependencies.sort_by(|a, b| {
424 a.dep
425 .path
426 .cmp(&b.dep.path)
427 .then(a.dep.line.cmp(&b.dep.line))
428 .then(a.dep.package_name.cmp(&b.dep.package_name))
429 });
430
431 self.unused_optional_dependencies.sort_by(|a, b| {
432 a.dep
433 .path
434 .cmp(&b.dep.path)
435 .then(a.dep.line.cmp(&b.dep.line))
436 .then(a.dep.package_name.cmp(&b.dep.package_name))
437 });
438
439 self.unused_enum_members.sort_by(|a, b| {
440 a.member
441 .path
442 .cmp(&b.member.path)
443 .then(a.member.line.cmp(&b.member.line))
444 .then(a.member.parent_name.cmp(&b.member.parent_name))
445 .then(a.member.member_name.cmp(&b.member.member_name))
446 });
447
448 self.unused_class_members.sort_by(|a, b| {
449 a.member
450 .path
451 .cmp(&b.member.path)
452 .then(a.member.line.cmp(&b.member.line))
453 .then(a.member.parent_name.cmp(&b.member.parent_name))
454 .then(a.member.member_name.cmp(&b.member.member_name))
455 });
456
457 self.unresolved_imports.sort_by(|a, b| {
458 a.import
459 .path
460 .cmp(&b.import.path)
461 .then(a.import.line.cmp(&b.import.line))
462 .then(a.import.col.cmp(&b.import.col))
463 .then(a.import.specifier.cmp(&b.import.specifier))
464 });
465
466 self.unlisted_dependencies
467 .sort_by(|a, b| a.dep.package_name.cmp(&b.dep.package_name));
468 for dep in &mut self.unlisted_dependencies {
469 dep.dep
470 .imported_from
471 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
472 }
473
474 self.duplicate_exports
475 .sort_by(|a, b| a.export.export_name.cmp(&b.export.export_name));
476 for dup in &mut self.duplicate_exports {
477 dup.export
478 .locations
479 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
480 }
481
482 self.type_only_dependencies.sort_by(|a, b| {
483 a.dep
484 .path
485 .cmp(&b.dep.path)
486 .then(a.dep.line.cmp(&b.dep.line))
487 .then(a.dep.package_name.cmp(&b.dep.package_name))
488 });
489
490 self.test_only_dependencies.sort_by(|a, b| {
491 a.dep
492 .path
493 .cmp(&b.dep.path)
494 .then(a.dep.line.cmp(&b.dep.line))
495 .then(a.dep.package_name.cmp(&b.dep.package_name))
496 });
497
498 self.circular_dependencies.sort_by(|a, b| {
499 a.cycle
500 .files
501 .cmp(&b.cycle.files)
502 .then(a.cycle.length.cmp(&b.cycle.length))
503 });
504
505 self.re_export_cycles
506 .sort_by(|a, b| a.cycle.files.cmp(&b.cycle.files));
507
508 self.boundary_violations.sort_by(|a, b| {
509 a.violation
510 .from_path
511 .cmp(&b.violation.from_path)
512 .then(a.violation.line.cmp(&b.violation.line))
513 .then(a.violation.col.cmp(&b.violation.col))
514 .then(a.violation.to_path.cmp(&b.violation.to_path))
515 });
516
517 self.stale_suppressions.sort_by(|a, b| {
518 a.path
519 .cmp(&b.path)
520 .then(a.line.cmp(&b.line))
521 .then(a.col.cmp(&b.col))
522 });
523
524 self.unused_catalog_entries.sort_by(|a, b| {
525 a.entry
526 .path
527 .cmp(&b.entry.path)
528 .then_with(|| {
529 catalog_sort_key(&a.entry.catalog_name)
530 .cmp(&catalog_sort_key(&b.entry.catalog_name))
531 })
532 .then(a.entry.catalog_name.cmp(&b.entry.catalog_name))
533 .then(a.entry.entry_name.cmp(&b.entry.entry_name))
534 });
535 for finding in &mut self.unused_catalog_entries {
536 finding.entry.hardcoded_consumers.sort();
537 finding.entry.hardcoded_consumers.dedup();
538 }
539
540 self.empty_catalog_groups.sort_by(|a, b| {
541 a.group
542 .path
543 .cmp(&b.group.path)
544 .then_with(|| {
545 catalog_sort_key(&a.group.catalog_name)
546 .cmp(&catalog_sort_key(&b.group.catalog_name))
547 })
548 .then(a.group.catalog_name.cmp(&b.group.catalog_name))
549 .then(a.group.line.cmp(&b.group.line))
550 });
551
552 self.unresolved_catalog_references.sort_by(|a, b| {
553 a.reference
554 .path
555 .cmp(&b.reference.path)
556 .then(a.reference.line.cmp(&b.reference.line))
557 .then_with(|| {
558 catalog_sort_key(&a.reference.catalog_name)
559 .cmp(&catalog_sort_key(&b.reference.catalog_name))
560 })
561 .then(a.reference.catalog_name.cmp(&b.reference.catalog_name))
562 .then(a.reference.entry_name.cmp(&b.reference.entry_name))
563 });
564 for finding in &mut self.unresolved_catalog_references {
565 finding.reference.available_in_catalogs.sort();
566 finding.reference.available_in_catalogs.dedup();
567 }
568
569 self.unused_dependency_overrides.sort_by(|a, b| {
570 a.entry
571 .path
572 .cmp(&b.entry.path)
573 .then(a.entry.line.cmp(&b.entry.line))
574 .then(a.entry.raw_key.cmp(&b.entry.raw_key))
575 });
576
577 self.misconfigured_dependency_overrides.sort_by(|a, b| {
578 a.entry
579 .path
580 .cmp(&b.entry.path)
581 .then(a.entry.line.cmp(&b.entry.line))
582 .then(a.entry.raw_key.cmp(&b.entry.raw_key))
583 });
584
585 self.feature_flags.sort_by(|a, b| {
586 a.path
587 .cmp(&b.path)
588 .then(a.line.cmp(&b.line))
589 .then(a.flag_name.cmp(&b.flag_name))
590 });
591
592 for usage in &mut self.export_usages {
593 usage.reference_locations.sort_by(|a, b| {
594 a.path
595 .cmp(&b.path)
596 .then(a.line.cmp(&b.line))
597 .then(a.col.cmp(&b.col))
598 });
599 }
600 self.export_usages.sort_by(|a, b| {
601 a.path
602 .cmp(&b.path)
603 .then(a.line.cmp(&b.line))
604 .then(a.export_name.cmp(&b.export_name))
605 });
606 }
607}
608
609fn catalog_sort_key(name: &str) -> (u8, &str) {
611 if name == "default" {
612 (0, name)
613 } else {
614 (1, name)
615 }
616}
617
618#[derive(Debug, Clone, Serialize)]
620#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
621pub struct UnusedFile {
622 #[serde(serialize_with = "serde_path::serialize")]
624 pub path: PathBuf,
625}
626
627#[derive(Debug, Clone, Serialize)]
629#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
630pub struct UnusedExport {
631 #[serde(serialize_with = "serde_path::serialize")]
633 pub path: PathBuf,
634 pub export_name: String,
636 pub is_type_only: bool,
638 pub line: u32,
640 pub col: u32,
642 pub span_start: u32,
644 pub is_re_export: bool,
646}
647
648#[derive(Debug, Clone, Serialize)]
650#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
651pub struct PrivateTypeLeak {
652 #[serde(serialize_with = "serde_path::serialize")]
654 pub path: PathBuf,
655 pub export_name: String,
657 pub type_name: String,
659 pub line: u32,
661 pub col: u32,
663 pub span_start: u32,
665}
666
667#[derive(Debug, Clone, Serialize)]
669#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
670pub struct UnusedDependency {
671 pub package_name: String,
673 pub location: DependencyLocation,
675 #[serde(serialize_with = "serde_path::serialize")]
678 pub path: PathBuf,
679 pub line: u32,
681 #[serde(
683 serialize_with = "serde_path::serialize_vec",
684 skip_serializing_if = "Vec::is_empty"
685 )]
686 #[cfg_attr(feature = "schema", schemars(default))]
687 pub used_in_workspaces: Vec<PathBuf>,
688}
689
690#[derive(Debug, Clone, Serialize)]
707#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
708#[serde(rename_all = "camelCase")]
709pub enum DependencyLocation {
710 Dependencies,
712 DevDependencies,
714 OptionalDependencies,
716}
717
718#[derive(Debug, Clone, Serialize)]
720#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
721pub struct UnusedMember {
722 #[serde(serialize_with = "serde_path::serialize")]
724 pub path: PathBuf,
725 pub parent_name: String,
727 pub member_name: String,
729 pub kind: MemberKind,
731 pub line: u32,
733 pub col: u32,
735}
736
737#[derive(Debug, Clone, Serialize)]
739#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
740pub struct UnresolvedImport {
741 #[serde(serialize_with = "serde_path::serialize")]
743 pub path: PathBuf,
744 pub specifier: String,
746 pub line: u32,
748 pub col: u32,
750 pub specifier_col: u32,
753}
754
755#[derive(Debug, Clone, Serialize)]
757#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
758pub struct UnlistedDependency {
759 pub package_name: String,
762 pub imported_from: Vec<ImportSite>,
764}
765
766#[derive(Debug, Clone, Serialize)]
768#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
769pub struct ImportSite {
770 #[serde(serialize_with = "serde_path::serialize")]
772 pub path: PathBuf,
773 pub line: u32,
775 pub col: u32,
777}
778
779#[derive(Debug, Clone, Serialize)]
781#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
782pub struct DuplicateExport {
783 pub export_name: String,
785 pub locations: Vec<DuplicateLocation>,
787}
788
789#[derive(Debug, Clone, Serialize)]
791#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
792pub struct DuplicateLocation {
793 #[serde(serialize_with = "serde_path::serialize")]
795 pub path: PathBuf,
796 pub line: u32,
798 pub col: u32,
800}
801
802#[derive(Debug, Clone, Serialize)]
806#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
807pub struct TypeOnlyDependency {
808 pub package_name: String,
810 #[serde(serialize_with = "serde_path::serialize")]
812 pub path: PathBuf,
813 pub line: u32,
815}
816
817#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
820#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
821#[serde(rename_all = "kebab-case")]
822pub enum SecurityFindingKind {
823 ClientServerLeak,
826 TaintedSink,
830}
831
832#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
834#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
835#[serde(rename_all = "kebab-case")]
836pub enum TraceHopRole {
837 ClientBoundary,
839 Intermediate,
841 SecretSource,
843 Sink,
847}
848
849#[derive(Debug, Clone, Serialize)]
853#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
854pub struct TraceHop {
855 #[serde(serialize_with = "serde_path::serialize")]
857 pub path: PathBuf,
858 pub line: u32,
862 pub col: u32,
864 pub role: TraceHopRole,
866}
867
868#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
876#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
877pub struct SecurityReachability {
878 pub reachable_from_entry: bool,
883 pub blast_radius: u32,
887 pub crosses_boundary: bool,
892}
893
894#[derive(Debug, Clone, Serialize)]
900#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
901pub struct SecurityFinding {
902 pub kind: SecurityFindingKind,
904 #[serde(default, skip_serializing_if = "Option::is_none")]
907 pub category: Option<String>,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
911 pub cwe: Option<u32>,
912 #[serde(serialize_with = "serde_path::serialize")]
915 pub path: PathBuf,
916 pub line: u32,
918 pub col: u32,
920 pub evidence: String,
922 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
931 pub source_backed: bool,
932 pub trace: Vec<TraceHop>,
936 pub actions: Vec<IssueAction>,
941 #[serde(default, skip_serializing_if = "Option::is_none")]
946 pub reachability: Option<SecurityReachability>,
947}
948
949#[derive(Debug, Clone, Serialize)]
955#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
956pub struct UnusedCatalogEntry {
957 pub entry_name: String,
959 pub catalog_name: String,
962 #[serde(serialize_with = "serde_path::serialize")]
964 pub path: PathBuf,
965 pub line: u32,
967 #[serde(
972 default,
973 serialize_with = "serde_path::serialize_vec",
974 skip_serializing_if = "Vec::is_empty"
975 )]
976 pub hardcoded_consumers: Vec<PathBuf>,
977}
978
979#[derive(Debug, Clone, Serialize)]
981#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
982pub struct EmptyCatalogGroup {
983 pub catalog_name: String,
985 #[serde(serialize_with = "serde_path::serialize")]
987 pub path: PathBuf,
988 pub line: u32,
990}
991
992#[derive(Debug, Clone, Serialize)]
1003#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1004pub struct UnresolvedCatalogReference {
1005 pub entry_name: String,
1007 pub catalog_name: String,
1010 #[serde(serialize_with = "serde_path::serialize")]
1017 pub path: PathBuf,
1018 pub line: u32,
1020 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1025 pub available_in_catalogs: Vec<String>,
1026}
1027
1028#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1032#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1033pub enum DependencyOverrideSource {
1034 #[serde(rename = "pnpm-workspace.yaml")]
1036 PnpmWorkspaceYaml,
1037 #[serde(rename = "package.json")]
1039 PnpmPackageJson,
1040}
1041
1042impl DependencyOverrideSource {
1043 #[must_use]
1046 pub const fn as_label(&self) -> &'static str {
1047 match self {
1048 Self::PnpmWorkspaceYaml => "pnpm-workspace.yaml",
1049 Self::PnpmPackageJson => "package.json",
1050 }
1051 }
1052}
1053
1054impl std::fmt::Display for DependencyOverrideSource {
1055 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1056 f.write_str(self.as_label())
1057 }
1058}
1059
1060#[derive(Debug, Clone, Serialize)]
1066#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1067pub struct UnusedDependencyOverride {
1068 pub raw_key: String,
1072 pub target_package: String,
1075 #[serde(default, skip_serializing_if = "Option::is_none")]
1077 pub parent_package: Option<String>,
1078 #[serde(default, skip_serializing_if = "Option::is_none")]
1081 pub version_constraint: Option<String>,
1082 pub version_range: String,
1084 pub source: DependencyOverrideSource,
1087 #[serde(serialize_with = "serde_path::serialize")]
1094 pub path: PathBuf,
1095 pub line: u32,
1097 #[serde(default, skip_serializing_if = "Option::is_none")]
1102 pub hint: Option<String>,
1103}
1104
1105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1109#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1110#[serde(rename_all = "kebab-case")]
1111pub enum DependencyOverrideMisconfigReason {
1112 UnparsableKey,
1115 EmptyValue,
1117}
1118
1119impl DependencyOverrideMisconfigReason {
1120 #[must_use]
1122 pub const fn describe(self) -> &'static str {
1123 match self {
1124 Self::UnparsableKey => "override key cannot be parsed",
1125 Self::EmptyValue => "override value is missing or empty",
1126 }
1127 }
1128}
1129
1130#[derive(Debug, Clone, Serialize)]
1134#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1135pub struct MisconfiguredDependencyOverride {
1136 pub raw_key: String,
1138 #[serde(default, skip_serializing_if = "Option::is_none")]
1146 pub target_package: Option<String>,
1147 pub raw_value: String,
1150 pub reason: DependencyOverrideMisconfigReason,
1154 pub source: DependencyOverrideSource,
1156 #[serde(serialize_with = "serde_path::serialize")]
1160 pub path: PathBuf,
1161 pub line: u32,
1163}
1164
1165#[derive(Debug, Clone, Serialize)]
1168#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1169pub struct TestOnlyDependency {
1170 pub package_name: String,
1173 #[serde(serialize_with = "serde_path::serialize")]
1175 pub path: PathBuf,
1176 pub line: u32,
1178}
1179
1180#[derive(Debug, Clone, Serialize, Deserialize)]
1193#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1194pub struct CircularDependencyEdge {
1195 #[serde(serialize_with = "serde_path::serialize")]
1197 pub path: PathBuf,
1198 pub line: u32,
1200 pub col: u32,
1202}
1203
1204#[derive(Debug, Clone, Serialize, Deserialize)]
1222#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1223#[cfg_attr(feature = "schema", schemars(extend("required" = ["files", "length", "line", "col"])))]
1224pub struct CircularDependency {
1225 #[serde(serialize_with = "serde_path::serialize_vec")]
1227 pub files: Vec<PathBuf>,
1228 pub length: usize,
1230 #[serde(default)]
1232 pub line: u32,
1233 #[serde(default)]
1235 pub col: u32,
1236 #[serde(default)]
1243 pub edges: Vec<CircularDependencyEdge>,
1244 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1246 pub is_cross_package: bool,
1247}
1248
1249#[derive(Debug, Clone, Serialize, Deserialize)]
1259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1260pub struct ReExportCycle {
1261 #[serde(serialize_with = "serde_path::serialize_vec")]
1264 pub files: Vec<PathBuf>,
1265 pub kind: ReExportCycleKind,
1267}
1268
1269#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1271#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1272#[serde(rename_all = "kebab-case")]
1273pub enum ReExportCycleKind {
1274 MultiNode,
1277 SelfLoop,
1279}
1280
1281#[derive(Debug, Clone, Serialize)]
1283#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1284pub struct BoundaryViolation {
1285 #[serde(serialize_with = "serde_path::serialize")]
1287 pub from_path: PathBuf,
1288 #[serde(serialize_with = "serde_path::serialize")]
1290 pub to_path: PathBuf,
1291 pub from_zone: String,
1293 pub to_zone: String,
1295 pub import_specifier: String,
1297 pub line: u32,
1299 pub col: u32,
1301}
1302
1303#[derive(Debug, Clone, Serialize)]
1305#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1306#[serde(rename_all = "snake_case", tag = "type")]
1307pub enum SuppressionOrigin {
1308 Comment {
1310 #[serde(default, skip_serializing_if = "Option::is_none")]
1312 issue_kind: Option<String>,
1313 is_file_level: bool,
1315 #[serde(default = "default_true", skip_serializing_if = "is_true")]
1322 kind_known: bool,
1323 },
1324 JsdocTag {
1326 export_name: String,
1328 },
1329}
1330
1331#[expect(
1332 clippy::trivially_copy_pass_by_ref,
1333 reason = "serde skip_serializing_if takes a reference by contract"
1334)]
1335const fn is_true(b: &bool) -> bool {
1336 *b
1337}
1338
1339#[cfg_attr(
1354 not(feature = "schema"),
1355 expect(
1356 dead_code,
1357 reason = "referenced via #[serde(default = ...)]; only consumed by schemars under the `schema` feature, dead on default builds today"
1358 )
1359)]
1360const fn default_true() -> bool {
1361 true
1362}
1363
1364#[derive(Debug, Clone, Serialize)]
1366#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1367pub struct StaleSuppression {
1368 #[serde(serialize_with = "serde_path::serialize")]
1370 pub path: PathBuf,
1371 pub line: u32,
1373 pub col: u32,
1375 pub origin: SuppressionOrigin,
1377}
1378
1379impl StaleSuppression {
1380 #[must_use]
1382 pub fn description(&self) -> String {
1383 match &self.origin {
1384 SuppressionOrigin::Comment {
1385 issue_kind,
1386 is_file_level,
1387 ..
1388 } => {
1389 let directive = if *is_file_level {
1390 "fallow-ignore-file"
1391 } else {
1392 "fallow-ignore-next-line"
1393 };
1394 match issue_kind {
1395 Some(kind) => format!("// {directive} {kind}"),
1396 None => format!("// {directive}"),
1397 }
1398 }
1399 SuppressionOrigin::JsdocTag { export_name } => {
1400 format!("@expected-unused on {export_name}")
1401 }
1402 }
1403 }
1404
1405 #[must_use]
1412 pub fn explanation(&self) -> String {
1413 match &self.origin {
1414 SuppressionOrigin::Comment {
1415 issue_kind,
1416 is_file_level,
1417 kind_known,
1418 } => {
1419 let scope = if *is_file_level {
1420 "in this file"
1421 } else {
1422 "on the next line"
1423 };
1424 match issue_kind {
1425 Some(kind) if !*kind_known => match closest_known_kind_name(kind) {
1426 Some(suggestion) => format!(
1427 "'{kind}' is not a recognized fallow issue kind. Did you mean '{suggestion}'? Other tokens on this line still apply."
1428 ),
1429 None => format!(
1430 "'{kind}' is not a recognized fallow issue kind. Other tokens on this line still apply."
1431 ),
1432 },
1433 Some(kind) => format!("no {kind} issue found {scope}"),
1434 None => format!("no issues found {scope}"),
1435 }
1436 }
1437 SuppressionOrigin::JsdocTag { export_name } => {
1438 format!("{export_name} is now used")
1439 }
1440 }
1441 }
1442
1443 #[must_use]
1448 pub fn suppressed_kind(&self) -> Option<IssueKind> {
1449 match &self.origin {
1450 SuppressionOrigin::Comment {
1451 issue_kind,
1452 kind_known: true,
1453 ..
1454 } => issue_kind.as_deref().and_then(IssueKind::parse),
1455 SuppressionOrigin::Comment { .. } | SuppressionOrigin::JsdocTag { .. } => None,
1456 }
1457 }
1458
1459 #[must_use]
1466 pub fn display_message(&self) -> String {
1467 match &self.origin {
1468 SuppressionOrigin::Comment {
1469 kind_known: false, ..
1470 } => format!("{} ({})", self.description(), self.explanation()),
1471 SuppressionOrigin::Comment { .. } | SuppressionOrigin::JsdocTag { .. } => {
1472 self.description()
1473 }
1474 }
1475 }
1476}
1477
1478#[derive(Debug, Clone)]
1492pub struct ActiveSuppression {
1493 pub path: PathBuf,
1495 pub kind: Option<String>,
1498 pub is_file_level: bool,
1501}
1502
1503#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1505#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1506#[serde(rename_all = "snake_case")]
1507pub enum FlagKind {
1508 EnvironmentVariable,
1510 SdkCall,
1512 ConfigObject,
1514}
1515
1516#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1518#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1519#[serde(rename_all = "snake_case")]
1520pub enum FlagConfidence {
1521 Low,
1523 Medium,
1525 High,
1527}
1528
1529#[derive(Debug, Clone, Serialize)]
1531#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1532pub struct FeatureFlag {
1533 #[serde(serialize_with = "serde_path::serialize")]
1535 pub path: PathBuf,
1536 pub flag_name: String,
1538 pub kind: FlagKind,
1540 pub confidence: FlagConfidence,
1542 pub line: u32,
1544 pub col: u32,
1546 #[serde(skip)]
1548 pub guard_span_start: Option<u32>,
1549 #[serde(skip)]
1551 pub guard_span_end: Option<u32>,
1552 #[serde(default, skip_serializing_if = "Option::is_none")]
1554 pub sdk_name: Option<String>,
1555 #[serde(skip)]
1558 pub guard_line_start: Option<u32>,
1559 #[serde(skip)]
1561 pub guard_line_end: Option<u32>,
1562 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1565 pub guarded_dead_exports: Vec<String>,
1566}
1567
1568const _: () = assert!(std::mem::size_of::<FeatureFlag>() <= 160);
1570
1571#[derive(Debug, Clone, Serialize)]
1574#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1575pub struct ExportUsage {
1576 #[serde(serialize_with = "serde_path::serialize")]
1578 pub path: PathBuf,
1579 pub export_name: String,
1581 pub line: u32,
1583 pub col: u32,
1585 pub reference_count: usize,
1587 pub reference_locations: Vec<ReferenceLocation>,
1590}
1591
1592#[derive(Debug, Clone, Serialize)]
1594#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1595pub struct ReferenceLocation {
1596 #[serde(serialize_with = "serde_path::serialize")]
1598 pub path: PathBuf,
1599 pub line: u32,
1601 pub col: u32,
1603}
1604
1605#[cfg(test)]
1606mod tests {
1607 use super::*;
1608 use crate::output_dead_code::{
1609 BoundaryViolationFinding, CircularDependencyFinding, UnresolvedImportFinding,
1610 UnusedClassMemberFinding, UnusedEnumMemberFinding, UnusedExportFinding, UnusedFileFinding,
1611 UnusedTypeFinding,
1612 };
1613
1614 #[test]
1615 fn empty_results_no_issues() {
1616 let results = AnalysisResults::default();
1617 assert_eq!(results.total_issues(), 0);
1618 assert!(!results.has_issues());
1619 }
1620
1621 #[test]
1622 fn results_with_unused_file() {
1623 let mut results = AnalysisResults::default();
1624 results
1625 .unused_files
1626 .push(UnusedFileFinding::with_actions(UnusedFile {
1627 path: PathBuf::from("test.ts"),
1628 }));
1629 assert_eq!(results.total_issues(), 1);
1630 assert!(results.has_issues());
1631 }
1632
1633 #[test]
1634 fn results_with_unused_export() {
1635 let mut results = AnalysisResults::default();
1636 results
1637 .unused_exports
1638 .push(UnusedExportFinding::with_actions(UnusedExport {
1639 path: PathBuf::from("test.ts"),
1640 export_name: "foo".to_string(),
1641 is_type_only: false,
1642 line: 1,
1643 col: 0,
1644 span_start: 0,
1645 is_re_export: false,
1646 }));
1647 assert_eq!(results.total_issues(), 1);
1648 assert!(results.has_issues());
1649 }
1650
1651 fn test_unused_export(path: &str, export_name: &str, is_type_only: bool) -> UnusedExport {
1652 UnusedExport {
1653 path: PathBuf::from(path),
1654 export_name: export_name.to_string(),
1655 is_type_only,
1656 line: 1,
1657 col: 0,
1658 span_start: 0,
1659 is_re_export: false,
1660 }
1661 }
1662
1663 fn test_unused_dependency(
1664 package_name: &str,
1665 location: DependencyLocation,
1666 ) -> UnusedDependency {
1667 UnusedDependency {
1668 package_name: package_name.to_string(),
1669 location,
1670 path: PathBuf::from("package.json"),
1671 line: 5,
1672 used_in_workspaces: Vec::new(),
1673 }
1674 }
1675
1676 fn test_unused_member(member_name: &str, kind: MemberKind) -> UnusedMember {
1677 UnusedMember {
1678 path: PathBuf::from("members.ts"),
1679 parent_name: "Parent".to_string(),
1680 member_name: member_name.to_string(),
1681 kind,
1682 line: 1,
1683 col: 0,
1684 }
1685 }
1686
1687 #[test]
1688 fn results_total_counts_all_types() {
1689 let results = AnalysisResults {
1690 unused_files: vec![UnusedFileFinding::with_actions(UnusedFile {
1691 path: PathBuf::from("a.ts"),
1692 })],
1693 unused_exports: vec![UnusedExportFinding::with_actions(test_unused_export(
1694 "b.ts", "x", false,
1695 ))],
1696 unused_types: vec![UnusedTypeFinding::with_actions(test_unused_export(
1697 "c.ts", "T", true,
1698 ))],
1699 unused_dependencies: vec![UnusedDependencyFinding::with_actions(
1700 test_unused_dependency("dep", DependencyLocation::Dependencies),
1701 )],
1702 unused_dev_dependencies: vec![UnusedDevDependencyFinding::with_actions(
1703 test_unused_dependency("dev", DependencyLocation::DevDependencies),
1704 )],
1705 unused_enum_members: vec![UnusedEnumMemberFinding::with_actions(test_unused_member(
1706 "A",
1707 MemberKind::EnumMember,
1708 ))],
1709 unused_class_members: vec![UnusedClassMemberFinding::with_actions(test_unused_member(
1710 "m",
1711 MemberKind::ClassMethod,
1712 ))],
1713 unresolved_imports: vec![UnresolvedImportFinding::with_actions(UnresolvedImport {
1714 path: PathBuf::from("f.ts"),
1715 specifier: "./missing".to_string(),
1716 line: 1,
1717 col: 0,
1718 specifier_col: 0,
1719 })],
1720 unlisted_dependencies: vec![UnlistedDependencyFinding::with_actions(
1721 UnlistedDependency {
1722 package_name: "unlisted".to_string(),
1723 imported_from: vec![ImportSite {
1724 path: PathBuf::from("g.ts"),
1725 line: 1,
1726 col: 0,
1727 }],
1728 },
1729 )],
1730 duplicate_exports: vec![DuplicateExportFinding::with_actions(DuplicateExport {
1731 export_name: "dup".to_string(),
1732 locations: vec![
1733 DuplicateLocation {
1734 path: PathBuf::from("h.ts"),
1735 line: 15,
1736 col: 0,
1737 },
1738 DuplicateLocation {
1739 path: PathBuf::from("i.ts"),
1740 line: 30,
1741 col: 0,
1742 },
1743 ],
1744 })],
1745 unused_optional_dependencies: vec![UnusedOptionalDependencyFinding::with_actions(
1746 test_unused_dependency("optional", DependencyLocation::OptionalDependencies),
1747 )],
1748 type_only_dependencies: vec![TypeOnlyDependencyFinding::with_actions(
1749 TypeOnlyDependency {
1750 package_name: "type-only".to_string(),
1751 path: PathBuf::from("package.json"),
1752 line: 8,
1753 },
1754 )],
1755 test_only_dependencies: vec![TestOnlyDependencyFinding::with_actions(
1756 TestOnlyDependency {
1757 package_name: "test-only".to_string(),
1758 path: PathBuf::from("package.json"),
1759 line: 9,
1760 },
1761 )],
1762 circular_dependencies: vec![CircularDependencyFinding::with_actions(
1763 CircularDependency {
1764 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1765 length: 2,
1766 line: 3,
1767 col: 0,
1768 edges: Vec::new(),
1769 is_cross_package: false,
1770 },
1771 )],
1772 boundary_violations: vec![BoundaryViolationFinding::with_actions(BoundaryViolation {
1773 from_path: PathBuf::from("src/ui/Button.tsx"),
1774 to_path: PathBuf::from("src/db/queries.ts"),
1775 from_zone: "ui".to_string(),
1776 to_zone: "database".to_string(),
1777 import_specifier: "../db/queries".to_string(),
1778 line: 3,
1779 col: 0,
1780 })],
1781 ..Default::default()
1782 };
1783
1784 assert_eq!(results.total_issues(), 15);
1786 assert!(results.has_issues());
1787 }
1788
1789 #[test]
1792 fn total_issues_and_has_issues_are_consistent() {
1793 let results = AnalysisResults::default();
1794 assert_eq!(results.total_issues(), 0);
1795 assert!(!results.has_issues());
1796 assert_eq!(results.total_issues() > 0, results.has_issues());
1797 }
1798
1799 #[test]
1802 fn total_issues_sums_all_categories_independently() {
1803 let mut results = AnalysisResults::default();
1804 results
1805 .unused_files
1806 .push(UnusedFileFinding::with_actions(UnusedFile {
1807 path: PathBuf::from("a.ts"),
1808 }));
1809 assert_eq!(results.total_issues(), 1);
1810
1811 results
1812 .unused_files
1813 .push(UnusedFileFinding::with_actions(UnusedFile {
1814 path: PathBuf::from("b.ts"),
1815 }));
1816 assert_eq!(results.total_issues(), 2);
1817
1818 results
1819 .unresolved_imports
1820 .push(UnresolvedImportFinding::with_actions(UnresolvedImport {
1821 path: PathBuf::from("c.ts"),
1822 specifier: "./missing".to_string(),
1823 line: 1,
1824 col: 0,
1825 specifier_col: 0,
1826 }));
1827 assert_eq!(results.total_issues(), 3);
1828 }
1829
1830 #[test]
1833 fn default_results_all_fields_empty() {
1834 let r = AnalysisResults::default();
1835 assert!(r.unused_files.is_empty());
1836 assert!(r.unused_exports.is_empty());
1837 assert!(r.unused_types.is_empty());
1838 assert!(r.unused_dependencies.is_empty());
1839 assert!(r.unused_dev_dependencies.is_empty());
1840 assert!(r.unused_optional_dependencies.is_empty());
1841 assert!(r.unused_enum_members.is_empty());
1842 assert!(r.unused_class_members.is_empty());
1843 assert!(r.unresolved_imports.is_empty());
1844 assert!(r.unlisted_dependencies.is_empty());
1845 assert!(r.duplicate_exports.is_empty());
1846 assert!(r.type_only_dependencies.is_empty());
1847 assert!(r.test_only_dependencies.is_empty());
1848 assert!(r.circular_dependencies.is_empty());
1849 assert!(r.boundary_violations.is_empty());
1850 assert!(r.unused_catalog_entries.is_empty());
1851 assert!(r.unresolved_catalog_references.is_empty());
1852 assert!(r.export_usages.is_empty());
1853 }
1854
1855 #[test]
1858 fn entry_point_summary_default() {
1859 let summary = EntryPointSummary::default();
1860 assert_eq!(summary.total, 0);
1861 assert!(summary.by_source.is_empty());
1862 }
1863
1864 #[test]
1865 fn entry_point_summary_not_in_default_results() {
1866 let r = AnalysisResults::default();
1867 assert!(r.entry_point_summary.is_none());
1868 }
1869
1870 #[test]
1871 fn entry_point_summary_some_preserves_data() {
1872 let r = AnalysisResults {
1873 entry_point_summary: Some(EntryPointSummary {
1874 total: 5,
1875 by_source: vec![("package.json".to_string(), 2), ("plugin".to_string(), 3)],
1876 }),
1877 ..AnalysisResults::default()
1878 };
1879 let summary = r.entry_point_summary.as_ref().unwrap();
1880 assert_eq!(summary.total, 5);
1881 assert_eq!(summary.by_source.len(), 2);
1882 assert_eq!(summary.by_source[0], ("package.json".to_string(), 2));
1883 }
1884
1885 #[test]
1888 fn sort_unused_files_by_path() {
1889 let mut r = AnalysisResults::default();
1890 r.unused_files
1891 .push(UnusedFileFinding::with_actions(UnusedFile {
1892 path: PathBuf::from("z.ts"),
1893 }));
1894 r.unused_files
1895 .push(UnusedFileFinding::with_actions(UnusedFile {
1896 path: PathBuf::from("a.ts"),
1897 }));
1898 r.unused_files
1899 .push(UnusedFileFinding::with_actions(UnusedFile {
1900 path: PathBuf::from("m.ts"),
1901 }));
1902 r.sort();
1903 let paths: Vec<_> = r
1904 .unused_files
1905 .iter()
1906 .map(|f| f.file.path.to_string_lossy().to_string())
1907 .collect();
1908 assert_eq!(paths, vec!["a.ts", "m.ts", "z.ts"]);
1909 }
1910
1911 #[test]
1914 fn sort_unused_exports_by_path_line_name() {
1915 let mut r = AnalysisResults::default();
1916 let mk = |path: &str, line: u32, name: &str| {
1917 UnusedExportFinding::with_actions(UnusedExport {
1918 path: PathBuf::from(path),
1919 export_name: name.to_string(),
1920 is_type_only: false,
1921 line,
1922 col: 0,
1923 span_start: 0,
1924 is_re_export: false,
1925 })
1926 };
1927 r.unused_exports.push(mk("b.ts", 5, "beta"));
1928 r.unused_exports.push(mk("a.ts", 10, "zeta"));
1929 r.unused_exports.push(mk("a.ts", 10, "alpha"));
1930 r.unused_exports.push(mk("a.ts", 1, "gamma"));
1931 r.sort();
1932 let keys: Vec<_> = r
1933 .unused_exports
1934 .iter()
1935 .map(|e| {
1936 format!(
1937 "{}:{}:{}",
1938 e.export.path.to_string_lossy(),
1939 e.export.line,
1940 e.export.export_name
1941 )
1942 })
1943 .collect();
1944 assert_eq!(
1945 keys,
1946 vec![
1947 "a.ts:1:gamma",
1948 "a.ts:10:alpha",
1949 "a.ts:10:zeta",
1950 "b.ts:5:beta"
1951 ]
1952 );
1953 }
1954
1955 #[test]
1958 fn sort_unused_types_by_path_line_name() {
1959 let mut r = AnalysisResults::default();
1960 let mk = |path: &str, line: u32, name: &str| {
1961 UnusedTypeFinding::with_actions(UnusedExport {
1962 path: PathBuf::from(path),
1963 export_name: name.to_string(),
1964 is_type_only: true,
1965 line,
1966 col: 0,
1967 span_start: 0,
1968 is_re_export: false,
1969 })
1970 };
1971 r.unused_types.push(mk("z.ts", 1, "Z"));
1972 r.unused_types.push(mk("a.ts", 1, "A"));
1973 r.sort();
1974 assert_eq!(r.unused_types[0].export.path, PathBuf::from("a.ts"));
1975 assert_eq!(r.unused_types[1].export.path, PathBuf::from("z.ts"));
1976 }
1977
1978 #[test]
1981 fn sort_unused_dependencies_by_path_line_name() {
1982 let mut r = AnalysisResults::default();
1983 let mk = |path: &str, line: u32, name: &str| {
1984 UnusedDependencyFinding::with_actions(UnusedDependency {
1985 package_name: name.to_string(),
1986 location: DependencyLocation::Dependencies,
1987 path: PathBuf::from(path),
1988 line,
1989 used_in_workspaces: Vec::new(),
1990 })
1991 };
1992 r.unused_dependencies.push(mk("b/package.json", 3, "zlib"));
1993 r.unused_dependencies.push(mk("a/package.json", 5, "react"));
1994 r.unused_dependencies.push(mk("a/package.json", 5, "axios"));
1995 r.sort();
1996 let names: Vec<_> = r
1997 .unused_dependencies
1998 .iter()
1999 .map(|d| d.dep.package_name.as_str())
2000 .collect();
2001 assert_eq!(names, vec!["axios", "react", "zlib"]);
2002 }
2003
2004 #[test]
2007 fn sort_unused_dev_dependencies() {
2008 let mut r = AnalysisResults::default();
2009 r.unused_dev_dependencies
2010 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
2011 package_name: "vitest".to_string(),
2012 location: DependencyLocation::DevDependencies,
2013 path: PathBuf::from("package.json"),
2014 line: 10,
2015 used_in_workspaces: Vec::new(),
2016 }));
2017 r.unused_dev_dependencies
2018 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
2019 package_name: "jest".to_string(),
2020 location: DependencyLocation::DevDependencies,
2021 path: PathBuf::from("package.json"),
2022 line: 5,
2023 used_in_workspaces: Vec::new(),
2024 }));
2025 r.sort();
2026 assert_eq!(r.unused_dev_dependencies[0].dep.package_name, "jest");
2027 assert_eq!(r.unused_dev_dependencies[1].dep.package_name, "vitest");
2028 }
2029
2030 #[test]
2033 fn sort_unused_optional_dependencies() {
2034 let mut r = AnalysisResults::default();
2035 r.unused_optional_dependencies
2036 .push(UnusedOptionalDependencyFinding::with_actions(
2037 UnusedDependency {
2038 package_name: "zod".to_string(),
2039 location: DependencyLocation::OptionalDependencies,
2040 path: PathBuf::from("package.json"),
2041 line: 3,
2042 used_in_workspaces: Vec::new(),
2043 },
2044 ));
2045 r.unused_optional_dependencies
2046 .push(UnusedOptionalDependencyFinding::with_actions(
2047 UnusedDependency {
2048 package_name: "ajv".to_string(),
2049 location: DependencyLocation::OptionalDependencies,
2050 path: PathBuf::from("package.json"),
2051 line: 2,
2052 used_in_workspaces: Vec::new(),
2053 },
2054 ));
2055 r.sort();
2056 assert_eq!(r.unused_optional_dependencies[0].dep.package_name, "ajv");
2057 assert_eq!(r.unused_optional_dependencies[1].dep.package_name, "zod");
2058 }
2059
2060 #[test]
2063 fn sort_unused_enum_members_by_path_line_parent_member() {
2064 let mut r = AnalysisResults::default();
2065 let mk = |path: &str, line: u32, parent: &str, member: &str| {
2066 UnusedEnumMemberFinding::with_actions(UnusedMember {
2067 path: PathBuf::from(path),
2068 parent_name: parent.to_string(),
2069 member_name: member.to_string(),
2070 kind: MemberKind::EnumMember,
2071 line,
2072 col: 0,
2073 })
2074 };
2075 r.unused_enum_members.push(mk("a.ts", 5, "Status", "Z"));
2076 r.unused_enum_members.push(mk("a.ts", 5, "Status", "A"));
2077 r.unused_enum_members.push(mk("a.ts", 1, "Direction", "Up"));
2078 r.sort();
2079 let keys: Vec<_> = r
2080 .unused_enum_members
2081 .iter()
2082 .map(|m| format!("{}:{}", m.member.parent_name, m.member.member_name))
2083 .collect();
2084 assert_eq!(keys, vec!["Direction:Up", "Status:A", "Status:Z"]);
2085 }
2086
2087 #[test]
2090 fn sort_unused_class_members() {
2091 let mut r = AnalysisResults::default();
2092 let mk = |path: &str, line: u32, parent: &str, member: &str| {
2093 UnusedClassMemberFinding::with_actions(UnusedMember {
2094 path: PathBuf::from(path),
2095 parent_name: parent.to_string(),
2096 member_name: member.to_string(),
2097 kind: MemberKind::ClassMethod,
2098 line,
2099 col: 0,
2100 })
2101 };
2102 r.unused_class_members.push(mk("b.ts", 1, "Foo", "z"));
2103 r.unused_class_members.push(mk("a.ts", 1, "Bar", "a"));
2104 r.sort();
2105 assert_eq!(r.unused_class_members[0].member.path, PathBuf::from("a.ts"));
2106 assert_eq!(r.unused_class_members[1].member.path, PathBuf::from("b.ts"));
2107 }
2108
2109 #[test]
2112 fn sort_unresolved_imports_by_path_line_col_specifier() {
2113 let mut r = AnalysisResults::default();
2114 let mk = |path: &str, line: u32, col: u32, spec: &str| {
2115 UnresolvedImportFinding::with_actions(UnresolvedImport {
2116 path: PathBuf::from(path),
2117 specifier: spec.to_string(),
2118 line,
2119 col,
2120 specifier_col: 0,
2121 })
2122 };
2123 r.unresolved_imports.push(mk("a.ts", 5, 0, "./z"));
2124 r.unresolved_imports.push(mk("a.ts", 5, 0, "./a"));
2125 r.unresolved_imports.push(mk("a.ts", 1, 0, "./m"));
2126 r.sort();
2127 let specs: Vec<_> = r
2128 .unresolved_imports
2129 .iter()
2130 .map(|i| i.import.specifier.as_str())
2131 .collect();
2132 assert_eq!(specs, vec!["./m", "./a", "./z"]);
2133 }
2134
2135 #[test]
2138 fn sort_unlisted_dependencies_by_name_and_inner_sites() {
2139 let mut r = AnalysisResults::default();
2140 r.unlisted_dependencies
2141 .push(UnlistedDependencyFinding::with_actions(
2142 UnlistedDependency {
2143 package_name: "zod".to_string(),
2144 imported_from: vec![
2145 ImportSite {
2146 path: PathBuf::from("b.ts"),
2147 line: 10,
2148 col: 0,
2149 },
2150 ImportSite {
2151 path: PathBuf::from("a.ts"),
2152 line: 1,
2153 col: 0,
2154 },
2155 ],
2156 },
2157 ));
2158 r.unlisted_dependencies
2159 .push(UnlistedDependencyFinding::with_actions(
2160 UnlistedDependency {
2161 package_name: "axios".to_string(),
2162 imported_from: vec![ImportSite {
2163 path: PathBuf::from("c.ts"),
2164 line: 1,
2165 col: 0,
2166 }],
2167 },
2168 ));
2169 r.sort();
2170
2171 assert_eq!(r.unlisted_dependencies[0].dep.package_name, "axios");
2173 assert_eq!(r.unlisted_dependencies[1].dep.package_name, "zod");
2174
2175 let zod_sites: Vec<_> = r.unlisted_dependencies[1]
2177 .dep
2178 .imported_from
2179 .iter()
2180 .map(|s| s.path.to_string_lossy().to_string())
2181 .collect();
2182 assert_eq!(zod_sites, vec!["a.ts", "b.ts"]);
2183 }
2184
2185 #[test]
2188 fn sort_duplicate_exports_by_name_and_inner_locations() {
2189 let mut r = AnalysisResults::default();
2190 r.duplicate_exports
2191 .push(DuplicateExportFinding::with_actions(DuplicateExport {
2192 export_name: "z".to_string(),
2193 locations: vec![
2194 DuplicateLocation {
2195 path: PathBuf::from("c.ts"),
2196 line: 1,
2197 col: 0,
2198 },
2199 DuplicateLocation {
2200 path: PathBuf::from("a.ts"),
2201 line: 5,
2202 col: 0,
2203 },
2204 ],
2205 }));
2206 r.duplicate_exports
2207 .push(DuplicateExportFinding::with_actions(DuplicateExport {
2208 export_name: "a".to_string(),
2209 locations: vec![DuplicateLocation {
2210 path: PathBuf::from("b.ts"),
2211 line: 1,
2212 col: 0,
2213 }],
2214 }));
2215 r.sort();
2216
2217 assert_eq!(r.duplicate_exports[0].export.export_name, "a");
2219 assert_eq!(r.duplicate_exports[1].export.export_name, "z");
2220
2221 let z_locs: Vec<_> = r.duplicate_exports[1]
2223 .export
2224 .locations
2225 .iter()
2226 .map(|l| l.path.to_string_lossy().to_string())
2227 .collect();
2228 assert_eq!(z_locs, vec!["a.ts", "c.ts"]);
2229 }
2230
2231 #[test]
2234 fn sort_type_only_dependencies() {
2235 let mut r = AnalysisResults::default();
2236 r.type_only_dependencies
2237 .push(TypeOnlyDependencyFinding::with_actions(
2238 TypeOnlyDependency {
2239 package_name: "zod".to_string(),
2240 path: PathBuf::from("package.json"),
2241 line: 10,
2242 },
2243 ));
2244 r.type_only_dependencies
2245 .push(TypeOnlyDependencyFinding::with_actions(
2246 TypeOnlyDependency {
2247 package_name: "ajv".to_string(),
2248 path: PathBuf::from("package.json"),
2249 line: 5,
2250 },
2251 ));
2252 r.sort();
2253 assert_eq!(r.type_only_dependencies[0].dep.package_name, "ajv");
2254 assert_eq!(r.type_only_dependencies[1].dep.package_name, "zod");
2255 }
2256
2257 #[test]
2260 fn sort_test_only_dependencies() {
2261 let mut r = AnalysisResults::default();
2262 r.test_only_dependencies
2263 .push(TestOnlyDependencyFinding::with_actions(
2264 TestOnlyDependency {
2265 package_name: "vitest".to_string(),
2266 path: PathBuf::from("package.json"),
2267 line: 15,
2268 },
2269 ));
2270 r.test_only_dependencies
2271 .push(TestOnlyDependencyFinding::with_actions(
2272 TestOnlyDependency {
2273 package_name: "jest".to_string(),
2274 path: PathBuf::from("package.json"),
2275 line: 10,
2276 },
2277 ));
2278 r.sort();
2279 assert_eq!(r.test_only_dependencies[0].dep.package_name, "jest");
2280 assert_eq!(r.test_only_dependencies[1].dep.package_name, "vitest");
2281 }
2282
2283 #[test]
2286 fn sort_circular_dependencies_by_files_then_length() {
2287 let mut r = AnalysisResults::default();
2288 r.circular_dependencies
2289 .push(CircularDependencyFinding::with_actions(
2290 CircularDependency {
2291 files: vec![PathBuf::from("b.ts"), PathBuf::from("c.ts")],
2292 length: 2,
2293 line: 1,
2294 col: 0,
2295 edges: Vec::new(),
2296 is_cross_package: false,
2297 },
2298 ));
2299 r.circular_dependencies
2300 .push(CircularDependencyFinding::with_actions(
2301 CircularDependency {
2302 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
2303 length: 2,
2304 line: 1,
2305 col: 0,
2306 edges: Vec::new(),
2307 is_cross_package: true,
2308 },
2309 ));
2310 r.sort();
2311 assert_eq!(
2312 r.circular_dependencies[0].cycle.files[0],
2313 PathBuf::from("a.ts")
2314 );
2315 assert_eq!(
2316 r.circular_dependencies[1].cycle.files[0],
2317 PathBuf::from("b.ts")
2318 );
2319 }
2320
2321 #[test]
2324 fn sort_boundary_violations() {
2325 let mut r = AnalysisResults::default();
2326 let mk = |from: &str, line: u32, col: u32, to: &str| {
2327 BoundaryViolationFinding::with_actions(BoundaryViolation {
2328 from_path: PathBuf::from(from),
2329 to_path: PathBuf::from(to),
2330 from_zone: "a".to_string(),
2331 to_zone: "b".to_string(),
2332 import_specifier: to.to_string(),
2333 line,
2334 col,
2335 })
2336 };
2337 r.boundary_violations.push(mk("z.ts", 1, 0, "a.ts"));
2338 r.boundary_violations.push(mk("a.ts", 5, 0, "b.ts"));
2339 r.boundary_violations.push(mk("a.ts", 1, 0, "c.ts"));
2340 r.sort();
2341 let from_paths: Vec<_> = r
2342 .boundary_violations
2343 .iter()
2344 .map(|v| {
2345 format!(
2346 "{}:{}",
2347 v.violation.from_path.to_string_lossy(),
2348 v.violation.line
2349 )
2350 })
2351 .collect();
2352 assert_eq!(from_paths, vec!["a.ts:1", "a.ts:5", "z.ts:1"]);
2353 }
2354
2355 #[test]
2358 fn sort_export_usages_and_inner_reference_locations() {
2359 let mut r = AnalysisResults::default();
2360 r.export_usages.push(ExportUsage {
2361 path: PathBuf::from("z.ts"),
2362 export_name: "foo".to_string(),
2363 line: 1,
2364 col: 0,
2365 reference_count: 2,
2366 reference_locations: vec![
2367 ReferenceLocation {
2368 path: PathBuf::from("c.ts"),
2369 line: 10,
2370 col: 0,
2371 },
2372 ReferenceLocation {
2373 path: PathBuf::from("a.ts"),
2374 line: 5,
2375 col: 0,
2376 },
2377 ],
2378 });
2379 r.export_usages.push(ExportUsage {
2380 path: PathBuf::from("a.ts"),
2381 export_name: "bar".to_string(),
2382 line: 1,
2383 col: 0,
2384 reference_count: 1,
2385 reference_locations: vec![ReferenceLocation {
2386 path: PathBuf::from("b.ts"),
2387 line: 1,
2388 col: 0,
2389 }],
2390 });
2391 r.sort();
2392
2393 assert_eq!(r.export_usages[0].path, PathBuf::from("a.ts"));
2395 assert_eq!(r.export_usages[1].path, PathBuf::from("z.ts"));
2396
2397 let refs: Vec<_> = r.export_usages[1]
2399 .reference_locations
2400 .iter()
2401 .map(|l| l.path.to_string_lossy().to_string())
2402 .collect();
2403 assert_eq!(refs, vec!["a.ts", "c.ts"]);
2404 }
2405
2406 #[test]
2409 fn sort_empty_results_is_noop() {
2410 let mut r = AnalysisResults::default();
2411 r.sort(); assert_eq!(r.total_issues(), 0);
2413 }
2414
2415 #[test]
2418 fn sort_single_element_lists_stable() {
2419 let mut r = AnalysisResults::default();
2420 r.unused_files
2421 .push(UnusedFileFinding::with_actions(UnusedFile {
2422 path: PathBuf::from("only.ts"),
2423 }));
2424 r.sort();
2425 assert_eq!(r.unused_files[0].file.path, PathBuf::from("only.ts"));
2426 }
2427
2428 #[test]
2431 fn serialize_empty_results() {
2432 let r = AnalysisResults::default();
2433 let json = serde_json::to_value(&r).unwrap();
2434
2435 assert!(json["unused_files"].as_array().unwrap().is_empty());
2437 assert!(json["unused_exports"].as_array().unwrap().is_empty());
2438 assert!(json["circular_dependencies"].as_array().unwrap().is_empty());
2439
2440 assert!(json.get("export_usages").is_none());
2442 assert!(json.get("entry_point_summary").is_none());
2443 }
2444
2445 #[test]
2446 fn serialize_unused_file_path() {
2447 let r = UnusedFile {
2448 path: PathBuf::from("src/utils/index.ts"),
2449 };
2450 let json = serde_json::to_value(&r).unwrap();
2451 assert_eq!(json["path"], "src/utils/index.ts");
2452 }
2453
2454 #[test]
2455 fn serialize_dependency_location_camel_case() {
2456 let dep = UnusedDependency {
2457 package_name: "react".to_string(),
2458 location: DependencyLocation::DevDependencies,
2459 path: PathBuf::from("package.json"),
2460 line: 5,
2461 used_in_workspaces: Vec::new(),
2462 };
2463 let json = serde_json::to_value(&dep).unwrap();
2464 assert_eq!(json["location"], "devDependencies");
2465
2466 let dep2 = UnusedDependency {
2467 package_name: "react".to_string(),
2468 location: DependencyLocation::Dependencies,
2469 path: PathBuf::from("package.json"),
2470 line: 3,
2471 used_in_workspaces: Vec::new(),
2472 };
2473 let json2 = serde_json::to_value(&dep2).unwrap();
2474 assert_eq!(json2["location"], "dependencies");
2475
2476 let dep3 = UnusedDependency {
2477 package_name: "fsevents".to_string(),
2478 location: DependencyLocation::OptionalDependencies,
2479 path: PathBuf::from("package.json"),
2480 line: 7,
2481 used_in_workspaces: Vec::new(),
2482 };
2483 let json3 = serde_json::to_value(&dep3).unwrap();
2484 assert_eq!(json3["location"], "optionalDependencies");
2485 }
2486
2487 #[test]
2488 fn serialize_circular_dependency_skips_false_cross_package() {
2489 let cd = CircularDependency {
2490 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
2491 length: 2,
2492 line: 1,
2493 col: 0,
2494 edges: Vec::new(),
2495 is_cross_package: false,
2496 };
2497 let json = serde_json::to_value(&cd).unwrap();
2498 assert!(json.get("is_cross_package").is_none());
2500 }
2501
2502 #[test]
2503 fn serialize_circular_dependency_includes_true_cross_package() {
2504 let cd = CircularDependency {
2505 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
2506 length: 2,
2507 line: 1,
2508 col: 0,
2509 edges: Vec::new(),
2510 is_cross_package: true,
2511 };
2512 let json = serde_json::to_value(&cd).unwrap();
2513 assert_eq!(json["is_cross_package"], true);
2514 }
2515
2516 #[test]
2517 fn serialize_unused_export_fields() {
2518 let e = UnusedExport {
2519 path: PathBuf::from("src/mod.ts"),
2520 export_name: "helper".to_string(),
2521 is_type_only: true,
2522 line: 42,
2523 col: 7,
2524 span_start: 100,
2525 is_re_export: true,
2526 };
2527 let json = serde_json::to_value(&e).unwrap();
2528 assert_eq!(json["path"], "src/mod.ts");
2529 assert_eq!(json["export_name"], "helper");
2530 assert_eq!(json["is_type_only"], true);
2531 assert_eq!(json["line"], 42);
2532 assert_eq!(json["col"], 7);
2533 assert_eq!(json["span_start"], 100);
2534 assert_eq!(json["is_re_export"], true);
2535 }
2536
2537 #[test]
2538 fn serialize_boundary_violation_fields() {
2539 let v = BoundaryViolation {
2540 from_path: PathBuf::from("src/ui/button.tsx"),
2541 to_path: PathBuf::from("src/db/queries.ts"),
2542 from_zone: "ui".to_string(),
2543 to_zone: "db".to_string(),
2544 import_specifier: "../db/queries".to_string(),
2545 line: 3,
2546 col: 0,
2547 };
2548 let json = serde_json::to_value(&v).unwrap();
2549 assert_eq!(json["from_path"], "src/ui/button.tsx");
2550 assert_eq!(json["to_path"], "src/db/queries.ts");
2551 assert_eq!(json["from_zone"], "ui");
2552 assert_eq!(json["to_zone"], "db");
2553 assert_eq!(json["import_specifier"], "../db/queries");
2554 }
2555
2556 #[test]
2557 fn serialize_unlisted_dependency_with_import_sites() {
2558 let d = UnlistedDependency {
2559 package_name: "chalk".to_string(),
2560 imported_from: vec![
2561 ImportSite {
2562 path: PathBuf::from("a.ts"),
2563 line: 1,
2564 col: 0,
2565 },
2566 ImportSite {
2567 path: PathBuf::from("b.ts"),
2568 line: 5,
2569 col: 3,
2570 },
2571 ],
2572 };
2573 let json = serde_json::to_value(&d).unwrap();
2574 assert_eq!(json["package_name"], "chalk");
2575 let sites = json["imported_from"].as_array().unwrap();
2576 assert_eq!(sites.len(), 2);
2577 assert_eq!(sites[0]["path"], "a.ts");
2578 assert_eq!(sites[1]["line"], 5);
2579 }
2580
2581 #[test]
2582 fn serialize_duplicate_export_with_locations() {
2583 let d = DuplicateExport {
2584 export_name: "Button".to_string(),
2585 locations: vec![
2586 DuplicateLocation {
2587 path: PathBuf::from("src/a.ts"),
2588 line: 10,
2589 col: 0,
2590 },
2591 DuplicateLocation {
2592 path: PathBuf::from("src/b.ts"),
2593 line: 20,
2594 col: 5,
2595 },
2596 ],
2597 };
2598 let json = serde_json::to_value(&d).unwrap();
2599 assert_eq!(json["export_name"], "Button");
2600 let locs = json["locations"].as_array().unwrap();
2601 assert_eq!(locs.len(), 2);
2602 assert_eq!(locs[0]["line"], 10);
2603 assert_eq!(locs[1]["col"], 5);
2604 }
2605
2606 #[test]
2607 fn serialize_type_only_dependency() {
2608 let d = TypeOnlyDependency {
2609 package_name: "@types/react".to_string(),
2610 path: PathBuf::from("package.json"),
2611 line: 12,
2612 };
2613 let json = serde_json::to_value(&d).unwrap();
2614 assert_eq!(json["package_name"], "@types/react");
2615 assert_eq!(json["line"], 12);
2616 }
2617
2618 #[test]
2619 fn serialize_test_only_dependency() {
2620 let d = TestOnlyDependency {
2621 package_name: "vitest".to_string(),
2622 path: PathBuf::from("package.json"),
2623 line: 8,
2624 };
2625 let json = serde_json::to_value(&d).unwrap();
2626 assert_eq!(json["package_name"], "vitest");
2627 assert_eq!(json["line"], 8);
2628 }
2629
2630 #[test]
2631 fn serialize_unused_member() {
2632 let m = UnusedMember {
2633 path: PathBuf::from("enums.ts"),
2634 parent_name: "Status".to_string(),
2635 member_name: "Pending".to_string(),
2636 kind: MemberKind::EnumMember,
2637 line: 3,
2638 col: 4,
2639 };
2640 let json = serde_json::to_value(&m).unwrap();
2641 assert_eq!(json["parent_name"], "Status");
2642 assert_eq!(json["member_name"], "Pending");
2643 assert_eq!(json["line"], 3);
2644 }
2645
2646 #[test]
2647 fn serialize_unresolved_import() {
2648 let i = UnresolvedImport {
2649 path: PathBuf::from("app.ts"),
2650 specifier: "./missing-module".to_string(),
2651 line: 7,
2652 col: 0,
2653 specifier_col: 21,
2654 };
2655 let json = serde_json::to_value(&i).unwrap();
2656 assert_eq!(json["specifier"], "./missing-module");
2657 assert_eq!(json["specifier_col"], 21);
2658 }
2659
2660 #[test]
2663 fn deserialize_circular_dependency_with_defaults() {
2664 let json = r#"{"files":["a.ts","b.ts"],"length":2}"#;
2666 let cd: CircularDependency = serde_json::from_str(json).unwrap();
2667 assert_eq!(cd.files.len(), 2);
2668 assert_eq!(cd.length, 2);
2669 assert_eq!(cd.line, 0);
2670 assert_eq!(cd.col, 0);
2671 assert!(!cd.is_cross_package);
2672 }
2673
2674 #[test]
2675 fn deserialize_circular_dependency_with_all_fields() {
2676 let json =
2677 r#"{"files":["a.ts","b.ts"],"length":2,"line":5,"col":10,"is_cross_package":true}"#;
2678 let cd: CircularDependency = serde_json::from_str(json).unwrap();
2679 assert_eq!(cd.line, 5);
2680 assert_eq!(cd.col, 10);
2681 assert!(cd.is_cross_package);
2682 }
2683
2684 #[test]
2687 fn clone_results_are_independent() {
2688 let mut r = AnalysisResults::default();
2689 r.unused_files
2690 .push(UnusedFileFinding::with_actions(UnusedFile {
2691 path: PathBuf::from("a.ts"),
2692 }));
2693 let mut cloned = r.clone();
2694 cloned
2695 .unused_files
2696 .push(UnusedFileFinding::with_actions(UnusedFile {
2697 path: PathBuf::from("b.ts"),
2698 }));
2699 assert_eq!(r.total_issues(), 1);
2700 assert_eq!(cloned.total_issues(), 2);
2701 }
2702
2703 #[test]
2706 fn export_usages_not_counted_in_total_issues() {
2707 let mut r = AnalysisResults::default();
2708 r.export_usages.push(ExportUsage {
2709 path: PathBuf::from("mod.ts"),
2710 export_name: "foo".to_string(),
2711 line: 1,
2712 col: 0,
2713 reference_count: 3,
2714 reference_locations: vec![],
2715 });
2716 assert_eq!(r.total_issues(), 0);
2718 assert!(!r.has_issues());
2719 }
2720
2721 #[test]
2724 fn entry_point_summary_not_counted_in_total_issues() {
2725 let r = AnalysisResults {
2726 entry_point_summary: Some(EntryPointSummary {
2727 total: 10,
2728 by_source: vec![("config".to_string(), 10)],
2729 }),
2730 ..AnalysisResults::default()
2731 };
2732 assert_eq!(r.total_issues(), 0);
2733 assert!(!r.has_issues());
2734 }
2735}