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