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