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