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)]
1191#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1192#[cfg_attr(feature = "schema", schemars(extend("required" = ["files", "length", "line", "col"])))]
1193pub struct CircularDependency {
1194 #[serde(serialize_with = "serde_path::serialize_vec")]
1196 pub files: Vec<PathBuf>,
1197 pub length: usize,
1199 #[serde(default)]
1201 pub line: u32,
1202 #[serde(default)]
1204 pub col: u32,
1205 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1207 pub is_cross_package: bool,
1208}
1209
1210#[derive(Debug, Clone, Serialize, Deserialize)]
1220#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1221pub struct ReExportCycle {
1222 #[serde(serialize_with = "serde_path::serialize_vec")]
1225 pub files: Vec<PathBuf>,
1226 pub kind: ReExportCycleKind,
1228}
1229
1230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1232#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1233#[serde(rename_all = "kebab-case")]
1234pub enum ReExportCycleKind {
1235 MultiNode,
1238 SelfLoop,
1240}
1241
1242#[derive(Debug, Clone, Serialize)]
1244#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1245pub struct BoundaryViolation {
1246 #[serde(serialize_with = "serde_path::serialize")]
1248 pub from_path: PathBuf,
1249 #[serde(serialize_with = "serde_path::serialize")]
1251 pub to_path: PathBuf,
1252 pub from_zone: String,
1254 pub to_zone: String,
1256 pub import_specifier: String,
1258 pub line: u32,
1260 pub col: u32,
1262}
1263
1264#[derive(Debug, Clone, Serialize)]
1266#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1267#[serde(rename_all = "snake_case", tag = "type")]
1268pub enum SuppressionOrigin {
1269 Comment {
1271 #[serde(default, skip_serializing_if = "Option::is_none")]
1273 issue_kind: Option<String>,
1274 is_file_level: bool,
1276 #[serde(default = "default_true", skip_serializing_if = "is_true")]
1283 kind_known: bool,
1284 },
1285 JsdocTag {
1287 export_name: String,
1289 },
1290}
1291
1292#[expect(
1293 clippy::trivially_copy_pass_by_ref,
1294 reason = "serde skip_serializing_if takes a reference by contract"
1295)]
1296const fn is_true(b: &bool) -> bool {
1297 *b
1298}
1299
1300#[cfg_attr(
1315 not(feature = "schema"),
1316 expect(
1317 dead_code,
1318 reason = "referenced via #[serde(default = ...)]; only consumed by schemars under the `schema` feature, dead on default builds today"
1319 )
1320)]
1321const fn default_true() -> bool {
1322 true
1323}
1324
1325#[derive(Debug, Clone, Serialize)]
1327#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1328pub struct StaleSuppression {
1329 #[serde(serialize_with = "serde_path::serialize")]
1331 pub path: PathBuf,
1332 pub line: u32,
1334 pub col: u32,
1336 pub origin: SuppressionOrigin,
1338}
1339
1340impl StaleSuppression {
1341 #[must_use]
1343 pub fn description(&self) -> String {
1344 match &self.origin {
1345 SuppressionOrigin::Comment {
1346 issue_kind,
1347 is_file_level,
1348 ..
1349 } => {
1350 let directive = if *is_file_level {
1351 "fallow-ignore-file"
1352 } else {
1353 "fallow-ignore-next-line"
1354 };
1355 match issue_kind {
1356 Some(kind) => format!("// {directive} {kind}"),
1357 None => format!("// {directive}"),
1358 }
1359 }
1360 SuppressionOrigin::JsdocTag { export_name } => {
1361 format!("@expected-unused on {export_name}")
1362 }
1363 }
1364 }
1365
1366 #[must_use]
1373 pub fn explanation(&self) -> String {
1374 match &self.origin {
1375 SuppressionOrigin::Comment {
1376 issue_kind,
1377 is_file_level,
1378 kind_known,
1379 } => {
1380 let scope = if *is_file_level {
1381 "in this file"
1382 } else {
1383 "on the next line"
1384 };
1385 match issue_kind {
1386 Some(kind) if !*kind_known => match closest_known_kind_name(kind) {
1387 Some(suggestion) => format!(
1388 "'{kind}' is not a recognized fallow issue kind. Did you mean '{suggestion}'? Other tokens on this line still apply."
1389 ),
1390 None => format!(
1391 "'{kind}' is not a recognized fallow issue kind. Other tokens on this line still apply."
1392 ),
1393 },
1394 Some(kind) => format!("no {kind} issue found {scope}"),
1395 None => format!("no issues found {scope}"),
1396 }
1397 }
1398 SuppressionOrigin::JsdocTag { export_name } => {
1399 format!("{export_name} is now used")
1400 }
1401 }
1402 }
1403
1404 #[must_use]
1409 pub fn suppressed_kind(&self) -> Option<IssueKind> {
1410 match &self.origin {
1411 SuppressionOrigin::Comment {
1412 issue_kind,
1413 kind_known: true,
1414 ..
1415 } => issue_kind.as_deref().and_then(IssueKind::parse),
1416 SuppressionOrigin::Comment { .. } | SuppressionOrigin::JsdocTag { .. } => None,
1417 }
1418 }
1419
1420 #[must_use]
1427 pub fn display_message(&self) -> String {
1428 match &self.origin {
1429 SuppressionOrigin::Comment {
1430 kind_known: false, ..
1431 } => format!("{} ({})", self.description(), self.explanation()),
1432 SuppressionOrigin::Comment { .. } | SuppressionOrigin::JsdocTag { .. } => {
1433 self.description()
1434 }
1435 }
1436 }
1437}
1438
1439#[derive(Debug, Clone)]
1453pub struct ActiveSuppression {
1454 pub path: PathBuf,
1456 pub kind: Option<String>,
1459 pub is_file_level: bool,
1462}
1463
1464#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1466#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1467#[serde(rename_all = "snake_case")]
1468pub enum FlagKind {
1469 EnvironmentVariable,
1471 SdkCall,
1473 ConfigObject,
1475}
1476
1477#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1479#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1480#[serde(rename_all = "snake_case")]
1481pub enum FlagConfidence {
1482 Low,
1484 Medium,
1486 High,
1488}
1489
1490#[derive(Debug, Clone, Serialize)]
1492#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1493pub struct FeatureFlag {
1494 #[serde(serialize_with = "serde_path::serialize")]
1496 pub path: PathBuf,
1497 pub flag_name: String,
1499 pub kind: FlagKind,
1501 pub confidence: FlagConfidence,
1503 pub line: u32,
1505 pub col: u32,
1507 #[serde(skip)]
1509 pub guard_span_start: Option<u32>,
1510 #[serde(skip)]
1512 pub guard_span_end: Option<u32>,
1513 #[serde(default, skip_serializing_if = "Option::is_none")]
1515 pub sdk_name: Option<String>,
1516 #[serde(skip)]
1519 pub guard_line_start: Option<u32>,
1520 #[serde(skip)]
1522 pub guard_line_end: Option<u32>,
1523 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1526 pub guarded_dead_exports: Vec<String>,
1527}
1528
1529const _: () = assert!(std::mem::size_of::<FeatureFlag>() <= 160);
1531
1532#[derive(Debug, Clone, Serialize)]
1535#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1536pub struct ExportUsage {
1537 #[serde(serialize_with = "serde_path::serialize")]
1539 pub path: PathBuf,
1540 pub export_name: String,
1542 pub line: u32,
1544 pub col: u32,
1546 pub reference_count: usize,
1548 pub reference_locations: Vec<ReferenceLocation>,
1551}
1552
1553#[derive(Debug, Clone, Serialize)]
1555#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1556pub struct ReferenceLocation {
1557 #[serde(serialize_with = "serde_path::serialize")]
1559 pub path: PathBuf,
1560 pub line: u32,
1562 pub col: u32,
1564}
1565
1566#[cfg(test)]
1567mod tests {
1568 use super::*;
1569 use crate::output_dead_code::{
1570 BoundaryViolationFinding, CircularDependencyFinding, UnresolvedImportFinding,
1571 UnusedClassMemberFinding, UnusedEnumMemberFinding, UnusedExportFinding, UnusedFileFinding,
1572 UnusedTypeFinding,
1573 };
1574
1575 #[test]
1576 fn empty_results_no_issues() {
1577 let results = AnalysisResults::default();
1578 assert_eq!(results.total_issues(), 0);
1579 assert!(!results.has_issues());
1580 }
1581
1582 #[test]
1583 fn results_with_unused_file() {
1584 let mut results = AnalysisResults::default();
1585 results
1586 .unused_files
1587 .push(UnusedFileFinding::with_actions(UnusedFile {
1588 path: PathBuf::from("test.ts"),
1589 }));
1590 assert_eq!(results.total_issues(), 1);
1591 assert!(results.has_issues());
1592 }
1593
1594 #[test]
1595 fn results_with_unused_export() {
1596 let mut results = AnalysisResults::default();
1597 results
1598 .unused_exports
1599 .push(UnusedExportFinding::with_actions(UnusedExport {
1600 path: PathBuf::from("test.ts"),
1601 export_name: "foo".to_string(),
1602 is_type_only: false,
1603 line: 1,
1604 col: 0,
1605 span_start: 0,
1606 is_re_export: false,
1607 }));
1608 assert_eq!(results.total_issues(), 1);
1609 assert!(results.has_issues());
1610 }
1611
1612 fn test_unused_export(path: &str, export_name: &str, is_type_only: bool) -> UnusedExport {
1613 UnusedExport {
1614 path: PathBuf::from(path),
1615 export_name: export_name.to_string(),
1616 is_type_only,
1617 line: 1,
1618 col: 0,
1619 span_start: 0,
1620 is_re_export: false,
1621 }
1622 }
1623
1624 fn test_unused_dependency(
1625 package_name: &str,
1626 location: DependencyLocation,
1627 ) -> UnusedDependency {
1628 UnusedDependency {
1629 package_name: package_name.to_string(),
1630 location,
1631 path: PathBuf::from("package.json"),
1632 line: 5,
1633 used_in_workspaces: Vec::new(),
1634 }
1635 }
1636
1637 fn test_unused_member(member_name: &str, kind: MemberKind) -> UnusedMember {
1638 UnusedMember {
1639 path: PathBuf::from("members.ts"),
1640 parent_name: "Parent".to_string(),
1641 member_name: member_name.to_string(),
1642 kind,
1643 line: 1,
1644 col: 0,
1645 }
1646 }
1647
1648 #[test]
1649 fn results_total_counts_all_types() {
1650 let results = AnalysisResults {
1651 unused_files: vec![UnusedFileFinding::with_actions(UnusedFile {
1652 path: PathBuf::from("a.ts"),
1653 })],
1654 unused_exports: vec![UnusedExportFinding::with_actions(test_unused_export(
1655 "b.ts", "x", false,
1656 ))],
1657 unused_types: vec![UnusedTypeFinding::with_actions(test_unused_export(
1658 "c.ts", "T", true,
1659 ))],
1660 unused_dependencies: vec![UnusedDependencyFinding::with_actions(
1661 test_unused_dependency("dep", DependencyLocation::Dependencies),
1662 )],
1663 unused_dev_dependencies: vec![UnusedDevDependencyFinding::with_actions(
1664 test_unused_dependency("dev", DependencyLocation::DevDependencies),
1665 )],
1666 unused_enum_members: vec![UnusedEnumMemberFinding::with_actions(test_unused_member(
1667 "A",
1668 MemberKind::EnumMember,
1669 ))],
1670 unused_class_members: vec![UnusedClassMemberFinding::with_actions(test_unused_member(
1671 "m",
1672 MemberKind::ClassMethod,
1673 ))],
1674 unresolved_imports: vec![UnresolvedImportFinding::with_actions(UnresolvedImport {
1675 path: PathBuf::from("f.ts"),
1676 specifier: "./missing".to_string(),
1677 line: 1,
1678 col: 0,
1679 specifier_col: 0,
1680 })],
1681 unlisted_dependencies: vec![UnlistedDependencyFinding::with_actions(
1682 UnlistedDependency {
1683 package_name: "unlisted".to_string(),
1684 imported_from: vec![ImportSite {
1685 path: PathBuf::from("g.ts"),
1686 line: 1,
1687 col: 0,
1688 }],
1689 },
1690 )],
1691 duplicate_exports: vec![DuplicateExportFinding::with_actions(DuplicateExport {
1692 export_name: "dup".to_string(),
1693 locations: vec![
1694 DuplicateLocation {
1695 path: PathBuf::from("h.ts"),
1696 line: 15,
1697 col: 0,
1698 },
1699 DuplicateLocation {
1700 path: PathBuf::from("i.ts"),
1701 line: 30,
1702 col: 0,
1703 },
1704 ],
1705 })],
1706 unused_optional_dependencies: vec![UnusedOptionalDependencyFinding::with_actions(
1707 test_unused_dependency("optional", DependencyLocation::OptionalDependencies),
1708 )],
1709 type_only_dependencies: vec![TypeOnlyDependencyFinding::with_actions(
1710 TypeOnlyDependency {
1711 package_name: "type-only".to_string(),
1712 path: PathBuf::from("package.json"),
1713 line: 8,
1714 },
1715 )],
1716 test_only_dependencies: vec![TestOnlyDependencyFinding::with_actions(
1717 TestOnlyDependency {
1718 package_name: "test-only".to_string(),
1719 path: PathBuf::from("package.json"),
1720 line: 9,
1721 },
1722 )],
1723 circular_dependencies: vec![CircularDependencyFinding::with_actions(
1724 CircularDependency {
1725 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1726 length: 2,
1727 line: 3,
1728 col: 0,
1729 is_cross_package: false,
1730 },
1731 )],
1732 boundary_violations: vec![BoundaryViolationFinding::with_actions(BoundaryViolation {
1733 from_path: PathBuf::from("src/ui/Button.tsx"),
1734 to_path: PathBuf::from("src/db/queries.ts"),
1735 from_zone: "ui".to_string(),
1736 to_zone: "database".to_string(),
1737 import_specifier: "../db/queries".to_string(),
1738 line: 3,
1739 col: 0,
1740 })],
1741 ..Default::default()
1742 };
1743
1744 assert_eq!(results.total_issues(), 15);
1746 assert!(results.has_issues());
1747 }
1748
1749 #[test]
1752 fn total_issues_and_has_issues_are_consistent() {
1753 let results = AnalysisResults::default();
1754 assert_eq!(results.total_issues(), 0);
1755 assert!(!results.has_issues());
1756 assert_eq!(results.total_issues() > 0, results.has_issues());
1757 }
1758
1759 #[test]
1762 fn total_issues_sums_all_categories_independently() {
1763 let mut results = AnalysisResults::default();
1764 results
1765 .unused_files
1766 .push(UnusedFileFinding::with_actions(UnusedFile {
1767 path: PathBuf::from("a.ts"),
1768 }));
1769 assert_eq!(results.total_issues(), 1);
1770
1771 results
1772 .unused_files
1773 .push(UnusedFileFinding::with_actions(UnusedFile {
1774 path: PathBuf::from("b.ts"),
1775 }));
1776 assert_eq!(results.total_issues(), 2);
1777
1778 results
1779 .unresolved_imports
1780 .push(UnresolvedImportFinding::with_actions(UnresolvedImport {
1781 path: PathBuf::from("c.ts"),
1782 specifier: "./missing".to_string(),
1783 line: 1,
1784 col: 0,
1785 specifier_col: 0,
1786 }));
1787 assert_eq!(results.total_issues(), 3);
1788 }
1789
1790 #[test]
1793 fn default_results_all_fields_empty() {
1794 let r = AnalysisResults::default();
1795 assert!(r.unused_files.is_empty());
1796 assert!(r.unused_exports.is_empty());
1797 assert!(r.unused_types.is_empty());
1798 assert!(r.unused_dependencies.is_empty());
1799 assert!(r.unused_dev_dependencies.is_empty());
1800 assert!(r.unused_optional_dependencies.is_empty());
1801 assert!(r.unused_enum_members.is_empty());
1802 assert!(r.unused_class_members.is_empty());
1803 assert!(r.unresolved_imports.is_empty());
1804 assert!(r.unlisted_dependencies.is_empty());
1805 assert!(r.duplicate_exports.is_empty());
1806 assert!(r.type_only_dependencies.is_empty());
1807 assert!(r.test_only_dependencies.is_empty());
1808 assert!(r.circular_dependencies.is_empty());
1809 assert!(r.boundary_violations.is_empty());
1810 assert!(r.unused_catalog_entries.is_empty());
1811 assert!(r.unresolved_catalog_references.is_empty());
1812 assert!(r.export_usages.is_empty());
1813 }
1814
1815 #[test]
1818 fn entry_point_summary_default() {
1819 let summary = EntryPointSummary::default();
1820 assert_eq!(summary.total, 0);
1821 assert!(summary.by_source.is_empty());
1822 }
1823
1824 #[test]
1825 fn entry_point_summary_not_in_default_results() {
1826 let r = AnalysisResults::default();
1827 assert!(r.entry_point_summary.is_none());
1828 }
1829
1830 #[test]
1831 fn entry_point_summary_some_preserves_data() {
1832 let r = AnalysisResults {
1833 entry_point_summary: Some(EntryPointSummary {
1834 total: 5,
1835 by_source: vec![("package.json".to_string(), 2), ("plugin".to_string(), 3)],
1836 }),
1837 ..AnalysisResults::default()
1838 };
1839 let summary = r.entry_point_summary.as_ref().unwrap();
1840 assert_eq!(summary.total, 5);
1841 assert_eq!(summary.by_source.len(), 2);
1842 assert_eq!(summary.by_source[0], ("package.json".to_string(), 2));
1843 }
1844
1845 #[test]
1848 fn sort_unused_files_by_path() {
1849 let mut r = AnalysisResults::default();
1850 r.unused_files
1851 .push(UnusedFileFinding::with_actions(UnusedFile {
1852 path: PathBuf::from("z.ts"),
1853 }));
1854 r.unused_files
1855 .push(UnusedFileFinding::with_actions(UnusedFile {
1856 path: PathBuf::from("a.ts"),
1857 }));
1858 r.unused_files
1859 .push(UnusedFileFinding::with_actions(UnusedFile {
1860 path: PathBuf::from("m.ts"),
1861 }));
1862 r.sort();
1863 let paths: Vec<_> = r
1864 .unused_files
1865 .iter()
1866 .map(|f| f.file.path.to_string_lossy().to_string())
1867 .collect();
1868 assert_eq!(paths, vec!["a.ts", "m.ts", "z.ts"]);
1869 }
1870
1871 #[test]
1874 fn sort_unused_exports_by_path_line_name() {
1875 let mut r = AnalysisResults::default();
1876 let mk = |path: &str, line: u32, name: &str| {
1877 UnusedExportFinding::with_actions(UnusedExport {
1878 path: PathBuf::from(path),
1879 export_name: name.to_string(),
1880 is_type_only: false,
1881 line,
1882 col: 0,
1883 span_start: 0,
1884 is_re_export: false,
1885 })
1886 };
1887 r.unused_exports.push(mk("b.ts", 5, "beta"));
1888 r.unused_exports.push(mk("a.ts", 10, "zeta"));
1889 r.unused_exports.push(mk("a.ts", 10, "alpha"));
1890 r.unused_exports.push(mk("a.ts", 1, "gamma"));
1891 r.sort();
1892 let keys: Vec<_> = r
1893 .unused_exports
1894 .iter()
1895 .map(|e| {
1896 format!(
1897 "{}:{}:{}",
1898 e.export.path.to_string_lossy(),
1899 e.export.line,
1900 e.export.export_name
1901 )
1902 })
1903 .collect();
1904 assert_eq!(
1905 keys,
1906 vec![
1907 "a.ts:1:gamma",
1908 "a.ts:10:alpha",
1909 "a.ts:10:zeta",
1910 "b.ts:5:beta"
1911 ]
1912 );
1913 }
1914
1915 #[test]
1918 fn sort_unused_types_by_path_line_name() {
1919 let mut r = AnalysisResults::default();
1920 let mk = |path: &str, line: u32, name: &str| {
1921 UnusedTypeFinding::with_actions(UnusedExport {
1922 path: PathBuf::from(path),
1923 export_name: name.to_string(),
1924 is_type_only: true,
1925 line,
1926 col: 0,
1927 span_start: 0,
1928 is_re_export: false,
1929 })
1930 };
1931 r.unused_types.push(mk("z.ts", 1, "Z"));
1932 r.unused_types.push(mk("a.ts", 1, "A"));
1933 r.sort();
1934 assert_eq!(r.unused_types[0].export.path, PathBuf::from("a.ts"));
1935 assert_eq!(r.unused_types[1].export.path, PathBuf::from("z.ts"));
1936 }
1937
1938 #[test]
1941 fn sort_unused_dependencies_by_path_line_name() {
1942 let mut r = AnalysisResults::default();
1943 let mk = |path: &str, line: u32, name: &str| {
1944 UnusedDependencyFinding::with_actions(UnusedDependency {
1945 package_name: name.to_string(),
1946 location: DependencyLocation::Dependencies,
1947 path: PathBuf::from(path),
1948 line,
1949 used_in_workspaces: Vec::new(),
1950 })
1951 };
1952 r.unused_dependencies.push(mk("b/package.json", 3, "zlib"));
1953 r.unused_dependencies.push(mk("a/package.json", 5, "react"));
1954 r.unused_dependencies.push(mk("a/package.json", 5, "axios"));
1955 r.sort();
1956 let names: Vec<_> = r
1957 .unused_dependencies
1958 .iter()
1959 .map(|d| d.dep.package_name.as_str())
1960 .collect();
1961 assert_eq!(names, vec!["axios", "react", "zlib"]);
1962 }
1963
1964 #[test]
1967 fn sort_unused_dev_dependencies() {
1968 let mut r = AnalysisResults::default();
1969 r.unused_dev_dependencies
1970 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
1971 package_name: "vitest".to_string(),
1972 location: DependencyLocation::DevDependencies,
1973 path: PathBuf::from("package.json"),
1974 line: 10,
1975 used_in_workspaces: Vec::new(),
1976 }));
1977 r.unused_dev_dependencies
1978 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
1979 package_name: "jest".to_string(),
1980 location: DependencyLocation::DevDependencies,
1981 path: PathBuf::from("package.json"),
1982 line: 5,
1983 used_in_workspaces: Vec::new(),
1984 }));
1985 r.sort();
1986 assert_eq!(r.unused_dev_dependencies[0].dep.package_name, "jest");
1987 assert_eq!(r.unused_dev_dependencies[1].dep.package_name, "vitest");
1988 }
1989
1990 #[test]
1993 fn sort_unused_optional_dependencies() {
1994 let mut r = AnalysisResults::default();
1995 r.unused_optional_dependencies
1996 .push(UnusedOptionalDependencyFinding::with_actions(
1997 UnusedDependency {
1998 package_name: "zod".to_string(),
1999 location: DependencyLocation::OptionalDependencies,
2000 path: PathBuf::from("package.json"),
2001 line: 3,
2002 used_in_workspaces: Vec::new(),
2003 },
2004 ));
2005 r.unused_optional_dependencies
2006 .push(UnusedOptionalDependencyFinding::with_actions(
2007 UnusedDependency {
2008 package_name: "ajv".to_string(),
2009 location: DependencyLocation::OptionalDependencies,
2010 path: PathBuf::from("package.json"),
2011 line: 2,
2012 used_in_workspaces: Vec::new(),
2013 },
2014 ));
2015 r.sort();
2016 assert_eq!(r.unused_optional_dependencies[0].dep.package_name, "ajv");
2017 assert_eq!(r.unused_optional_dependencies[1].dep.package_name, "zod");
2018 }
2019
2020 #[test]
2023 fn sort_unused_enum_members_by_path_line_parent_member() {
2024 let mut r = AnalysisResults::default();
2025 let mk = |path: &str, line: u32, parent: &str, member: &str| {
2026 UnusedEnumMemberFinding::with_actions(UnusedMember {
2027 path: PathBuf::from(path),
2028 parent_name: parent.to_string(),
2029 member_name: member.to_string(),
2030 kind: MemberKind::EnumMember,
2031 line,
2032 col: 0,
2033 })
2034 };
2035 r.unused_enum_members.push(mk("a.ts", 5, "Status", "Z"));
2036 r.unused_enum_members.push(mk("a.ts", 5, "Status", "A"));
2037 r.unused_enum_members.push(mk("a.ts", 1, "Direction", "Up"));
2038 r.sort();
2039 let keys: Vec<_> = r
2040 .unused_enum_members
2041 .iter()
2042 .map(|m| format!("{}:{}", m.member.parent_name, m.member.member_name))
2043 .collect();
2044 assert_eq!(keys, vec!["Direction:Up", "Status:A", "Status:Z"]);
2045 }
2046
2047 #[test]
2050 fn sort_unused_class_members() {
2051 let mut r = AnalysisResults::default();
2052 let mk = |path: &str, line: u32, parent: &str, member: &str| {
2053 UnusedClassMemberFinding::with_actions(UnusedMember {
2054 path: PathBuf::from(path),
2055 parent_name: parent.to_string(),
2056 member_name: member.to_string(),
2057 kind: MemberKind::ClassMethod,
2058 line,
2059 col: 0,
2060 })
2061 };
2062 r.unused_class_members.push(mk("b.ts", 1, "Foo", "z"));
2063 r.unused_class_members.push(mk("a.ts", 1, "Bar", "a"));
2064 r.sort();
2065 assert_eq!(r.unused_class_members[0].member.path, PathBuf::from("a.ts"));
2066 assert_eq!(r.unused_class_members[1].member.path, PathBuf::from("b.ts"));
2067 }
2068
2069 #[test]
2072 fn sort_unresolved_imports_by_path_line_col_specifier() {
2073 let mut r = AnalysisResults::default();
2074 let mk = |path: &str, line: u32, col: u32, spec: &str| {
2075 UnresolvedImportFinding::with_actions(UnresolvedImport {
2076 path: PathBuf::from(path),
2077 specifier: spec.to_string(),
2078 line,
2079 col,
2080 specifier_col: 0,
2081 })
2082 };
2083 r.unresolved_imports.push(mk("a.ts", 5, 0, "./z"));
2084 r.unresolved_imports.push(mk("a.ts", 5, 0, "./a"));
2085 r.unresolved_imports.push(mk("a.ts", 1, 0, "./m"));
2086 r.sort();
2087 let specs: Vec<_> = r
2088 .unresolved_imports
2089 .iter()
2090 .map(|i| i.import.specifier.as_str())
2091 .collect();
2092 assert_eq!(specs, vec!["./m", "./a", "./z"]);
2093 }
2094
2095 #[test]
2098 fn sort_unlisted_dependencies_by_name_and_inner_sites() {
2099 let mut r = AnalysisResults::default();
2100 r.unlisted_dependencies
2101 .push(UnlistedDependencyFinding::with_actions(
2102 UnlistedDependency {
2103 package_name: "zod".to_string(),
2104 imported_from: vec![
2105 ImportSite {
2106 path: PathBuf::from("b.ts"),
2107 line: 10,
2108 col: 0,
2109 },
2110 ImportSite {
2111 path: PathBuf::from("a.ts"),
2112 line: 1,
2113 col: 0,
2114 },
2115 ],
2116 },
2117 ));
2118 r.unlisted_dependencies
2119 .push(UnlistedDependencyFinding::with_actions(
2120 UnlistedDependency {
2121 package_name: "axios".to_string(),
2122 imported_from: vec![ImportSite {
2123 path: PathBuf::from("c.ts"),
2124 line: 1,
2125 col: 0,
2126 }],
2127 },
2128 ));
2129 r.sort();
2130
2131 assert_eq!(r.unlisted_dependencies[0].dep.package_name, "axios");
2133 assert_eq!(r.unlisted_dependencies[1].dep.package_name, "zod");
2134
2135 let zod_sites: Vec<_> = r.unlisted_dependencies[1]
2137 .dep
2138 .imported_from
2139 .iter()
2140 .map(|s| s.path.to_string_lossy().to_string())
2141 .collect();
2142 assert_eq!(zod_sites, vec!["a.ts", "b.ts"]);
2143 }
2144
2145 #[test]
2148 fn sort_duplicate_exports_by_name_and_inner_locations() {
2149 let mut r = AnalysisResults::default();
2150 r.duplicate_exports
2151 .push(DuplicateExportFinding::with_actions(DuplicateExport {
2152 export_name: "z".to_string(),
2153 locations: vec![
2154 DuplicateLocation {
2155 path: PathBuf::from("c.ts"),
2156 line: 1,
2157 col: 0,
2158 },
2159 DuplicateLocation {
2160 path: PathBuf::from("a.ts"),
2161 line: 5,
2162 col: 0,
2163 },
2164 ],
2165 }));
2166 r.duplicate_exports
2167 .push(DuplicateExportFinding::with_actions(DuplicateExport {
2168 export_name: "a".to_string(),
2169 locations: vec![DuplicateLocation {
2170 path: PathBuf::from("b.ts"),
2171 line: 1,
2172 col: 0,
2173 }],
2174 }));
2175 r.sort();
2176
2177 assert_eq!(r.duplicate_exports[0].export.export_name, "a");
2179 assert_eq!(r.duplicate_exports[1].export.export_name, "z");
2180
2181 let z_locs: Vec<_> = r.duplicate_exports[1]
2183 .export
2184 .locations
2185 .iter()
2186 .map(|l| l.path.to_string_lossy().to_string())
2187 .collect();
2188 assert_eq!(z_locs, vec!["a.ts", "c.ts"]);
2189 }
2190
2191 #[test]
2194 fn sort_type_only_dependencies() {
2195 let mut r = AnalysisResults::default();
2196 r.type_only_dependencies
2197 .push(TypeOnlyDependencyFinding::with_actions(
2198 TypeOnlyDependency {
2199 package_name: "zod".to_string(),
2200 path: PathBuf::from("package.json"),
2201 line: 10,
2202 },
2203 ));
2204 r.type_only_dependencies
2205 .push(TypeOnlyDependencyFinding::with_actions(
2206 TypeOnlyDependency {
2207 package_name: "ajv".to_string(),
2208 path: PathBuf::from("package.json"),
2209 line: 5,
2210 },
2211 ));
2212 r.sort();
2213 assert_eq!(r.type_only_dependencies[0].dep.package_name, "ajv");
2214 assert_eq!(r.type_only_dependencies[1].dep.package_name, "zod");
2215 }
2216
2217 #[test]
2220 fn sort_test_only_dependencies() {
2221 let mut r = AnalysisResults::default();
2222 r.test_only_dependencies
2223 .push(TestOnlyDependencyFinding::with_actions(
2224 TestOnlyDependency {
2225 package_name: "vitest".to_string(),
2226 path: PathBuf::from("package.json"),
2227 line: 15,
2228 },
2229 ));
2230 r.test_only_dependencies
2231 .push(TestOnlyDependencyFinding::with_actions(
2232 TestOnlyDependency {
2233 package_name: "jest".to_string(),
2234 path: PathBuf::from("package.json"),
2235 line: 10,
2236 },
2237 ));
2238 r.sort();
2239 assert_eq!(r.test_only_dependencies[0].dep.package_name, "jest");
2240 assert_eq!(r.test_only_dependencies[1].dep.package_name, "vitest");
2241 }
2242
2243 #[test]
2246 fn sort_circular_dependencies_by_files_then_length() {
2247 let mut r = AnalysisResults::default();
2248 r.circular_dependencies
2249 .push(CircularDependencyFinding::with_actions(
2250 CircularDependency {
2251 files: vec![PathBuf::from("b.ts"), PathBuf::from("c.ts")],
2252 length: 2,
2253 line: 1,
2254 col: 0,
2255 is_cross_package: false,
2256 },
2257 ));
2258 r.circular_dependencies
2259 .push(CircularDependencyFinding::with_actions(
2260 CircularDependency {
2261 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
2262 length: 2,
2263 line: 1,
2264 col: 0,
2265 is_cross_package: true,
2266 },
2267 ));
2268 r.sort();
2269 assert_eq!(
2270 r.circular_dependencies[0].cycle.files[0],
2271 PathBuf::from("a.ts")
2272 );
2273 assert_eq!(
2274 r.circular_dependencies[1].cycle.files[0],
2275 PathBuf::from("b.ts")
2276 );
2277 }
2278
2279 #[test]
2282 fn sort_boundary_violations() {
2283 let mut r = AnalysisResults::default();
2284 let mk = |from: &str, line: u32, col: u32, to: &str| {
2285 BoundaryViolationFinding::with_actions(BoundaryViolation {
2286 from_path: PathBuf::from(from),
2287 to_path: PathBuf::from(to),
2288 from_zone: "a".to_string(),
2289 to_zone: "b".to_string(),
2290 import_specifier: to.to_string(),
2291 line,
2292 col,
2293 })
2294 };
2295 r.boundary_violations.push(mk("z.ts", 1, 0, "a.ts"));
2296 r.boundary_violations.push(mk("a.ts", 5, 0, "b.ts"));
2297 r.boundary_violations.push(mk("a.ts", 1, 0, "c.ts"));
2298 r.sort();
2299 let from_paths: Vec<_> = r
2300 .boundary_violations
2301 .iter()
2302 .map(|v| {
2303 format!(
2304 "{}:{}",
2305 v.violation.from_path.to_string_lossy(),
2306 v.violation.line
2307 )
2308 })
2309 .collect();
2310 assert_eq!(from_paths, vec!["a.ts:1", "a.ts:5", "z.ts:1"]);
2311 }
2312
2313 #[test]
2316 fn sort_export_usages_and_inner_reference_locations() {
2317 let mut r = AnalysisResults::default();
2318 r.export_usages.push(ExportUsage {
2319 path: PathBuf::from("z.ts"),
2320 export_name: "foo".to_string(),
2321 line: 1,
2322 col: 0,
2323 reference_count: 2,
2324 reference_locations: vec![
2325 ReferenceLocation {
2326 path: PathBuf::from("c.ts"),
2327 line: 10,
2328 col: 0,
2329 },
2330 ReferenceLocation {
2331 path: PathBuf::from("a.ts"),
2332 line: 5,
2333 col: 0,
2334 },
2335 ],
2336 });
2337 r.export_usages.push(ExportUsage {
2338 path: PathBuf::from("a.ts"),
2339 export_name: "bar".to_string(),
2340 line: 1,
2341 col: 0,
2342 reference_count: 1,
2343 reference_locations: vec![ReferenceLocation {
2344 path: PathBuf::from("b.ts"),
2345 line: 1,
2346 col: 0,
2347 }],
2348 });
2349 r.sort();
2350
2351 assert_eq!(r.export_usages[0].path, PathBuf::from("a.ts"));
2353 assert_eq!(r.export_usages[1].path, PathBuf::from("z.ts"));
2354
2355 let refs: Vec<_> = r.export_usages[1]
2357 .reference_locations
2358 .iter()
2359 .map(|l| l.path.to_string_lossy().to_string())
2360 .collect();
2361 assert_eq!(refs, vec!["a.ts", "c.ts"]);
2362 }
2363
2364 #[test]
2367 fn sort_empty_results_is_noop() {
2368 let mut r = AnalysisResults::default();
2369 r.sort(); assert_eq!(r.total_issues(), 0);
2371 }
2372
2373 #[test]
2376 fn sort_single_element_lists_stable() {
2377 let mut r = AnalysisResults::default();
2378 r.unused_files
2379 .push(UnusedFileFinding::with_actions(UnusedFile {
2380 path: PathBuf::from("only.ts"),
2381 }));
2382 r.sort();
2383 assert_eq!(r.unused_files[0].file.path, PathBuf::from("only.ts"));
2384 }
2385
2386 #[test]
2389 fn serialize_empty_results() {
2390 let r = AnalysisResults::default();
2391 let json = serde_json::to_value(&r).unwrap();
2392
2393 assert!(json["unused_files"].as_array().unwrap().is_empty());
2395 assert!(json["unused_exports"].as_array().unwrap().is_empty());
2396 assert!(json["circular_dependencies"].as_array().unwrap().is_empty());
2397
2398 assert!(json.get("export_usages").is_none());
2400 assert!(json.get("entry_point_summary").is_none());
2401 }
2402
2403 #[test]
2404 fn serialize_unused_file_path() {
2405 let r = UnusedFile {
2406 path: PathBuf::from("src/utils/index.ts"),
2407 };
2408 let json = serde_json::to_value(&r).unwrap();
2409 assert_eq!(json["path"], "src/utils/index.ts");
2410 }
2411
2412 #[test]
2413 fn serialize_dependency_location_camel_case() {
2414 let dep = UnusedDependency {
2415 package_name: "react".to_string(),
2416 location: DependencyLocation::DevDependencies,
2417 path: PathBuf::from("package.json"),
2418 line: 5,
2419 used_in_workspaces: Vec::new(),
2420 };
2421 let json = serde_json::to_value(&dep).unwrap();
2422 assert_eq!(json["location"], "devDependencies");
2423
2424 let dep2 = UnusedDependency {
2425 package_name: "react".to_string(),
2426 location: DependencyLocation::Dependencies,
2427 path: PathBuf::from("package.json"),
2428 line: 3,
2429 used_in_workspaces: Vec::new(),
2430 };
2431 let json2 = serde_json::to_value(&dep2).unwrap();
2432 assert_eq!(json2["location"], "dependencies");
2433
2434 let dep3 = UnusedDependency {
2435 package_name: "fsevents".to_string(),
2436 location: DependencyLocation::OptionalDependencies,
2437 path: PathBuf::from("package.json"),
2438 line: 7,
2439 used_in_workspaces: Vec::new(),
2440 };
2441 let json3 = serde_json::to_value(&dep3).unwrap();
2442 assert_eq!(json3["location"], "optionalDependencies");
2443 }
2444
2445 #[test]
2446 fn serialize_circular_dependency_skips_false_cross_package() {
2447 let cd = CircularDependency {
2448 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
2449 length: 2,
2450 line: 1,
2451 col: 0,
2452 is_cross_package: false,
2453 };
2454 let json = serde_json::to_value(&cd).unwrap();
2455 assert!(json.get("is_cross_package").is_none());
2457 }
2458
2459 #[test]
2460 fn serialize_circular_dependency_includes_true_cross_package() {
2461 let cd = CircularDependency {
2462 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
2463 length: 2,
2464 line: 1,
2465 col: 0,
2466 is_cross_package: true,
2467 };
2468 let json = serde_json::to_value(&cd).unwrap();
2469 assert_eq!(json["is_cross_package"], true);
2470 }
2471
2472 #[test]
2473 fn serialize_unused_export_fields() {
2474 let e = UnusedExport {
2475 path: PathBuf::from("src/mod.ts"),
2476 export_name: "helper".to_string(),
2477 is_type_only: true,
2478 line: 42,
2479 col: 7,
2480 span_start: 100,
2481 is_re_export: true,
2482 };
2483 let json = serde_json::to_value(&e).unwrap();
2484 assert_eq!(json["path"], "src/mod.ts");
2485 assert_eq!(json["export_name"], "helper");
2486 assert_eq!(json["is_type_only"], true);
2487 assert_eq!(json["line"], 42);
2488 assert_eq!(json["col"], 7);
2489 assert_eq!(json["span_start"], 100);
2490 assert_eq!(json["is_re_export"], true);
2491 }
2492
2493 #[test]
2494 fn serialize_boundary_violation_fields() {
2495 let v = BoundaryViolation {
2496 from_path: PathBuf::from("src/ui/button.tsx"),
2497 to_path: PathBuf::from("src/db/queries.ts"),
2498 from_zone: "ui".to_string(),
2499 to_zone: "db".to_string(),
2500 import_specifier: "../db/queries".to_string(),
2501 line: 3,
2502 col: 0,
2503 };
2504 let json = serde_json::to_value(&v).unwrap();
2505 assert_eq!(json["from_path"], "src/ui/button.tsx");
2506 assert_eq!(json["to_path"], "src/db/queries.ts");
2507 assert_eq!(json["from_zone"], "ui");
2508 assert_eq!(json["to_zone"], "db");
2509 assert_eq!(json["import_specifier"], "../db/queries");
2510 }
2511
2512 #[test]
2513 fn serialize_unlisted_dependency_with_import_sites() {
2514 let d = UnlistedDependency {
2515 package_name: "chalk".to_string(),
2516 imported_from: vec![
2517 ImportSite {
2518 path: PathBuf::from("a.ts"),
2519 line: 1,
2520 col: 0,
2521 },
2522 ImportSite {
2523 path: PathBuf::from("b.ts"),
2524 line: 5,
2525 col: 3,
2526 },
2527 ],
2528 };
2529 let json = serde_json::to_value(&d).unwrap();
2530 assert_eq!(json["package_name"], "chalk");
2531 let sites = json["imported_from"].as_array().unwrap();
2532 assert_eq!(sites.len(), 2);
2533 assert_eq!(sites[0]["path"], "a.ts");
2534 assert_eq!(sites[1]["line"], 5);
2535 }
2536
2537 #[test]
2538 fn serialize_duplicate_export_with_locations() {
2539 let d = DuplicateExport {
2540 export_name: "Button".to_string(),
2541 locations: vec![
2542 DuplicateLocation {
2543 path: PathBuf::from("src/a.ts"),
2544 line: 10,
2545 col: 0,
2546 },
2547 DuplicateLocation {
2548 path: PathBuf::from("src/b.ts"),
2549 line: 20,
2550 col: 5,
2551 },
2552 ],
2553 };
2554 let json = serde_json::to_value(&d).unwrap();
2555 assert_eq!(json["export_name"], "Button");
2556 let locs = json["locations"].as_array().unwrap();
2557 assert_eq!(locs.len(), 2);
2558 assert_eq!(locs[0]["line"], 10);
2559 assert_eq!(locs[1]["col"], 5);
2560 }
2561
2562 #[test]
2563 fn serialize_type_only_dependency() {
2564 let d = TypeOnlyDependency {
2565 package_name: "@types/react".to_string(),
2566 path: PathBuf::from("package.json"),
2567 line: 12,
2568 };
2569 let json = serde_json::to_value(&d).unwrap();
2570 assert_eq!(json["package_name"], "@types/react");
2571 assert_eq!(json["line"], 12);
2572 }
2573
2574 #[test]
2575 fn serialize_test_only_dependency() {
2576 let d = TestOnlyDependency {
2577 package_name: "vitest".to_string(),
2578 path: PathBuf::from("package.json"),
2579 line: 8,
2580 };
2581 let json = serde_json::to_value(&d).unwrap();
2582 assert_eq!(json["package_name"], "vitest");
2583 assert_eq!(json["line"], 8);
2584 }
2585
2586 #[test]
2587 fn serialize_unused_member() {
2588 let m = UnusedMember {
2589 path: PathBuf::from("enums.ts"),
2590 parent_name: "Status".to_string(),
2591 member_name: "Pending".to_string(),
2592 kind: MemberKind::EnumMember,
2593 line: 3,
2594 col: 4,
2595 };
2596 let json = serde_json::to_value(&m).unwrap();
2597 assert_eq!(json["parent_name"], "Status");
2598 assert_eq!(json["member_name"], "Pending");
2599 assert_eq!(json["line"], 3);
2600 }
2601
2602 #[test]
2603 fn serialize_unresolved_import() {
2604 let i = UnresolvedImport {
2605 path: PathBuf::from("app.ts"),
2606 specifier: "./missing-module".to_string(),
2607 line: 7,
2608 col: 0,
2609 specifier_col: 21,
2610 };
2611 let json = serde_json::to_value(&i).unwrap();
2612 assert_eq!(json["specifier"], "./missing-module");
2613 assert_eq!(json["specifier_col"], 21);
2614 }
2615
2616 #[test]
2619 fn deserialize_circular_dependency_with_defaults() {
2620 let json = r#"{"files":["a.ts","b.ts"],"length":2}"#;
2622 let cd: CircularDependency = serde_json::from_str(json).unwrap();
2623 assert_eq!(cd.files.len(), 2);
2624 assert_eq!(cd.length, 2);
2625 assert_eq!(cd.line, 0);
2626 assert_eq!(cd.col, 0);
2627 assert!(!cd.is_cross_package);
2628 }
2629
2630 #[test]
2631 fn deserialize_circular_dependency_with_all_fields() {
2632 let json =
2633 r#"{"files":["a.ts","b.ts"],"length":2,"line":5,"col":10,"is_cross_package":true}"#;
2634 let cd: CircularDependency = serde_json::from_str(json).unwrap();
2635 assert_eq!(cd.line, 5);
2636 assert_eq!(cd.col, 10);
2637 assert!(cd.is_cross_package);
2638 }
2639
2640 #[test]
2643 fn clone_results_are_independent() {
2644 let mut r = AnalysisResults::default();
2645 r.unused_files
2646 .push(UnusedFileFinding::with_actions(UnusedFile {
2647 path: PathBuf::from("a.ts"),
2648 }));
2649 let mut cloned = r.clone();
2650 cloned
2651 .unused_files
2652 .push(UnusedFileFinding::with_actions(UnusedFile {
2653 path: PathBuf::from("b.ts"),
2654 }));
2655 assert_eq!(r.total_issues(), 1);
2656 assert_eq!(cloned.total_issues(), 2);
2657 }
2658
2659 #[test]
2662 fn export_usages_not_counted_in_total_issues() {
2663 let mut r = AnalysisResults::default();
2664 r.export_usages.push(ExportUsage {
2665 path: PathBuf::from("mod.ts"),
2666 export_name: "foo".to_string(),
2667 line: 1,
2668 col: 0,
2669 reference_count: 3,
2670 reference_locations: vec![],
2671 });
2672 assert_eq!(r.total_issues(), 0);
2674 assert!(!r.has_issues());
2675 }
2676
2677 #[test]
2680 fn entry_point_summary_not_counted_in_total_issues() {
2681 let r = AnalysisResults {
2682 entry_point_summary: Some(EntryPointSummary {
2683 total: 10,
2684 by_source: vec![("config".to_string(), 10)],
2685 }),
2686 ..AnalysisResults::default()
2687 };
2688 assert_eq!(r.total_issues(), 0);
2689 assert!(!r.has_issues());
2690 }
2691}