fob_graph/
import.rs

1use serde::{Deserialize, Serialize};
2
3use super::{ModuleId, SourceSpan};
4
5/// Individual import binding from a module.
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub enum ImportSpecifier {
8    /// `import { foo } from 'mod'`
9    Named(String),
10    /// `import foo from 'mod'`
11    Default,
12    /// `import * as foo from 'mod'`
13    Namespace(String),
14}
15
16/// Mechanism used to load the dependency.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum ImportKind {
19    /// Static `import` declaration.
20    Static,
21    /// Dynamic `import()` expression.
22    Dynamic,
23    /// CommonJS `require()` call.
24    Require,
25    /// TypeScript `import type` declaration removed at runtime.
26    TypeOnly,
27    /// `export { foo } from 'mod'` style re-export.
28    ReExport,
29}
30
31impl ImportKind {
32    /// Returns `true` for imports that execute at runtime.
33    pub fn is_runtime(&self) -> bool {
34        !matches!(self, Self::TypeOnly)
35    }
36
37    /// Returns `true` for static, eagerly-resolved imports.
38    pub fn is_static(&self) -> bool {
39        matches!(self, Self::Static | Self::Require | Self::ReExport)
40    }
41}
42
43/// Complete analysis of a dependency edge.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Import {
46    pub source: String,
47    pub specifiers: Vec<ImportSpecifier>,
48    pub kind: ImportKind,
49    pub resolved_to: Option<ModuleId>,
50    pub span: SourceSpan,
51}
52
53impl Import {
54    /// Convenience constructor for building imports in tests/fixtures.
55    pub fn new(
56        source: impl Into<String>,
57        specifiers: Vec<ImportSpecifier>,
58        kind: ImportKind,
59        resolved_to: Option<ModuleId>,
60        span: SourceSpan,
61    ) -> Self {
62        Self {
63            source: source.into(),
64            specifiers,
65            kind,
66            resolved_to,
67            span,
68        }
69    }
70
71    /// Returns `true` for side-effect-only imports (`import 'polyfill'`).
72    pub fn is_side_effect_only(&self) -> bool {
73        self.specifiers.is_empty() && self.kind.is_runtime()
74    }
75
76    /// Returns `true` when the import references a package on npm (no relative prefix).
77    pub fn is_external(&self) -> bool {
78        !self.source.is_empty()
79            && !self.source.starts_with('.')
80            && !self.source.starts_with('/')
81            && !self.source.starts_with('\\')
82            && !self.source.starts_with("virtual:")
83    }
84
85    /// Returns `true` if the import only contributes types.
86    pub fn is_type_only(&self) -> bool {
87        matches!(self.kind, ImportKind::TypeOnly)
88    }
89
90    /// Check if this is a namespace import (`import * as foo`).
91    pub fn is_namespace_import(&self) -> bool {
92        self.specifiers
93            .iter()
94            .any(|spec| matches!(spec, ImportSpecifier::Namespace(_)))
95    }
96
97    /// Get the namespace binding name if this is a namespace import.
98    ///
99    /// Returns `Some(name)` for `import * as name from 'mod'`, otherwise `None`.
100    pub fn namespace_name(&self) -> Option<&str> {
101        self.specifiers.iter().find_map(|spec| {
102            if let ImportSpecifier::Namespace(name) = spec {
103                Some(name.as_str())
104            } else {
105                None
106            }
107        })
108    }
109
110    /// Check if this import contributes to runtime execution.
111    ///
112    /// This returns `true` for:
113    /// - Side-effect imports (`import 'polyfill'`)
114    /// - Regular runtime imports
115    ///
116    /// Returns `false` for type-only imports.
117    pub fn has_runtime_effect(&self) -> bool {
118        self.kind.is_runtime()
119    }
120}