fob_graph/
from_collection.rs

1//! Convert intermediate collection types to final graph representation.
2//!
3//! This module handles the conversion from `CollectionState` (populated during
4//! analysis or bundling) to a fully-formed `ModuleGraph`.
5//!
6//! # Design Decisions
7//!
8//! - **Local bindings dropped**: Import specifiers only retain imported names,
9//!   not local aliases. This is sufficient for dependency analysis but may
10//!   need enhancement for advanced symbol tracking.
11//! - **External vs local**: Resolved imports become edges; unresolved become
12//!   external dependencies in `graph.external_dependencies()`.
13//! - **Error handling**: Uses `CollectionGraphError` to provide context on
14//!   conversion failures (e.g., invalid module IDs, graph initialization).
15
16use std::collections::HashMap;
17use std::path::PathBuf;
18
19use thiserror::Error;
20
21use super::collection::{
22    CollectedExport, CollectedImportKind, CollectedImportSpecifier, CollectedModule,
23};
24use super::module::ExportsKind as FobExportsKind;
25use super::{
26    Export, ExportKind, Import, ImportKind, ImportSpecifier as FobImportSpecifier, ModuleId,
27    ModuleIdError, SourceSpan,
28};
29
30/// Errors that can occur during collection-to-graph conversion
31#[derive(Debug, Error)]
32pub enum CollectionGraphError {
33    /// Failed to initialize the module graph
34    #[error("failed to initialize module graph: {0}")]
35    GraphInitialization(String),
36
37    /// Module ID conversion failed
38    #[error("module id conversion failed for '{path}': {source}")]
39    ModuleIdConversion {
40        path: String,
41        #[source]
42        source: ModuleIdError,
43    },
44}
45
46pub struct PendingImport {
47    pub import: Import,
48    pub target: Option<String>,
49}
50
51// PendingModule is no longer used - conversion happens directly in memory.rs and core.rs
52
53// Helper functions for converting CollectionState to ModuleGraph
54// These are used by the in-memory ModuleGraph implementation
55
56pub fn convert_collected_module_id(path: &str) -> Result<ModuleId, CollectionGraphError> {
57    let path_buf = PathBuf::from(path);
58    ModuleId::new(&path_buf).map_err(|source| CollectionGraphError::ModuleIdConversion {
59        path: path.to_string(),
60        source,
61    })
62}
63
64pub fn convert_collected_exports(collected: &CollectedModule, module_id: &ModuleId) -> Vec<Export> {
65    let mut exports = Vec::new();
66
67    for export in &collected.exports {
68        match export {
69            CollectedExport::Named { exported, local: _ } => {
70                let kind = if exported == "default" {
71                    ExportKind::Default
72                } else {
73                    ExportKind::Named
74                };
75                exports.push(Export::new(
76                    exported.clone(),
77                    kind,
78                    false, // is_used
79                    false, // is_type_only
80                    None,  // re_exported_from
81                    false, // is_framework_used
82                    false, // came_from_commonjs
83                    SourceSpan::new(module_id.as_path(), 0, 0),
84                ));
85            }
86            CollectedExport::Default => {
87                exports.push(Export::new(
88                    "default".to_string(),
89                    ExportKind::Default,
90                    false,
91                    false,
92                    None,
93                    false,
94                    false,
95                    SourceSpan::new(module_id.as_path(), 0, 0),
96                ));
97            }
98            CollectedExport::All { source } => {
99                exports.push(Export::new(
100                    "*".to_string(),
101                    ExportKind::StarReExport,
102                    false,
103                    false,
104                    Some(source.clone()),
105                    false,
106                    false,
107                    SourceSpan::new(module_id.as_path(), 0, 0),
108                ));
109            }
110        }
111    }
112
113    exports
114}
115
116pub fn convert_collected_imports(
117    collected: &CollectedModule,
118    module_id: &ModuleId,
119    _path_to_id: &HashMap<String, ModuleId>,
120) -> Vec<PendingImport> {
121    let mut imports = Vec::new();
122
123    for import in &collected.imports {
124        let specifiers = import
125            .specifiers
126            .iter()
127            .map(convert_import_specifier)
128            .collect();
129
130        let kind = match import.kind {
131            CollectedImportKind::Dynamic => ImportKind::Dynamic,
132            CollectedImportKind::Static => ImportKind::Static,
133            CollectedImportKind::TypeOnly => ImportKind::TypeOnly,
134        };
135
136        let fob_import = Import::new(
137            import.source.clone(),
138            specifiers,
139            kind,
140            None, // resolved_to will be set later
141            SourceSpan::new(module_id.as_path(), 0, 0),
142        );
143
144        // Use resolved_path if available (for local modules), otherwise use source (for externals)
145        let target = if let Some(ref resolved_path) = import.resolved_path {
146            // Local module - use resolved path to look up ModuleId
147            Some(resolved_path.clone())
148        } else {
149            // External or unresolved - use source specifier
150            Some(import.source.clone())
151        };
152
153        imports.push(PendingImport {
154            import: fob_import,
155            target,
156        });
157    }
158
159    imports
160}
161
162pub fn convert_import_specifier(spec: &CollectedImportSpecifier) -> FobImportSpecifier {
163    match spec {
164        CollectedImportSpecifier::Named { imported, local: _ } => {
165            FobImportSpecifier::Named(imported.clone())
166        }
167        CollectedImportSpecifier::Default { local: _ } => FobImportSpecifier::Default,
168        CollectedImportSpecifier::Namespace { local: _ } => {
169            FobImportSpecifier::Namespace("*".to_string())
170        }
171    }
172}
173
174pub fn infer_exports_kind(exports: &[CollectedExport]) -> FobExportsKind {
175    // If we have any exports, assume ESM; otherwise None
176    if exports.is_empty() {
177        FobExportsKind::None
178    } else {
179        FobExportsKind::Esm
180    }
181}
182
183pub fn has_star_export(exports: &[CollectedExport]) -> bool {
184    exports
185        .iter()
186        .any(|e| matches!(e, CollectedExport::All { .. }))
187}