use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use clap::ValueEnum;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct AnalysisResult {
pub mode: AnalysisMode,
pub target: AnalysisTarget,
pub repo_root: PathBuf,
pub source_file_count: usize,
pub summary: Summary,
pub workspaces: Vec<Workspace>,
pub roots: Vec<RootImpact>,
pub nodes: Vec<GraphNode>,
pub edges: Vec<GraphEdge>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Workspace {
pub name: String,
pub root: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct RootImpact {
pub file: String,
pub affected: usize,
pub direct: usize,
pub indirect: usize,
pub max_depth: usize,
pub packages: usize,
pub files: Vec<RootImpactFile>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RootImpactFile {
pub path: String,
pub endpoint: bool,
}
pub fn package_key(rel_path: &str, workspaces: &[Workspace]) -> String {
if let Some(workspace) = workspaces.iter().find(|workspace| {
workspace.root.is_empty()
|| rel_path == workspace.root
|| rel_path.starts_with(&format!("{}/", workspace.root))
}) {
if workspace.root.is_empty() {
".".to_string()
} else {
workspace.root.clone()
}
} else {
match rel_path.split_once('/') {
Some((head, _)) => head.to_string(),
None => ".".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AnalysisMode {
Export,
File,
Files,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AnalysisTarget {
Export { file: PathBuf, export_name: String },
File { file: PathBuf },
Files { files: Vec<PathBuf> },
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct Summary {
pub directly_affected_files: usize,
pub transitively_affected_files: usize,
pub total_affected_files: usize,
pub unresolved_imports: usize,
pub ambiguous_edges: usize,
pub parse_failures: usize,
pub skipped_inputs: usize,
pub risk_tier: RiskTier,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, ValueEnum,
)]
#[serde(rename_all = "snake_case")]
pub enum RiskTier {
#[default]
Minor,
Moderate,
Risky,
High,
}
pub fn compute_tier(affected: usize, packages: usize) -> RiskTier {
if affected == 0 {
RiskTier::Minor
} else if affected > 25 || packages >= 3 {
RiskTier::High
} else if affected <= 3 && packages <= 1 {
RiskTier::Minor
} else if affected <= 10 && packages <= 2 {
RiskTier::Moderate
} else {
RiskTier::Risky
}
}
#[derive(Debug, Clone, Serialize)]
pub struct GraphNode {
pub id: String,
pub label: String,
pub file: PathBuf,
pub symbol: Option<String>,
pub kind: NodeKind,
pub depth: usize,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NodeKind {
File,
Export,
}
#[derive(Debug, Clone, Serialize)]
pub struct GraphEdge {
pub from: String,
pub to: String,
pub kind: EdgeKind,
pub is_ambiguous: bool,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EdgeKind {
ImportsNamed,
ImportsDefault,
ImportsNamespace,
ReexportsNamed,
ReexportsStar,
UsesJsxComponent,
RequiresModule,
CommonJsExport,
}
#[derive(Debug, Clone)]
pub struct ModuleState {
pub file: PathBuf,
pub public_exports: BTreeSet<String>,
pub export_to_locals: BTreeMap<String, BTreeSet<String>>,
pub local_to_exports: BTreeMap<String, BTreeSet<String>>,
}
impl ModuleState {
pub fn new(file: PathBuf) -> Self {
Self {
file,
public_exports: BTreeSet::new(),
export_to_locals: BTreeMap::new(),
local_to_exports: BTreeMap::new(),
}
}
pub fn add_export(&mut self, exported: impl Into<String>, local: Option<String>) {
let exported = exported.into();
self.public_exports.insert(exported.clone());
if let Some(local) = local {
self.export_to_locals
.entry(exported.clone())
.or_default()
.insert(local.clone());
self.local_to_exports
.entry(local)
.or_default()
.insert(exported);
}
}
}