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, Serialize)]
874#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
875pub struct SecurityFinding {
876 pub kind: SecurityFindingKind,
878 #[serde(default, skip_serializing_if = "Option::is_none")]
881 pub category: Option<String>,
882 #[serde(default, skip_serializing_if = "Option::is_none")]
885 pub cwe: Option<u32>,
886 #[serde(serialize_with = "serde_path::serialize")]
889 pub path: PathBuf,
890 pub line: u32,
892 pub col: u32,
894 pub evidence: String,
896 pub trace: Vec<TraceHop>,
900 pub actions: Vec<IssueAction>,
905}
906
907#[derive(Debug, Clone, Serialize)]
913#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
914pub struct UnusedCatalogEntry {
915 pub entry_name: String,
917 pub catalog_name: String,
920 #[serde(serialize_with = "serde_path::serialize")]
922 pub path: PathBuf,
923 pub line: u32,
925 #[serde(
930 default,
931 serialize_with = "serde_path::serialize_vec",
932 skip_serializing_if = "Vec::is_empty"
933 )]
934 pub hardcoded_consumers: Vec<PathBuf>,
935}
936
937#[derive(Debug, Clone, Serialize)]
939#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
940pub struct EmptyCatalogGroup {
941 pub catalog_name: String,
943 #[serde(serialize_with = "serde_path::serialize")]
945 pub path: PathBuf,
946 pub line: u32,
948}
949
950#[derive(Debug, Clone, Serialize)]
961#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
962pub struct UnresolvedCatalogReference {
963 pub entry_name: String,
965 pub catalog_name: String,
968 #[serde(serialize_with = "serde_path::serialize")]
975 pub path: PathBuf,
976 pub line: u32,
978 #[serde(default, skip_serializing_if = "Vec::is_empty")]
983 pub available_in_catalogs: Vec<String>,
984}
985
986#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
990#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
991pub enum DependencyOverrideSource {
992 #[serde(rename = "pnpm-workspace.yaml")]
994 PnpmWorkspaceYaml,
995 #[serde(rename = "package.json")]
997 PnpmPackageJson,
998}
999
1000impl DependencyOverrideSource {
1001 #[must_use]
1004 pub const fn as_label(&self) -> &'static str {
1005 match self {
1006 Self::PnpmWorkspaceYaml => "pnpm-workspace.yaml",
1007 Self::PnpmPackageJson => "package.json",
1008 }
1009 }
1010}
1011
1012impl std::fmt::Display for DependencyOverrideSource {
1013 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1014 f.write_str(self.as_label())
1015 }
1016}
1017
1018#[derive(Debug, Clone, Serialize)]
1024#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1025pub struct UnusedDependencyOverride {
1026 pub raw_key: String,
1030 pub target_package: String,
1033 #[serde(default, skip_serializing_if = "Option::is_none")]
1035 pub parent_package: Option<String>,
1036 #[serde(default, skip_serializing_if = "Option::is_none")]
1039 pub version_constraint: Option<String>,
1040 pub version_range: String,
1042 pub source: DependencyOverrideSource,
1045 #[serde(serialize_with = "serde_path::serialize")]
1052 pub path: PathBuf,
1053 pub line: u32,
1055 #[serde(default, skip_serializing_if = "Option::is_none")]
1060 pub hint: Option<String>,
1061}
1062
1063#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1067#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1068#[serde(rename_all = "kebab-case")]
1069pub enum DependencyOverrideMisconfigReason {
1070 UnparsableKey,
1073 EmptyValue,
1075}
1076
1077impl DependencyOverrideMisconfigReason {
1078 #[must_use]
1080 pub const fn describe(self) -> &'static str {
1081 match self {
1082 Self::UnparsableKey => "override key cannot be parsed",
1083 Self::EmptyValue => "override value is missing or empty",
1084 }
1085 }
1086}
1087
1088#[derive(Debug, Clone, Serialize)]
1092#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1093pub struct MisconfiguredDependencyOverride {
1094 pub raw_key: String,
1096 #[serde(default, skip_serializing_if = "Option::is_none")]
1104 pub target_package: Option<String>,
1105 pub raw_value: String,
1108 pub reason: DependencyOverrideMisconfigReason,
1112 pub source: DependencyOverrideSource,
1114 #[serde(serialize_with = "serde_path::serialize")]
1118 pub path: PathBuf,
1119 pub line: u32,
1121}
1122
1123#[derive(Debug, Clone, Serialize)]
1126#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1127pub struct TestOnlyDependency {
1128 pub package_name: String,
1131 #[serde(serialize_with = "serde_path::serialize")]
1133 pub path: PathBuf,
1134 pub line: u32,
1136}
1137
1138#[derive(Debug, Clone, Serialize, Deserialize)]
1149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1150#[cfg_attr(feature = "schema", schemars(extend("required" = ["files", "length", "line", "col"])))]
1151pub struct CircularDependency {
1152 #[serde(serialize_with = "serde_path::serialize_vec")]
1154 pub files: Vec<PathBuf>,
1155 pub length: usize,
1157 #[serde(default)]
1159 pub line: u32,
1160 #[serde(default)]
1162 pub col: u32,
1163 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1165 pub is_cross_package: bool,
1166}
1167
1168#[derive(Debug, Clone, Serialize, Deserialize)]
1178#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1179pub struct ReExportCycle {
1180 #[serde(serialize_with = "serde_path::serialize_vec")]
1183 pub files: Vec<PathBuf>,
1184 pub kind: ReExportCycleKind,
1186}
1187
1188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1190#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1191#[serde(rename_all = "kebab-case")]
1192pub enum ReExportCycleKind {
1193 MultiNode,
1196 SelfLoop,
1198}
1199
1200#[derive(Debug, Clone, Serialize)]
1202#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1203pub struct BoundaryViolation {
1204 #[serde(serialize_with = "serde_path::serialize")]
1206 pub from_path: PathBuf,
1207 #[serde(serialize_with = "serde_path::serialize")]
1209 pub to_path: PathBuf,
1210 pub from_zone: String,
1212 pub to_zone: String,
1214 pub import_specifier: String,
1216 pub line: u32,
1218 pub col: u32,
1220}
1221
1222#[derive(Debug, Clone, Serialize)]
1224#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1225#[serde(rename_all = "snake_case", tag = "type")]
1226pub enum SuppressionOrigin {
1227 Comment {
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1231 issue_kind: Option<String>,
1232 is_file_level: bool,
1234 #[serde(default = "default_true", skip_serializing_if = "is_true")]
1241 kind_known: bool,
1242 },
1243 JsdocTag {
1245 export_name: String,
1247 },
1248}
1249
1250#[expect(
1251 clippy::trivially_copy_pass_by_ref,
1252 reason = "serde skip_serializing_if takes a reference by contract"
1253)]
1254const fn is_true(b: &bool) -> bool {
1255 *b
1256}
1257
1258#[cfg_attr(
1273 not(feature = "schema"),
1274 expect(
1275 dead_code,
1276 reason = "referenced via #[serde(default = ...)]; only consumed by schemars under the `schema` feature, dead on default builds today"
1277 )
1278)]
1279const fn default_true() -> bool {
1280 true
1281}
1282
1283#[derive(Debug, Clone, Serialize)]
1285#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1286pub struct StaleSuppression {
1287 #[serde(serialize_with = "serde_path::serialize")]
1289 pub path: PathBuf,
1290 pub line: u32,
1292 pub col: u32,
1294 pub origin: SuppressionOrigin,
1296}
1297
1298impl StaleSuppression {
1299 #[must_use]
1301 pub fn description(&self) -> String {
1302 match &self.origin {
1303 SuppressionOrigin::Comment {
1304 issue_kind,
1305 is_file_level,
1306 ..
1307 } => {
1308 let directive = if *is_file_level {
1309 "fallow-ignore-file"
1310 } else {
1311 "fallow-ignore-next-line"
1312 };
1313 match issue_kind {
1314 Some(kind) => format!("// {directive} {kind}"),
1315 None => format!("// {directive}"),
1316 }
1317 }
1318 SuppressionOrigin::JsdocTag { export_name } => {
1319 format!("@expected-unused on {export_name}")
1320 }
1321 }
1322 }
1323
1324 #[must_use]
1331 pub fn explanation(&self) -> String {
1332 match &self.origin {
1333 SuppressionOrigin::Comment {
1334 issue_kind,
1335 is_file_level,
1336 kind_known,
1337 } => {
1338 let scope = if *is_file_level {
1339 "in this file"
1340 } else {
1341 "on the next line"
1342 };
1343 match issue_kind {
1344 Some(kind) if !*kind_known => match closest_known_kind_name(kind) {
1345 Some(suggestion) => format!(
1346 "'{kind}' is not a recognized fallow issue kind. Did you mean '{suggestion}'? Other tokens on this line still apply."
1347 ),
1348 None => format!(
1349 "'{kind}' is not a recognized fallow issue kind. Other tokens on this line still apply."
1350 ),
1351 },
1352 Some(kind) => format!("no {kind} issue found {scope}"),
1353 None => format!("no issues found {scope}"),
1354 }
1355 }
1356 SuppressionOrigin::JsdocTag { export_name } => {
1357 format!("{export_name} is now used")
1358 }
1359 }
1360 }
1361
1362 #[must_use]
1367 pub fn suppressed_kind(&self) -> Option<IssueKind> {
1368 match &self.origin {
1369 SuppressionOrigin::Comment {
1370 issue_kind,
1371 kind_known: true,
1372 ..
1373 } => issue_kind.as_deref().and_then(IssueKind::parse),
1374 SuppressionOrigin::Comment { .. } | SuppressionOrigin::JsdocTag { .. } => None,
1375 }
1376 }
1377
1378 #[must_use]
1385 pub fn display_message(&self) -> String {
1386 match &self.origin {
1387 SuppressionOrigin::Comment {
1388 kind_known: false, ..
1389 } => format!("{} ({})", self.description(), self.explanation()),
1390 SuppressionOrigin::Comment { .. } | SuppressionOrigin::JsdocTag { .. } => {
1391 self.description()
1392 }
1393 }
1394 }
1395}
1396
1397#[derive(Debug, Clone)]
1411pub struct ActiveSuppression {
1412 pub path: PathBuf,
1414 pub kind: Option<String>,
1417 pub is_file_level: bool,
1420}
1421
1422#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1424#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1425#[serde(rename_all = "snake_case")]
1426pub enum FlagKind {
1427 EnvironmentVariable,
1429 SdkCall,
1431 ConfigObject,
1433}
1434
1435#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1437#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1438#[serde(rename_all = "snake_case")]
1439pub enum FlagConfidence {
1440 Low,
1442 Medium,
1444 High,
1446}
1447
1448#[derive(Debug, Clone, Serialize)]
1450#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1451pub struct FeatureFlag {
1452 #[serde(serialize_with = "serde_path::serialize")]
1454 pub path: PathBuf,
1455 pub flag_name: String,
1457 pub kind: FlagKind,
1459 pub confidence: FlagConfidence,
1461 pub line: u32,
1463 pub col: u32,
1465 #[serde(skip)]
1467 pub guard_span_start: Option<u32>,
1468 #[serde(skip)]
1470 pub guard_span_end: Option<u32>,
1471 #[serde(default, skip_serializing_if = "Option::is_none")]
1473 pub sdk_name: Option<String>,
1474 #[serde(skip)]
1477 pub guard_line_start: Option<u32>,
1478 #[serde(skip)]
1480 pub guard_line_end: Option<u32>,
1481 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1484 pub guarded_dead_exports: Vec<String>,
1485}
1486
1487const _: () = assert!(std::mem::size_of::<FeatureFlag>() <= 160);
1489
1490#[derive(Debug, Clone, Serialize)]
1493#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1494pub struct ExportUsage {
1495 #[serde(serialize_with = "serde_path::serialize")]
1497 pub path: PathBuf,
1498 pub export_name: String,
1500 pub line: u32,
1502 pub col: u32,
1504 pub reference_count: usize,
1506 pub reference_locations: Vec<ReferenceLocation>,
1509}
1510
1511#[derive(Debug, Clone, Serialize)]
1513#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1514pub struct ReferenceLocation {
1515 #[serde(serialize_with = "serde_path::serialize")]
1517 pub path: PathBuf,
1518 pub line: u32,
1520 pub col: u32,
1522}
1523
1524#[cfg(test)]
1525mod tests {
1526 use super::*;
1527 use crate::output_dead_code::{
1528 BoundaryViolationFinding, CircularDependencyFinding, UnresolvedImportFinding,
1529 UnusedClassMemberFinding, UnusedEnumMemberFinding, UnusedExportFinding, UnusedFileFinding,
1530 UnusedTypeFinding,
1531 };
1532
1533 #[test]
1534 fn empty_results_no_issues() {
1535 let results = AnalysisResults::default();
1536 assert_eq!(results.total_issues(), 0);
1537 assert!(!results.has_issues());
1538 }
1539
1540 #[test]
1541 fn results_with_unused_file() {
1542 let mut results = AnalysisResults::default();
1543 results
1544 .unused_files
1545 .push(UnusedFileFinding::with_actions(UnusedFile {
1546 path: PathBuf::from("test.ts"),
1547 }));
1548 assert_eq!(results.total_issues(), 1);
1549 assert!(results.has_issues());
1550 }
1551
1552 #[test]
1553 fn results_with_unused_export() {
1554 let mut results = AnalysisResults::default();
1555 results
1556 .unused_exports
1557 .push(UnusedExportFinding::with_actions(UnusedExport {
1558 path: PathBuf::from("test.ts"),
1559 export_name: "foo".to_string(),
1560 is_type_only: false,
1561 line: 1,
1562 col: 0,
1563 span_start: 0,
1564 is_re_export: false,
1565 }));
1566 assert_eq!(results.total_issues(), 1);
1567 assert!(results.has_issues());
1568 }
1569
1570 fn test_unused_export(path: &str, export_name: &str, is_type_only: bool) -> UnusedExport {
1571 UnusedExport {
1572 path: PathBuf::from(path),
1573 export_name: export_name.to_string(),
1574 is_type_only,
1575 line: 1,
1576 col: 0,
1577 span_start: 0,
1578 is_re_export: false,
1579 }
1580 }
1581
1582 fn test_unused_dependency(
1583 package_name: &str,
1584 location: DependencyLocation,
1585 ) -> UnusedDependency {
1586 UnusedDependency {
1587 package_name: package_name.to_string(),
1588 location,
1589 path: PathBuf::from("package.json"),
1590 line: 5,
1591 used_in_workspaces: Vec::new(),
1592 }
1593 }
1594
1595 fn test_unused_member(member_name: &str, kind: MemberKind) -> UnusedMember {
1596 UnusedMember {
1597 path: PathBuf::from("members.ts"),
1598 parent_name: "Parent".to_string(),
1599 member_name: member_name.to_string(),
1600 kind,
1601 line: 1,
1602 col: 0,
1603 }
1604 }
1605
1606 #[test]
1607 fn results_total_counts_all_types() {
1608 let results = AnalysisResults {
1609 unused_files: vec![UnusedFileFinding::with_actions(UnusedFile {
1610 path: PathBuf::from("a.ts"),
1611 })],
1612 unused_exports: vec![UnusedExportFinding::with_actions(test_unused_export(
1613 "b.ts", "x", false,
1614 ))],
1615 unused_types: vec![UnusedTypeFinding::with_actions(test_unused_export(
1616 "c.ts", "T", true,
1617 ))],
1618 unused_dependencies: vec![UnusedDependencyFinding::with_actions(
1619 test_unused_dependency("dep", DependencyLocation::Dependencies),
1620 )],
1621 unused_dev_dependencies: vec![UnusedDevDependencyFinding::with_actions(
1622 test_unused_dependency("dev", DependencyLocation::DevDependencies),
1623 )],
1624 unused_enum_members: vec![UnusedEnumMemberFinding::with_actions(test_unused_member(
1625 "A",
1626 MemberKind::EnumMember,
1627 ))],
1628 unused_class_members: vec![UnusedClassMemberFinding::with_actions(test_unused_member(
1629 "m",
1630 MemberKind::ClassMethod,
1631 ))],
1632 unresolved_imports: vec![UnresolvedImportFinding::with_actions(UnresolvedImport {
1633 path: PathBuf::from("f.ts"),
1634 specifier: "./missing".to_string(),
1635 line: 1,
1636 col: 0,
1637 specifier_col: 0,
1638 })],
1639 unlisted_dependencies: vec![UnlistedDependencyFinding::with_actions(
1640 UnlistedDependency {
1641 package_name: "unlisted".to_string(),
1642 imported_from: vec![ImportSite {
1643 path: PathBuf::from("g.ts"),
1644 line: 1,
1645 col: 0,
1646 }],
1647 },
1648 )],
1649 duplicate_exports: vec![DuplicateExportFinding::with_actions(DuplicateExport {
1650 export_name: "dup".to_string(),
1651 locations: vec![
1652 DuplicateLocation {
1653 path: PathBuf::from("h.ts"),
1654 line: 15,
1655 col: 0,
1656 },
1657 DuplicateLocation {
1658 path: PathBuf::from("i.ts"),
1659 line: 30,
1660 col: 0,
1661 },
1662 ],
1663 })],
1664 unused_optional_dependencies: vec![UnusedOptionalDependencyFinding::with_actions(
1665 test_unused_dependency("optional", DependencyLocation::OptionalDependencies),
1666 )],
1667 type_only_dependencies: vec![TypeOnlyDependencyFinding::with_actions(
1668 TypeOnlyDependency {
1669 package_name: "type-only".to_string(),
1670 path: PathBuf::from("package.json"),
1671 line: 8,
1672 },
1673 )],
1674 test_only_dependencies: vec![TestOnlyDependencyFinding::with_actions(
1675 TestOnlyDependency {
1676 package_name: "test-only".to_string(),
1677 path: PathBuf::from("package.json"),
1678 line: 9,
1679 },
1680 )],
1681 circular_dependencies: vec![CircularDependencyFinding::with_actions(
1682 CircularDependency {
1683 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1684 length: 2,
1685 line: 3,
1686 col: 0,
1687 is_cross_package: false,
1688 },
1689 )],
1690 boundary_violations: vec![BoundaryViolationFinding::with_actions(BoundaryViolation {
1691 from_path: PathBuf::from("src/ui/Button.tsx"),
1692 to_path: PathBuf::from("src/db/queries.ts"),
1693 from_zone: "ui".to_string(),
1694 to_zone: "database".to_string(),
1695 import_specifier: "../db/queries".to_string(),
1696 line: 3,
1697 col: 0,
1698 })],
1699 ..Default::default()
1700 };
1701
1702 assert_eq!(results.total_issues(), 15);
1704 assert!(results.has_issues());
1705 }
1706
1707 #[test]
1710 fn total_issues_and_has_issues_are_consistent() {
1711 let results = AnalysisResults::default();
1712 assert_eq!(results.total_issues(), 0);
1713 assert!(!results.has_issues());
1714 assert_eq!(results.total_issues() > 0, results.has_issues());
1715 }
1716
1717 #[test]
1720 fn total_issues_sums_all_categories_independently() {
1721 let mut results = AnalysisResults::default();
1722 results
1723 .unused_files
1724 .push(UnusedFileFinding::with_actions(UnusedFile {
1725 path: PathBuf::from("a.ts"),
1726 }));
1727 assert_eq!(results.total_issues(), 1);
1728
1729 results
1730 .unused_files
1731 .push(UnusedFileFinding::with_actions(UnusedFile {
1732 path: PathBuf::from("b.ts"),
1733 }));
1734 assert_eq!(results.total_issues(), 2);
1735
1736 results
1737 .unresolved_imports
1738 .push(UnresolvedImportFinding::with_actions(UnresolvedImport {
1739 path: PathBuf::from("c.ts"),
1740 specifier: "./missing".to_string(),
1741 line: 1,
1742 col: 0,
1743 specifier_col: 0,
1744 }));
1745 assert_eq!(results.total_issues(), 3);
1746 }
1747
1748 #[test]
1751 fn default_results_all_fields_empty() {
1752 let r = AnalysisResults::default();
1753 assert!(r.unused_files.is_empty());
1754 assert!(r.unused_exports.is_empty());
1755 assert!(r.unused_types.is_empty());
1756 assert!(r.unused_dependencies.is_empty());
1757 assert!(r.unused_dev_dependencies.is_empty());
1758 assert!(r.unused_optional_dependencies.is_empty());
1759 assert!(r.unused_enum_members.is_empty());
1760 assert!(r.unused_class_members.is_empty());
1761 assert!(r.unresolved_imports.is_empty());
1762 assert!(r.unlisted_dependencies.is_empty());
1763 assert!(r.duplicate_exports.is_empty());
1764 assert!(r.type_only_dependencies.is_empty());
1765 assert!(r.test_only_dependencies.is_empty());
1766 assert!(r.circular_dependencies.is_empty());
1767 assert!(r.boundary_violations.is_empty());
1768 assert!(r.unused_catalog_entries.is_empty());
1769 assert!(r.unresolved_catalog_references.is_empty());
1770 assert!(r.export_usages.is_empty());
1771 }
1772
1773 #[test]
1776 fn entry_point_summary_default() {
1777 let summary = EntryPointSummary::default();
1778 assert_eq!(summary.total, 0);
1779 assert!(summary.by_source.is_empty());
1780 }
1781
1782 #[test]
1783 fn entry_point_summary_not_in_default_results() {
1784 let r = AnalysisResults::default();
1785 assert!(r.entry_point_summary.is_none());
1786 }
1787
1788 #[test]
1789 fn entry_point_summary_some_preserves_data() {
1790 let r = AnalysisResults {
1791 entry_point_summary: Some(EntryPointSummary {
1792 total: 5,
1793 by_source: vec![("package.json".to_string(), 2), ("plugin".to_string(), 3)],
1794 }),
1795 ..AnalysisResults::default()
1796 };
1797 let summary = r.entry_point_summary.as_ref().unwrap();
1798 assert_eq!(summary.total, 5);
1799 assert_eq!(summary.by_source.len(), 2);
1800 assert_eq!(summary.by_source[0], ("package.json".to_string(), 2));
1801 }
1802
1803 #[test]
1806 fn sort_unused_files_by_path() {
1807 let mut r = AnalysisResults::default();
1808 r.unused_files
1809 .push(UnusedFileFinding::with_actions(UnusedFile {
1810 path: PathBuf::from("z.ts"),
1811 }));
1812 r.unused_files
1813 .push(UnusedFileFinding::with_actions(UnusedFile {
1814 path: PathBuf::from("a.ts"),
1815 }));
1816 r.unused_files
1817 .push(UnusedFileFinding::with_actions(UnusedFile {
1818 path: PathBuf::from("m.ts"),
1819 }));
1820 r.sort();
1821 let paths: Vec<_> = r
1822 .unused_files
1823 .iter()
1824 .map(|f| f.file.path.to_string_lossy().to_string())
1825 .collect();
1826 assert_eq!(paths, vec!["a.ts", "m.ts", "z.ts"]);
1827 }
1828
1829 #[test]
1832 fn sort_unused_exports_by_path_line_name() {
1833 let mut r = AnalysisResults::default();
1834 let mk = |path: &str, line: u32, name: &str| {
1835 UnusedExportFinding::with_actions(UnusedExport {
1836 path: PathBuf::from(path),
1837 export_name: name.to_string(),
1838 is_type_only: false,
1839 line,
1840 col: 0,
1841 span_start: 0,
1842 is_re_export: false,
1843 })
1844 };
1845 r.unused_exports.push(mk("b.ts", 5, "beta"));
1846 r.unused_exports.push(mk("a.ts", 10, "zeta"));
1847 r.unused_exports.push(mk("a.ts", 10, "alpha"));
1848 r.unused_exports.push(mk("a.ts", 1, "gamma"));
1849 r.sort();
1850 let keys: Vec<_> = r
1851 .unused_exports
1852 .iter()
1853 .map(|e| {
1854 format!(
1855 "{}:{}:{}",
1856 e.export.path.to_string_lossy(),
1857 e.export.line,
1858 e.export.export_name
1859 )
1860 })
1861 .collect();
1862 assert_eq!(
1863 keys,
1864 vec![
1865 "a.ts:1:gamma",
1866 "a.ts:10:alpha",
1867 "a.ts:10:zeta",
1868 "b.ts:5:beta"
1869 ]
1870 );
1871 }
1872
1873 #[test]
1876 fn sort_unused_types_by_path_line_name() {
1877 let mut r = AnalysisResults::default();
1878 let mk = |path: &str, line: u32, name: &str| {
1879 UnusedTypeFinding::with_actions(UnusedExport {
1880 path: PathBuf::from(path),
1881 export_name: name.to_string(),
1882 is_type_only: true,
1883 line,
1884 col: 0,
1885 span_start: 0,
1886 is_re_export: false,
1887 })
1888 };
1889 r.unused_types.push(mk("z.ts", 1, "Z"));
1890 r.unused_types.push(mk("a.ts", 1, "A"));
1891 r.sort();
1892 assert_eq!(r.unused_types[0].export.path, PathBuf::from("a.ts"));
1893 assert_eq!(r.unused_types[1].export.path, PathBuf::from("z.ts"));
1894 }
1895
1896 #[test]
1899 fn sort_unused_dependencies_by_path_line_name() {
1900 let mut r = AnalysisResults::default();
1901 let mk = |path: &str, line: u32, name: &str| {
1902 UnusedDependencyFinding::with_actions(UnusedDependency {
1903 package_name: name.to_string(),
1904 location: DependencyLocation::Dependencies,
1905 path: PathBuf::from(path),
1906 line,
1907 used_in_workspaces: Vec::new(),
1908 })
1909 };
1910 r.unused_dependencies.push(mk("b/package.json", 3, "zlib"));
1911 r.unused_dependencies.push(mk("a/package.json", 5, "react"));
1912 r.unused_dependencies.push(mk("a/package.json", 5, "axios"));
1913 r.sort();
1914 let names: Vec<_> = r
1915 .unused_dependencies
1916 .iter()
1917 .map(|d| d.dep.package_name.as_str())
1918 .collect();
1919 assert_eq!(names, vec!["axios", "react", "zlib"]);
1920 }
1921
1922 #[test]
1925 fn sort_unused_dev_dependencies() {
1926 let mut r = AnalysisResults::default();
1927 r.unused_dev_dependencies
1928 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
1929 package_name: "vitest".to_string(),
1930 location: DependencyLocation::DevDependencies,
1931 path: PathBuf::from("package.json"),
1932 line: 10,
1933 used_in_workspaces: Vec::new(),
1934 }));
1935 r.unused_dev_dependencies
1936 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
1937 package_name: "jest".to_string(),
1938 location: DependencyLocation::DevDependencies,
1939 path: PathBuf::from("package.json"),
1940 line: 5,
1941 used_in_workspaces: Vec::new(),
1942 }));
1943 r.sort();
1944 assert_eq!(r.unused_dev_dependencies[0].dep.package_name, "jest");
1945 assert_eq!(r.unused_dev_dependencies[1].dep.package_name, "vitest");
1946 }
1947
1948 #[test]
1951 fn sort_unused_optional_dependencies() {
1952 let mut r = AnalysisResults::default();
1953 r.unused_optional_dependencies
1954 .push(UnusedOptionalDependencyFinding::with_actions(
1955 UnusedDependency {
1956 package_name: "zod".to_string(),
1957 location: DependencyLocation::OptionalDependencies,
1958 path: PathBuf::from("package.json"),
1959 line: 3,
1960 used_in_workspaces: Vec::new(),
1961 },
1962 ));
1963 r.unused_optional_dependencies
1964 .push(UnusedOptionalDependencyFinding::with_actions(
1965 UnusedDependency {
1966 package_name: "ajv".to_string(),
1967 location: DependencyLocation::OptionalDependencies,
1968 path: PathBuf::from("package.json"),
1969 line: 2,
1970 used_in_workspaces: Vec::new(),
1971 },
1972 ));
1973 r.sort();
1974 assert_eq!(r.unused_optional_dependencies[0].dep.package_name, "ajv");
1975 assert_eq!(r.unused_optional_dependencies[1].dep.package_name, "zod");
1976 }
1977
1978 #[test]
1981 fn sort_unused_enum_members_by_path_line_parent_member() {
1982 let mut r = AnalysisResults::default();
1983 let mk = |path: &str, line: u32, parent: &str, member: &str| {
1984 UnusedEnumMemberFinding::with_actions(UnusedMember {
1985 path: PathBuf::from(path),
1986 parent_name: parent.to_string(),
1987 member_name: member.to_string(),
1988 kind: MemberKind::EnumMember,
1989 line,
1990 col: 0,
1991 })
1992 };
1993 r.unused_enum_members.push(mk("a.ts", 5, "Status", "Z"));
1994 r.unused_enum_members.push(mk("a.ts", 5, "Status", "A"));
1995 r.unused_enum_members.push(mk("a.ts", 1, "Direction", "Up"));
1996 r.sort();
1997 let keys: Vec<_> = r
1998 .unused_enum_members
1999 .iter()
2000 .map(|m| format!("{}:{}", m.member.parent_name, m.member.member_name))
2001 .collect();
2002 assert_eq!(keys, vec!["Direction:Up", "Status:A", "Status:Z"]);
2003 }
2004
2005 #[test]
2008 fn sort_unused_class_members() {
2009 let mut r = AnalysisResults::default();
2010 let mk = |path: &str, line: u32, parent: &str, member: &str| {
2011 UnusedClassMemberFinding::with_actions(UnusedMember {
2012 path: PathBuf::from(path),
2013 parent_name: parent.to_string(),
2014 member_name: member.to_string(),
2015 kind: MemberKind::ClassMethod,
2016 line,
2017 col: 0,
2018 })
2019 };
2020 r.unused_class_members.push(mk("b.ts", 1, "Foo", "z"));
2021 r.unused_class_members.push(mk("a.ts", 1, "Bar", "a"));
2022 r.sort();
2023 assert_eq!(r.unused_class_members[0].member.path, PathBuf::from("a.ts"));
2024 assert_eq!(r.unused_class_members[1].member.path, PathBuf::from("b.ts"));
2025 }
2026
2027 #[test]
2030 fn sort_unresolved_imports_by_path_line_col_specifier() {
2031 let mut r = AnalysisResults::default();
2032 let mk = |path: &str, line: u32, col: u32, spec: &str| {
2033 UnresolvedImportFinding::with_actions(UnresolvedImport {
2034 path: PathBuf::from(path),
2035 specifier: spec.to_string(),
2036 line,
2037 col,
2038 specifier_col: 0,
2039 })
2040 };
2041 r.unresolved_imports.push(mk("a.ts", 5, 0, "./z"));
2042 r.unresolved_imports.push(mk("a.ts", 5, 0, "./a"));
2043 r.unresolved_imports.push(mk("a.ts", 1, 0, "./m"));
2044 r.sort();
2045 let specs: Vec<_> = r
2046 .unresolved_imports
2047 .iter()
2048 .map(|i| i.import.specifier.as_str())
2049 .collect();
2050 assert_eq!(specs, vec!["./m", "./a", "./z"]);
2051 }
2052
2053 #[test]
2056 fn sort_unlisted_dependencies_by_name_and_inner_sites() {
2057 let mut r = AnalysisResults::default();
2058 r.unlisted_dependencies
2059 .push(UnlistedDependencyFinding::with_actions(
2060 UnlistedDependency {
2061 package_name: "zod".to_string(),
2062 imported_from: vec![
2063 ImportSite {
2064 path: PathBuf::from("b.ts"),
2065 line: 10,
2066 col: 0,
2067 },
2068 ImportSite {
2069 path: PathBuf::from("a.ts"),
2070 line: 1,
2071 col: 0,
2072 },
2073 ],
2074 },
2075 ));
2076 r.unlisted_dependencies
2077 .push(UnlistedDependencyFinding::with_actions(
2078 UnlistedDependency {
2079 package_name: "axios".to_string(),
2080 imported_from: vec![ImportSite {
2081 path: PathBuf::from("c.ts"),
2082 line: 1,
2083 col: 0,
2084 }],
2085 },
2086 ));
2087 r.sort();
2088
2089 assert_eq!(r.unlisted_dependencies[0].dep.package_name, "axios");
2091 assert_eq!(r.unlisted_dependencies[1].dep.package_name, "zod");
2092
2093 let zod_sites: Vec<_> = r.unlisted_dependencies[1]
2095 .dep
2096 .imported_from
2097 .iter()
2098 .map(|s| s.path.to_string_lossy().to_string())
2099 .collect();
2100 assert_eq!(zod_sites, vec!["a.ts", "b.ts"]);
2101 }
2102
2103 #[test]
2106 fn sort_duplicate_exports_by_name_and_inner_locations() {
2107 let mut r = AnalysisResults::default();
2108 r.duplicate_exports
2109 .push(DuplicateExportFinding::with_actions(DuplicateExport {
2110 export_name: "z".to_string(),
2111 locations: vec![
2112 DuplicateLocation {
2113 path: PathBuf::from("c.ts"),
2114 line: 1,
2115 col: 0,
2116 },
2117 DuplicateLocation {
2118 path: PathBuf::from("a.ts"),
2119 line: 5,
2120 col: 0,
2121 },
2122 ],
2123 }));
2124 r.duplicate_exports
2125 .push(DuplicateExportFinding::with_actions(DuplicateExport {
2126 export_name: "a".to_string(),
2127 locations: vec![DuplicateLocation {
2128 path: PathBuf::from("b.ts"),
2129 line: 1,
2130 col: 0,
2131 }],
2132 }));
2133 r.sort();
2134
2135 assert_eq!(r.duplicate_exports[0].export.export_name, "a");
2137 assert_eq!(r.duplicate_exports[1].export.export_name, "z");
2138
2139 let z_locs: Vec<_> = r.duplicate_exports[1]
2141 .export
2142 .locations
2143 .iter()
2144 .map(|l| l.path.to_string_lossy().to_string())
2145 .collect();
2146 assert_eq!(z_locs, vec!["a.ts", "c.ts"]);
2147 }
2148
2149 #[test]
2152 fn sort_type_only_dependencies() {
2153 let mut r = AnalysisResults::default();
2154 r.type_only_dependencies
2155 .push(TypeOnlyDependencyFinding::with_actions(
2156 TypeOnlyDependency {
2157 package_name: "zod".to_string(),
2158 path: PathBuf::from("package.json"),
2159 line: 10,
2160 },
2161 ));
2162 r.type_only_dependencies
2163 .push(TypeOnlyDependencyFinding::with_actions(
2164 TypeOnlyDependency {
2165 package_name: "ajv".to_string(),
2166 path: PathBuf::from("package.json"),
2167 line: 5,
2168 },
2169 ));
2170 r.sort();
2171 assert_eq!(r.type_only_dependencies[0].dep.package_name, "ajv");
2172 assert_eq!(r.type_only_dependencies[1].dep.package_name, "zod");
2173 }
2174
2175 #[test]
2178 fn sort_test_only_dependencies() {
2179 let mut r = AnalysisResults::default();
2180 r.test_only_dependencies
2181 .push(TestOnlyDependencyFinding::with_actions(
2182 TestOnlyDependency {
2183 package_name: "vitest".to_string(),
2184 path: PathBuf::from("package.json"),
2185 line: 15,
2186 },
2187 ));
2188 r.test_only_dependencies
2189 .push(TestOnlyDependencyFinding::with_actions(
2190 TestOnlyDependency {
2191 package_name: "jest".to_string(),
2192 path: PathBuf::from("package.json"),
2193 line: 10,
2194 },
2195 ));
2196 r.sort();
2197 assert_eq!(r.test_only_dependencies[0].dep.package_name, "jest");
2198 assert_eq!(r.test_only_dependencies[1].dep.package_name, "vitest");
2199 }
2200
2201 #[test]
2204 fn sort_circular_dependencies_by_files_then_length() {
2205 let mut r = AnalysisResults::default();
2206 r.circular_dependencies
2207 .push(CircularDependencyFinding::with_actions(
2208 CircularDependency {
2209 files: vec![PathBuf::from("b.ts"), PathBuf::from("c.ts")],
2210 length: 2,
2211 line: 1,
2212 col: 0,
2213 is_cross_package: false,
2214 },
2215 ));
2216 r.circular_dependencies
2217 .push(CircularDependencyFinding::with_actions(
2218 CircularDependency {
2219 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
2220 length: 2,
2221 line: 1,
2222 col: 0,
2223 is_cross_package: true,
2224 },
2225 ));
2226 r.sort();
2227 assert_eq!(
2228 r.circular_dependencies[0].cycle.files[0],
2229 PathBuf::from("a.ts")
2230 );
2231 assert_eq!(
2232 r.circular_dependencies[1].cycle.files[0],
2233 PathBuf::from("b.ts")
2234 );
2235 }
2236
2237 #[test]
2240 fn sort_boundary_violations() {
2241 let mut r = AnalysisResults::default();
2242 let mk = |from: &str, line: u32, col: u32, to: &str| {
2243 BoundaryViolationFinding::with_actions(BoundaryViolation {
2244 from_path: PathBuf::from(from),
2245 to_path: PathBuf::from(to),
2246 from_zone: "a".to_string(),
2247 to_zone: "b".to_string(),
2248 import_specifier: to.to_string(),
2249 line,
2250 col,
2251 })
2252 };
2253 r.boundary_violations.push(mk("z.ts", 1, 0, "a.ts"));
2254 r.boundary_violations.push(mk("a.ts", 5, 0, "b.ts"));
2255 r.boundary_violations.push(mk("a.ts", 1, 0, "c.ts"));
2256 r.sort();
2257 let from_paths: Vec<_> = r
2258 .boundary_violations
2259 .iter()
2260 .map(|v| {
2261 format!(
2262 "{}:{}",
2263 v.violation.from_path.to_string_lossy(),
2264 v.violation.line
2265 )
2266 })
2267 .collect();
2268 assert_eq!(from_paths, vec!["a.ts:1", "a.ts:5", "z.ts:1"]);
2269 }
2270
2271 #[test]
2274 fn sort_export_usages_and_inner_reference_locations() {
2275 let mut r = AnalysisResults::default();
2276 r.export_usages.push(ExportUsage {
2277 path: PathBuf::from("z.ts"),
2278 export_name: "foo".to_string(),
2279 line: 1,
2280 col: 0,
2281 reference_count: 2,
2282 reference_locations: vec![
2283 ReferenceLocation {
2284 path: PathBuf::from("c.ts"),
2285 line: 10,
2286 col: 0,
2287 },
2288 ReferenceLocation {
2289 path: PathBuf::from("a.ts"),
2290 line: 5,
2291 col: 0,
2292 },
2293 ],
2294 });
2295 r.export_usages.push(ExportUsage {
2296 path: PathBuf::from("a.ts"),
2297 export_name: "bar".to_string(),
2298 line: 1,
2299 col: 0,
2300 reference_count: 1,
2301 reference_locations: vec![ReferenceLocation {
2302 path: PathBuf::from("b.ts"),
2303 line: 1,
2304 col: 0,
2305 }],
2306 });
2307 r.sort();
2308
2309 assert_eq!(r.export_usages[0].path, PathBuf::from("a.ts"));
2311 assert_eq!(r.export_usages[1].path, PathBuf::from("z.ts"));
2312
2313 let refs: Vec<_> = r.export_usages[1]
2315 .reference_locations
2316 .iter()
2317 .map(|l| l.path.to_string_lossy().to_string())
2318 .collect();
2319 assert_eq!(refs, vec!["a.ts", "c.ts"]);
2320 }
2321
2322 #[test]
2325 fn sort_empty_results_is_noop() {
2326 let mut r = AnalysisResults::default();
2327 r.sort(); assert_eq!(r.total_issues(), 0);
2329 }
2330
2331 #[test]
2334 fn sort_single_element_lists_stable() {
2335 let mut r = AnalysisResults::default();
2336 r.unused_files
2337 .push(UnusedFileFinding::with_actions(UnusedFile {
2338 path: PathBuf::from("only.ts"),
2339 }));
2340 r.sort();
2341 assert_eq!(r.unused_files[0].file.path, PathBuf::from("only.ts"));
2342 }
2343
2344 #[test]
2347 fn serialize_empty_results() {
2348 let r = AnalysisResults::default();
2349 let json = serde_json::to_value(&r).unwrap();
2350
2351 assert!(json["unused_files"].as_array().unwrap().is_empty());
2353 assert!(json["unused_exports"].as_array().unwrap().is_empty());
2354 assert!(json["circular_dependencies"].as_array().unwrap().is_empty());
2355
2356 assert!(json.get("export_usages").is_none());
2358 assert!(json.get("entry_point_summary").is_none());
2359 }
2360
2361 #[test]
2362 fn serialize_unused_file_path() {
2363 let r = UnusedFile {
2364 path: PathBuf::from("src/utils/index.ts"),
2365 };
2366 let json = serde_json::to_value(&r).unwrap();
2367 assert_eq!(json["path"], "src/utils/index.ts");
2368 }
2369
2370 #[test]
2371 fn serialize_dependency_location_camel_case() {
2372 let dep = UnusedDependency {
2373 package_name: "react".to_string(),
2374 location: DependencyLocation::DevDependencies,
2375 path: PathBuf::from("package.json"),
2376 line: 5,
2377 used_in_workspaces: Vec::new(),
2378 };
2379 let json = serde_json::to_value(&dep).unwrap();
2380 assert_eq!(json["location"], "devDependencies");
2381
2382 let dep2 = UnusedDependency {
2383 package_name: "react".to_string(),
2384 location: DependencyLocation::Dependencies,
2385 path: PathBuf::from("package.json"),
2386 line: 3,
2387 used_in_workspaces: Vec::new(),
2388 };
2389 let json2 = serde_json::to_value(&dep2).unwrap();
2390 assert_eq!(json2["location"], "dependencies");
2391
2392 let dep3 = UnusedDependency {
2393 package_name: "fsevents".to_string(),
2394 location: DependencyLocation::OptionalDependencies,
2395 path: PathBuf::from("package.json"),
2396 line: 7,
2397 used_in_workspaces: Vec::new(),
2398 };
2399 let json3 = serde_json::to_value(&dep3).unwrap();
2400 assert_eq!(json3["location"], "optionalDependencies");
2401 }
2402
2403 #[test]
2404 fn serialize_circular_dependency_skips_false_cross_package() {
2405 let cd = CircularDependency {
2406 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
2407 length: 2,
2408 line: 1,
2409 col: 0,
2410 is_cross_package: false,
2411 };
2412 let json = serde_json::to_value(&cd).unwrap();
2413 assert!(json.get("is_cross_package").is_none());
2415 }
2416
2417 #[test]
2418 fn serialize_circular_dependency_includes_true_cross_package() {
2419 let cd = CircularDependency {
2420 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
2421 length: 2,
2422 line: 1,
2423 col: 0,
2424 is_cross_package: true,
2425 };
2426 let json = serde_json::to_value(&cd).unwrap();
2427 assert_eq!(json["is_cross_package"], true);
2428 }
2429
2430 #[test]
2431 fn serialize_unused_export_fields() {
2432 let e = UnusedExport {
2433 path: PathBuf::from("src/mod.ts"),
2434 export_name: "helper".to_string(),
2435 is_type_only: true,
2436 line: 42,
2437 col: 7,
2438 span_start: 100,
2439 is_re_export: true,
2440 };
2441 let json = serde_json::to_value(&e).unwrap();
2442 assert_eq!(json["path"], "src/mod.ts");
2443 assert_eq!(json["export_name"], "helper");
2444 assert_eq!(json["is_type_only"], true);
2445 assert_eq!(json["line"], 42);
2446 assert_eq!(json["col"], 7);
2447 assert_eq!(json["span_start"], 100);
2448 assert_eq!(json["is_re_export"], true);
2449 }
2450
2451 #[test]
2452 fn serialize_boundary_violation_fields() {
2453 let v = BoundaryViolation {
2454 from_path: PathBuf::from("src/ui/button.tsx"),
2455 to_path: PathBuf::from("src/db/queries.ts"),
2456 from_zone: "ui".to_string(),
2457 to_zone: "db".to_string(),
2458 import_specifier: "../db/queries".to_string(),
2459 line: 3,
2460 col: 0,
2461 };
2462 let json = serde_json::to_value(&v).unwrap();
2463 assert_eq!(json["from_path"], "src/ui/button.tsx");
2464 assert_eq!(json["to_path"], "src/db/queries.ts");
2465 assert_eq!(json["from_zone"], "ui");
2466 assert_eq!(json["to_zone"], "db");
2467 assert_eq!(json["import_specifier"], "../db/queries");
2468 }
2469
2470 #[test]
2471 fn serialize_unlisted_dependency_with_import_sites() {
2472 let d = UnlistedDependency {
2473 package_name: "chalk".to_string(),
2474 imported_from: vec![
2475 ImportSite {
2476 path: PathBuf::from("a.ts"),
2477 line: 1,
2478 col: 0,
2479 },
2480 ImportSite {
2481 path: PathBuf::from("b.ts"),
2482 line: 5,
2483 col: 3,
2484 },
2485 ],
2486 };
2487 let json = serde_json::to_value(&d).unwrap();
2488 assert_eq!(json["package_name"], "chalk");
2489 let sites = json["imported_from"].as_array().unwrap();
2490 assert_eq!(sites.len(), 2);
2491 assert_eq!(sites[0]["path"], "a.ts");
2492 assert_eq!(sites[1]["line"], 5);
2493 }
2494
2495 #[test]
2496 fn serialize_duplicate_export_with_locations() {
2497 let d = DuplicateExport {
2498 export_name: "Button".to_string(),
2499 locations: vec![
2500 DuplicateLocation {
2501 path: PathBuf::from("src/a.ts"),
2502 line: 10,
2503 col: 0,
2504 },
2505 DuplicateLocation {
2506 path: PathBuf::from("src/b.ts"),
2507 line: 20,
2508 col: 5,
2509 },
2510 ],
2511 };
2512 let json = serde_json::to_value(&d).unwrap();
2513 assert_eq!(json["export_name"], "Button");
2514 let locs = json["locations"].as_array().unwrap();
2515 assert_eq!(locs.len(), 2);
2516 assert_eq!(locs[0]["line"], 10);
2517 assert_eq!(locs[1]["col"], 5);
2518 }
2519
2520 #[test]
2521 fn serialize_type_only_dependency() {
2522 let d = TypeOnlyDependency {
2523 package_name: "@types/react".to_string(),
2524 path: PathBuf::from("package.json"),
2525 line: 12,
2526 };
2527 let json = serde_json::to_value(&d).unwrap();
2528 assert_eq!(json["package_name"], "@types/react");
2529 assert_eq!(json["line"], 12);
2530 }
2531
2532 #[test]
2533 fn serialize_test_only_dependency() {
2534 let d = TestOnlyDependency {
2535 package_name: "vitest".to_string(),
2536 path: PathBuf::from("package.json"),
2537 line: 8,
2538 };
2539 let json = serde_json::to_value(&d).unwrap();
2540 assert_eq!(json["package_name"], "vitest");
2541 assert_eq!(json["line"], 8);
2542 }
2543
2544 #[test]
2545 fn serialize_unused_member() {
2546 let m = UnusedMember {
2547 path: PathBuf::from("enums.ts"),
2548 parent_name: "Status".to_string(),
2549 member_name: "Pending".to_string(),
2550 kind: MemberKind::EnumMember,
2551 line: 3,
2552 col: 4,
2553 };
2554 let json = serde_json::to_value(&m).unwrap();
2555 assert_eq!(json["parent_name"], "Status");
2556 assert_eq!(json["member_name"], "Pending");
2557 assert_eq!(json["line"], 3);
2558 }
2559
2560 #[test]
2561 fn serialize_unresolved_import() {
2562 let i = UnresolvedImport {
2563 path: PathBuf::from("app.ts"),
2564 specifier: "./missing-module".to_string(),
2565 line: 7,
2566 col: 0,
2567 specifier_col: 21,
2568 };
2569 let json = serde_json::to_value(&i).unwrap();
2570 assert_eq!(json["specifier"], "./missing-module");
2571 assert_eq!(json["specifier_col"], 21);
2572 }
2573
2574 #[test]
2577 fn deserialize_circular_dependency_with_defaults() {
2578 let json = r#"{"files":["a.ts","b.ts"],"length":2}"#;
2580 let cd: CircularDependency = serde_json::from_str(json).unwrap();
2581 assert_eq!(cd.files.len(), 2);
2582 assert_eq!(cd.length, 2);
2583 assert_eq!(cd.line, 0);
2584 assert_eq!(cd.col, 0);
2585 assert!(!cd.is_cross_package);
2586 }
2587
2588 #[test]
2589 fn deserialize_circular_dependency_with_all_fields() {
2590 let json =
2591 r#"{"files":["a.ts","b.ts"],"length":2,"line":5,"col":10,"is_cross_package":true}"#;
2592 let cd: CircularDependency = serde_json::from_str(json).unwrap();
2593 assert_eq!(cd.line, 5);
2594 assert_eq!(cd.col, 10);
2595 assert!(cd.is_cross_package);
2596 }
2597
2598 #[test]
2601 fn clone_results_are_independent() {
2602 let mut r = AnalysisResults::default();
2603 r.unused_files
2604 .push(UnusedFileFinding::with_actions(UnusedFile {
2605 path: PathBuf::from("a.ts"),
2606 }));
2607 let mut cloned = r.clone();
2608 cloned
2609 .unused_files
2610 .push(UnusedFileFinding::with_actions(UnusedFile {
2611 path: PathBuf::from("b.ts"),
2612 }));
2613 assert_eq!(r.total_issues(), 1);
2614 assert_eq!(cloned.total_issues(), 2);
2615 }
2616
2617 #[test]
2620 fn export_usages_not_counted_in_total_issues() {
2621 let mut r = AnalysisResults::default();
2622 r.export_usages.push(ExportUsage {
2623 path: PathBuf::from("mod.ts"),
2624 export_name: "foo".to_string(),
2625 line: 1,
2626 col: 0,
2627 reference_count: 3,
2628 reference_locations: vec![],
2629 });
2630 assert_eq!(r.total_issues(), 0);
2632 assert!(!r.has_issues());
2633 }
2634
2635 #[test]
2638 fn entry_point_summary_not_counted_in_total_issues() {
2639 let r = AnalysisResults {
2640 entry_point_summary: Some(EntryPointSummary {
2641 total: 10,
2642 by_source: vec![("config".to_string(), 10)],
2643 }),
2644 ..AnalysisResults::default()
2645 };
2646 assert_eq!(r.total_issues(), 0);
2647 assert!(!r.has_issues());
2648 }
2649}