use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::extract::MemberKind;
use crate::serde_path;
#[derive(Debug, Default, Clone, Serialize)]
pub struct AnalysisResults {
pub unused_files: Vec<UnusedFile>,
pub unused_exports: Vec<UnusedExport>,
pub unused_types: Vec<UnusedExport>,
pub unused_dependencies: Vec<UnusedDependency>,
pub unused_dev_dependencies: Vec<UnusedDependency>,
pub unused_optional_dependencies: Vec<UnusedDependency>,
pub unused_enum_members: Vec<UnusedMember>,
pub unused_class_members: Vec<UnusedMember>,
pub unresolved_imports: Vec<UnresolvedImport>,
pub unlisted_dependencies: Vec<UnlistedDependency>,
pub duplicate_exports: Vec<DuplicateExport>,
pub type_only_dependencies: Vec<TypeOnlyDependency>,
#[serde(default)]
pub test_only_dependencies: Vec<TestOnlyDependency>,
pub circular_dependencies: Vec<CircularDependency>,
#[serde(default)]
pub boundary_violations: Vec<BoundaryViolation>,
#[serde(skip)]
pub export_usages: Vec<ExportUsage>,
}
impl AnalysisResults {
#[must_use]
pub const fn total_issues(&self) -> usize {
self.unused_files.len()
+ self.unused_exports.len()
+ self.unused_types.len()
+ self.unused_dependencies.len()
+ self.unused_dev_dependencies.len()
+ self.unused_optional_dependencies.len()
+ self.unused_enum_members.len()
+ self.unused_class_members.len()
+ self.unresolved_imports.len()
+ self.unlisted_dependencies.len()
+ self.duplicate_exports.len()
+ self.type_only_dependencies.len()
+ self.test_only_dependencies.len()
+ self.circular_dependencies.len()
+ self.boundary_violations.len()
}
#[must_use]
pub const fn has_issues(&self) -> bool {
self.total_issues() > 0
}
pub fn sort(&mut self) {
self.unused_files.sort_by(|a, b| a.path.cmp(&b.path));
self.unused_exports.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.export_name.cmp(&b.export_name))
});
self.unused_types.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.export_name.cmp(&b.export_name))
});
self.unused_dependencies.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.package_name.cmp(&b.package_name))
});
self.unused_dev_dependencies.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.package_name.cmp(&b.package_name))
});
self.unused_optional_dependencies.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.package_name.cmp(&b.package_name))
});
self.unused_enum_members.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.parent_name.cmp(&b.parent_name))
.then(a.member_name.cmp(&b.member_name))
});
self.unused_class_members.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.parent_name.cmp(&b.parent_name))
.then(a.member_name.cmp(&b.member_name))
});
self.unresolved_imports.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.col.cmp(&b.col))
.then(a.specifier.cmp(&b.specifier))
});
self.unlisted_dependencies
.sort_by(|a, b| a.package_name.cmp(&b.package_name));
for dep in &mut self.unlisted_dependencies {
dep.imported_from
.sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
}
self.duplicate_exports
.sort_by(|a, b| a.export_name.cmp(&b.export_name));
for dup in &mut self.duplicate_exports {
dup.locations
.sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
}
self.type_only_dependencies.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.package_name.cmp(&b.package_name))
});
self.test_only_dependencies.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.package_name.cmp(&b.package_name))
});
self.circular_dependencies
.sort_by(|a, b| a.files.cmp(&b.files).then(a.length.cmp(&b.length)));
self.boundary_violations.sort_by(|a, b| {
a.from_path
.cmp(&b.from_path)
.then(a.line.cmp(&b.line))
.then(a.col.cmp(&b.col))
.then(a.to_path.cmp(&b.to_path))
});
for usage in &mut self.export_usages {
usage.reference_locations.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.col.cmp(&b.col))
});
}
self.export_usages.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.export_name.cmp(&b.export_name))
});
}
}
#[derive(Debug, Clone, Serialize)]
pub struct UnusedFile {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
}
#[derive(Debug, Clone, Serialize)]
pub struct UnusedExport {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub export_name: String,
pub is_type_only: bool,
pub line: u32,
pub col: u32,
pub span_start: u32,
pub is_re_export: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct UnusedDependency {
pub package_name: String,
pub location: DependencyLocation,
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub line: u32,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum DependencyLocation {
Dependencies,
DevDependencies,
OptionalDependencies,
}
#[derive(Debug, Clone, Serialize)]
pub struct UnusedMember {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub parent_name: String,
pub member_name: String,
pub kind: MemberKind,
pub line: u32,
pub col: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct UnresolvedImport {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub specifier: String,
pub line: u32,
pub col: u32,
pub specifier_col: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct UnlistedDependency {
pub package_name: String,
pub imported_from: Vec<ImportSite>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ImportSite {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub line: u32,
pub col: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct DuplicateExport {
pub export_name: String,
pub locations: Vec<DuplicateLocation>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DuplicateLocation {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub line: u32,
pub col: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct TypeOnlyDependency {
pub package_name: String,
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub line: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct TestOnlyDependency {
pub package_name: String,
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub line: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CircularDependency {
#[serde(serialize_with = "serde_path::serialize_vec")]
pub files: Vec<PathBuf>,
pub length: usize,
#[serde(default)]
pub line: u32,
#[serde(default)]
pub col: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct BoundaryViolation {
#[serde(serialize_with = "serde_path::serialize")]
pub from_path: PathBuf,
#[serde(serialize_with = "serde_path::serialize")]
pub to_path: PathBuf,
pub from_zone: String,
pub to_zone: String,
pub import_specifier: String,
pub line: u32,
pub col: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct ExportUsage {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub export_name: String,
pub line: u32,
pub col: u32,
pub reference_count: usize,
pub reference_locations: Vec<ReferenceLocation>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ReferenceLocation {
#[serde(serialize_with = "serde_path::serialize")]
pub path: PathBuf,
pub line: u32,
pub col: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_results_no_issues() {
let results = AnalysisResults::default();
assert_eq!(results.total_issues(), 0);
assert!(!results.has_issues());
}
#[test]
fn results_with_unused_file() {
let mut results = AnalysisResults::default();
results.unused_files.push(UnusedFile {
path: PathBuf::from("test.ts"),
});
assert_eq!(results.total_issues(), 1);
assert!(results.has_issues());
}
#[test]
fn results_with_unused_export() {
let mut results = AnalysisResults::default();
results.unused_exports.push(UnusedExport {
path: PathBuf::from("test.ts"),
export_name: "foo".to_string(),
is_type_only: false,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
});
assert_eq!(results.total_issues(), 1);
assert!(results.has_issues());
}
#[test]
fn results_total_counts_all_types() {
let mut results = AnalysisResults::default();
results.unused_files.push(UnusedFile {
path: PathBuf::from("a.ts"),
});
results.unused_exports.push(UnusedExport {
path: PathBuf::from("b.ts"),
export_name: "x".to_string(),
is_type_only: false,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
});
results.unused_types.push(UnusedExport {
path: PathBuf::from("c.ts"),
export_name: "T".to_string(),
is_type_only: true,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
});
results.unused_dependencies.push(UnusedDependency {
package_name: "dep".to_string(),
location: DependencyLocation::Dependencies,
path: PathBuf::from("package.json"),
line: 5,
});
results.unused_dev_dependencies.push(UnusedDependency {
package_name: "dev".to_string(),
location: DependencyLocation::DevDependencies,
path: PathBuf::from("package.json"),
line: 5,
});
results.unused_enum_members.push(UnusedMember {
path: PathBuf::from("d.ts"),
parent_name: "E".to_string(),
member_name: "A".to_string(),
kind: MemberKind::EnumMember,
line: 1,
col: 0,
});
results.unused_class_members.push(UnusedMember {
path: PathBuf::from("e.ts"),
parent_name: "C".to_string(),
member_name: "m".to_string(),
kind: MemberKind::ClassMethod,
line: 1,
col: 0,
});
results.unresolved_imports.push(UnresolvedImport {
path: PathBuf::from("f.ts"),
specifier: "./missing".to_string(),
line: 1,
col: 0,
specifier_col: 0,
});
results.unlisted_dependencies.push(UnlistedDependency {
package_name: "unlisted".to_string(),
imported_from: vec![ImportSite {
path: PathBuf::from("g.ts"),
line: 1,
col: 0,
}],
});
results.duplicate_exports.push(DuplicateExport {
export_name: "dup".to_string(),
locations: vec![
DuplicateLocation {
path: PathBuf::from("h.ts"),
line: 15,
col: 0,
},
DuplicateLocation {
path: PathBuf::from("i.ts"),
line: 30,
col: 0,
},
],
});
results.unused_optional_dependencies.push(UnusedDependency {
package_name: "optional".to_string(),
location: DependencyLocation::OptionalDependencies,
path: PathBuf::from("package.json"),
line: 5,
});
results.type_only_dependencies.push(TypeOnlyDependency {
package_name: "type-only".to_string(),
path: PathBuf::from("package.json"),
line: 8,
});
results.test_only_dependencies.push(TestOnlyDependency {
package_name: "test-only".to_string(),
path: PathBuf::from("package.json"),
line: 9,
});
results.circular_dependencies.push(CircularDependency {
files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
length: 2,
line: 3,
col: 0,
});
results.boundary_violations.push(BoundaryViolation {
from_path: PathBuf::from("src/ui/Button.tsx"),
to_path: PathBuf::from("src/db/queries.ts"),
from_zone: "ui".to_string(),
to_zone: "database".to_string(),
import_specifier: "../db/queries".to_string(),
line: 3,
col: 0,
});
assert_eq!(results.total_issues(), 15);
assert!(results.has_issues());
}
#[test]
fn total_issues_and_has_issues_are_consistent() {
let results = AnalysisResults::default();
assert_eq!(results.total_issues(), 0);
assert!(!results.has_issues());
assert_eq!(results.total_issues() > 0, results.has_issues());
}
#[test]
fn total_issues_sums_all_categories_independently() {
let mut results = AnalysisResults::default();
results.unused_files.push(UnusedFile {
path: PathBuf::from("a.ts"),
});
assert_eq!(results.total_issues(), 1);
results.unused_files.push(UnusedFile {
path: PathBuf::from("b.ts"),
});
assert_eq!(results.total_issues(), 2);
results.unresolved_imports.push(UnresolvedImport {
path: PathBuf::from("c.ts"),
specifier: "./missing".to_string(),
line: 1,
col: 0,
specifier_col: 0,
});
assert_eq!(results.total_issues(), 3);
}
#[test]
fn default_results_all_fields_empty() {
let r = AnalysisResults::default();
assert!(r.unused_files.is_empty());
assert!(r.unused_exports.is_empty());
assert!(r.unused_types.is_empty());
assert!(r.unused_dependencies.is_empty());
assert!(r.unused_dev_dependencies.is_empty());
assert!(r.unused_optional_dependencies.is_empty());
assert!(r.unused_enum_members.is_empty());
assert!(r.unused_class_members.is_empty());
assert!(r.unresolved_imports.is_empty());
assert!(r.unlisted_dependencies.is_empty());
assert!(r.duplicate_exports.is_empty());
assert!(r.type_only_dependencies.is_empty());
assert!(r.test_only_dependencies.is_empty());
assert!(r.circular_dependencies.is_empty());
assert!(r.boundary_violations.is_empty());
assert!(r.export_usages.is_empty());
}
}