fob_graph/
module.rs

1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5
6use super::symbol::SymbolTable;
7use super::{Export, Import, ModuleId};
8
9/// Resolved module metadata used by graph algorithms and builders.
10///
11/// Heavy collections (imports, exports, symbol_table) are wrapped in Arc
12/// to make cloning cheap when returning modules from the graph.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Module {
15    pub id: ModuleId,
16    pub path: PathBuf,
17    pub source_type: SourceType,
18    #[serde(with = "arc_vec_serde")]
19    pub imports: Arc<Vec<Import>>,
20    #[serde(with = "arc_vec_serde")]
21    pub exports: Arc<Vec<Export>>,
22    pub has_side_effects: bool,
23    pub is_entry: bool,
24    pub is_external: bool,
25    pub original_size: usize,
26    pub bundled_size: Option<usize>,
27    /// Symbol table from semantic analysis (intra-file dead code detection)
28    #[serde(with = "arc_symbol_table_serde")]
29    pub symbol_table: Arc<SymbolTable>,
30    /// Module format (ESM vs CJS) from rolldown analysis
31    pub module_format: ModuleFormat,
32    /// Export structure kind (ESM, CJS, or None)
33    pub exports_kind: ExportsKind,
34    /// True if module has star re-exports (`export * from`)
35    pub has_star_exports: bool,
36    /// Execution order in module graph (topological sort)
37    pub execution_order: Option<u32>,
38}
39
40// Serde helper for Arc<Vec<T>>
41mod arc_vec_serde {
42    use super::*;
43    use serde::de::Deserializer;
44    use serde::ser::Serializer;
45
46    pub fn serialize<S, T>(value: &Arc<Vec<T>>, serializer: S) -> Result<S::Ok, S::Error>
47    where
48        S: Serializer,
49        T: Serialize,
50    {
51        value.as_ref().serialize(serializer)
52    }
53
54    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Arc<Vec<T>>, D::Error>
55    where
56        D: Deserializer<'de>,
57        T: Deserialize<'de>,
58    {
59        Vec::deserialize(deserializer).map(Arc::new)
60    }
61}
62
63// Serde helper for Arc<SymbolTable>
64mod arc_symbol_table_serde {
65    use super::*;
66    use serde::de::Deserializer;
67    use serde::ser::Serializer;
68
69    pub fn serialize<S>(value: &Arc<SymbolTable>, serializer: S) -> Result<S::Ok, S::Error>
70    where
71        S: Serializer,
72    {
73        value.as_ref().serialize(serializer)
74    }
75
76    pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<SymbolTable>, D::Error>
77    where
78        D: Deserializer<'de>,
79    {
80        SymbolTable::deserialize(deserializer).map(Arc::new)
81    }
82}
83
84impl Module {
85    /// Create a new module builder with sensible defaults.
86    pub fn builder(id: ModuleId, path: PathBuf, source_type: SourceType) -> ModuleBuilder {
87        ModuleBuilder {
88            module: Self {
89                id,
90                path,
91                source_type,
92                imports: Arc::new(Vec::new()),
93                exports: Arc::new(Vec::new()),
94                has_side_effects: false,
95                is_entry: false,
96                is_external: false,
97                original_size: 0,
98                bundled_size: None,
99                symbol_table: Arc::new(SymbolTable::new()),
100                module_format: ModuleFormat::Unknown,
101                exports_kind: ExportsKind::None,
102                has_star_exports: false,
103                execution_order: None,
104            },
105        }
106    }
107
108    /// Mark the module as an entry module.
109    pub fn mark_entry(&mut self) {
110        self.is_entry = true;
111    }
112
113    /// Mark the module as an external dependency.
114    pub fn mark_external(&mut self) {
115        self.is_external = true;
116    }
117
118    /// Toggle side-effect tracking on the module.
119    pub fn set_side_effects(&mut self, has_side_effects: bool) {
120        self.has_side_effects = has_side_effects;
121    }
122
123    /// Update bundled size information (if available).
124    pub fn set_bundled_size(&mut self, size: Option<usize>) {
125        self.bundled_size = size;
126    }
127
128    /// Get an iterator over imports.
129    pub fn imports_iter(&self) -> impl Iterator<Item = &Import> {
130        self.imports.iter()
131    }
132
133    /// Get an iterator over exports.
134    pub fn exports_iter(&self) -> impl Iterator<Item = &Export> {
135        self.exports.iter()
136    }
137
138    /// Get mutable access to exports (for external tools like framework rules).
139    ///
140    /// This uses Arc::make_mut to create a mutable copy only when needed.
141    ///
142    /// # Example
143    ///
144    /// ```rust
145    /// # use fob_graph::Module;
146    /// # use fob_graph::{ModuleId, SourceType};
147    /// # use std::path::PathBuf;
148    /// # let mut module = Module::builder(ModuleId::new_virtual("test.ts"), PathBuf::from("test.ts"), SourceType::TypeScript).build();
149    /// for export in module.exports_mut() {
150    ///     if export.name.starts_with("use") {
151    ///         // export.mark_framework_used(); // Method might not exist on Export, check usage
152    ///     }
153    /// }
154    /// ```
155    pub fn exports_mut(&mut self) -> &mut Vec<Export> {
156        Arc::make_mut(&mut self.exports)
157    }
158
159    /// Get imports that reference a specific module.
160    ///
161    /// # Example
162    ///
163    /// ```rust
164    /// use fob_graph::ModuleId;
165    /// # use fob_graph::Module;
166    /// # use fob_graph::SourceType;
167    /// # use std::path::PathBuf;
168    ///
169    /// # let module = Module::builder(ModuleId::new_virtual("test.ts"), PathBuf::from("test.ts"), SourceType::TypeScript).build();
170    /// let react_id = ModuleId::new("node_modules/react/index.js")?;
171    /// let imports = module.imports_from(&react_id);
172    /// assert_eq!(imports.len(), 0);
173    /// # Ok::<(), fob_graph::ModuleIdError>(())
174    /// ```
175    pub fn imports_from(&self, target: &ModuleId) -> Vec<&Import> {
176        self.imports
177            .iter()
178            .filter(|imp| imp.resolved_to.as_ref() == Some(target))
179            .collect()
180    }
181
182    /// Check if this module imports from a specific source specifier.
183    ///
184    /// This is useful for framework detection - checking if a module imports
185    /// from "react", "vue", "svelte", etc.
186    ///
187    /// # Example
188    ///
189    /// ```rust
190    /// # use fob_graph::Module;
191    /// # use fob_graph::{ModuleId, SourceType};
192    /// # use std::path::PathBuf;
193    /// # let module = Module::builder(ModuleId::new_virtual("test.ts"), PathBuf::from("test.ts"), SourceType::TypeScript).build();
194    /// if module.has_import_from("react") {
195    ///     // This is a React module
196    /// }
197    /// ```
198    pub fn has_import_from(&self, source: &str) -> bool {
199        self.imports.iter().any(|imp| imp.source == source)
200    }
201
202    /// Get all import sources (for dependency analysis).
203    ///
204    /// Returns a vector of source specifiers that this module imports from.
205    ///
206    /// # Example
207    ///
208    /// ```rust
209    /// # use fob_graph::Module;
210    /// # use fob_graph::{ModuleId, SourceType};
211    /// # use std::path::PathBuf;
212    /// # let module = Module::builder(ModuleId::new_virtual("test.ts"), PathBuf::from("test.ts"), SourceType::TypeScript).build();
213    /// let sources = module.import_sources();
214    /// // sources = ["react", "lodash", "./utils"]
215    /// ```
216    pub fn import_sources(&self) -> Vec<&str> {
217        self.imports.iter().map(|imp| imp.source.as_str()).collect()
218    }
219}
220
221/// Builder for `Module` to avoid long argument lists in constructors.
222pub struct ModuleBuilder {
223    module: Module,
224}
225
226impl ModuleBuilder {
227    pub fn imports(mut self, imports: Vec<Import>) -> Self {
228        self.module.imports = Arc::new(imports);
229        self
230    }
231
232    pub fn exports(mut self, exports: Vec<Export>) -> Self {
233        self.module.exports = Arc::new(exports);
234        self
235    }
236
237    pub fn side_effects(mut self, has_side_effects: bool) -> Self {
238        self.module.has_side_effects = has_side_effects;
239        self
240    }
241
242    pub fn entry(mut self, is_entry: bool) -> Self {
243        self.module.is_entry = is_entry;
244        self
245    }
246
247    pub fn external(mut self, is_external: bool) -> Self {
248        self.module.is_external = is_external;
249        self
250    }
251
252    pub fn original_size(mut self, original_size: usize) -> Self {
253        self.module.original_size = original_size;
254        self
255    }
256
257    pub fn bundled_size(mut self, bundled_size: Option<usize>) -> Self {
258        self.module.bundled_size = bundled_size;
259        self
260    }
261
262    pub fn symbol_table(mut self, symbol_table: SymbolTable) -> Self {
263        self.module.symbol_table = Arc::new(symbol_table);
264        self
265    }
266
267    pub fn module_format(mut self, module_format: ModuleFormat) -> Self {
268        self.module.module_format = module_format;
269        self
270    }
271
272    pub fn exports_kind(mut self, exports_kind: ExportsKind) -> Self {
273        self.module.exports_kind = exports_kind;
274        self
275    }
276
277    pub fn has_star_exports(mut self, has_star_exports: bool) -> Self {
278        self.module.has_star_exports = has_star_exports;
279        self
280    }
281
282    pub fn execution_order(mut self, execution_order: Option<u32>) -> Self {
283        self.module.execution_order = execution_order;
284        self
285    }
286
287    pub fn build(self) -> Module {
288        self.module
289    }
290}
291
292/// Module definition format (ESM vs CJS).
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294pub enum ModuleFormat {
295    /// ECMAScript Module (.mjs or "type": "module")
296    EsmMjs,
297    /// ECMAScript Module (package.json "type": "module")
298    EsmPackageJson,
299    /// ECMAScript Module (regular .js with ESM syntax)
300    Esm,
301    /// CommonJS (package.json "type": "commonjs")
302    CjsPackageJson,
303    /// CommonJS (regular require/module.exports)
304    Cjs,
305    /// Unknown format
306    Unknown,
307}
308
309/// Export structure kind.
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
311pub enum ExportsKind {
312    /// Module uses ESM exports
313    Esm,
314    /// Module uses CommonJS exports
315    CommonJs,
316    /// No exports detected
317    None,
318}
319
320/// Resolved module source type derived from file extensions.
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
322pub enum SourceType {
323    JavaScript,
324    TypeScript,
325    Jsx,
326    Tsx,
327    Json,
328    Css,
329    Unknown,
330}
331
332impl SourceType {
333    /// Derive the source type from a file extension string.
334    pub fn from_extension(ext: &str) -> Self {
335        match ext {
336            "js" | "mjs" | "cjs" => Self::JavaScript,
337            "ts" | "mts" | "cts" => Self::TypeScript,
338            "jsx" => Self::Jsx,
339            "tsx" => Self::Tsx,
340            "json" => Self::Json,
341            "css" => Self::Css,
342            _ => Self::Unknown,
343        }
344    }
345
346    /// Attempt to infer the source type from a file path.
347    pub fn from_path(path: &Path) -> Self {
348        path.extension()
349            .and_then(|ext| ext.to_str())
350            .map_or(Self::Unknown, Self::from_extension)
351    }
352
353    /// Returns true if the file is JavaScript/TypeScript based.
354    pub fn is_javascript_like(&self) -> bool {
355        matches!(
356            self,
357            Self::JavaScript | Self::TypeScript | Self::Jsx | Self::Tsx
358        )
359    }
360}