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    /// True when this module declares at least one Angular `@Component({
33    /// templateUrl: ... })` (the visitor emits a SideEffect import for each
34    /// such templateUrl, and this flag is set in the same branch). Used by
35    /// the CRAP-inherit walker (`crates/cli/src/health/scoring.rs::build_template_inherit_contexts`)
36    /// to gate the discriminator `coverage_source == "estimated_component_inherited"`:
37    /// a `.ts` file that imports an `.html` via plain `import './x.html'` is
38    /// NOT a component owner and must not trigger the inherit path. Without
39    /// this gate, the contract documented on `ComplexityViolation.inherited_from`
40    /// (Angular component `.ts` reached via the inverse `templateUrl` edge)
41    /// is silently violated for any non-Angular file importing an `.html`.
42    pub has_angular_component_template_url: bool,
43    /// xxh3 hash of the file content for incremental caching.
44    pub content_hash: u64,
45    /// Inline suppression directives parsed from comments.
46    pub suppressions: Vec<Suppression>,
47    /// Local names of import bindings that are never referenced in this file.
48    /// Populated via `oxc_semantic` scope analysis. Used at graph-build time
49    /// to skip adding references for imports whose binding is never read,
50    /// improving unused-export detection precision.
51    pub unused_import_bindings: Vec<String>,
52    /// Local import bindings that are referenced from TypeScript type positions.
53    /// Used to distinguish value-namespace and type-namespace references when a
54    /// module exports both `const X` and `type X`.
55    pub type_referenced_import_bindings: Vec<String>,
56    /// Local import bindings that are referenced from runtime/value positions.
57    /// Used alongside `type_referenced_import_bindings` for TS namespace-split
58    /// exports that share the same name.
59    pub value_referenced_import_bindings: Vec<String>,
60    /// Pre-computed byte offsets where each line starts, for O(log N) byte-to-line/col conversion.
61    /// Entry `i` is the byte offset of the start of line `i` (0-indexed).
62    /// Example: for "abc\ndef\n", `line_offsets` = \[0, 4\].
63    pub line_offsets: Vec<u32>,
64    /// Per-function complexity metrics computed during AST traversal.
65    /// Used by the `fallow health` subcommand to report high-complexity functions.
66    pub complexity: Vec<FunctionComplexity>,
67    /// Feature flag use sites detected during AST traversal.
68    /// Used by the `fallow flags` subcommand to report feature flag patterns.
69    pub flag_uses: Vec<FlagUse>,
70    /// Heritage metadata for exported classes that declare `implements`.
71    /// Used to scope `usedClassMembers` rules during analysis.
72    pub class_heritage: Vec<ClassHeritageInfo>,
73    /// Local type-capable declarations in this module.
74    /// Used to detect exported signatures that expose a same-file private type.
75    pub local_type_declarations: Vec<LocalTypeDeclaration>,
76    /// Type references that appear in exported symbols' public signatures.
77    /// The analysis layer checks these against `local_type_declarations`.
78    pub public_signature_type_references: Vec<PublicSignatureTypeReference>,
79    /// Aliases of namespace imports re-exported through an object literal
80    /// (`import * as foo from './bar'; export const API = { foo }`).
81    ///
82    /// Each entry says: "downstream consumer accessing `<my-export>.<suffix>.<X>`
83    /// is really accessing `<X>` on the namespace whose local name is
84    /// `namespace_local`". The graph layer uses these to propagate references
85    /// from cross-package consumers to the namespace's source module so that
86    /// `<X>` is not falsely reported as `unused-export`. See issue #303.
87    pub namespace_object_aliases: Vec<NamespaceObjectAlias>,
88}
89
90/// One alias entry tying an exported object's dotted property path to a
91/// namespace import on the same module.
92///
93/// Produced when the visitor sees `export const API = { foo }` (or any deeper
94/// nesting) and detects that the property's source identifier is a namespace
95/// import (`import * as foo from './bar'`). The graph layer reads these to
96/// resolve cross-package consumer accesses like `API.foo.bar` so that `bar`
97/// is credited as referenced on `./bar.ts`.
98#[derive(Debug, Clone)]
99pub struct NamespaceObjectAlias {
100    /// Canonical export name on this module (the `API` in `export const API = { foo }`).
101    pub via_export_name: String,
102    /// Dotted suffix of the property path relative to the export
103    /// (e.g. `"foo"` for `API.foo`, `"motionNet.adEngine"` for `API.motionNet.adEngine`).
104    pub suffix: String,
105    /// Local name of the namespace import on this module
106    /// (the `foo` in `import * as foo from './bar'`).
107    pub namespace_local: String,
108}
109
110/// Compute a table of line-start byte offsets from source text.
111///
112/// The returned vec contains one entry per line: `line_offsets[i]` is the byte
113/// offset where line `i` starts (0-indexed). The first entry is always `0`.
114///
115/// # Examples
116///
117/// ```
118/// use fallow_types::extract::compute_line_offsets;
119///
120/// let offsets = compute_line_offsets("abc\ndef\nghi");
121/// assert_eq!(offsets, vec![0, 4, 8]);
122/// ```
123#[must_use]
124#[expect(
125    clippy::cast_possible_truncation,
126    reason = "source files are practically < 4GB"
127)]
128pub fn compute_line_offsets(source: &str) -> Vec<u32> {
129    let mut offsets = vec![0u32];
130    for (i, byte) in source.bytes().enumerate() {
131        if byte == b'\n' {
132            debug_assert!(
133                u32::try_from(i + 1).is_ok(),
134                "source file exceeds u32::MAX bytes — line offsets would overflow"
135            );
136            offsets.push((i + 1) as u32);
137        }
138    }
139    offsets
140}
141
142/// Convert a byte offset to a 1-based line number and 0-based byte column
143/// using a pre-computed line offset table (from [`compute_line_offsets`]).
144///
145/// Uses binary search for O(log L) lookup where L is the number of lines.
146///
147/// # Examples
148///
149/// ```
150/// use fallow_types::extract::{compute_line_offsets, byte_offset_to_line_col};
151///
152/// let offsets = compute_line_offsets("abc\ndef\nghi");
153/// assert_eq!(byte_offset_to_line_col(&offsets, 0), (1, 0)); // 'a' on line 1
154/// assert_eq!(byte_offset_to_line_col(&offsets, 4), (2, 0)); // 'd' on line 2
155/// assert_eq!(byte_offset_to_line_col(&offsets, 9), (3, 1)); // 'h' on line 3
156/// ```
157#[must_use]
158#[expect(
159    clippy::cast_possible_truncation,
160    reason = "line count is bounded by source size"
161)]
162pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
163    // Binary search: find the last line whose start is <= byte_offset
164    let line_idx = match line_offsets.binary_search(&byte_offset) {
165        Ok(idx) => idx,
166        Err(idx) => idx.saturating_sub(1),
167    };
168    let line = line_idx as u32 + 1; // 1-based
169    let col = byte_offset - line_offsets[line_idx];
170    (line, col)
171}
172
173/// Complexity metrics for a single function/method/arrow.
174#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
175pub struct FunctionComplexity {
176    /// Function name (or `"<anonymous>"` for unnamed functions/arrows).
177    pub name: String,
178    /// 1-based line number where the function starts.
179    pub line: u32,
180    /// 0-based byte column where the function starts.
181    pub col: u32,
182    /// `McCabe` cyclomatic complexity (1 + decision points).
183    pub cyclomatic: u16,
184    /// `SonarSource` cognitive complexity (structural + nesting penalty).
185    pub cognitive: u16,
186    /// Number of lines in the function body.
187    pub line_count: u32,
188    /// Number of parameters (excluding TypeScript's `this` parameter).
189    pub param_count: u8,
190}
191
192/// The kind of feature flag pattern detected.
193#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
194pub enum FlagUseKind {
195    /// `process.env.FEATURE_X` pattern.
196    EnvVar,
197    /// SDK function call like `useFlag('name')`.
198    SdkCall,
199    /// Config object access like `config.features.x`.
200    ConfigObject,
201}
202
203/// A feature flag use site detected during AST traversal.
204#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
205pub struct FlagUse {
206    /// Name/identifier of the flag (e.g., `ENABLE_NEW_CHECKOUT`, `new-checkout`).
207    pub flag_name: String,
208    /// How the flag was detected.
209    pub kind: FlagUseKind,
210    /// 1-based line number.
211    pub line: u32,
212    /// 0-based byte column offset.
213    pub col: u32,
214    /// Start byte offset of the guarded code block (if-branch span), if detected.
215    pub guard_span_start: Option<u32>,
216    /// End byte offset of the guarded code block (if-branch span), if detected.
217    pub guard_span_end: Option<u32>,
218    /// SDK/provider name if detected from SDK call pattern (e.g., "LaunchDarkly").
219    pub sdk_name: Option<String>,
220}
221
222// Size assertion: FlagUse is stored in a Vec per file in the cache.
223const _: () = assert!(std::mem::size_of::<FlagUse>() <= 96);
224
225/// A dynamic import with a pattern that can be partially resolved (e.g., template literals).
226#[derive(Debug, Clone)]
227pub struct DynamicImportPattern {
228    /// Static prefix of the import path (e.g., "./locales/"). May contain glob characters.
229    pub prefix: String,
230    /// Static suffix of the import path (e.g., ".json"), if any.
231    pub suffix: Option<String>,
232    /// Source span in the original file.
233    pub span: Span,
234}
235
236/// Visibility tag from JSDoc/TSDoc comments.
237///
238/// Controls whether an export is reported as unused. All non-`None` variants
239/// suppress unused-export detection, but preserve the semantic distinction
240/// for API surface reporting and filtering.
241#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
242#[serde(rename_all = "lowercase")]
243#[repr(u8)]
244pub enum VisibilityTag {
245    /// No visibility tag present.
246    #[default]
247    None = 0,
248    /// `@public` or `@api public` -- part of the public API surface.
249    Public = 1,
250    /// `@internal` -- exported for internal use (sister packages, build tools).
251    Internal = 2,
252    /// `@beta` -- public but unstable, may change without notice.
253    Beta = 3,
254    /// `@alpha` -- early preview, may change drastically without notice.
255    Alpha = 4,
256    /// `@expected-unused` -- intentionally unused, should warn when it becomes used.
257    ExpectedUnused = 5,
258}
259
260impl VisibilityTag {
261    /// Whether this tag permanently suppresses unused-export detection.
262    /// `ExpectedUnused` is handled separately (conditionally suppresses,
263    /// reports stale when the export becomes used).
264    pub const fn suppresses_unused(self) -> bool {
265        matches!(
266            self,
267            Self::Public | Self::Internal | Self::Beta | Self::Alpha
268        )
269    }
270
271    /// For serde `skip_serializing_if`.
272    pub fn is_none(&self) -> bool {
273        matches!(self, Self::None)
274    }
275}
276
277/// An export declaration.
278#[derive(Debug, Clone, serde::Serialize)]
279pub struct ExportInfo {
280    /// The exported name (named or default).
281    pub name: ExportName,
282    /// The local binding name, if different from the exported name.
283    pub local_name: Option<String>,
284    /// Whether this is a type-only export (`export type`).
285    pub is_type_only: bool,
286    /// Whether this export is registered through a runtime side effect at module
287    /// load time (e.g. a Lit `@customElement('tag')` class decorator or a
288    /// `customElements.define('tag', ClassRef)` call). Such classes are
289    /// referenced by their registered tag string, not by their identifier, so
290    /// no other file imports them by name. The unused-export detector treats
291    /// this flag as an effective reference.
292    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
293    pub is_side_effect_used: bool,
294    /// Visibility tag from JSDoc/TSDoc comment (`@public`, `@internal`, `@alpha`, `@beta`).
295    /// Exports with any visibility tag are never reported as unused.
296    #[serde(default, skip_serializing_if = "VisibilityTag::is_none")]
297    pub visibility: VisibilityTag,
298    /// Source span of the export declaration.
299    #[serde(serialize_with = "serialize_span")]
300    pub span: Span,
301    /// Members of this export (for enums, classes, and namespaces).
302    #[serde(default, skip_serializing_if = "Vec::is_empty")]
303    pub members: Vec<MemberInfo>,
304    /// The local name of the parent class from `extends` clause, if any.
305    /// Used to build inheritance maps for unused class member detection.
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub super_class: Option<String>,
308}
309
310/// Additional heritage metadata for an exported class.
311#[derive(
312    Debug,
313    Clone,
314    serde::Serialize,
315    serde::Deserialize,
316    bitcode::Encode,
317    bitcode::Decode,
318    PartialEq,
319    Eq,
320)]
321pub struct ClassHeritageInfo {
322    /// Export name (`default` for default-exported classes).
323    pub export_name: String,
324    /// Parent class name from the `extends` clause, if any.
325    pub super_class: Option<String>,
326    /// Interface names from the class `implements` clause.
327    pub implements: Vec<String>,
328    /// Typed instance bindings on this class: pairs of `(local_name, type_name)`
329    /// from typed constructor parameters with accessibility modifiers
330    /// (`constructor(public svc: Svc)`), non-private typed property
331    /// declarations (`svc: Svc`), and non-private typed getters
332    /// (`get svc(): Svc`).
333    ///
334    /// Used by the analysis layer to resolve typed member-access chains
335    /// (`factory.service.getTotal()`) and Angular template member-access chains
336    /// on external templates (`templateUrl`), where the HTML file is parsed
337    /// independently and cannot see the component's constructor types.
338    /// For `constructor(public dataService: DataService)` in a component that
339    /// uses an external template with `{{ dataService.getTotal() }}`, this
340    /// field carries `("dataService", "DataService")` so the bridge can credit
341    /// `DataService.getTotal` as used.
342    #[serde(default, skip_serializing_if = "Vec::is_empty")]
343    pub instance_bindings: Vec<(String, String)>,
344}
345
346/// A module-scope declaration that can be used as a TypeScript type.
347#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
348pub struct LocalTypeDeclaration {
349    /// Local declaration name.
350    pub name: String,
351    /// Declaration identifier span.
352    #[serde(serialize_with = "serialize_span")]
353    pub span: Span,
354}
355
356/// A reference from an exported symbol's public signature to a type name.
357#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
358pub struct PublicSignatureTypeReference {
359    /// Exported symbol whose signature contains the reference.
360    pub export_name: String,
361    /// Referenced type name. Qualified names are reduced to their root identifier.
362    pub type_name: String,
363    /// Reference span.
364    #[serde(serialize_with = "serialize_span")]
365    pub span: Span,
366}
367
368/// A member of an enum, class, or namespace.
369#[derive(Debug, Clone, serde::Serialize)]
370pub struct MemberInfo {
371    /// Member name.
372    pub name: String,
373    /// The kind of member (enum, class method/property, or namespace member).
374    pub kind: MemberKind,
375    /// Source span of the member declaration.
376    #[serde(serialize_with = "serialize_span")]
377    pub span: Span,
378    /// Whether this member has decorators (e.g., `@Column()`, `@Inject()`).
379    /// Decorated members are used by frameworks at runtime and should not be
380    /// flagged as unused class members.
381    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
382    pub has_decorator: bool,
383    /// True when this is a static class method that returns a fresh instance
384    /// of the same class: either via `return new this()` / `return new
385    /// <SameClassName>()` in the body's last statement, or via a declared
386    /// return type matching the class name. Consumers calling such a static
387    /// method receive an instance, so the call result's member accesses are
388    /// credited against the class. See issues #346, #387.
389    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
390    pub is_instance_returning_static: bool,
391    /// True when this is an instance class method whose call result is an
392    /// instance of the same class. Qualifies when the declared return type
393    /// matches the class name (`setX(): EventBuilder { ... }`) or when the
394    /// body's last statement is `return this`. The analyze layer walks fluent
395    /// chains (`Class.factory().setX().setY()`) only through methods carrying
396    /// this flag, so the chain stops at a non-self-returning method like
397    /// `.build()`. See issue #387.
398    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
399    pub is_self_returning: bool,
400}
401
402/// The kind of member.
403///
404/// # Examples
405///
406/// ```
407/// use fallow_types::extract::MemberKind;
408///
409/// let kind = MemberKind::EnumMember;
410/// assert_eq!(kind, MemberKind::EnumMember);
411/// assert_ne!(kind, MemberKind::ClassMethod);
412/// assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
413/// ```
414#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
415#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
416#[serde(rename_all = "snake_case")]
417pub enum MemberKind {
418    /// A TypeScript enum member.
419    EnumMember,
420    /// A class method.
421    ClassMethod,
422    /// A class property.
423    ClassProperty,
424    /// A member exported from a TypeScript namespace.
425    NamespaceMember,
426}
427
428/// A static member access expression (e.g., `Status.Active`, `MyClass.create()`).
429///
430/// # Examples
431///
432/// ```
433/// use fallow_types::extract::MemberAccess;
434///
435/// let access = MemberAccess {
436///     object: "Status".to_string(),
437///     member: "Active".to_string(),
438/// };
439/// assert_eq!(access.object, "Status");
440/// assert_eq!(access.member, "Active");
441/// ```
442#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
443pub struct MemberAccess {
444    /// The identifier being accessed (the import name).
445    pub object: String,
446    /// The member being accessed.
447    pub member: String,
448}
449
450#[expect(
451    clippy::trivially_copy_pass_by_ref,
452    reason = "serde serialize_with requires &T"
453)]
454fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
455    use serde::ser::SerializeMap;
456    let mut map = serializer.serialize_map(Some(2))?;
457    map.serialize_entry("start", &span.start)?;
458    map.serialize_entry("end", &span.end)?;
459    map.end()
460}
461
462/// Export identifier.
463///
464/// # Examples
465///
466/// ```
467/// use fallow_types::extract::ExportName;
468///
469/// let named = ExportName::Named("foo".to_string());
470/// assert_eq!(named.to_string(), "foo");
471/// assert!(named.matches_str("foo"));
472///
473/// let default = ExportName::Default;
474/// assert_eq!(default.to_string(), "default");
475/// assert!(default.matches_str("default"));
476/// ```
477#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
478pub enum ExportName {
479    /// A named export (e.g., `export const foo`).
480    Named(String),
481    /// The default export.
482    Default,
483}
484
485impl ExportName {
486    /// Compare against a string without allocating (avoids `to_string()`).
487    #[must_use]
488    pub fn matches_str(&self, s: &str) -> bool {
489        match self {
490            Self::Named(n) => n == s,
491            Self::Default => s == "default",
492        }
493    }
494}
495
496impl std::fmt::Display for ExportName {
497    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
498        match self {
499            Self::Named(n) => write!(f, "{n}"),
500            Self::Default => write!(f, "default"),
501        }
502    }
503}
504
505/// An import declaration.
506#[derive(Debug, Clone)]
507pub struct ImportInfo {
508    /// The import specifier (e.g., `./utils` or `react`).
509    pub source: String,
510    /// How the symbol is imported (named, default, namespace, or side-effect).
511    pub imported_name: ImportedName,
512    /// The local binding name in the importing module.
513    pub local_name: String,
514    /// Whether this is a type-only import (`import type`).
515    pub is_type_only: bool,
516    /// Whether this import originated from a CSS-context (an SFC `<style lang="scss">` block,
517    /// `<style src="...">` reference, or other style-section parser). The resolver uses this
518    /// to enable SCSS partial / include-path / node_modules fallbacks for SFC importers
519    /// without applying them to JS-context imports from the same file.
520    pub from_style: bool,
521    /// Source span of the import declaration.
522    pub span: Span,
523    /// Span of the source string literal (e.g., the `'./utils'` in `import { foo } from './utils'`).
524    /// Used by the LSP to highlight just the specifier in diagnostics.
525    pub source_span: Span,
526}
527
528/// How a symbol is imported.
529///
530/// # Examples
531///
532/// ```
533/// use fallow_types::extract::ImportedName;
534///
535/// let named = ImportedName::Named("useState".to_string());
536/// assert_eq!(named, ImportedName::Named("useState".to_string()));
537/// assert_ne!(named, ImportedName::Default);
538///
539/// // Side-effect imports have no binding
540/// assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
541/// ```
542#[derive(Debug, Clone, PartialEq, Eq)]
543pub enum ImportedName {
544    /// A named import (e.g., `import { foo }`).
545    Named(String),
546    /// A default import (e.g., `import React`).
547    Default,
548    /// A namespace import (e.g., `import * as utils`).
549    Namespace,
550    /// A side-effect import (e.g., `import './styles.css'`).
551    SideEffect,
552}
553
554// Size assertions to prevent memory regressions in hot-path types.
555// These types are stored in Vecs inside `ModuleInfo` (one per file) and are
556// iterated during graph construction and analysis. Keeping them compact
557// improves cache locality on large projects with thousands of files.
558#[cfg(target_pointer_width = "64")]
559const _: () = assert!(std::mem::size_of::<ExportInfo>() == 112);
560#[cfg(target_pointer_width = "64")]
561const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
562#[cfg(target_pointer_width = "64")]
563const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
564#[cfg(target_pointer_width = "64")]
565const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
566#[cfg(target_pointer_width = "64")]
567const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
568// `ModuleInfo` is the per-file extraction result, stored in a Vec during parallel parsing.
569#[cfg(target_pointer_width = "64")]
570const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 472);
571
572/// A re-export declaration.
573#[derive(Debug, Clone)]
574pub struct ReExportInfo {
575    /// The module being re-exported from.
576    pub source: String,
577    /// The name imported from the source module (or `*` for star re-exports).
578    pub imported_name: String,
579    /// The name exported from this module.
580    pub exported_name: String,
581    /// Whether this is a type-only re-export.
582    pub is_type_only: bool,
583    /// Source span of the re-export declaration on this module.
584    /// Used for line-number reporting when an unused re-export is detected.
585    /// Defaults to `Span::default()` (0, 0) for re-exports without a meaningful
586    /// source location (e.g., synthesized in the graph layer).
587    pub span: oxc_span::Span,
588}
589
590/// A dynamic `import()` call.
591#[derive(Debug, Clone)]
592pub struct DynamicImportInfo {
593    /// The import specifier.
594    pub source: String,
595    /// Source span of the `import()` expression.
596    pub span: Span,
597    /// Names destructured from the dynamic import result.
598    /// Non-empty means `const { a, b } = await import(...)` -> Named imports.
599    /// Empty means simple `import(...)` or `const x = await import(...)` -> Namespace.
600    pub destructured_names: Vec<String>,
601    /// The local variable name for `const x = await import(...)`.
602    /// Used for namespace import narrowing via member access tracking.
603    pub local_name: Option<String>,
604    /// True when this dynamic import was synthesised by fallow rather than
605    /// appearing in user source (e.g. the Vitest `__mocks__/<file>` auto-mock
606    /// sibling that pairs with a `vi.mock('./foo')` call). When the resolver
607    /// cannot find the synthesised target, the entry is dropped silently
608    /// instead of surfacing as an `unresolved-import` finding pointing at a
609    /// path the user never wrote.
610    pub is_speculative: bool,
611}
612
613/// A `require()` call.
614#[derive(Debug, Clone)]
615pub struct RequireCallInfo {
616    /// The require specifier.
617    pub source: String,
618    /// Source span of the `require()` call.
619    pub span: Span,
620    /// Names destructured from the `require()` result.
621    /// Non-empty means `const { a, b } = require(...)` -> Named imports.
622    /// Empty means simple `require(...)` or `const x = require(...)` -> Namespace.
623    pub destructured_names: Vec<String>,
624    /// The local variable name for `const x = require(...)`.
625    /// Used for namespace import narrowing via member access tracking.
626    pub local_name: Option<String>,
627}
628
629/// Result of parsing all files, including incremental cache statistics.
630pub struct ParseResult {
631    /// Extracted module information for all successfully parsed files.
632    pub modules: Vec<ModuleInfo>,
633    /// Number of files whose parse results were loaded from cache (unchanged).
634    pub cache_hits: usize,
635    /// Number of files that required a full parse (new or changed).
636    pub cache_misses: usize,
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642
643    // ── compute_line_offsets ──────────────────────────────────────────
644
645    #[test]
646    fn line_offsets_empty_string() {
647        assert_eq!(compute_line_offsets(""), vec![0]);
648    }
649
650    #[test]
651    fn line_offsets_single_line_no_newline() {
652        assert_eq!(compute_line_offsets("hello"), vec![0]);
653    }
654
655    #[test]
656    fn line_offsets_single_line_with_newline() {
657        // "hello\n" => line 0 starts at 0, line 1 starts at 6
658        assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
659    }
660
661    #[test]
662    fn line_offsets_multiple_lines() {
663        // "abc\ndef\nghi"
664        // line 0: offset 0 ("abc")
665        // line 1: offset 4 ("def")
666        // line 2: offset 8 ("ghi")
667        assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
668    }
669
670    #[test]
671    fn line_offsets_trailing_newline() {
672        // "abc\ndef\n"
673        // line 0: offset 0, line 1: offset 4, line 2: offset 8 (empty line after trailing \n)
674        assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
675    }
676
677    #[test]
678    fn line_offsets_consecutive_newlines() {
679        // "\n\n\n" = 3 newlines => 4 lines
680        assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
681    }
682
683    #[test]
684    fn line_offsets_multibyte_utf8() {
685        // "á\n" => 'á' is 2 bytes (0xC3 0xA1), '\n' at byte 2 => next line at byte 3
686        assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
687    }
688
689    // ── byte_offset_to_line_col ──────────────────────────────────────
690
691    #[test]
692    fn line_col_offset_zero() {
693        let offsets = compute_line_offsets("abc\ndef\nghi");
694        let (line, col) = byte_offset_to_line_col(&offsets, 0);
695        assert_eq!((line, col), (1, 0)); // line 1, col 0
696    }
697
698    #[test]
699    fn line_col_middle_of_first_line() {
700        let offsets = compute_line_offsets("abc\ndef\nghi");
701        let (line, col) = byte_offset_to_line_col(&offsets, 2);
702        assert_eq!((line, col), (1, 2)); // 'c' in "abc"
703    }
704
705    #[test]
706    fn line_col_start_of_second_line() {
707        let offsets = compute_line_offsets("abc\ndef\nghi");
708        // byte 4 = start of "def"
709        let (line, col) = byte_offset_to_line_col(&offsets, 4);
710        assert_eq!((line, col), (2, 0));
711    }
712
713    #[test]
714    fn line_col_middle_of_second_line() {
715        let offsets = compute_line_offsets("abc\ndef\nghi");
716        // byte 5 = 'e' in "def"
717        let (line, col) = byte_offset_to_line_col(&offsets, 5);
718        assert_eq!((line, col), (2, 1));
719    }
720
721    #[test]
722    fn line_col_start_of_third_line() {
723        let offsets = compute_line_offsets("abc\ndef\nghi");
724        // byte 8 = start of "ghi"
725        let (line, col) = byte_offset_to_line_col(&offsets, 8);
726        assert_eq!((line, col), (3, 0));
727    }
728
729    #[test]
730    fn line_col_end_of_file() {
731        let offsets = compute_line_offsets("abc\ndef\nghi");
732        // byte 10 = 'i' (last char)
733        let (line, col) = byte_offset_to_line_col(&offsets, 10);
734        assert_eq!((line, col), (3, 2));
735    }
736
737    #[test]
738    fn line_col_single_line() {
739        let offsets = compute_line_offsets("hello");
740        let (line, col) = byte_offset_to_line_col(&offsets, 3);
741        assert_eq!((line, col), (1, 3));
742    }
743
744    #[test]
745    fn line_col_at_newline_byte() {
746        let offsets = compute_line_offsets("abc\ndef");
747        // byte 3 = the '\n' character itself, still part of line 1
748        let (line, col) = byte_offset_to_line_col(&offsets, 3);
749        assert_eq!((line, col), (1, 3));
750    }
751
752    // ── ExportName ───────────────────────────────────────────────────
753
754    #[test]
755    fn export_name_matches_str_named() {
756        let name = ExportName::Named("foo".to_string());
757        assert!(name.matches_str("foo"));
758        assert!(!name.matches_str("bar"));
759        assert!(!name.matches_str("default"));
760    }
761
762    #[test]
763    fn export_name_matches_str_default() {
764        let name = ExportName::Default;
765        assert!(name.matches_str("default"));
766        assert!(!name.matches_str("foo"));
767    }
768
769    #[test]
770    fn export_name_display_named() {
771        let name = ExportName::Named("myExport".to_string());
772        assert_eq!(name.to_string(), "myExport");
773    }
774
775    #[test]
776    fn export_name_display_default() {
777        let name = ExportName::Default;
778        assert_eq!(name.to_string(), "default");
779    }
780
781    // ── ExportName equality & hashing ────────────────────────────
782
783    #[test]
784    fn export_name_equality_named() {
785        let a = ExportName::Named("foo".to_string());
786        let b = ExportName::Named("foo".to_string());
787        let c = ExportName::Named("bar".to_string());
788        assert_eq!(a, b);
789        assert_ne!(a, c);
790    }
791
792    #[test]
793    fn export_name_equality_default() {
794        let a = ExportName::Default;
795        let b = ExportName::Default;
796        assert_eq!(a, b);
797    }
798
799    #[test]
800    fn export_name_named_not_equal_to_default() {
801        let named = ExportName::Named("default".to_string());
802        let default = ExportName::Default;
803        assert_ne!(named, default);
804    }
805
806    #[test]
807    fn export_name_hash_consistency() {
808        use std::collections::hash_map::DefaultHasher;
809        use std::hash::{Hash, Hasher};
810
811        let mut h1 = DefaultHasher::new();
812        let mut h2 = DefaultHasher::new();
813        ExportName::Named("foo".to_string()).hash(&mut h1);
814        ExportName::Named("foo".to_string()).hash(&mut h2);
815        assert_eq!(h1.finish(), h2.finish());
816    }
817
818    // ── ExportName::matches_str edge cases ───────────────────────
819
820    #[test]
821    fn export_name_matches_str_empty_string() {
822        let name = ExportName::Named(String::new());
823        assert!(name.matches_str(""));
824        assert!(!name.matches_str("foo"));
825    }
826
827    #[test]
828    fn export_name_default_does_not_match_empty() {
829        let name = ExportName::Default;
830        assert!(!name.matches_str(""));
831    }
832
833    // ── ImportedName equality ────────────────────────────────────
834
835    #[test]
836    fn imported_name_equality() {
837        assert_eq!(
838            ImportedName::Named("foo".to_string()),
839            ImportedName::Named("foo".to_string())
840        );
841        assert_ne!(
842            ImportedName::Named("foo".to_string()),
843            ImportedName::Named("bar".to_string())
844        );
845        assert_eq!(ImportedName::Default, ImportedName::Default);
846        assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
847        assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
848        assert_ne!(ImportedName::Default, ImportedName::Namespace);
849        assert_ne!(
850            ImportedName::Named("default".to_string()),
851            ImportedName::Default
852        );
853    }
854
855    // ── MemberKind equality ────────────────────────────────────
856
857    #[test]
858    fn member_kind_equality() {
859        assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
860        assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
861        assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
862        assert_eq!(MemberKind::NamespaceMember, MemberKind::NamespaceMember);
863        assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
864        assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
865        assert_ne!(MemberKind::NamespaceMember, MemberKind::EnumMember);
866    }
867
868    // ── MemberKind bitcode roundtrip ─────────────────────────────
869
870    #[test]
871    fn member_kind_bitcode_roundtrip() {
872        let kinds = [
873            MemberKind::EnumMember,
874            MemberKind::ClassMethod,
875            MemberKind::ClassProperty,
876            MemberKind::NamespaceMember,
877        ];
878        for kind in &kinds {
879            let bytes = bitcode::encode(kind);
880            let decoded: MemberKind = bitcode::decode(&bytes).unwrap();
881            assert_eq!(&decoded, kind);
882        }
883    }
884
885    // ── MemberAccess bitcode roundtrip ─────────────────────────
886
887    #[test]
888    fn member_access_bitcode_roundtrip() {
889        let access = MemberAccess {
890            object: "Status".to_string(),
891            member: "Active".to_string(),
892        };
893        let bytes = bitcode::encode(&access);
894        let decoded: MemberAccess = bitcode::decode(&bytes).unwrap();
895        assert_eq!(decoded.object, "Status");
896        assert_eq!(decoded.member, "Active");
897    }
898
899    // ── compute_line_offsets with Windows line endings ───────────
900
901    #[test]
902    fn line_offsets_crlf_only_counts_lf() {
903        // \r\n should produce offsets at the \n boundary
904        // "ab\r\ncd" => bytes: a(0) b(1) \r(2) \n(3) c(4) d(5)
905        // Line 0: offset 0, line 1: offset 4
906        let offsets = compute_line_offsets("ab\r\ncd");
907        assert_eq!(offsets, vec![0, 4]);
908    }
909
910    // ── byte_offset_to_line_col edge cases ──────────────────────
911
912    #[test]
913    fn line_col_empty_file_offset_zero() {
914        let offsets = compute_line_offsets("");
915        let (line, col) = byte_offset_to_line_col(&offsets, 0);
916        assert_eq!((line, col), (1, 0));
917    }
918
919    // ── FunctionComplexity bitcode roundtrip ──────────────────────
920
921    #[test]
922    fn function_complexity_bitcode_roundtrip() {
923        let fc = FunctionComplexity {
924            name: "processData".to_string(),
925            line: 42,
926            col: 4,
927            cyclomatic: 15,
928            cognitive: 25,
929            line_count: 80,
930            param_count: 3,
931        };
932        let bytes = bitcode::encode(&fc);
933        let decoded: FunctionComplexity = bitcode::decode(&bytes).unwrap();
934        assert_eq!(decoded.name, "processData");
935        assert_eq!(decoded.line, 42);
936        assert_eq!(decoded.col, 4);
937        assert_eq!(decoded.cyclomatic, 15);
938        assert_eq!(decoded.cognitive, 25);
939        assert_eq!(decoded.line_count, 80);
940    }
941}