1use std::collections::{BTreeMap, BTreeSet};
2use std::path::PathBuf;
3
4use clap::ValueEnum;
5use serde::Serialize;
6
7#[derive(Debug, Clone, Serialize)]
8pub struct AnalysisResult {
9 pub mode: AnalysisMode,
10 pub target: AnalysisTarget,
11 pub repo_root: PathBuf,
12 pub source_file_count: usize,
13 pub summary: Summary,
14 pub workspaces: Vec<Workspace>,
15 pub roots: Vec<RootImpact>,
17 pub nodes: Vec<GraphNode>,
18 pub edges: Vec<GraphEdge>,
19 pub warnings: Vec<String>,
20}
21
22#[derive(Debug, Clone, Serialize)]
25pub struct Workspace {
26 pub name: String,
27 pub root: String,
29}
30
31#[derive(Debug, Clone, Serialize)]
34pub struct RootImpact {
35 pub file: String,
36 pub affected: usize,
37 pub direct: usize,
38 pub indirect: usize,
39 pub max_depth: usize,
40 pub packages: usize,
41 pub files: Vec<RootImpactFile>,
42}
43
44#[derive(Debug, Clone, Serialize)]
46pub struct RootImpactFile {
47 pub path: String,
48 pub endpoint: bool,
49}
50
51pub fn package_key(rel_path: &str, workspaces: &[Workspace]) -> String {
55 if let Some(workspace) = workspaces.iter().find(|workspace| {
56 workspace.root.is_empty()
57 || rel_path == workspace.root
58 || rel_path.starts_with(&format!("{}/", workspace.root))
59 }) {
60 if workspace.root.is_empty() {
61 ".".to_string()
62 } else {
63 workspace.root.clone()
64 }
65 } else {
66 match rel_path.split_once('/') {
67 Some((head, _)) => head.to_string(),
68 None => ".".to_string(),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize)]
74#[serde(rename_all = "snake_case")]
75pub enum AnalysisMode {
76 Export,
77 File,
78 Files,
79}
80
81#[derive(Debug, Clone, Serialize)]
82#[serde(tag = "kind", rename_all = "snake_case")]
83pub enum AnalysisTarget {
84 Export { file: PathBuf, export_name: String },
85 File { file: PathBuf },
86 Files { files: Vec<PathBuf> },
87}
88
89#[derive(Debug, Clone, Serialize, Default)]
90pub struct Summary {
91 pub directly_affected_files: usize,
92 pub transitively_affected_files: usize,
93 pub total_affected_files: usize,
94 pub unresolved_imports: usize,
95 pub ambiguous_edges: usize,
96 pub parse_failures: usize,
97 pub skipped_inputs: usize,
100 pub risk_tier: RiskTier,
102}
103
104#[derive(
107 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, ValueEnum,
108)]
109#[serde(rename_all = "snake_case")]
110pub enum RiskTier {
111 #[default]
112 Minor,
113 Moderate,
114 Risky,
115 High,
116}
117
118pub fn compute_tier(affected: usize, packages: usize) -> RiskTier {
123 if affected == 0 {
124 RiskTier::Minor
125 } else if affected > 25 || packages >= 3 {
126 RiskTier::High
127 } else if affected <= 3 && packages <= 1 {
128 RiskTier::Minor
129 } else if affected <= 10 && packages <= 2 {
130 RiskTier::Moderate
131 } else {
132 RiskTier::Risky
133 }
134}
135
136#[derive(Debug, Clone, Serialize)]
137pub struct GraphNode {
138 pub id: String,
139 pub label: String,
140 pub file: PathBuf,
141 pub symbol: Option<String>,
142 pub kind: NodeKind,
143 pub depth: usize,
144}
145
146#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
147#[serde(rename_all = "snake_case")]
148pub enum NodeKind {
149 File,
150 Export,
151}
152
153#[derive(Debug, Clone, Serialize)]
154pub struct GraphEdge {
155 pub from: String,
156 pub to: String,
157 pub kind: EdgeKind,
158 pub is_ambiguous: bool,
159}
160
161#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
162#[serde(rename_all = "snake_case")]
163pub enum EdgeKind {
164 ImportsNamed,
165 ImportsDefault,
166 ImportsNamespace,
167 ReexportsNamed,
168 ReexportsStar,
169 UsesJsxComponent,
170 RequiresModule,
171 CommonJsExport,
172}
173
174#[derive(Debug, Clone)]
175pub struct ModuleState {
176 pub file: PathBuf,
177 pub public_exports: BTreeSet<String>,
178 pub export_to_locals: BTreeMap<String, BTreeSet<String>>,
179 pub local_to_exports: BTreeMap<String, BTreeSet<String>>,
180}
181
182impl ModuleState {
183 pub fn new(file: PathBuf) -> Self {
184 Self {
185 file,
186 public_exports: BTreeSet::new(),
187 export_to_locals: BTreeMap::new(),
188 local_to_exports: BTreeMap::new(),
189 }
190 }
191
192 pub fn add_export(&mut self, exported: impl Into<String>, local: Option<String>) {
193 let exported = exported.into();
194 self.public_exports.insert(exported.clone());
195 if let Some(local) = local {
196 self.export_to_locals
197 .entry(exported.clone())
198 .or_default()
199 .insert(local.clone());
200 self.local_to_exports
201 .entry(local)
202 .or_default()
203 .insert(exported);
204 }
205 }
206}