Skip to main content

fallow_types/
extract.rs

1//! Module extraction types: exports, imports, re-exports, members, and parse results.
2
3use oxc_span::Span;
4
5use crate::discover::FileId;
6use crate::suppress::Suppression;
7
8/// Extracted module information from a single file.
9#[derive(Debug, Clone)]
10pub struct ModuleInfo {
11    /// Unique identifier for this file.
12    pub file_id: FileId,
13    /// All export declarations in this module.
14    pub exports: Vec<ExportInfo>,
15    /// All import declarations in this module.
16    pub imports: Vec<ImportInfo>,
17    /// All re-export declarations (e.g., `export { foo } from './bar'`).
18    pub re_exports: Vec<ReExportInfo>,
19    /// All dynamic `import()` calls with string literal sources.
20    pub dynamic_imports: Vec<DynamicImportInfo>,
21    /// Dynamic import patterns from template literals, string concat, or `import.meta.glob`.
22    pub dynamic_import_patterns: Vec<DynamicImportPattern>,
23    /// All `require()` calls.
24    pub require_calls: Vec<RequireCallInfo>,
25    /// Static member access expressions (e.g., `Status.Active`).
26    pub member_accesses: Vec<MemberAccess>,
27    /// Identifiers used in "all members consumed" patterns
28    /// (Object.values, Object.keys, Object.entries, Object.getOwnPropertyNames, for..in, spread, computed dynamic access).
29    pub whole_object_uses: Vec<String>,
30    /// Whether this module uses `CommonJS` exports (`module.exports` or `exports.*`).
31    pub has_cjs_exports: bool,
32    /// xxh3 hash of the file content for incremental caching.
33    pub content_hash: u64,
34    /// Inline suppression directives parsed from comments.
35    pub suppressions: Vec<Suppression>,
36    /// Local names of import bindings that are never referenced in this file.
37    /// Populated via `oxc_semantic` scope analysis. Used at graph-build time
38    /// to skip adding references for imports whose binding is never read,
39    /// improving unused-export detection precision.
40    pub unused_import_bindings: Vec<String>,
41    /// Pre-computed byte offsets where each line starts, for O(log N) byte-to-line/col conversion.
42    /// Entry `i` is the byte offset of the start of line `i` (0-indexed).
43    /// Example: for "abc\ndef\n", `line_offsets` = \[0, 4\].
44    pub line_offsets: Vec<u32>,
45}
46
47/// Compute a table of line-start byte offsets from source text.
48///
49/// The returned vec contains one entry per line: `line_offsets[i]` is the byte
50/// offset where line `i` starts (0-indexed). The first entry is always `0`.
51pub fn compute_line_offsets(source: &str) -> Vec<u32> {
52    let mut offsets = vec![0u32];
53    for (i, byte) in source.bytes().enumerate() {
54        if byte == b'\n' {
55            offsets.push((i + 1) as u32);
56        }
57    }
58    offsets
59}
60
61/// Convert a byte offset to a 1-based line number and 0-based byte column
62/// using a pre-computed line offset table (from [`compute_line_offsets`]).
63///
64/// Uses binary search for O(log L) lookup where L is the number of lines.
65pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
66    // Binary search: find the last line whose start is <= byte_offset
67    let line_idx = match line_offsets.binary_search(&byte_offset) {
68        Ok(idx) => idx,
69        Err(idx) => idx.saturating_sub(1),
70    };
71    let line = line_idx as u32 + 1; // 1-based
72    let col = byte_offset - line_offsets[line_idx];
73    (line, col)
74}
75
76/// A dynamic import with a pattern that can be partially resolved (e.g., template literals).
77#[derive(Debug, Clone)]
78pub struct DynamicImportPattern {
79    /// Static prefix of the import path (e.g., "./locales/"). May contain glob characters.
80    pub prefix: String,
81    /// Static suffix of the import path (e.g., ".json"), if any.
82    pub suffix: Option<String>,
83    /// Source span in the original file.
84    pub span: Span,
85}
86
87/// An export declaration.
88#[derive(Debug, Clone, serde::Serialize)]
89pub struct ExportInfo {
90    /// The exported name (named or default).
91    pub name: ExportName,
92    /// The local binding name, if different from the exported name.
93    pub local_name: Option<String>,
94    /// Whether this is a type-only export (`export type`).
95    pub is_type_only: bool,
96    /// Whether this export has a `@public` JSDoc/TSDoc tag.
97    /// Exports marked `@public` are never reported as unused — they are
98    /// assumed to be consumed by external consumers (library API surface).
99    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
100    pub is_public: bool,
101    /// Source span of the export declaration.
102    #[serde(serialize_with = "serialize_span")]
103    pub span: Span,
104    /// Members of this export (for enums and classes).
105    #[serde(default, skip_serializing_if = "Vec::is_empty")]
106    pub members: Vec<MemberInfo>,
107}
108
109/// A member of an enum or class.
110#[derive(Debug, Clone, serde::Serialize)]
111pub struct MemberInfo {
112    /// Member name.
113    pub name: String,
114    /// Whether this is an enum member, class method, or class property.
115    pub kind: MemberKind,
116    /// Source span of the member declaration.
117    #[serde(serialize_with = "serialize_span")]
118    pub span: Span,
119    /// Whether this member has decorators (e.g., `@Column()`, `@Inject()`).
120    /// Decorated members are used by frameworks at runtime and should not be
121    /// flagged as unused class members.
122    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
123    pub has_decorator: bool,
124}
125
126/// The kind of member.
127#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bincode::Encode, bincode::Decode)]
128#[serde(rename_all = "snake_case")]
129pub enum MemberKind {
130    /// A TypeScript enum member.
131    EnumMember,
132    /// A class method.
133    ClassMethod,
134    /// A class property.
135    ClassProperty,
136}
137
138/// A static member access expression (e.g., `Status.Active`, `MyClass.create()`).
139#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
140pub struct MemberAccess {
141    /// The identifier being accessed (the import name).
142    pub object: String,
143    /// The member being accessed.
144    pub member: String,
145}
146
147#[expect(clippy::trivially_copy_pass_by_ref)] // serde serialize_with requires &T
148fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
149    use serde::ser::SerializeMap;
150    let mut map = serializer.serialize_map(Some(2))?;
151    map.serialize_entry("start", &span.start)?;
152    map.serialize_entry("end", &span.end)?;
153    map.end()
154}
155
156/// Export identifier.
157#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
158pub enum ExportName {
159    /// A named export (e.g., `export const foo`).
160    Named(String),
161    /// The default export.
162    Default,
163}
164
165impl ExportName {
166    /// Compare against a string without allocating (avoids `to_string()`).
167    pub fn matches_str(&self, s: &str) -> bool {
168        match self {
169            Self::Named(n) => n == s,
170            Self::Default => s == "default",
171        }
172    }
173}
174
175impl std::fmt::Display for ExportName {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        match self {
178            Self::Named(n) => write!(f, "{n}"),
179            Self::Default => write!(f, "default"),
180        }
181    }
182}
183
184/// An import declaration.
185#[derive(Debug, Clone)]
186pub struct ImportInfo {
187    /// The import specifier (e.g., `./utils` or `react`).
188    pub source: String,
189    /// How the symbol is imported (named, default, namespace, or side-effect).
190    pub imported_name: ImportedName,
191    /// The local binding name in the importing module.
192    pub local_name: String,
193    /// Whether this is a type-only import (`import type`).
194    pub is_type_only: bool,
195    /// Source span of the import declaration.
196    pub span: Span,
197}
198
199/// How a symbol is imported.
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum ImportedName {
202    /// A named import (e.g., `import { foo }`).
203    Named(String),
204    /// A default import (e.g., `import React`).
205    Default,
206    /// A namespace import (e.g., `import * as utils`).
207    Namespace,
208    /// A side-effect import (e.g., `import './styles.css'`).
209    SideEffect,
210}
211
212// Size assertions to prevent memory regressions in hot-path types.
213// These types are stored in Vecs inside `ModuleInfo` (one per file) and are
214// iterated during graph construction and analysis. Keeping them compact
215// improves cache locality on large projects with thousands of files.
216#[cfg(target_pointer_width = "64")]
217const _: () = assert!(std::mem::size_of::<ExportInfo>() == 88);
218#[cfg(target_pointer_width = "64")]
219const _: () = assert!(std::mem::size_of::<ImportInfo>() == 88);
220#[cfg(target_pointer_width = "64")]
221const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
222#[cfg(target_pointer_width = "64")]
223const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
224#[cfg(target_pointer_width = "64")]
225const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
226// `ModuleInfo` is the per-file extraction result — stored in a Vec during parallel parsing.
227#[cfg(target_pointer_width = "64")]
228const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 280);
229
230/// A re-export declaration.
231#[derive(Debug, Clone)]
232pub struct ReExportInfo {
233    /// The module being re-exported from.
234    pub source: String,
235    /// The name imported from the source module (or `*` for star re-exports).
236    pub imported_name: String,
237    /// The name exported from this module.
238    pub exported_name: String,
239    /// Whether this is a type-only re-export.
240    pub is_type_only: bool,
241}
242
243/// A dynamic `import()` call.
244#[derive(Debug, Clone)]
245pub struct DynamicImportInfo {
246    /// The import specifier.
247    pub source: String,
248    /// Source span of the `import()` expression.
249    pub span: Span,
250    /// Names destructured from the dynamic import result.
251    /// Non-empty means `const { a, b } = await import(...)` -> Named imports.
252    /// Empty means simple `import(...)` or `const x = await import(...)` -> Namespace.
253    pub destructured_names: Vec<String>,
254    /// The local variable name for `const x = await import(...)`.
255    /// Used for namespace import narrowing via member access tracking.
256    pub local_name: Option<String>,
257}
258
259/// A `require()` call.
260#[derive(Debug, Clone)]
261pub struct RequireCallInfo {
262    /// The require specifier.
263    pub source: String,
264    /// Source span of the `require()` call.
265    pub span: Span,
266    /// Names destructured from the `require()` result.
267    /// Non-empty means `const { a, b } = require(...)` -> Named imports.
268    /// Empty means simple `require(...)` or `const x = require(...)` -> Namespace.
269    pub destructured_names: Vec<String>,
270    /// The local variable name for `const x = require(...)`.
271    /// Used for namespace import narrowing via member access tracking.
272    pub local_name: Option<String>,
273}
274
275/// Result of parsing all files, including incremental cache statistics.
276pub struct ParseResult {
277    /// Extracted module information for all successfully parsed files.
278    pub modules: Vec<ModuleInfo>,
279    /// Number of files whose parse results were loaded from cache (unchanged).
280    pub cache_hits: usize,
281    /// Number of files that required a full parse (new or changed).
282    pub cache_misses: usize,
283}