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 /// Pre-computed byte offsets where each line starts, for O(log N) byte-to-line/col conversion.
37 /// Entry `i` is the byte offset of the start of line `i` (0-indexed).
38 /// Example: for "abc\ndef\n", `line_offsets` = \[0, 4\].
39 pub line_offsets: Vec<u32>,
40}
41
42/// Compute a table of line-start byte offsets from source text.
43///
44/// The returned vec contains one entry per line: `line_offsets[i]` is the byte
45/// offset where line `i` starts (0-indexed). The first entry is always `0`.
46pub fn compute_line_offsets(source: &str) -> Vec<u32> {
47 let mut offsets = vec![0u32];
48 for (i, byte) in source.bytes().enumerate() {
49 if byte == b'\n' {
50 offsets.push((i + 1) as u32);
51 }
52 }
53 offsets
54}
55
56/// Convert a byte offset to a 1-based line number and 0-based byte column
57/// using a pre-computed line offset table (from [`compute_line_offsets`]).
58///
59/// Uses binary search for O(log L) lookup where L is the number of lines.
60pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
61 // Binary search: find the last line whose start is <= byte_offset
62 let line_idx = match line_offsets.binary_search(&byte_offset) {
63 Ok(idx) => idx,
64 Err(idx) => idx.saturating_sub(1),
65 };
66 let line = line_idx as u32 + 1; // 1-based
67 let col = byte_offset - line_offsets[line_idx];
68 (line, col)
69}
70
71/// A dynamic import with a pattern that can be partially resolved (e.g., template literals).
72#[derive(Debug, Clone)]
73pub struct DynamicImportPattern {
74 /// Static prefix of the import path (e.g., "./locales/"). May contain glob characters.
75 pub prefix: String,
76 /// Static suffix of the import path (e.g., ".json"), if any.
77 pub suffix: Option<String>,
78 /// Source span in the original file.
79 pub span: Span,
80}
81
82/// An export declaration.
83#[derive(Debug, Clone, serde::Serialize)]
84pub struct ExportInfo {
85 /// The exported name (named or default).
86 pub name: ExportName,
87 /// The local binding name, if different from the exported name.
88 pub local_name: Option<String>,
89 /// Whether this is a type-only export (`export type`).
90 pub is_type_only: bool,
91 /// Source span of the export declaration.
92 #[serde(serialize_with = "serialize_span")]
93 pub span: Span,
94 /// Members of this export (for enums and classes).
95 #[serde(default, skip_serializing_if = "Vec::is_empty")]
96 pub members: Vec<MemberInfo>,
97}
98
99/// A member of an enum or class.
100#[derive(Debug, Clone, serde::Serialize)]
101pub struct MemberInfo {
102 /// Member name.
103 pub name: String,
104 /// Whether this is an enum member, class method, or class property.
105 pub kind: MemberKind,
106 /// Source span of the member declaration.
107 #[serde(serialize_with = "serialize_span")]
108 pub span: Span,
109 /// Whether this member has decorators (e.g., `@Column()`, `@Inject()`).
110 /// Decorated members are used by frameworks at runtime and should not be
111 /// flagged as unused class members.
112 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
113 pub has_decorator: bool,
114}
115
116/// The kind of member.
117#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bincode::Encode, bincode::Decode)]
118#[serde(rename_all = "snake_case")]
119pub enum MemberKind {
120 /// A TypeScript enum member.
121 EnumMember,
122 /// A class method.
123 ClassMethod,
124 /// A class property.
125 ClassProperty,
126}
127
128/// A static member access expression (e.g., `Status.Active`, `MyClass.create()`).
129#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
130pub struct MemberAccess {
131 /// The identifier being accessed (the import name).
132 pub object: String,
133 /// The member being accessed.
134 pub member: String,
135}
136
137#[allow(clippy::trivially_copy_pass_by_ref)] // serde serialize_with requires &T
138fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
139 use serde::ser::SerializeMap;
140 let mut map = serializer.serialize_map(Some(2))?;
141 map.serialize_entry("start", &span.start)?;
142 map.serialize_entry("end", &span.end)?;
143 map.end()
144}
145
146/// Export identifier.
147#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
148pub enum ExportName {
149 /// A named export (e.g., `export const foo`).
150 Named(String),
151 /// The default export.
152 Default,
153}
154
155impl std::fmt::Display for ExportName {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self {
158 Self::Named(n) => write!(f, "{n}"),
159 Self::Default => write!(f, "default"),
160 }
161 }
162}
163
164/// An import declaration.
165#[derive(Debug, Clone)]
166pub struct ImportInfo {
167 /// The import specifier (e.g., `./utils` or `react`).
168 pub source: String,
169 /// How the symbol is imported (named, default, namespace, or side-effect).
170 pub imported_name: ImportedName,
171 /// The local binding name in the importing module.
172 pub local_name: String,
173 /// Whether this is a type-only import (`import type`).
174 pub is_type_only: bool,
175 /// Source span of the import declaration.
176 pub span: Span,
177}
178
179/// How a symbol is imported.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum ImportedName {
182 /// A named import (e.g., `import { foo }`).
183 Named(String),
184 /// A default import (e.g., `import React`).
185 Default,
186 /// A namespace import (e.g., `import * as utils`).
187 Namespace,
188 /// A side-effect import (e.g., `import './styles.css'`).
189 SideEffect,
190}
191
192// Size assertions to prevent memory regressions in hot-path types.
193// These types are stored in Vecs inside `ModuleInfo` (one per file) and are
194// iterated during graph construction and analysis. Keeping them compact
195// improves cache locality on large projects with thousands of files.
196#[cfg(target_pointer_width = "64")]
197const _: () = assert!(std::mem::size_of::<ExportInfo>() == 88);
198#[cfg(target_pointer_width = "64")]
199const _: () = assert!(std::mem::size_of::<ImportInfo>() == 88);
200#[cfg(target_pointer_width = "64")]
201const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
202#[cfg(target_pointer_width = "64")]
203const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
204#[cfg(target_pointer_width = "64")]
205const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
206
207/// A re-export declaration.
208#[derive(Debug, Clone)]
209pub struct ReExportInfo {
210 /// The module being re-exported from.
211 pub source: String,
212 /// The name imported from the source module (or `*` for star re-exports).
213 pub imported_name: String,
214 /// The name exported from this module.
215 pub exported_name: String,
216 /// Whether this is a type-only re-export.
217 pub is_type_only: bool,
218}
219
220/// A dynamic `import()` call.
221#[derive(Debug, Clone)]
222pub struct DynamicImportInfo {
223 /// The import specifier.
224 pub source: String,
225 /// Source span of the `import()` expression.
226 pub span: Span,
227 /// Names destructured from the dynamic import result.
228 /// Non-empty means `const { a, b } = await import(...)` -> Named imports.
229 /// Empty means simple `import(...)` or `const x = await import(...)` -> Namespace.
230 pub destructured_names: Vec<String>,
231 /// The local variable name for `const x = await import(...)`.
232 /// Used for namespace import narrowing via member access tracking.
233 pub local_name: Option<String>,
234}
235
236/// A `require()` call.
237#[derive(Debug, Clone)]
238pub struct RequireCallInfo {
239 /// The require specifier.
240 pub source: String,
241 /// Source span of the `require()` call.
242 pub span: Span,
243 /// Names destructured from the `require()` result.
244 /// Non-empty means `const { a, b } = require(...)` -> Named imports.
245 /// Empty means simple `require(...)` or `const x = require(...)` -> Namespace.
246 pub destructured_names: Vec<String>,
247 /// The local variable name for `const x = require(...)`.
248 /// Used for namespace import narrowing via member access tracking.
249 pub local_name: Option<String>,
250}
251
252/// Result of parsing all files, including incremental cache statistics.
253pub struct ParseResult {
254 /// Extracted module information for all successfully parsed files.
255 pub modules: Vec<ModuleInfo>,
256 /// Number of files whose parse results were loaded from cache (unchanged).
257 pub cache_hits: usize,
258 /// Number of files that required a full parse (new or changed).
259 pub cache_misses: usize,
260}