Skip to main content

fallow_types/
extract.rs

1//! Module extraction types.
2
3use oxc_span::Span;
4
5use crate::discover::FileId;
6use crate::suppress::{Suppression, UnknownSuppressionKind};
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.
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 whole-object access patterns.
28    pub whole_object_uses: Vec<String>,
29    /// Whether this module uses CommonJS exports.
30    pub has_cjs_exports: bool,
31    /// Whether this module declares an Angular component `templateUrl`.
32    pub has_angular_component_template_url: bool,
33    /// xxh3 hash of the file content for incremental caching.
34    pub content_hash: u64,
35    /// Inline suppression directives parsed from comments.
36    pub suppressions: Vec<Suppression>,
37    /// Suppression tokens that did not parse to any known `IssueKind`.
38    /// Surfaced as `StaleSuppression` findings via `find_stale` so users see
39    /// typos or obsolete kind names instead of having the entire marker
40    /// silently discarded. See issue #449.
41    pub unknown_suppression_kinds: Vec<UnknownSuppressionKind>,
42    /// Local names of import bindings that are never referenced in this file.
43    /// Populated via `oxc_semantic` scope analysis. Used at graph-build time
44    /// to skip adding references for imports whose binding is never read,
45    /// improving unused-export detection precision.
46    pub unused_import_bindings: Vec<String>,
47    /// Local import bindings that are referenced from TypeScript type positions.
48    /// Used to distinguish value-namespace and type-namespace references when a
49    /// module exports both `const X` and `type X`.
50    pub type_referenced_import_bindings: Vec<String>,
51    /// Local import bindings referenced from runtime/value positions.
52    pub value_referenced_import_bindings: Vec<String>,
53    /// Pre-computed byte offsets where each line starts.
54    pub line_offsets: Vec<u32>,
55    /// Per-function complexity metrics.
56    pub complexity: Vec<FunctionComplexity>,
57    /// Feature flag use sites.
58    pub flag_uses: Vec<FlagUse>,
59    /// Heritage metadata for exported classes that declare `implements`.
60    pub class_heritage: Vec<ClassHeritageInfo>,
61    /// Local type-capable declarations.
62    pub local_type_declarations: Vec<LocalTypeDeclaration>,
63    /// Type references in exported public signatures.
64    pub public_signature_type_references: Vec<PublicSignatureTypeReference>,
65    /// Aliases of namespace imports re-exported through an object literal.
66    pub namespace_object_aliases: Vec<NamespaceObjectAlias>,
67    /// Deduped Iconify collection prefixes found in static icon props.
68    pub iconify_prefixes: Vec<String>,
69    /// Bare identifiers that may be resolved by framework auto-imports.
70    pub auto_import_candidates: Vec<String>,
71    /// File-level string directives in source order (e.g. `"use client"`,
72    /// `"use server"`, `"use strict"`). Captured from `Program::directives`.
73    /// Consumed by the security `client-server-leak` detector to identify
74    /// React Server Component client boundaries.
75    pub directives: Vec<String>,
76    /// Captured non-literal security sink sites (category-blind). Consumed by
77    /// the catalogue-driven `tainted_sink` detector. Captured only by JS/TS
78    /// extraction; empty for CSS/MDX/etc. See `security_matchers.toml`.
79    pub security_sinks: Vec<SinkSite>,
80    /// Count of sink-shaped nodes whose callee could not be flattened to a
81    /// static path (dynamic dispatch, computed members, aliased bindings).
82    /// Surfaced in-band so an empty catalogue result with a non-zero count is
83    /// not a clean bill.
84    pub security_sinks_skipped: u32,
85    /// Local bindings whose initializer (or destructured object) is a flattened
86    /// member-access path. Used by the security `tainted_sink` detector to
87    /// back-trace a sink argument to a known untrusted source: the analyze layer
88    /// matches each binding's `source_path` against the data-driven source
89    /// catalogue (`security_matchers.toml` `[[source]]` rows) and treats the
90    /// matching `local` names as source-tainted. Intra-module and name-based
91    /// (no scope analysis); a conservative association, never a taint proof.
92    pub tainted_bindings: Vec<TaintedBinding>,
93    /// Sink arguments that were recognized as sanitizer calls at extraction
94    /// time. Used for direct sink calls such as
95    /// `el.innerHTML = DOMPurify.sanitize(input)`.
96    pub sanitized_sink_args: Vec<SanitizedSinkArg>,
97}
98
99/// Sanitizer output domain. Kept intentionally narrow so a sanitizer for one
100/// domain cannot suppress a different sink family.
101#[derive(
102    Debug,
103    Clone,
104    Copy,
105    PartialEq,
106    Eq,
107    serde::Serialize,
108    serde::Deserialize,
109    bitcode::Encode,
110    bitcode::Decode,
111)]
112pub enum SanitizerScope {
113    /// HTML markup sanitized by DOMPurify-compatible APIs.
114    Html,
115    /// URL or redirect target checked against a literal-backed allowlist.
116    Url,
117    /// Path value checked against a high-confidence containment guard.
118    Path,
119}
120
121/// A captured sink argument that is itself a recognized sanitizer call.
122#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
123pub struct SanitizedSinkArg {
124    /// Byte offset of the owning sink span start.
125    pub span_start: u32,
126    /// The positional argument index on the owning sink.
127    pub arg_index: u32,
128    /// The sanitizer output domain for this argument.
129    pub scope: SanitizerScope,
130}
131
132/// A local binding tied to the flattened member-access path it was initialized
133/// from. The analyze layer matches `source_path` against the data-driven source
134/// catalogue; when it matches, `local` is treated as carrying untrusted input.
135///
136/// Captured for two shapes: a direct assignment (`const id = req.query.id` ->
137/// `{ local: "id", source_path: "req.query" }`, the literal-key tail dropped so
138/// the path matches a catalogue prefix) and an object destructure
139/// (`const { id } = req.query` -> `{ local: "id", source_path: "req.query" }`).
140#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
141pub struct TaintedBinding {
142    /// The local binding name introduced by the declarator.
143    pub local: String,
144    /// The flattened object member-access path the binding was sourced from.
145    pub source_path: String,
146}
147
148/// The syntactic shape of a captured security sink site. Category-blind: the
149/// extractor records the shape and the dotted/bare callee path; the analyze
150/// layer matches it against the data-driven catalogue. See
151/// `crates/core/data/security_matchers.toml`.
152#[derive(
153    Debug,
154    Clone,
155    Copy,
156    PartialEq,
157    Eq,
158    serde::Serialize,
159    serde::Deserialize,
160    bitcode::Encode,
161    bitcode::Decode,
162)]
163pub enum SinkShape {
164    /// A call to a bare identifier (e.g. `eval(x)`).
165    Call,
166    /// A call to a dotted member path (e.g. `child_process.exec(x)`).
167    MemberCall,
168    /// An assignment to a member target (e.g. `el.innerHTML = x`).
169    MemberAssign,
170    /// A tagged template expression (e.g. ``sql`...${x}...` ``).
171    TaggedTemplate,
172    /// A JSX attribute value (e.g. `dangerouslySetInnerHTML={x}`).
173    JsxAttr,
174}
175
176/// The shape of the non-literal argument captured at a sink site. Category-blind
177/// like [`SinkShape`], but finer-grained: it lets the catalogue matcher require
178/// or exclude specific argument shapes. The discriminator is what distinguishes
179/// an unsafe SQL string concatenation or template-into-`.execute()` from a
180/// safely-parameterized `` sql`${x}` `` tagged template or an object-literal
181/// `.execute({ sql, args })` argument.
182#[derive(
183    Debug,
184    Clone,
185    Copy,
186    PartialEq,
187    Eq,
188    serde::Serialize,
189    serde::Deserialize,
190    bitcode::Encode,
191    bitcode::Decode,
192)]
193pub enum SinkArgKind {
194    /// A template literal with at least one `${...}` substitution (e.g.
195    /// `` `SELECT ${x}` ``). On a `tagged-template` shape this is the tag's
196    /// quasi; on a `call`/`member-call` shape it is the positional argument.
197    TemplateWithSubst,
198    /// A binary `+` string concatenation (e.g. `"SELECT " + x`).
199    Concat,
200    /// An object literal (e.g. `.execute({ sql, args })`, the parameterized form).
201    Object,
202    /// A call expression argument (e.g. `query(buildSql())`).
203    Call,
204    /// Any other non-literal expression (bare identifier, member access, etc.).
205    Other,
206}
207
208/// A captured non-literal sink site. The visitor records EVERY call /
209/// member-assign / member-call / tagged-template / jsx-attr whose relevant
210/// argument is non-literal; it knows nothing about CWE categories. A
211/// fully-literal argument is never captured (conservative trigger).
212#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
213pub struct SinkSite {
214    /// The syntactic shape of the sink site.
215    pub sink_shape: SinkShape,
216    /// The flattened dotted/bare callee or member path.
217    pub callee_path: String,
218    /// The positional index of the non-literal argument.
219    pub arg_index: u32,
220    /// Whether the relevant argument is non-literal (always true when captured).
221    pub arg_is_non_literal: bool,
222    /// The finer-grained shape of the captured non-literal argument. Lets the
223    /// catalogue require unsafe shapes (concat / template-with-substitution) and
224    /// exclude safe ones (object literal, the parameterized form). See
225    /// [`SinkArgKind`].
226    pub arg_kind: SinkArgKind,
227    /// Identifier names referenced anywhere inside the captured non-literal sink
228    /// argument (deduped, source order). Used by the analyze layer to back-trace
229    /// the sink argument to a known untrusted source: a sink is "source-backed"
230    /// when one of these names was bound from a source-shaped expression (see
231    /// `ModuleInfo::tainted_bindings`). Intra-module, name-based, conservative;
232    /// it is never a taint proof. Empty when the argument references no bare
233    /// identifiers (e.g. a pure member-call result).
234    pub arg_idents: Vec<String>,
235    /// Byte offset of the sink span start. Stored as `u32` (not `Span`) so the
236    /// struct is bitcode-encodable and can be persisted directly in the cache.
237    pub span_start: u32,
238    /// Byte offset of the sink span end.
239    pub span_end: u32,
240}
241
242impl SinkSite {
243    /// Reconstruct the source span from the stored byte offsets.
244    #[must_use]
245    pub fn span(&self) -> Span {
246        Span::new(self.span_start, self.span_end)
247    }
248}
249
250/// One alias entry tying an exported object's dotted property path to a namespace import.
251#[derive(Debug, Clone)]
252pub struct NamespaceObjectAlias {
253    /// Canonical export name.
254    pub via_export_name: String,
255    /// Dotted suffix of the property path relative to the export.
256    pub suffix: String,
257    /// Local name of the namespace import.
258    pub namespace_local: String,
259}
260
261/// Compute a table of line-start byte offsets from source text.
262#[must_use]
263#[expect(
264    clippy::cast_possible_truncation,
265    reason = "source files are practically < 4GB"
266)]
267pub fn compute_line_offsets(source: &str) -> Vec<u32> {
268    let mut offsets = vec![0u32];
269    for (i, byte) in source.bytes().enumerate() {
270        if byte == b'\n' {
271            debug_assert!(
272                u32::try_from(i + 1).is_ok(),
273                "source file exceeds u32::MAX bytes — line offsets would overflow"
274            );
275            offsets.push((i + 1) as u32);
276        }
277    }
278    offsets
279}
280
281/// Convert a byte offset to a 1-based line number and 0-based byte column.
282#[must_use]
283#[expect(
284    clippy::cast_possible_truncation,
285    reason = "line count is bounded by source size"
286)]
287pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
288    let line_idx = match line_offsets.binary_search(&byte_offset) {
289        Ok(idx) => idx,
290        Err(idx) => idx.saturating_sub(1),
291    };
292    let line = line_idx as u32 + 1;
293    let col = byte_offset - line_offsets[line_idx];
294    (line, col)
295}
296
297/// Complexity metrics for a single function/method/arrow.
298#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
299pub struct FunctionComplexity {
300    /// Function name (or `"<anonymous>"` for unnamed functions/arrows).
301    pub name: String,
302    /// 1-based line number where the function starts.
303    pub line: u32,
304    /// 0-based byte column where the function starts.
305    pub col: u32,
306    /// `McCabe` cyclomatic complexity (1 + decision points).
307    pub cyclomatic: u16,
308    /// `SonarSource` cognitive complexity (structural + nesting penalty).
309    pub cognitive: u16,
310    /// Number of lines in the function body.
311    pub line_count: u32,
312    /// Number of parameters (excluding TypeScript's `this` parameter).
313    pub param_count: u8,
314    /// Content digest of the function's full-span source slice.
315    pub source_hash: Option<String>,
316}
317
318/// The kind of feature flag pattern detected.
319#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
320pub enum FlagUseKind {
321    /// `process.env.FEATURE_X` pattern.
322    EnvVar,
323    /// SDK function call like `useFlag('name')`.
324    SdkCall,
325    /// Config object access like `config.features.x`.
326    ConfigObject,
327}
328
329/// A feature flag use site.
330#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
331pub struct FlagUse {
332    /// Flag identifier.
333    pub flag_name: String,
334    /// Detection kind.
335    pub kind: FlagUseKind,
336    /// 1-based line number.
337    pub line: u32,
338    /// 0-based byte column offset.
339    pub col: u32,
340    /// Start byte offset of the guarded block.
341    pub guard_span_start: Option<u32>,
342    /// End byte offset of the guarded block.
343    pub guard_span_end: Option<u32>,
344    /// SDK/provider name.
345    pub sdk_name: Option<String>,
346}
347
348const _: () = assert!(std::mem::size_of::<FlagUse>() <= 96);
349
350/// A dynamic import with a partially resolved pattern.
351#[derive(Debug, Clone)]
352pub struct DynamicImportPattern {
353    /// Static prefix of the import path (e.g., "./locales/"). May contain glob characters.
354    pub prefix: String,
355    /// Static suffix of the import path (e.g., ".json"), if any.
356    pub suffix: Option<String>,
357    /// Source span in the original file.
358    pub span: Span,
359}
360
361/// Visibility tag from JSDoc/TSDoc comments that suppresses unused-export detection.
362#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
363#[serde(rename_all = "lowercase")]
364#[repr(u8)]
365pub enum VisibilityTag {
366    /// No visibility tag present.
367    #[default]
368    None = 0,
369    /// `@public` or `@api public` -- part of the public API surface.
370    Public = 1,
371    /// `@internal` -- exported for internal use (sister packages, build tools).
372    Internal = 2,
373    /// `@beta` -- public but unstable, may change without notice.
374    Beta = 3,
375    /// `@alpha` -- early preview, may change drastically without notice.
376    Alpha = 4,
377    /// `@expected-unused` -- intentionally unused, should warn when it becomes used.
378    ExpectedUnused = 5,
379}
380
381impl VisibilityTag {
382    /// Whether this tag permanently suppresses unused-export detection.
383    /// `ExpectedUnused` is handled separately (conditionally suppresses,
384    /// reports stale when the export becomes used).
385    pub const fn suppresses_unused(self) -> bool {
386        matches!(
387            self,
388            Self::Public | Self::Internal | Self::Beta | Self::Alpha
389        )
390    }
391
392    /// For serde `skip_serializing_if`.
393    pub fn is_none(&self) -> bool {
394        matches!(self, Self::None)
395    }
396}
397
398/// An export declaration.
399#[derive(Debug, Clone, serde::Serialize)]
400pub struct ExportInfo {
401    /// The exported name (named or default).
402    pub name: ExportName,
403    /// The local binding name, if different from the exported name.
404    pub local_name: Option<String>,
405    /// Whether this is a type-only export (`export type`).
406    pub is_type_only: bool,
407    /// Whether this export is registered through a runtime side effect at module load time.
408    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
409    pub is_side_effect_used: bool,
410    /// Visibility tag from JSDoc/TSDoc comment.
411    #[serde(default, skip_serializing_if = "VisibilityTag::is_none")]
412    pub visibility: VisibilityTag,
413    /// Source span of the export declaration.
414    #[serde(serialize_with = "serialize_span")]
415    pub span: Span,
416    /// Members of this export (for enums, classes, and namespaces).
417    #[serde(default, skip_serializing_if = "Vec::is_empty")]
418    pub members: Vec<MemberInfo>,
419    /// The local name of the parent class from `extends` clause, if any.
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub super_class: Option<String>,
422}
423
424/// Additional heritage metadata for an exported class.
425#[derive(
426    Debug,
427    Clone,
428    serde::Serialize,
429    serde::Deserialize,
430    bitcode::Encode,
431    bitcode::Decode,
432    PartialEq,
433    Eq,
434)]
435pub struct ClassHeritageInfo {
436    /// Export name (`default` for default-exported classes).
437    pub export_name: String,
438    /// Parent class name from the `extends` clause, if any.
439    pub super_class: Option<String>,
440    /// Interface names from the class `implements` clause.
441    pub implements: Vec<String>,
442    /// Typed instance bindings used to resolve member-access chains in external templates.
443    #[serde(default, skip_serializing_if = "Vec::is_empty")]
444    pub instance_bindings: Vec<(String, String)>,
445}
446
447/// A module-scope declaration that can be used as a TypeScript type.
448#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
449pub struct LocalTypeDeclaration {
450    /// Local declaration name.
451    pub name: String,
452    /// Declaration identifier span.
453    #[serde(serialize_with = "serialize_span")]
454    pub span: Span,
455}
456
457/// A reference from an exported symbol's public signature to a type name.
458#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
459pub struct PublicSignatureTypeReference {
460    /// Exported symbol whose signature contains the reference.
461    pub export_name: String,
462    /// Referenced type name. Qualified names are reduced to their root identifier.
463    pub type_name: String,
464    /// Reference span.
465    #[serde(serialize_with = "serialize_span")]
466    pub span: Span,
467}
468
469/// A member of an enum, class, or namespace.
470#[derive(Debug, Clone, serde::Serialize)]
471pub struct MemberInfo {
472    /// Member name.
473    pub name: String,
474    /// The kind of member (enum, class method/property, or namespace member).
475    pub kind: MemberKind,
476    /// Source span of the member declaration.
477    #[serde(serialize_with = "serialize_span")]
478    pub span: Span,
479    /// Whether this member has decorators (e.g., `@Column()`, `@Inject()`).
480    /// Decorated members are used by frameworks at runtime and should not be
481    /// flagged as unused class members, unless every decorator on the member
482    /// is opted out via `FallowConfig.ignore_decorators`.
483    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
484    pub has_decorator: bool,
485    /// Full dotted path of each decorator on this member, in source order.
486    /// `@step("x")` stores `"step"`; `@ns.foo` stores `"ns.foo"`. Empty for
487    /// undecorated members, Angular signal-initializer properties (which set
488    /// `has_decorator` without a literal decorator AST node), and decorators
489    /// whose expression is not an identifier ladder (the entry is the empty
490    /// string in that case, treated as never-matching by the predicate).
491    #[serde(default, skip_serializing_if = "Vec::is_empty")]
492    pub decorator_names: Vec<String>,
493    /// True when this is a static class method that returns a fresh instance
494    /// of the same class: either via `return new this()` / `return new
495    /// <SameClassName>()` in the body's last statement, or via a declared
496    /// return type matching the class name. Consumers calling such a static
497    /// method receive an instance, so the call result's member accesses are
498    /// credited against the class. See issues #346, #387.
499    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
500    pub is_instance_returning_static: bool,
501    /// True when this is an instance class method whose call result is an
502    /// instance of the same class. Qualifies when the declared return type
503    /// matches the class name (`setX(): EventBuilder { ... }`) or when the
504    /// body's last statement is `return this`. The analyze layer walks fluent
505    /// chains (`Class.factory().setX().setY()`) only through methods carrying
506    /// this flag, so the chain stops at a non-self-returning method like
507    /// `.build()`. See issue #387.
508    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
509    pub is_self_returning: bool,
510}
511
512/// The kind of member.
513#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
514#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
515#[serde(rename_all = "snake_case")]
516pub enum MemberKind {
517    /// A TypeScript enum member.
518    EnumMember,
519    /// A class method.
520    ClassMethod,
521    /// A class property.
522    ClassProperty,
523    /// A member exported from a TypeScript namespace.
524    NamespaceMember,
525}
526
527/// A static member access expression (e.g., `Status.Active`, `MyClass.create()`).
528#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
529pub struct MemberAccess {
530    /// The identifier being accessed (the import name).
531    pub object: String,
532    /// The member being accessed.
533    pub member: String,
534}
535
536#[expect(
537    clippy::trivially_copy_pass_by_ref,
538    reason = "serde serialize_with requires &T"
539)]
540fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
541    use serde::ser::SerializeMap;
542    let mut map = serializer.serialize_map(Some(2))?;
543    map.serialize_entry("start", &span.start)?;
544    map.serialize_entry("end", &span.end)?;
545    map.end()
546}
547
548/// Export identifier.
549#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
550pub enum ExportName {
551    /// A named export (e.g., `export const foo`).
552    Named(String),
553    /// The default export.
554    Default,
555}
556
557impl ExportName {
558    /// Compare against a string without allocating (avoids `to_string()`).
559    #[must_use]
560    pub fn matches_str(&self, s: &str) -> bool {
561        match self {
562            Self::Named(n) => n == s,
563            Self::Default => s == "default",
564        }
565    }
566}
567
568impl std::fmt::Display for ExportName {
569    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570        match self {
571            Self::Named(n) => write!(f, "{n}"),
572            Self::Default => write!(f, "default"),
573        }
574    }
575}
576
577/// An import declaration.
578#[derive(Debug, Clone)]
579pub struct ImportInfo {
580    /// The import specifier (e.g., `./utils` or `react`).
581    pub source: String,
582    /// How the symbol is imported (named, default, namespace, or side-effect).
583    pub imported_name: ImportedName,
584    /// The local binding name in the importing module.
585    pub local_name: String,
586    /// Whether this is a type-only import (`import type`).
587    pub is_type_only: bool,
588    /// Whether this import originated from a CSS-context.
589    pub from_style: bool,
590    /// Source span of the import declaration.
591    pub span: Span,
592    /// Span of the source string literal used by the LSP to highlight the specifier.
593    pub source_span: Span,
594}
595
596/// How a symbol is imported.
597#[derive(Debug, Clone, PartialEq, Eq)]
598pub enum ImportedName {
599    /// A named import (e.g., `import { foo }`).
600    Named(String),
601    /// A default import (e.g., `import React`).
602    Default,
603    /// A namespace import (e.g., `import * as utils`).
604    Namespace,
605    /// A side-effect import (e.g., `import './styles.css'`).
606    SideEffect,
607}
608
609#[cfg(target_pointer_width = "64")]
610const _: () = assert!(std::mem::size_of::<ExportInfo>() == 112);
611#[cfg(target_pointer_width = "64")]
612const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
613#[cfg(target_pointer_width = "64")]
614const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
615#[cfg(target_pointer_width = "64")]
616const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
617#[cfg(target_pointer_width = "64")]
618const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
619#[cfg(target_pointer_width = "64")]
620const _: () = assert!(std::mem::size_of::<SinkSite>() == 64);
621#[cfg(target_pointer_width = "64")]
622const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 648);
623
624/// A re-export declaration.
625#[derive(Debug, Clone)]
626pub struct ReExportInfo {
627    /// The module being re-exported from.
628    pub source: String,
629    /// The name imported from the source module (or `*` for star re-exports).
630    pub imported_name: String,
631    /// The name exported from this module.
632    pub exported_name: String,
633    /// Whether this is a type-only re-export.
634    pub is_type_only: bool,
635    /// Source span of the re-export declaration on this module.
636    pub span: oxc_span::Span,
637}
638
639/// A dynamic `import()` call.
640#[derive(Debug, Clone)]
641pub struct DynamicImportInfo {
642    /// The import specifier.
643    pub source: String,
644    /// Source span of the `import()` expression.
645    pub span: Span,
646    /// Names destructured from the dynamic import result.
647    /// Non-empty means `const { a, b } = await import(...)` -> Named imports.
648    /// Empty means simple `import(...)` or `const x = await import(...)` -> Namespace.
649    pub destructured_names: Vec<String>,
650    /// The local variable name for `const x = await import(...)`.
651    /// Used for namespace import narrowing via member access tracking.
652    pub local_name: Option<String>,
653    /// True when this dynamic import was synthesised by fallow rather than appearing in user source.
654    pub is_speculative: bool,
655}
656
657/// A `require()` call.
658#[derive(Debug, Clone)]
659pub struct RequireCallInfo {
660    /// The require specifier.
661    pub source: String,
662    /// Source span of the `require()` call.
663    pub span: Span,
664    /// Names destructured from the `require()` result.
665    pub destructured_names: Vec<String>,
666    /// The local variable name for `const x = require(...)`.
667    pub local_name: Option<String>,
668}
669
670/// Result of parsing all files, including incremental cache statistics.
671pub struct ParseResult {
672    /// Extracted module information for all successfully parsed files.
673    pub modules: Vec<ModuleInfo>,
674    /// Number of files whose parse results were loaded from cache (unchanged).
675    pub cache_hits: usize,
676    /// Number of files that required a full parse (new or changed).
677    pub cache_misses: usize,
678    /// Summed wall-clock time of the actual AST parses across all rayon workers.
679    pub parse_cpu_ms: f64,
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    #[test]
687    fn line_offsets_empty_string() {
688        assert_eq!(compute_line_offsets(""), vec![0]);
689    }
690
691    #[test]
692    fn sink_shape_bitcode_roundtrip() {
693        for shape in [
694            SinkShape::Call,
695            SinkShape::MemberCall,
696            SinkShape::MemberAssign,
697            SinkShape::TaggedTemplate,
698            SinkShape::JsxAttr,
699        ] {
700            let encoded = bitcode::encode(&shape);
701            let decoded: SinkShape = bitcode::decode(&encoded).expect("decode sink shape");
702            assert_eq!(shape, decoded);
703        }
704    }
705
706    #[test]
707    fn sink_arg_kind_bitcode_roundtrip() {
708        for kind in [
709            SinkArgKind::TemplateWithSubst,
710            SinkArgKind::Concat,
711            SinkArgKind::Object,
712            SinkArgKind::Call,
713            SinkArgKind::Other,
714        ] {
715            let encoded = bitcode::encode(&kind);
716            let decoded: SinkArgKind = bitcode::decode(&encoded).expect("decode sink arg kind");
717            assert_eq!(kind, decoded);
718        }
719    }
720
721    #[test]
722    fn sink_site_bitcode_roundtrip() {
723        let site = SinkSite {
724            sink_shape: SinkShape::MemberAssign,
725            callee_path: "el.innerHTML".to_string(),
726            arg_index: 0,
727            arg_is_non_literal: true,
728            arg_kind: SinkArgKind::Other,
729            arg_idents: vec!["userInput".to_string()],
730            span_start: 10,
731            span_end: 20,
732        };
733        let encoded = bitcode::encode(&site);
734        let decoded: SinkSite = bitcode::decode(&encoded).expect("decode sink site");
735        assert_eq!(decoded.sink_shape, site.sink_shape);
736        assert_eq!(decoded.callee_path, site.callee_path);
737        assert_eq!(decoded.arg_index, site.arg_index);
738        assert_eq!(decoded.arg_is_non_literal, site.arg_is_non_literal);
739        assert_eq!(decoded.arg_kind, site.arg_kind);
740        assert_eq!(decoded.arg_idents, site.arg_idents);
741        assert_eq!(decoded.span(), site.span());
742    }
743
744    #[test]
745    fn line_offsets_single_line_no_newline() {
746        assert_eq!(compute_line_offsets("hello"), vec![0]);
747    }
748
749    #[test]
750    fn line_offsets_single_line_with_newline() {
751        assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
752    }
753
754    #[test]
755    fn line_offsets_multiple_lines() {
756        assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
757    }
758
759    #[test]
760    fn line_offsets_trailing_newline() {
761        assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
762    }
763
764    #[test]
765    fn line_offsets_consecutive_newlines() {
766        assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
767    }
768
769    #[test]
770    fn line_offsets_multibyte_utf8() {
771        assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
772    }
773
774    #[test]
775    fn line_col_offset_zero() {
776        let offsets = compute_line_offsets("abc\ndef\nghi");
777        let (line, col) = byte_offset_to_line_col(&offsets, 0);
778        assert_eq!((line, col), (1, 0));
779    }
780
781    #[test]
782    fn line_col_middle_of_first_line() {
783        let offsets = compute_line_offsets("abc\ndef\nghi");
784        let (line, col) = byte_offset_to_line_col(&offsets, 2);
785        assert_eq!((line, col), (1, 2));
786    }
787
788    #[test]
789    fn line_col_start_of_second_line() {
790        let offsets = compute_line_offsets("abc\ndef\nghi");
791        let (line, col) = byte_offset_to_line_col(&offsets, 4);
792        assert_eq!((line, col), (2, 0));
793    }
794
795    #[test]
796    fn line_col_middle_of_second_line() {
797        let offsets = compute_line_offsets("abc\ndef\nghi");
798        let (line, col) = byte_offset_to_line_col(&offsets, 5);
799        assert_eq!((line, col), (2, 1));
800    }
801
802    #[test]
803    fn line_col_start_of_third_line() {
804        let offsets = compute_line_offsets("abc\ndef\nghi");
805        let (line, col) = byte_offset_to_line_col(&offsets, 8);
806        assert_eq!((line, col), (3, 0));
807    }
808
809    #[test]
810    fn line_col_end_of_file() {
811        let offsets = compute_line_offsets("abc\ndef\nghi");
812        let (line, col) = byte_offset_to_line_col(&offsets, 10);
813        assert_eq!((line, col), (3, 2));
814    }
815
816    #[test]
817    fn line_col_single_line() {
818        let offsets = compute_line_offsets("hello");
819        let (line, col) = byte_offset_to_line_col(&offsets, 3);
820        assert_eq!((line, col), (1, 3));
821    }
822
823    #[test]
824    fn line_col_at_newline_byte() {
825        let offsets = compute_line_offsets("abc\ndef");
826        let (line, col) = byte_offset_to_line_col(&offsets, 3);
827        assert_eq!((line, col), (1, 3));
828    }
829
830    #[test]
831    fn export_name_matches_str_named() {
832        let name = ExportName::Named("foo".to_string());
833        assert!(name.matches_str("foo"));
834        assert!(!name.matches_str("bar"));
835        assert!(!name.matches_str("default"));
836    }
837
838    #[test]
839    fn export_name_matches_str_default() {
840        let name = ExportName::Default;
841        assert!(name.matches_str("default"));
842        assert!(!name.matches_str("foo"));
843    }
844
845    #[test]
846    fn export_name_display_named() {
847        let name = ExportName::Named("myExport".to_string());
848        assert_eq!(name.to_string(), "myExport");
849    }
850
851    #[test]
852    fn export_name_display_default() {
853        let name = ExportName::Default;
854        assert_eq!(name.to_string(), "default");
855    }
856
857    #[test]
858    fn export_name_equality_named() {
859        let a = ExportName::Named("foo".to_string());
860        let b = ExportName::Named("foo".to_string());
861        let c = ExportName::Named("bar".to_string());
862        assert_eq!(a, b);
863        assert_ne!(a, c);
864    }
865
866    #[test]
867    fn export_name_equality_default() {
868        let a = ExportName::Default;
869        let b = ExportName::Default;
870        assert_eq!(a, b);
871    }
872
873    #[test]
874    fn export_name_named_not_equal_to_default() {
875        let named = ExportName::Named("default".to_string());
876        let default = ExportName::Default;
877        assert_ne!(named, default);
878    }
879
880    #[test]
881    fn export_name_hash_consistency() {
882        use std::collections::hash_map::DefaultHasher;
883        use std::hash::{Hash, Hasher};
884
885        let mut h1 = DefaultHasher::new();
886        let mut h2 = DefaultHasher::new();
887        ExportName::Named("foo".to_string()).hash(&mut h1);
888        ExportName::Named("foo".to_string()).hash(&mut h2);
889        assert_eq!(h1.finish(), h2.finish());
890    }
891
892    #[test]
893    fn export_name_matches_str_empty_string() {
894        let name = ExportName::Named(String::new());
895        assert!(name.matches_str(""));
896        assert!(!name.matches_str("foo"));
897    }
898
899    #[test]
900    fn export_name_default_does_not_match_empty() {
901        let name = ExportName::Default;
902        assert!(!name.matches_str(""));
903    }
904
905    #[test]
906    fn imported_name_equality() {
907        assert_eq!(
908            ImportedName::Named("foo".to_string()),
909            ImportedName::Named("foo".to_string())
910        );
911        assert_ne!(
912            ImportedName::Named("foo".to_string()),
913            ImportedName::Named("bar".to_string())
914        );
915        assert_eq!(ImportedName::Default, ImportedName::Default);
916        assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
917        assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
918        assert_ne!(ImportedName::Default, ImportedName::Namespace);
919        assert_ne!(
920            ImportedName::Named("default".to_string()),
921            ImportedName::Default
922        );
923    }
924
925    #[test]
926    fn member_kind_equality() {
927        assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
928        assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
929        assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
930        assert_eq!(MemberKind::NamespaceMember, MemberKind::NamespaceMember);
931        assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
932        assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
933        assert_ne!(MemberKind::NamespaceMember, MemberKind::EnumMember);
934    }
935
936    #[test]
937    fn member_kind_bitcode_roundtrip() {
938        let kinds = [
939            MemberKind::EnumMember,
940            MemberKind::ClassMethod,
941            MemberKind::ClassProperty,
942            MemberKind::NamespaceMember,
943        ];
944        for kind in &kinds {
945            let bytes = bitcode::encode(kind);
946            let decoded: MemberKind = bitcode::decode(&bytes).unwrap();
947            assert_eq!(&decoded, kind);
948        }
949    }
950
951    #[test]
952    fn member_access_bitcode_roundtrip() {
953        let access = MemberAccess {
954            object: "Status".to_string(),
955            member: "Active".to_string(),
956        };
957        let bytes = bitcode::encode(&access);
958        let decoded: MemberAccess = bitcode::decode(&bytes).unwrap();
959        assert_eq!(decoded.object, "Status");
960        assert_eq!(decoded.member, "Active");
961    }
962
963    #[test]
964    fn line_offsets_crlf_only_counts_lf() {
965        let offsets = compute_line_offsets("ab\r\ncd");
966        assert_eq!(offsets, vec![0, 4]);
967    }
968
969    #[test]
970    fn line_col_empty_file_offset_zero() {
971        let offsets = compute_line_offsets("");
972        let (line, col) = byte_offset_to_line_col(&offsets, 0);
973        assert_eq!((line, col), (1, 0));
974    }
975
976    #[test]
977    fn function_complexity_bitcode_roundtrip() {
978        let fc = FunctionComplexity {
979            name: "processData".to_string(),
980            line: 42,
981            col: 4,
982            cyclomatic: 15,
983            cognitive: 25,
984            line_count: 80,
985            param_count: 3,
986            source_hash: Some("0123456789abcdef".to_string()),
987        };
988        let bytes = bitcode::encode(&fc);
989        let decoded: FunctionComplexity = bitcode::decode(&bytes).unwrap();
990        assert_eq!(decoded.name, "processData");
991        assert_eq!(decoded.line, 42);
992        assert_eq!(decoded.col, 4);
993        assert_eq!(decoded.cyclomatic, 15);
994        assert_eq!(decoded.cognitive, 25);
995        assert_eq!(decoded.line_count, 80);
996        assert_eq!(decoded.source_hash.as_deref(), Some("0123456789abcdef"));
997    }
998}