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