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