Skip to main content

fallow_types/
extract.rs

1//! Module extraction types.
2
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use oxc_span::Span;
7
8use crate::discover::FileId;
9use crate::suppress::{Suppression, UnknownSuppressionKind};
10
11/// Extracted module information from a single file.
12///
13/// The `Arc<[T]>` fields are immutable after extraction and are shared by
14/// refcount with the resolver's per-file output, so the resolve/graph path
15/// does not deep-copy per-file extraction payloads.
16#[derive(Debug, Clone)]
17pub struct ModuleInfo {
18    /// Unique identifier for this file.
19    pub file_id: FileId,
20    /// All export declarations in this module.
21    pub exports: Arc<[ExportInfo]>,
22    /// All import declarations in this module.
23    pub imports: Vec<ImportInfo>,
24    /// All re-export declarations (e.g., `export { foo } from './bar'`).
25    pub re_exports: Vec<ReExportInfo>,
26    /// All dynamic `import()` calls with string literal sources.
27    pub dynamic_imports: Vec<DynamicImportInfo>,
28    /// Dynamic import patterns.
29    pub dynamic_import_patterns: Vec<DynamicImportPattern>,
30    /// All `require()` calls.
31    pub require_calls: Vec<RequireCallInfo>,
32    /// Package names statically referenced through package path resolution.
33    pub package_path_references: Box<[String]>,
34    /// Static member access expressions (e.g., `Status.Active`).
35    pub member_accesses: Arc<[MemberAccess]>,
36    /// Typed semantic facts produced by extraction for cross-layer analysis.
37    ///
38    /// This carries facts that were previously encoded as synthetic
39    /// `member_accesses` strings. Extraction and analysis now use typed facts.
40    pub semantic_facts: Arc<[SemanticFact]>,
41    /// Identifiers used in whole-object access patterns.
42    pub whole_object_uses: Arc<[String]>,
43    /// Whether this module uses CommonJS exports.
44    pub has_cjs_exports: bool,
45    /// Whether this module declares an Angular component `templateUrl`.
46    pub has_angular_component_template_url: bool,
47    /// xxh3 hash of the file content for incremental caching.
48    pub content_hash: u64,
49    /// Inline suppression directives parsed from comments.
50    pub suppressions: Vec<Suppression>,
51    /// Suppression tokens that did not parse to any known `IssueKind`.
52    /// Surfaced as `StaleSuppression` findings via `find_stale` so users see
53    /// typos or obsolete kind names instead of having the entire marker
54    /// silently discarded. See issue #449.
55    pub unknown_suppression_kinds: Vec<UnknownSuppressionKind>,
56    /// Local names of import bindings that are never referenced in this file.
57    /// Populated via `oxc_semantic` scope analysis. Used at graph-build time
58    /// to skip adding references for imports whose binding is never read,
59    /// improving unused-export detection precision.
60    pub unused_import_bindings: Vec<String>,
61    /// Local import bindings that are referenced from TypeScript type positions.
62    /// Used to distinguish value-namespace and type-namespace references when a
63    /// module exports both `const X` and `type X`.
64    pub type_referenced_import_bindings: Vec<String>,
65    /// Local import bindings referenced from runtime/value positions.
66    pub value_referenced_import_bindings: Vec<String>,
67    /// Pre-computed byte offsets where each line starts.
68    pub line_offsets: Vec<u32>,
69    /// Per-function complexity metrics.
70    pub complexity: Vec<FunctionComplexity>,
71    /// Feature flag use sites.
72    pub flag_uses: Vec<FlagUse>,
73    /// Heritage metadata for exported classes that declare `implements`.
74    pub class_heritage: Vec<ClassHeritageInfo>,
75    /// Exported free-function factories that provably return one class instance
76    /// (`export function useApi() { return new RESTApi() }`). Origin-module proof
77    /// that an exported function returns a class instance, so a cross-module
78    /// `const x = useApi(); x.member` consumer can credit the returned class.
79    /// See issue #1441 (Part A).
80    pub exported_factory_returns: Arc<[FactoryReturnExport]>,
81    /// Exported factories that return an OBJECT LITERAL whose property values are
82    /// class instances (`export function createUi() { return { orders: factory.ordersPage } }`).
83    /// Each entry maps a dotted property path (`orders`, `invoke.dashboard`) to the
84    /// returned class's local name within the factory module, so a cross-module
85    /// `const ui = createUi(); ui.orders.member` consumer can credit the class. Names
86    /// are local to this module; resolution is deferred to analyze time. See issue #1858.
87    pub exported_factory_return_object_shapes: Arc<[FactoryReturnObjectShapeExport]>,
88    /// Named-type property types declared by this module's top-level interfaces
89    /// and type-literal aliases (`interface Opts { c: OptDep }`). Names are
90    /// local to this module; resolution is deferred to analyze time. Consumed
91    /// by the `unused-class-member` typed-property-hop join and the Playwright
92    /// fixture-type resolution. See issue #1785.
93    pub type_member_types: Arc<[TypeMemberTypeEntry]>,
94    /// Angular `InjectionToken<Interface>` declarations, as
95    /// `(token_export_name, interface_name)` pairs. Recorded only for
96    /// `new InjectionToken<I>(...)` initializers whose `InjectionToken` is
97    /// imported from `@angular/core`. The analyze layer follows the token's
98    /// interface type argument to the classes that `implement` it so a template
99    /// member call through `inject(TOKEN)` credits the concrete implementation.
100    /// See issue #920 (follow-up to #911 / #913).
101    pub injection_tokens: Vec<(String, String)>,
102    /// Local type-capable declarations.
103    pub local_type_declarations: Vec<LocalTypeDeclaration>,
104    /// Type references in exported public signatures.
105    pub public_signature_type_references: Vec<PublicSignatureTypeReference>,
106    /// Aliases of namespace imports re-exported through an object literal.
107    pub namespace_object_aliases: Vec<NamespaceObjectAlias>,
108    /// Deduped Iconify collection prefixes found in static icon props.
109    pub iconify_prefixes: Vec<String>,
110    /// Deduped Nuxt UI `i-<collection>-<icon>` icon class suffixes found in
111    /// static script-side icon properties.
112    pub iconify_icon_names: Vec<String>,
113    /// Bare identifiers that may be resolved by framework auto-imports.
114    pub auto_import_candidates: Vec<String>,
115    /// File-level string directives in source order (e.g. `"use client"`,
116    /// `"use server"`, `"use strict"`). Captured from `Program::directives`.
117    /// Consumed by the security `client-server-leak` detector to identify
118    /// React Server Component client boundaries.
119    pub directives: Vec<String>,
120    /// Byte-offset starts of dynamic `import()` expressions wrapped in
121    /// `next/dynamic(() => import('./X'), { ssr: false })`. The ssr:false option
122    /// is Next.js's sanctioned way to pull a client-only module, so a server-only
123    /// module reached ONLY through such an import is NOT a client-server leak. The
124    /// security `client-server-leak` BFS resolves each dynamic import to a graph
125    /// edge; these span starts let the BFS exclude exactly those edges (matched
126    /// against the edge's `import_span`). Empty for files with no ssr:false
127    /// dynamic import. Captured only by JS/TS extraction.
128    pub client_only_dynamic_import_spans: Vec<u32>,
129    /// Captured security sink sites (category-blind). Consumed by the
130    /// catalogue-driven `tainted_sink` detector. Captured only by JS/TS
131    /// extraction; empty for CSS/MDX/etc. See `security_matchers.toml`.
132    pub security_sinks: Vec<SinkSite>,
133    /// Count of sink-shaped nodes whose callee could not be flattened to a
134    /// static path (dynamic dispatch, computed members, aliased bindings).
135    /// Surfaced in-band so an empty catalogue result with a non-zero count is
136    /// not a clean bill.
137    pub security_sinks_skipped: u32,
138    /// Compact span-level diagnostics for skipped security sink callees. Kept
139    /// next to `security_sinks_skipped` so warm-cache and cold-cache security
140    /// output can explain where the blind spots are concentrated without source
141    /// snippets.
142    pub security_unresolved_callee_sites: Vec<SkippedSecurityCalleeSite>,
143    /// Local bindings whose initializer (or destructured object) is a flattened
144    /// member-access path. Used by the security `tainted_sink` detector to
145    /// back-trace a sink argument to a known untrusted source: the analyze layer
146    /// matches each binding's `source_path` against the data-driven source
147    /// catalogue (`security_matchers.toml` `[[source]]` rows) and treats the
148    /// matching `local` names as source-tainted. Intra-module and name-based
149    /// (no scope analysis); a conservative association, never a taint proof.
150    pub tainted_bindings: Vec<TaintedBinding>,
151    /// Sink arguments that were recognized as sanitizer calls at extraction
152    /// time. Used for direct sink calls such as
153    /// `el.innerHTML = DOMPurify.sanitize(input)`.
154    pub sanitized_sink_args: Vec<SanitizedSinkArg>,
155    /// Known defensive control call sites found in this module. Consumed only by
156    /// the `fallow security --surface` agent JSON path.
157    pub security_control_sites: Vec<SecurityControlSite>,
158    /// Statically flattenable callee paths invoked in this module, deduped per
159    /// unique path (first occurrence wins). Consumed by the
160    /// `boundaries.calls.forbidden` detector. Captured unconditionally because
161    /// extraction is config-blind; the per-module cost is bounded by the
162    /// unique-callee count.
163    pub callee_uses: Vec<CalleeUse>,
164    /// `"use client"` / `"use server"` directive strings written as expression
165    /// statements in `program.body` (misplaced, NOT in the leading
166    /// prologue), so the RSC bundler silently ignores them. One entry per
167    /// occurrence. Consumed by the `misplaced-directive` detector. Captured
168    /// only by JS/TS extraction.
169    pub misplaced_directives: Vec<MisplacedDirectiveSite>,
170    /// Export LOCAL NAMES of exported functions / const-arrows whose body has an
171    /// inline `"use server"` directive (`export async function f() { "use server"
172    /// }`), captured in a NON-`"use server"` file. Consumed by the
173    /// `unused-server-action` detector to reclassify an unused inline Server
174    /// Action export out of `unused-export`. Captured only by JS/TS extraction.
175    pub inline_server_action_exports: Vec<String>,
176    /// Vue `provide`/`inject` and Svelte `setContext`/`getContext` call sites
177    /// keyed by an identifier symbol. Consumed by the `unprovided-inject`
178    /// detector to find an inject/getContext whose key is provided nowhere
179    /// project-wide. Only identifier-keyed sites are recorded (string-literal
180    /// and computed keys abstain). Captured by JS/TS and SFC extraction.
181    pub di_key_sites: Vec<DiKeySite>,
182    /// `true` when this module contains a `provide(...)` / `*.provide(...)` /
183    /// `setContext(...)` call whose key argument is NOT a plain identifier
184    /// (spread, computed, member, loop variable). Such a call can provide an
185    /// unknowable key, so the `unprovided-inject` detector abstains on ALL
186    /// inject findings project-wide when any reachable module sets this flag.
187    /// Mirrors the spread-return whole-object abstain used for Pinia stores.
188    pub has_dynamic_provide: bool,
189    /// Local names of import bindings that ARE referenced somewhere in this file
190    /// (script value/type position OR template/markup). The complement of
191    /// `unused_import_bindings` among `imports`. Derived by
192    /// `prepare_analysis_facts` while both source vectors are still present, so
193    /// it remains readable after the owned release path clears them. It is never
194    /// cached and is recomputed on every cache load. Consumed by the
195    /// `unrendered-component` detector to credit a
196    /// Vue/Svelte SFC that some file actually imports-and-uses, distinguishing it
197    /// from a component reachable only through a barrel re-export.
198    pub referenced_import_bindings: Vec<String>,
199    /// Vue `<script setup>` `defineProps` and Svelte 5 `$props()` declared
200    /// props. Consumed by the `unused-component-prop` detector to flag a prop
201    /// referenced nowhere in its own SFC. Each entry carries `used_in_script` /
202    /// `used_in_template`.
203    pub component_props: Vec<ComponentProp>,
204    /// `true` when the template spreads the whole props/attrs object
205    /// (`v-bind="$attrs"` / `v-bind="$props"` / `v-bind="props"`) or the props
206    /// return is destructured with a rest element. Either form can consume a prop
207    /// indirectly, so the detector abstains on the whole file.
208    pub has_props_attrs_fallthrough: bool,
209    /// `true` when the SFC calls `defineExpose(...)`. A prop may be re-exposed,
210    /// so the detector conservatively abstains on the whole file.
211    pub has_define_expose: bool,
212    /// `true` when the SFC calls `defineModel(...)`. Two-way model props are out
213    /// of scope for v1, so the detector abstains on the whole file.
214    pub has_define_model: bool,
215    /// `true` when props were declared through an unharvestable shape, such as a
216    /// Vue type-reference argument or an opaque Svelte `$props()` destructure.
217    /// The detector abstains on the whole file so a prop is never falsely
218    /// flagged.
219    pub has_unharvestable_props: bool,
220    /// Vue `<script setup>` `defineEmits` declared events. Consumed by the
221    /// `unused-component-emit` detector to flag an event emitted nowhere in its
222    /// own SFC. Each entry carries `used`.
223    pub component_emits: Vec<ComponentEmit>,
224    /// Angular component/directive inputs declared via `@Input()` decorators or
225    /// signal `input()` / `input.required()` / `model()` initializers. Consumed
226    /// by the `unused-component-input` detector to flag an input read nowhere in
227    /// its own component. Empty for every non-Angular class.
228    pub angular_inputs: Vec<AngularInputMember>,
229    /// Angular component/directive outputs declared via `@Output()` decorators or
230    /// signal `output()` / `outputFromObservable()` initializers. Consumed by the
231    /// `unused-component-output` detector to flag an output emitted nowhere in its
232    /// own component. A `model()` is recorded as an input only (see
233    /// `AngularOutputMember`). Empty for every non-Angular class.
234    pub angular_outputs: Vec<AngularOutputMember>,
235    /// Angular `@Component` declarations with their `selector` value(s), harvested
236    /// from `@Component({ selector: '...' })` decorators. Consumed by the Angular
237    /// arm of the `unrendered-component` detector. Empty for every non-Angular
238    /// class and for `@Directive`. See `AngularComponentSelector`.
239    pub angular_component_selectors: Vec<AngularComponentSelector>,
240    /// Lit / web-component custom elements REGISTERED in this file via
241    /// `@customElement('x-foo')` or `customElements.define('x-foo', C)`. Consumed
242    /// by the Lit arm of the `unrendered-component` detector, which flags a
243    /// registered element whose tag is rendered in NO `html` template
244    /// project-wide. Empty for non-Lit / non-web-component files. See
245    /// `RegisteredCustomElement`.
246    pub registered_custom_elements: Vec<RegisteredCustomElement>,
247    /// Custom-element tag names USED (rendered) in this file's `html` tagged
248    /// templates, e.g. `` html`<x-foo></x-foo>` `` -> `x-foo`. Only hyphenated
249    /// (custom-element) tags are recorded; native HTML tags are excluded by the
250    /// hyphen requirement. The detector unions these project-wide into the
251    /// rendered-tag set. Empty for files with no `html` templates.
252    pub used_custom_element_tags: Vec<String>,
253    /// Custom element selector tag names referenced in this file's Angular
254    /// templates (inline `@Component({ template })` and the linked external
255    /// `templateUrl` `.html` module), e.g. `<app-foo>` -> `app-foo`. Native HTML
256    /// tag names are excluded at harvest. The detector unions these project-wide
257    /// into the used-selector set. Empty for non-Angular files.
258    pub angular_used_selectors: Vec<String>,
259    /// Angular component class names referenced as a route entry or bootstrap
260    /// target: a route `component: Foo` / `loadComponent: () => import().then(m =>
261    /// m.Foo)` value, a `bootstrapApplication(Foo)` argument, or a
262    /// `bootstrap: [Foo]` NgModule entry. These are render-equivalent entry points
263    /// (Angular instantiates them without a template `<tag>`), so the Angular
264    /// `unrendered-component` detector abstains on a component whose class name is
265    /// in the project-wide union. A plain `declarations: [...]` / `imports: [...]`
266    /// registration is intentionally NOT harvested here (that is the dead case the
267    /// rule catches). Empty for non-Angular files.
268    pub angular_entry_component_refs: Vec<String>,
269    /// `true` when this file dynamically renders an Angular component fallow
270    /// cannot attribute to a literal class reference: a
271    /// `ViewContainerRef.createComponent(...)` / `*.createComponent(<ident>)`
272    /// call, or an `*ngComponentOutlet` template binding. The Angular
273    /// `unrendered-component` detector abstains project-wide when ANY reachable
274    /// module sets this (mirroring `unprovided-inject`'s `has_dynamic_provide`),
275    /// since a component could be rendered by a non-literal class reference.
276    pub has_dynamic_component_render: bool,
277    /// `true` when `defineEmits` was called with an unharvestable argument (a
278    /// type-reference type argument such as `defineEmits<MyEmits>()`, a
279    /// non-literal runtime form, or an unbound `defineEmits([...])`). The
280    /// detector abstains on the whole file so an emit is never falsely flagged.
281    pub has_unharvestable_emits: bool,
282    /// `true` when an `emit(<nonLiteral>)` call was seen (the emitted event name
283    /// cannot be known statically). The detector abstains on the whole file.
284    pub has_dynamic_emit: bool,
285    /// `true` when the `defineEmits` return binding was used as a WHOLE value
286    /// (passed to a function, returned, or spread), which can emit any event
287    /// opaquely. The detector abstains on the whole file.
288    pub has_emit_whole_object_use: bool,
289    /// SvelteKit `load()` return-object keys harvested from a
290    /// `+page.{ts,server.ts,js,server.js}` file's terminal return literal.
291    /// Consumed by the `unused-load-data-key` detector. Empty for every file
292    /// that is not a page-load producer (gated by basename at harvest time).
293    pub load_return_keys: Vec<LoadReturnKey>,
294    /// `true` when this file's `load()` body could not be harvested safely (a
295    /// spread return, a non-object/non-literal return, more than one top-level
296    /// `return`, a computed key, or a wrapped/re-exported `load`). The detector
297    /// abstains on the whole file so a key is never falsely flagged.
298    pub has_unharvestable_load: bool,
299    /// `true` when this file passes the whole `data` object opaquely (script
300    /// `const X = data`, `fn(data)` / `fn(...data)`, or template `data={data}` /
301    /// `{...data}` in a route component), so a child can read arbitrary keys the
302    /// detector cannot see. Name-gated on the `data` binding. Read ONLY by the
303    /// `unused-load-data-key` detector, so capturing it for all files is
304    /// byte-identity-safe. See FP-1 in the plan.
305    pub has_load_data_whole_use: bool,
306    /// `true` when this file uses the whole `page.data` / `$page.data` store
307    /// object opaquely (e.g. `Object.values(page.data)`, `{...$page.data}`), so a
308    /// reflective read could consume any route's key. Drives the
309    /// `unused-load-data-key` detector's project-wide abstain. Derived by
310    /// `prepare_analysis_facts` from `whole_object_uses` before the owned release
311    /// path clears that vector. It is never cached and is recomputed each run from
312    /// the cached `whole_object_uses`. Reassignment forms
313    /// (`const all = $page.data`) are not whole-object-tracked and stay out of
314    /// scope, matching the syntactic analyzer's conservative posture.
315    pub has_page_data_store_whole_use: bool,
316    /// `true` when a React Router or Remix route consumes the whole
317    /// `useLoaderData()` result opaquely. Derived by `prepare_analysis_facts`
318    /// from the synthetic route-loader marker before the owned release path
319    /// clears `whole_object_uses`. It is recomputed from cached extraction data.
320    pub has_route_loader_data_whole_use: bool,
321    /// React/JSX component definitions: functions/arrows whose body returns JSX.
322    /// Captured only for `.jsx`/`.tsx` files when a React/Preact dependency is
323    /// plausible. Consumed by the React `unused-component-prop` arm and the
324    /// complexity-fold phase. Empty for non-React files.
325    pub component_functions: Vec<ComponentFunction>,
326    /// React component props (reuses the shared `ComponentProp` struct). For
327    /// React, `used_in_template` is always false and `used_in_script` means
328    /// used-in-body. Empty for non-React files.
329    pub react_props: Vec<ComponentProp>,
330    /// React hook call sites (`useState` / `useEffect` / `useMemo` /
331    /// `useCallback` / custom `use*`). Drives hook-density complexity context.
332    /// Empty for non-React files.
333    pub hook_uses: Vec<HookUse>,
334    /// React render edges: one component rendering another. Captured with the
335    /// child's written name; child-to-`FileId` resolution is deferred to graph
336    /// build. Empty for non-React files.
337    pub render_edges: Vec<RenderEdge>,
338    /// Svelte custom events dispatched via `dispatch('<name>')` where `dispatch`
339    /// is the binding from `const dispatch = createEventDispatcher()`. Consumed
340    /// by the `unused-svelte-event` detector to flag an event dispatched here but
341    /// listened to nowhere project-wide. Each entry carries the literal event
342    /// name and its span. Empty for every non-Svelte file.
343    pub svelte_dispatched_events: Vec<DispatchedEvent>,
344    /// Svelte custom-event listener names harvested from template `on:<name>`
345    /// bindings on COMPONENT tags (PascalCase tag names). Lowercase DOM-element
346    /// `on:click` is a DOM event, not a custom event, and is excluded. Unioned
347    /// project-wide by the `unused-svelte-event` detector to build the liberal
348    /// "listened" set. Empty for every non-Svelte file.
349    pub svelte_listened_events: Vec<String>,
350    /// `true` when a `dispatch(<nonLiteral>)` call was seen (the dispatched event
351    /// name cannot be known statically), or the `dispatch` binding was used as a
352    /// whole value (passed / returned). The `unused-svelte-event` detector
353    /// abstains on the whole component so an event is never falsely flagged.
354    pub has_dynamic_dispatch: bool,
355}
356
357impl ModuleInfo {
358    /// A fully-zeroed `ModuleInfo` for the given file.
359    ///
360    /// Fixture builders and non-JS extraction paths start from this and set
361    /// only the fields they care about via struct-update syntax:
362    /// `ModuleInfo { exports: vec![..], ..ModuleInfo::empty(file_id) }`.
363    #[must_use]
364    pub fn empty(file_id: FileId) -> Self {
365        Self {
366            file_id,
367            exports: Arc::default(),
368            imports: Vec::new(),
369            re_exports: Vec::new(),
370            dynamic_imports: Vec::new(),
371            dynamic_import_patterns: Vec::new(),
372            require_calls: Vec::new(),
373            package_path_references: Box::default(),
374            member_accesses: Arc::default(),
375            semantic_facts: Arc::default(),
376            whole_object_uses: Arc::default(),
377            has_cjs_exports: false,
378            has_angular_component_template_url: false,
379            content_hash: 0,
380            suppressions: Vec::new(),
381            unknown_suppression_kinds: Vec::new(),
382            unused_import_bindings: Vec::new(),
383            type_referenced_import_bindings: Vec::new(),
384            value_referenced_import_bindings: Vec::new(),
385            line_offsets: Vec::new(),
386            complexity: Vec::new(),
387            flag_uses: Vec::new(),
388            class_heritage: Vec::new(),
389            exported_factory_returns: Arc::default(),
390            exported_factory_return_object_shapes: Arc::default(),
391            type_member_types: Arc::default(),
392            injection_tokens: Vec::new(),
393            local_type_declarations: Vec::new(),
394            public_signature_type_references: Vec::new(),
395            namespace_object_aliases: Vec::new(),
396            iconify_prefixes: Vec::new(),
397            iconify_icon_names: Vec::new(),
398            auto_import_candidates: Vec::new(),
399            directives: Vec::new(),
400            client_only_dynamic_import_spans: Vec::new(),
401            security_sinks: Vec::new(),
402            security_sinks_skipped: 0,
403            security_unresolved_callee_sites: Vec::new(),
404            tainted_bindings: Vec::new(),
405            sanitized_sink_args: Vec::new(),
406            security_control_sites: Vec::new(),
407            callee_uses: Vec::new(),
408            misplaced_directives: Vec::new(),
409            inline_server_action_exports: Vec::new(),
410            di_key_sites: Vec::new(),
411            has_dynamic_provide: false,
412            referenced_import_bindings: Vec::new(),
413            component_props: Vec::new(),
414            has_props_attrs_fallthrough: false,
415            has_define_expose: false,
416            has_define_model: false,
417            has_unharvestable_props: false,
418            component_emits: Vec::new(),
419            angular_inputs: Vec::new(),
420            angular_outputs: Vec::new(),
421            has_unharvestable_emits: false,
422            has_dynamic_emit: false,
423            has_emit_whole_object_use: false,
424            load_return_keys: Vec::new(),
425            has_unharvestable_load: false,
426            has_load_data_whole_use: false,
427            has_page_data_store_whole_use: false,
428            has_route_loader_data_whole_use: false,
429            component_functions: Vec::new(),
430            react_props: Vec::new(),
431            hook_uses: Vec::new(),
432            render_edges: Vec::new(),
433            svelte_dispatched_events: Vec::new(),
434            svelte_listened_events: Vec::new(),
435            angular_component_selectors: Vec::new(),
436            registered_custom_elements: Vec::new(),
437            used_custom_element_tags: Vec::new(),
438            angular_used_selectors: Vec::new(),
439            angular_entry_component_refs: Vec::new(),
440            has_dynamic_component_render: false,
441            has_dynamic_dispatch: false,
442        }
443    }
444
445    /// Derive compact detector facts from resolution payload before sharing.
446    ///
447    /// Shared analysis sessions keep the source payload for later graph runs,
448    /// but detectors still require the same derived facts that the owned
449    /// release path computes before clearing that payload.
450    #[doc(hidden)]
451    pub fn prepare_analysis_facts(&mut self) {
452        // The analyze-layer `unrendered-component` detector needs the compact
453        // complement of imports and unused bindings after resolution.
454        self.referenced_import_bindings = self
455            .imports
456            .iter()
457            .map(|import| import.local_name.clone())
458            .filter(|name| !name.is_empty() && !self.unused_import_bindings.contains(name))
459            .collect();
460        self.referenced_import_bindings.sort_unstable();
461        self.referenced_import_bindings.dedup();
462
463        // The `unused-load-data-key` detector needs the project-wide signal
464        // after `whole_object_uses` is released from owned artifacts.
465        self.has_page_data_store_whole_use = self
466            .whole_object_uses
467            .iter()
468            .any(|name| name == "page.data" || name == "$page.data");
469        self.has_route_loader_data_whole_use = self
470            .whole_object_uses
471            .iter()
472            .any(|name| name == "$fallow.routeLoaderData");
473    }
474
475    /// Release extraction payload that resolution has already copied into the graph.
476    ///
477    /// This keeps fields needed by analysis, health, security, LSP, coverage,
478    /// and hash drift checks, while dropping vectors that otherwise duplicate
479    /// data owned by `ResolvedModule` or already credited into the module graph.
480    pub fn release_resolution_payload(&mut self) {
481        self.prepare_analysis_facts();
482        Self::release_vec(&mut self.dynamic_imports);
483        Self::release_vec(&mut self.require_calls);
484        Self::release_boxed_slice(&mut self.package_path_references);
485        Self::release_arc_slice(&mut self.whole_object_uses);
486        Self::release_vec(&mut self.unused_import_bindings);
487        Self::release_vec(&mut self.type_referenced_import_bindings);
488        Self::release_vec(&mut self.value_referenced_import_bindings);
489        Self::release_vec(&mut self.namespace_object_aliases);
490        Self::release_vec(&mut self.auto_import_candidates);
491    }
492
493    fn release_vec<T>(values: &mut Vec<T>) {
494        *values = Vec::new();
495    }
496
497    fn release_boxed_slice<T>(values: &mut Box<[T]>) {
498        *values = Box::default();
499    }
500
501    /// Drop this module's refcount; the allocation itself is freed only once
502    /// the sharing `ResolvedModule` releases its clone too.
503    fn release_arc_slice<T>(values: &mut Arc<[T]>) {
504        *values = Arc::default();
505    }
506}
507
508/// Defensive control family detected on a source to sink path.
509#[derive(
510    Debug,
511    Clone,
512    Copy,
513    PartialEq,
514    Eq,
515    PartialOrd,
516    Ord,
517    serde::Serialize,
518    serde::Deserialize,
519    bitcode::Encode,
520    bitcode::Decode,
521)]
522#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
523#[serde(rename_all = "kebab-case")]
524pub enum SecurityControlKind {
525    /// Sanitization or escaping before a sink.
526    Sanitization,
527    /// Input validation or schema parsing.
528    Validation,
529    /// Authentication check or middleware.
530    Authentication,
531    /// Authorization or permission check.
532    Authorization,
533}
534
535/// A known defensive control call site.
536#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
537pub struct SecurityControlSite {
538    /// Control family.
539    pub kind: SecurityControlKind,
540    /// Flattened callee path or a stable synthetic name for guard-derived
541    /// controls.
542    pub callee_path: String,
543    /// Byte offset of the control span start.
544    pub span_start: u32,
545    /// Byte offset of the control span end.
546    pub span_end: u32,
547}
548
549/// Sanitizer output domain. Kept intentionally narrow so a sanitizer for one
550/// domain cannot suppress a different sink family.
551#[derive(
552    Debug,
553    Clone,
554    Copy,
555    PartialEq,
556    Eq,
557    PartialOrd,
558    Ord,
559    serde::Serialize,
560    serde::Deserialize,
561    bitcode::Encode,
562    bitcode::Decode,
563)]
564pub enum SanitizerScope {
565    /// HTML markup sanitized by DOMPurify-compatible APIs.
566    Html,
567    /// URL or redirect target checked against a literal-backed allowlist.
568    Url,
569    /// Path value checked against a high-confidence containment guard.
570    Path,
571    /// SQL identifier quoted with a helper that doubles embedded identifier quotes.
572    SqlIdentifier,
573}
574
575/// A captured sink argument that is itself a recognized sanitizer call.
576#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
577pub struct SanitizedSinkArg {
578    /// Byte offset of the owning sink span start.
579    pub span_start: u32,
580    /// The positional argument index on the owning sink.
581    pub arg_index: u32,
582    /// The sanitizer output domain for this argument.
583    pub scope: SanitizerScope,
584}
585
586/// A local binding tied to the flattened member-access path it was initialized
587/// from. The analyze layer matches `source_path` against the data-driven source
588/// catalogue; when it matches, `local` is treated as carrying untrusted input.
589///
590/// Captured for two shapes: a direct assignment (`const id = req.query.id` ->
591/// `{ local: "id", source_path: "req.query" }`, the literal-key tail dropped so
592/// the path matches a catalogue prefix) and an object destructure
593/// (`const { id } = req.query` -> `{ local: "id", source_path: "req.query" }`).
594#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
595pub struct TaintedBinding {
596    /// The local binding name introduced by the declarator.
597    pub local: String,
598    /// The flattened object member-access path the binding was sourced from.
599    pub source_path: String,
600    /// Byte offset of the source read (the member-access expression the binding
601    /// was sourced from), so the analyze layer can anchor a taint trace's source
602    /// node at the real read line instead of the module import line. Stored as a
603    /// `u32` (not `Span`) to stay bitcode-encodable for the cache. `0` when no
604    /// concrete read expression is available (synthetic framework-param /
605    /// helper-return bindings), in which case the analyze layer falls back to the
606    /// sink site rather than claiming a spurious line.
607    pub source_span_start: u32,
608}
609
610/// Why a sink-shaped callee could not be flattened into a static catalogue
611/// path.
612#[derive(
613    Debug,
614    Clone,
615    Copy,
616    PartialEq,
617    Eq,
618    PartialOrd,
619    Ord,
620    serde::Serialize,
621    serde::Deserialize,
622    bitcode::Encode,
623    bitcode::Decode,
624)]
625#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
626#[serde(rename_all = "kebab-case")]
627pub enum SkippedSecurityCalleeReason {
628    /// A computed member access such as `client[method](input)`.
629    ComputedMember,
630    /// A dynamic non-member callee such as `(factory())(input)`.
631    DynamicDispatch,
632    /// An assignment target whose object could not be flattened.
633    UnsupportedAssignmentObject,
634}
635
636/// Syntactic expression shape for a skipped security callee.
637#[derive(
638    Debug,
639    Clone,
640    Copy,
641    PartialEq,
642    Eq,
643    PartialOrd,
644    Ord,
645    serde::Serialize,
646    serde::Deserialize,
647    bitcode::Encode,
648    bitcode::Decode,
649)]
650#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
651#[serde(rename_all = "kebab-case")]
652pub enum SkippedSecurityCalleeExpressionKind {
653    /// `obj.prop(...)`.
654    StaticMemberExpression,
655    /// `obj[prop](...)`.
656    ComputedMemberExpression,
657    /// A bare identifier or private identifier callee.
658    Identifier,
659    /// Any other call-like expression that cannot be represented compactly.
660    Other,
661}
662
663/// Span-only diagnostic for a skipped security callee inside one module.
664#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
665pub struct SkippedSecurityCalleeSite {
666    /// Why the callee was skipped.
667    pub reason: SkippedSecurityCalleeReason,
668    /// Compact expression shape of the skipped callee.
669    pub expression_kind: SkippedSecurityCalleeExpressionKind,
670    /// Start byte offset of the skipped callee expression.
671    pub span_start: u32,
672    /// End byte offset of the skipped callee expression.
673    pub span_end: u32,
674}
675
676/// The syntactic shape of a captured security sink site. Category-blind: the
677/// extractor records the shape and the dotted/bare callee path; the analyze
678/// layer matches it against the data-driven catalogue. See
679/// `crates/security/data/security_matchers.toml`.
680#[derive(
681    Debug,
682    Clone,
683    Copy,
684    PartialEq,
685    Eq,
686    serde::Serialize,
687    serde::Deserialize,
688    bitcode::Encode,
689    bitcode::Decode,
690)]
691pub enum SinkShape {
692    /// A call to a bare identifier (e.g. `eval(x)`).
693    Call,
694    /// A call to a dotted member path (e.g. `child_process.exec(x)`).
695    MemberCall,
696    /// An assignment to a member target (e.g. `el.innerHTML = x`).
697    MemberAssign,
698    /// A tagged template expression (e.g. ``sql`...${x}...` ``).
699    TaggedTemplate,
700    /// A JSX attribute value (e.g. `dangerouslySetInnerHTML={x}`).
701    JsxAttr,
702    /// A constructor call (e.g. `new Function("return x")`).
703    NewExpression,
704    /// A static string literal assigned to a secret-shaped identifier or known
705    /// provider credential prefix.
706    SecretLiteral,
707}
708
709/// The shape of the argument captured at a sink site. Category-blind like
710/// [`SinkShape`], but finer-grained: it lets the catalogue matcher require or
711/// exclude specific argument shapes. The discriminator is what distinguishes an
712/// unsafe SQL string concatenation or template-into-`.execute()` from a
713/// safely-parameterized `` sql`${x}` `` tagged template, an object-literal
714/// `.execute({ sql, args })` argument, or a literal-aware sink argument.
715#[derive(
716    Debug,
717    Clone,
718    Copy,
719    PartialEq,
720    Eq,
721    serde::Serialize,
722    serde::Deserialize,
723    bitcode::Encode,
724    bitcode::Decode,
725)]
726pub enum SinkArgKind {
727    /// A template literal with at least one `${...}` substitution (e.g.
728    /// `` `SELECT ${x}` ``). On a `tagged-template` shape this is the tag's
729    /// quasi; on a `call`/`member-call` shape it is the positional argument.
730    TemplateWithSubst,
731    /// A binary `+` string concatenation (e.g. `"SELECT " + x`).
732    Concat,
733    /// An object literal (e.g. `.execute({ sql, args })`, the parameterized form).
734    Object,
735    /// A call expression argument (e.g. `query(buildSql())`).
736    Call,
737    /// A literal argument admitted by a literal-aware security matcher.
738    Literal,
739    /// A zero-argument sink captured because the callee itself is the signal.
740    NoArg,
741    /// Any other non-literal expression (bare identifier, member access, etc.).
742    Other,
743}
744
745/// Static URL construction shape captured for URL-shaped security sinks.
746#[derive(
747    Debug,
748    Clone,
749    Copy,
750    PartialEq,
751    Eq,
752    serde::Serialize,
753    serde::Deserialize,
754    bitcode::Encode,
755    bitcode::Decode,
756)]
757#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
758#[serde(rename_all = "kebab-case")]
759pub enum SecurityUrlShape {
760    /// The sink target has a fixed origin, scheme, or relative root while only
761    /// path or query components are dynamic.
762    FixedOriginDynamicPath,
763    /// The sink target's scheme or origin is dynamic or opaque.
764    DynamicOrigin,
765}
766
767/// Literal values attached to literal-aware security sink captures.
768#[derive(
769    Debug,
770    Clone,
771    PartialEq,
772    Eq,
773    serde::Serialize,
774    serde::Deserialize,
775    bitcode::Encode,
776    bitcode::Decode,
777)]
778pub enum SinkLiteralValue {
779    /// A string literal value.
780    String(String),
781    /// An integer numeric literal value.
782    Integer(i64),
783    /// A boolean literal value.
784    Boolean(bool),
785    /// A null literal value.
786    Null,
787}
788
789/// Static object-literal property metadata attached to a captured sink
790/// argument. Nested object paths are flattened with dot-separated keys.
791#[derive(
792    Debug,
793    Clone,
794    PartialEq,
795    Eq,
796    serde::Serialize,
797    serde::Deserialize,
798    bitcode::Encode,
799    bitcode::Decode,
800)]
801pub struct SinkObjectProperty {
802    /// Static property name. Nested object properties use dot-separated paths.
803    pub key: String,
804    /// Literal property value when statically knowable.
805    pub value: SinkLiteralValue,
806}
807
808/// A captured sink site. The visitor records every existing non-literal call /
809/// member-assign / member-call / tagged-template / jsx-attr sink site, and a
810/// small allowlist of literal-aware sites where the literal value is the signal.
811/// It knows nothing about CWE categories.
812#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
813pub struct SinkSite {
814    /// The syntactic shape of the sink site.
815    pub sink_shape: SinkShape,
816    /// The flattened dotted/bare callee or member path.
817    pub callee_path: String,
818    /// The positional argument index. For zero-argument captures this is 0.
819    pub arg_index: u32,
820    /// Whether the relevant argument is non-literal. Existing non-literal
821    /// catalogue rows require this to remain true.
822    pub arg_is_non_literal: bool,
823    /// The finer-grained shape of the captured argument. Lets the catalogue
824    /// require unsafe shapes (concat / template-with-substitution / literal /
825    /// no-arg) and exclude safe ones (object literal, the parameterized form).
826    /// See [`SinkArgKind`].
827    pub arg_kind: SinkArgKind,
828    /// Literal argument value for literal-aware rows.
829    pub arg_literal: Option<SinkLiteralValue>,
830    /// Risky regex fragment for structural ReDoS candidates.
831    pub regex_pattern: Option<String>,
832    /// Static object-literal properties for option-object rows.
833    pub object_properties: Vec<SinkObjectProperty>,
834    /// Static top-level object-literal keys, including keys whose values are not
835    /// literal. Used by missing-option rows that only need key presence.
836    pub object_property_keys: Vec<String>,
837    /// Whether [`object_property_keys`](Self::object_property_keys) is complete.
838    /// False for non-object arguments and object literals with spread or
839    /// non-static keys, where a missing-key claim would be speculative.
840    pub object_property_keys_complete: bool,
841    /// Identifier names referenced anywhere inside the captured non-literal sink
842    /// argument, or contextual names for zero-argument captures such as a
843    /// token-like `Math.random()` assignment target. Deduped in source order.
844    /// Used by the analyze layer to back-trace the sink argument to a known
845    /// untrusted source or to apply narrow context gates. Intra-module,
846    /// name-based, conservative; it is never a taint proof.
847    pub arg_idents: Vec<String>,
848    /// Flattened static member paths referenced inside the captured non-literal
849    /// sink argument. Includes both the full path and source-object path for
850    /// leaf reads (`process.env.SECRET` records `process.env.SECRET` and
851    /// `process.env`) so direct source expressions can be matched without an
852    /// intermediate local binding.
853    pub arg_source_paths: Vec<String>,
854    /// Byte offset of the sink span start. Stored as `u32` (not `Span`) so the
855    /// struct is bitcode-encodable and can be persisted directly in the cache.
856    pub span_start: u32,
857    /// Byte offset of the sink span end.
858    pub span_end: u32,
859    /// The arg-0 URL string literal of a network-shaped call (`fetch`, `axios.*`,
860    /// `got`, ...), captured so the `secret-to-network` category (#890) can carry
861    /// a destination-host signal on its candidate: `Some(literal)` when the
862    /// destination is a static string literal (almost always intended auth, e.g.
863    /// the credential's own provider), `None` when it is dynamic (the suspicious
864    /// case). `None` for non-call sinks and calls with no arg 0.
865    pub url_arg_literal: Option<String>,
866    /// URL construction shape for URL-like sink arguments when the extractor can
867    /// classify it syntactically. `None` for non-URL sinks and URL expressions
868    /// whose shape is not visible at the sink.
869    pub url_shape: Option<SecurityUrlShape>,
870}
871
872impl SinkSite {
873    /// Reconstruct the source span from the stored byte offsets.
874    #[must_use]
875    pub fn span(&self) -> Span {
876        Span::new(self.span_start, self.span_end)
877    }
878}
879
880/// Env var-name prefixes that frameworks inline into the client bundle by
881/// convention. A read of one of these is normal and safe, so it does NOT count
882/// as a secret source (issue #890). Shared by the extract layer (so public env
883/// vars never become source signals) and the bespoke `client-server-leak` rule.
884pub const PUBLIC_ENV_PREFIXES: &[&str] = &[
885    "NEXT_PUBLIC_",
886    "VITE_",
887    "NUXT_PUBLIC_",
888    "REACT_APP_",
889    "PUBLIC_",
890    "GATSBY_",
891    "EXPO_PUBLIC_",
892    "STORYBOOK_",
893];
894
895/// Exact env var names that are public by convention (no prefix).
896pub const PUBLIC_ENV_EXACT: &[&str] = &["NODE_ENV"];
897
898/// Env var-name tokens that usually describe public build or deployment
899/// metadata rather than secrets. Secret-shaped names win over these tokens.
900pub const PUBLIC_ENV_METADATA_TOKENS: &[&str] =
901    &["BRANCH", "ENVIRONMENT", "MODE", "REF", "SHA", "TAG"];
902
903/// Env var-name tokens that should keep a variable source-backed even when the
904/// name also contains public metadata tokens such as `REF` or `SHA`.
905pub const SECRET_ENV_TOKENS: &[&str] = &[
906    "AUTH",
907    "CREDENTIAL",
908    "CREDENTIALS",
909    "KEY",
910    "PASS",
911    "PASSWORD",
912    "PRIVATE",
913    "SECRET",
914    "TOKEN",
915];
916
917fn env_name_has_token(name: &str, tokens: &[&str]) -> bool {
918    name.split(|ch: char| !ch.is_ascii_alphanumeric())
919        .filter(|part| !part.is_empty())
920        .any(|part| tokens.contains(&part))
921}
922
923/// Whether an env var name is public-by-convention (build-inlined into the
924/// client bundle), and therefore not a secret.
925#[must_use]
926pub fn is_public_env_var(name: &str) -> bool {
927    if PUBLIC_ENV_EXACT.contains(&name) || PUBLIC_ENV_PREFIXES.iter().any(|p| name.starts_with(p)) {
928        return true;
929    }
930    env_name_has_token(name, PUBLIC_ENV_METADATA_TOKENS)
931        && !env_name_has_token(name, SECRET_ENV_TOKENS)
932}
933
934/// Whether a flattened member path is a PUBLIC env-secret read
935/// (`process.env.NEXT_PUBLIC_X`, `import.meta.env.VITE_Y`), which must not be
936/// recorded as a secret source. Non-env paths (`req.query.id`) are never public.
937#[must_use]
938pub fn is_public_env_path(path: &str) -> bool {
939    for object in ["process.env.", "import.meta.env."] {
940        if let Some(var) = path.strip_prefix(object) {
941            return is_public_env_var(var);
942        }
943    }
944    false
945}
946
947/// One alias entry tying an exported object's dotted property path to a namespace import.
948#[derive(Debug, Clone)]
949pub struct NamespaceObjectAlias {
950    /// Canonical export name.
951    pub via_export_name: String,
952    /// Dotted suffix of the property path relative to the export.
953    pub suffix: String,
954    /// Local name of the namespace import.
955    pub namespace_local: String,
956}
957
958/// Compute a table of line-start byte offsets from source text.
959#[must_use]
960#[expect(
961    clippy::cast_possible_truncation,
962    reason = "source files are practically < 4GB"
963)]
964pub fn compute_line_offsets(source: &str) -> Vec<u32> {
965    let mut offsets = vec![0u32];
966    for (i, byte) in source.bytes().enumerate() {
967        if byte == b'\n' {
968            debug_assert!(
969                u32::try_from(i + 1).is_ok(),
970                "source file exceeds u32::MAX bytes: line offsets would overflow"
971            );
972            offsets.push((i + 1) as u32);
973        }
974    }
975    offsets
976}
977
978/// Convert a byte offset to a 1-based line number and 0-based byte column.
979#[must_use]
980#[expect(
981    clippy::cast_possible_truncation,
982    reason = "line count is bounded by source size"
983)]
984pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
985    let line_idx = match line_offsets.binary_search(&byte_offset) {
986        Ok(idx) => idx,
987        Err(idx) => idx.saturating_sub(1),
988    };
989    let line = line_idx as u32 + 1;
990    let col = byte_offset - line_offsets[line_idx];
991    (line, col)
992}
993
994/// True when `name` identifies a synthetic template-family complexity unit:
995/// the per-file `<template>` unit every framework template scanner emits, or a
996/// Svelte `<snippet:NAME>` unit. These units are exercised only through their
997/// component, carry no directly measurable test coverage, and are therefore
998/// excluded from the CRAP dimension. The `<component>` rollup is NOT part of
999/// this family: it is an aggregate over class + template findings, not an
1000/// extracted unit.
1001#[must_use]
1002pub fn is_synthetic_template_unit(name: &str) -> bool {
1003    name == "<template>" || name.starts_with("<snippet:")
1004}
1005
1006/// Emitted name of the synthetic module-scope complexity unit.
1007pub const MODULE_UNIT_NAME: &str = "<module>";
1008
1009/// True when `name` identifies the synthetic per-file module-scope unit.
1010///
1011/// Extraction pushes one root frame per program so decision points outside
1012/// every function (an environment guard, a top-level `??` / `||` default, an
1013/// `?.` access on a config object) are counted instead of silently dropped.
1014/// The unit is emitted only when it actually branches, and it is
1015/// aggregate-only: it feeds vital signs, file scores, and branching
1016/// conservation, and never becomes a user-facing finding. "Extract a helper"
1017/// is not advice that applies to module scope, so the unit reports the
1018/// quantity without asking anyone to act on it.
1019///
1020/// Deliberately NOT part of [`is_synthetic_template_unit`]. That predicate
1021/// carries template-family CRAP, suppression, and display semantics, and
1022/// [`FileBranching::from_units`] filters on it: folding `<module>` in would
1023/// exclude module-scope branching from the branching totals, which is the
1024/// blind spot this unit exists to close.
1025#[must_use]
1026pub fn is_synthetic_module_unit(name: &str) -> bool {
1027    name == MODULE_UNIT_NAME
1028}
1029
1030/// Branching totals for one file: the quantity that survives extraction, and
1031/// the number of units now holding it.
1032///
1033/// A per-function cyclomatic ceiling constrains a partition, not a quantity.
1034/// `McCabe` gives a function `1 + one increment per decision point`, so across
1035/// a set of units the summed cyclomatic score is `functions + branch_points`.
1036/// Moving an `if` from one function into a new one removes an increment from
1037/// the first and adds it to the second: `branch_points` is unchanged and
1038/// `functions` rises. Reporting the two terms separately is what distinguishes
1039/// branching that left from branching that only moved.
1040///
1041/// Module scope is accounted for. Extraction pushes a root frame per program
1042/// and emits it as a synthetic `<module>` unit whenever it branches, so a
1043/// branch hoisted out of a function to the top level of the module keeps its
1044/// increment in `branch_points` and adds one to `functions`, exactly as moving
1045/// it into a new function would. A fall in `branch_points` is therefore a
1046/// statement about the file's measured decision points, not about a partition.
1047#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
1048pub struct FileBranching {
1049    /// Summed weight of `Cyclomatic` contributions. The conserved quantity.
1050    pub branch_points: u32,
1051    /// Number of accounted units. The tax a split adds.
1052    pub functions: u32,
1053    /// Highest single-unit cyclomatic score. Reported, never a verdict input:
1054    /// a split lowers it by construction.
1055    pub peak_cyclomatic: u16,
1056    /// Summed weight of `Cognitive` contributions, excluding `PropCount` and
1057    /// `HookDensity`. Both are cognitive-only with no cyclomatic counterpart,
1058    /// and `PropCount` records the excess over a floor, so it is superlinear in
1059    /// a split: one 14-prop component contributes `+10` while the same props
1060    /// across two 7-prop components contribute `+3` and `+3`. Including them
1061    /// would move this number with both `branch_points` and nesting flat.
1062    /// This therefore does NOT equal the sum of `FunctionComplexity::cognitive`.
1063    pub cognitive: u32,
1064    /// Summed `nesting` over the same cognitive contributions. Extraction
1065    /// rebases nesting to zero on every new frame, so a cognitive improvement
1066    /// that shows up here and not in `branch_points` came from repartitioning,
1067    /// not from removing branching.
1068    pub cognitive_nesting_weight: u32,
1069    /// Whether one of the counted units is the module body. Its branching is
1070    /// real and belongs in `branch_points`, but it is not a function anyone
1071    /// split into, so a consumer judging whether a file was split must not
1072    /// count it towards the function tax.
1073    pub has_module_unit: bool,
1074    /// Whether the file carried a synthetic template unit that these counts
1075    /// exclude. When it did, the numbers describe the file's script only, so a
1076    /// consumer must not present them as describing the whole file. The
1077    /// synthetic `<module>` unit is NOT a template unit and does not set this
1078    /// flag: it is counted like any other unit, because module-scope branching
1079    /// is part of the script the numbers describe.
1080    pub has_synthetic_units: bool,
1081}
1082
1083impl FileBranching {
1084    /// Aggregate one file's units.
1085    ///
1086    /// Synthetic template units are excluded: they are suppressed entirely when
1087    /// trivial, which would silently move any denominator that counted them.
1088    /// Suppression is deliberately not consulted, so a
1089    /// `fallow-ignore-next-line complexity` comment cannot remove a unit's
1090    /// branches from the total.
1091    ///
1092    /// The synthetic `<module>` unit IS counted. It carries the file's
1093    /// module-scope decision points, and leaving it out would restore the blind
1094    /// spot that let a branch disappear from the total by being hoisted out of
1095    /// every function.
1096    #[must_use]
1097    pub fn from_units(units: &[FunctionComplexity]) -> Self {
1098        let mut totals = Self {
1099            has_module_unit: units
1100                .iter()
1101                .any(|unit| is_synthetic_module_unit(&unit.name)),
1102            has_synthetic_units: units
1103                .iter()
1104                .any(|unit| is_synthetic_template_unit(&unit.name)),
1105            ..Self::default()
1106        };
1107        for unit in units
1108            .iter()
1109            .filter(|unit| !is_synthetic_template_unit(&unit.name))
1110        {
1111            totals.functions += 1;
1112            totals.peak_cyclomatic = totals.peak_cyclomatic.max(unit.cyclomatic);
1113            for contribution in &unit.contributions {
1114                match contribution.metric {
1115                    ComplexityMetric::Cyclomatic => {
1116                        totals.branch_points += u32::from(contribution.weight);
1117                    }
1118                    ComplexityMetric::Cognitive => {
1119                        if matches!(
1120                            contribution.kind,
1121                            ComplexityContributionKind::PropCount
1122                                | ComplexityContributionKind::HookDensity
1123                        ) {
1124                            continue;
1125                        }
1126                        totals.cognitive += u32::from(contribution.weight);
1127                        totals.cognitive_nesting_weight += u32::from(contribution.nesting);
1128                    }
1129                }
1130            }
1131        }
1132        totals
1133    }
1134
1135    /// Summed cyclomatic score implied by the identity `functions + branch_points`.
1136    ///
1137    /// Equals the direct sum of `FunctionComplexity::cyclomatic` over the same
1138    /// units unless a unit saturated `u16`, which is the one way the identity
1139    /// can break.
1140    #[must_use]
1141    pub const fn implied_cyclomatic(&self) -> u32 {
1142        self.functions + self.branch_points
1143    }
1144}
1145
1146/// Complexity metrics for a single function/method/arrow.
1147#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
1148pub struct FunctionComplexity {
1149    /// Function name (or `"<anonymous>"` for unnamed functions/arrows).
1150    pub name: String,
1151    /// Whether this function is an ECMAScript `#`-private class member.
1152    ///
1153    /// Kept separately from `name` because a public string-named method may
1154    /// legally begin with `#` and remains eligible for runtime coverage.
1155    pub is_private_member: bool,
1156    /// 1-based line number where the function starts.
1157    pub line: u32,
1158    /// 0-based byte column where the function starts.
1159    pub col: u32,
1160    /// `McCabe` cyclomatic complexity (1 + decision points).
1161    pub cyclomatic: u16,
1162    /// `SonarSource` cognitive complexity (structural + nesting penalty).
1163    pub cognitive: u16,
1164    /// Number of lines in the function body.
1165    pub line_count: u32,
1166    /// Number of parameters (excluding TypeScript's `this` parameter).
1167    pub param_count: u8,
1168    /// Number of React hook calls (`useState` / `useEffect` / `useMemo` /
1169    /// `useCallback` / custom `use*`) made directly in this function's body.
1170    /// Non-zero only for React components/hooks; descriptive context surfaced in
1171    /// the hotspot drill-down, never a tunable threshold (anti-numerology).
1172    pub react_hook_count: u16,
1173    /// Maximum JSX element nesting depth reached in this function's body (the
1174    /// deepest chain of element-inside-element). `0` when the function renders
1175    /// no JSX. Descriptive context surfaced in the hotspot drill-down, never a
1176    /// tunable threshold (anti-numerology).
1177    pub react_jsx_max_depth: u16,
1178    /// Number of props destructured from this component's first parameter (the
1179    /// `{ a, b, c }` props object). `0` for non-component functions and for
1180    /// components taking a bare `props` identifier (not statically countable).
1181    /// Descriptive context surfaced in the hotspot drill-down, never a tunable
1182    /// threshold (anti-numerology).
1183    pub react_prop_count: u16,
1184    /// Content digest of the function's full-span source slice.
1185    pub source_hash: Option<String>,
1186    /// Per-decision-point breakdown explaining WHICH constructs drove the
1187    /// cyclomatic and cognitive scores. One entry per increment event (an `if`
1188    /// emits one cyclomatic and one cognitive entry at the same line, because
1189    /// the two metrics accrue at different granularities). Always computed and
1190    /// cached; surfaced in JSON only behind `health --complexity-breakdown`.
1191    pub contributions: Vec<ComplexityContribution>,
1192}
1193
1194/// Structural CSS metrics for a single style rule, computed from the parsed CSS
1195/// syntax tree. A rule is recorded only when it crosses a structural floor (an
1196/// id selector, a complex selector, a `!important` declaration, or deep
1197/// nesting), so the vector stays bounded on normal stylesheets.
1198///
1199/// Not persisted in the extraction cache: `fallow health` computes these
1200/// on demand from the CSS source, so there is no `bitcode` derive.
1201#[derive(Debug, Clone, serde::Serialize)]
1202#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1203pub struct CssRuleMetric {
1204    /// 1-based line of the rule's first selector.
1205    pub line: u32,
1206    /// 1-based column of the rule's first selector.
1207    pub col: u32,
1208    /// Specificity component `a` (id selectors), max across the rule's selectors.
1209    pub specificity_a: u16,
1210    /// Specificity component `b` (class / attribute / pseudo-class selectors).
1211    pub specificity_b: u16,
1212    /// Specificity component `c` (type / pseudo-element selectors).
1213    pub specificity_c: u16,
1214    /// Largest selector component count across the rule's selector list.
1215    pub complexity: u16,
1216    /// Declaration count in the rule (normal plus `!important`).
1217    pub declaration_count: u16,
1218    /// `!important` declaration count in the rule.
1219    pub important_count: u16,
1220    /// Style-rule nesting depth (0 = top level).
1221    pub nesting_depth: u8,
1222}
1223
1224/// A style rule's declaration-block fingerprint and location, for cross-file
1225/// duplicate-block detection. Only rules with a meaningful number of
1226/// declarations are recorded (small blocks repeat legitimately). Internal
1227/// staging only: this is consumed in-process by the health layer to build the
1228/// grouped `duplicate_declaration_blocks` output and is never serialized.
1229#[derive(Debug, Clone)]
1230pub struct CssDeclarationBlock {
1231    /// xxh3 fingerprint over the rule's normalized (sorted, `!important`-tagged)
1232    /// declaration set.
1233    pub fingerprint: u64,
1234    /// 1-based line of the rule's first selector.
1235    pub line: u32,
1236    /// Declaration count in the rule (normal plus `!important`).
1237    pub declaration_count: u16,
1238}
1239
1240/// Located raw styling value authored directly in CSS rather than via a
1241/// custom property or design-token helper. Internal staging for the health
1242/// layer; public output adds actions and confidence.
1243#[derive(Debug, Clone, PartialEq, Eq)]
1244pub struct CssRawStyleValue {
1245    /// Value axis, e.g. `color`, `font-size`, `line-height`, `radius`, or `shadow`.
1246    pub axis: String,
1247    /// CSS property where the value appears.
1248    pub property: String,
1249    /// Rendered declaration value.
1250    pub value: String,
1251    /// 1-based line of the containing style rule.
1252    pub line: u32,
1253}
1254
1255/// Located CSS custom-property definition with its rendered value. Internal
1256/// staging for design-token reuse suggestions in the health layer.
1257#[derive(Debug, Clone, PartialEq, Eq)]
1258pub struct CssCustomPropertyDefinition {
1259    /// Custom property name, including the leading `--`.
1260    pub name: String,
1261    /// Rendered custom property value.
1262    pub value: String,
1263    /// 1-based line of the containing style rule.
1264    pub line: u32,
1265}
1266
1267/// Stylesheet-level structural CSS analytics, computed from the parsed CSS
1268/// syntax tree. Feeds `fallow health` penalty weights and located findings,
1269/// never a standalone CSS score.
1270#[derive(Debug, Clone, Default, serde::Serialize)]
1271#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1272pub struct CssAnalytics {
1273    /// Total declarations across every style rule (normal plus `!important`).
1274    pub total_declarations: u32,
1275    /// Total `!important` declarations across every style rule.
1276    pub important_declarations: u32,
1277    /// Number of style rules.
1278    pub rule_count: u32,
1279    /// Number of style rules with no declarations.
1280    pub empty_rule_count: u32,
1281    /// Deepest style-rule nesting depth observed (0 = no nesting).
1282    pub max_nesting_depth: u8,
1283    /// Rules that crossed the structural floor, in source order. Bounded; see
1284    /// [`Self::notable_truncated`]. The scalar aggregates above always reflect
1285    /// the full stylesheet regardless of truncation.
1286    pub notable_rules: Vec<CssRuleMetric>,
1287    /// `true` when more rules crossed the structural floor than `notable_rules`
1288    /// retains (compiled utility CSS can emit thousands of `!important` rules),
1289    /// so consumers can note that per-rule findings were capped.
1290    pub notable_truncated: bool,
1291    /// Distinct color VALUES in the stylesheet, sorted (a palette-size /
1292    /// design-token-sprawl signal). The parser canonicalizes notation, so the
1293    /// authored format is NOT preserved: `red`, `#f00`, `#ff0000`, and
1294    /// `rgb(255,0,0)` all collapse to one entry, and every legacy sRGB notation
1295    /// renders as hex. Notation-MIXING (hex vs rgb vs hsl) is therefore not
1296    /// detectable from this set; it would need a separate raw-token pass.
1297    pub colors: Vec<String>,
1298    /// Distinct `font-size` declaration values in the stylesheet, sorted.
1299    pub font_sizes: Vec<String>,
1300    /// Distinct `z-index` declaration values in the stylesheet, sorted.
1301    pub z_indexes: Vec<String>,
1302    /// Distinct `box-shadow` declaration values in the stylesheet, sorted. A
1303    /// high count signals an uncontrolled shadow scale (design-token sprawl).
1304    pub box_shadows: Vec<String>,
1305    /// Distinct `border-radius` declaration values in the stylesheet, sorted.
1306    pub border_radii: Vec<String>,
1307    /// Distinct `line-height` declaration values in the stylesheet, sorted.
1308    pub line_heights: Vec<String>,
1309    /// Bounded located raw styling values that bypass custom properties or
1310    /// token helpers. These are conservative declaration-level candidates for
1311    /// audit introduced-vs-base gating.
1312    #[serde(skip)]
1313    #[cfg_attr(feature = "schema", schemars(skip))]
1314    pub raw_style_values: Vec<CssRawStyleValue>,
1315    /// Located custom-property definitions with values. Internal staging
1316    /// consumed by the health layer for nearest-token suggestions.
1317    #[serde(skip)]
1318    #[cfg_attr(feature = "schema", schemars(skip))]
1319    pub custom_property_definitions: Vec<CssCustomPropertyDefinition>,
1320    /// Distinct custom properties (`--x`) DEFINED in the stylesheet, sorted.
1321    pub defined_custom_properties: Vec<String>,
1322    /// Distinct custom properties REFERENCED via `var()` in the stylesheet.
1323    pub referenced_custom_properties: Vec<String>,
1324    /// Distinct `@keyframes` names DEFINED in the stylesheet, sorted.
1325    pub defined_keyframes: Vec<String>,
1326    /// Distinct `@keyframes` names REFERENCED via `animation` / `animation-name`.
1327    pub referenced_keyframes: Vec<String>,
1328    /// Distinct custom properties REGISTERED via an `@property` rule, sorted.
1329    pub registered_custom_properties: Vec<String>,
1330    /// Distinct cascade layers DECLARED (via `@layer a, b;` statements or named
1331    /// `@layer a { }` blocks), sorted.
1332    pub declared_layers: Vec<String>,
1333    /// Distinct cascade layers POPULATED by a named `@layer a { }` block, sorted.
1334    /// A layer declared but never populated (and not imported into) is a
1335    /// cleanup candidate.
1336    pub populated_layers: Vec<String>,
1337    /// Distinct font families DECLARED by an `@font-face` rule in the stylesheet,
1338    /// sorted. A declared family referenced by no `font-family` anywhere is a
1339    /// dead web-font payload (cleanup candidate).
1340    pub defined_font_faces: Vec<String>,
1341    /// Distinct font families REFERENCED via `font-family` / `font` in the
1342    /// stylesheet, sorted (generic keywords like `serif` excluded).
1343    pub referenced_font_families: Vec<String>,
1344    /// Per-rule declaration-block fingerprints for rules at or above the minimum
1345    /// block size, used to detect duplicate declaration blocks across the
1346    /// project. Internal staging consumed by the health layer; never serialized
1347    /// (the public output is the grouped `duplicate_declaration_blocks`).
1348    #[serde(skip)]
1349    #[cfg_attr(feature = "schema", schemars(skip))]
1350    pub declaration_blocks: Vec<CssDeclarationBlock>,
1351}
1352
1353/// Which complexity metric a [`ComplexityContribution`] adds to.
1354#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
1355#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1356#[serde(rename_all = "kebab-case")]
1357pub enum ComplexityMetric {
1358    /// `McCabe` cyclomatic complexity (independent execution paths).
1359    Cyclomatic,
1360    /// `SonarSource` cognitive complexity (structural + nesting penalty).
1361    Cognitive,
1362}
1363
1364/// The syntactic construct that produced a single complexity increment.
1365///
1366/// Mirrors `SonarSource` cognitive-complexity vocabulary where it overlaps.
1367/// `Case` means a `case` label carrying a test; a bare `default` adds nothing
1368/// to cyclomatic complexity and so produces no contribution.
1369#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
1370#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1371#[serde(rename_all = "kebab-case")]
1372#[non_exhaustive]
1373pub enum ComplexityContributionKind {
1374    /// An `if` condition.
1375    If,
1376    /// A bare `else` branch (cognitive only).
1377    Else,
1378    /// An `else if` continuation (both metrics: cyclomatic +1, cognitive flat
1379    /// +1 with no nesting penalty).
1380    ElseIf,
1381    /// A `?:` conditional (ternary) expression.
1382    Ternary,
1383    /// A logical `&&` operator.
1384    LogicalAnd,
1385    /// A logical `||` operator.
1386    LogicalOr,
1387    /// A `??` nullish-coalescing operator.
1388    NullishCoalescing,
1389    /// A logical assignment operator (`&&=`, `||=`, `??=`); cyclomatic only.
1390    LogicalAssignment,
1391    /// An optional-chaining link (`?.`); cyclomatic only.
1392    OptionalChain,
1393    /// A `for` loop.
1394    For,
1395    /// A `for...in` loop.
1396    ForIn,
1397    /// A `for...of` loop.
1398    ForOf,
1399    /// A `while` loop.
1400    While,
1401    /// A `do...while` loop.
1402    DoWhile,
1403    /// A `switch` statement (cognitive only; each `case` adds cyclomatic).
1404    Switch,
1405    /// A `case` label carrying a test (cyclomatic only).
1406    Case,
1407    /// A `catch` clause.
1408    Catch,
1409    /// A labeled `break` (cognitive only).
1410    LabeledBreak,
1411    /// A labeled `continue` (cognitive only).
1412    LabeledContinue,
1413    /// Legacy JSX-depth contribution kind kept for schema compatibility. Current
1414    /// extraction records JSX nesting as descriptive `react_jsx_max_depth`
1415    /// context and does not emit this kind for layout depth.
1416    JsxDepth,
1417    /// React hook density (cognitive only). One contribution per hook call in a
1418    /// component body (`useState` / `useEffect` / `useMemo` / `useCallback` /
1419    /// custom `use*`); a hook-heavy component accrues cognitive load the same way
1420    /// branching does.
1421    HookDensity,
1422    /// React prop count past the comfortable floor (cognitive only). A component
1423    /// destructuring many props is doing many things; the props beyond the floor
1424    /// fold into cognitive so a wide-interface component surfaces as a hotspot.
1425    PropCount,
1426    /// A Svelte `{#await}` block.
1427    Await,
1428    /// A Svelte `{:then}` continuation.
1429    Then,
1430}
1431
1432/// A single complexity increment, located at its source line/column.
1433///
1434/// `weight` is the amount this construct added to `metric`; for nested
1435/// cognitive increments `weight == 1 + nesting`. Consumers that render inline
1436/// (the VS Code editor breakdown) group contributions by `line` and sum the
1437/// weights, deferring the per-kind list to a hover.
1438#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
1439#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1440pub struct ComplexityContribution {
1441    /// 1-based line number where the construct begins.
1442    pub line: u32,
1443    /// 0-based byte column where the construct begins.
1444    pub col: u32,
1445    /// Which metric this increment contributes to.
1446    pub metric: ComplexityMetric,
1447    /// The syntactic construct responsible for the increment.
1448    pub kind: ComplexityContributionKind,
1449    /// The amount added to `metric` at this site (`1 + nesting` for nested
1450    /// cognitive increments, otherwise `1`).
1451    pub weight: u16,
1452    /// The nesting depth at the increment site (`0` when not nested). Lets a
1453    /// consumer explain a cognitive `+3` as "+1 base, +2 nesting".
1454    pub nesting: u16,
1455}
1456
1457/// The kind of feature flag pattern detected.
1458#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
1459pub enum FlagUseKind {
1460    /// `process.env.FEATURE_X` pattern.
1461    EnvVar,
1462    /// SDK function call like `useFlag('name')`.
1463    SdkCall,
1464    /// Config object access like `config.features.x`.
1465    ConfigObject,
1466}
1467
1468/// A feature flag use site.
1469#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
1470pub struct FlagUse {
1471    /// Flag identifier.
1472    pub flag_name: String,
1473    /// Detection kind.
1474    pub kind: FlagUseKind,
1475    /// 1-based line number.
1476    pub line: u32,
1477    /// 0-based byte column offset.
1478    pub col: u32,
1479    /// Start byte offset of the guarded block.
1480    pub guard_span_start: Option<u32>,
1481    /// End byte offset of the guarded block.
1482    pub guard_span_end: Option<u32>,
1483    /// SDK/provider name.
1484    pub sdk_name: Option<String>,
1485}
1486
1487const _: () = assert!(std::mem::size_of::<FlagUse>() <= 96);
1488
1489/// The runtime mechanism used to load a module.
1490#[derive(
1491    Debug,
1492    Clone,
1493    Copy,
1494    PartialEq,
1495    Eq,
1496    Hash,
1497    serde::Serialize,
1498    serde::Deserialize,
1499    bitcode::Encode,
1500    bitcode::Decode,
1501)]
1502#[repr(u8)]
1503pub enum ModuleLoadMechanism {
1504    /// ECMAScript module loading through imports, re-exports, or import globs.
1505    EsModule = 0,
1506    /// CommonJS module loading through `require()` or `require.context`.
1507    CommonJsRequire = 1,
1508}
1509
1510/// A dynamic import with a partially resolved pattern.
1511#[derive(Debug, Clone)]
1512pub struct DynamicImportPattern {
1513    /// Static prefix of the import path (e.g., "./locales/"). May contain glob characters.
1514    pub prefix: String,
1515    /// Static suffix of the import path (e.g., ".json"), if any.
1516    pub suffix: Option<String>,
1517    /// Source span in the original file.
1518    pub span: Span,
1519    /// Runtime mechanism used to load modules matching this pattern.
1520    pub mechanism: ModuleLoadMechanism,
1521}
1522
1523/// Visibility tag from JSDoc/TSDoc comments that suppresses unused-export detection.
1524#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1525#[serde(rename_all = "lowercase")]
1526#[repr(u8)]
1527pub enum VisibilityTag {
1528    /// No visibility tag present.
1529    #[default]
1530    None = 0,
1531    /// `@public` or `@api public` -- part of the public API surface.
1532    Public = 1,
1533    /// `@internal` -- exported for internal use (sister packages, build tools).
1534    Internal = 2,
1535    /// `@beta` -- public but unstable, may change without notice.
1536    Beta = 3,
1537    /// `@alpha` -- early preview, may change drastically without notice.
1538    Alpha = 4,
1539    /// `@expected-unused` -- intentionally unused, should warn when it becomes used.
1540    ExpectedUnused = 5,
1541}
1542
1543impl VisibilityTag {
1544    /// Whether this tag permanently suppresses unused-export detection.
1545    /// `ExpectedUnused` is handled separately (conditionally suppresses,
1546    /// reports stale when the export becomes used).
1547    pub const fn suppresses_unused(self) -> bool {
1548        matches!(
1549            self,
1550            Self::Public | Self::Internal | Self::Beta | Self::Alpha
1551        )
1552    }
1553
1554    /// For serde `skip_serializing_if`.
1555    pub fn is_none(&self) -> bool {
1556        matches!(self, Self::None)
1557    }
1558}
1559
1560/// An export declaration.
1561#[derive(Debug, Clone, serde::Serialize)]
1562pub struct ExportInfo {
1563    /// The exported name (named or default).
1564    pub name: ExportName,
1565    /// The local binding name, if different from the exported name.
1566    pub local_name: Option<String>,
1567    /// Whether this is a type-only export (`export type`).
1568    pub is_type_only: bool,
1569    /// Whether this export is registered through a runtime side effect at module load time.
1570    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1571    pub is_side_effect_used: bool,
1572    /// Visibility tag from JSDoc/TSDoc comment.
1573    #[serde(default, skip_serializing_if = "VisibilityTag::is_none")]
1574    pub visibility: VisibilityTag,
1575    /// Human-authored reason on `@expected-unused -- <reason>`, when present.
1576    #[serde(default, skip_serializing_if = "Option::is_none")]
1577    pub expected_unused_reason: Option<String>,
1578    /// Source span of the export declaration.
1579    #[serde(serialize_with = "serialize_span")]
1580    pub span: Span,
1581    /// Members of this export (for enums, classes, and namespaces).
1582    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1583    pub members: Vec<MemberInfo>,
1584    /// The local name of the parent class from `extends` clause, if any.
1585    #[serde(default, skip_serializing_if = "Option::is_none")]
1586    pub super_class: Option<String>,
1587}
1588
1589/// Additional heritage metadata for an exported class.
1590#[derive(
1591    Debug,
1592    Clone,
1593    serde::Serialize,
1594    serde::Deserialize,
1595    bitcode::Encode,
1596    bitcode::Decode,
1597    PartialEq,
1598    Eq,
1599)]
1600pub struct ClassHeritageInfo {
1601    /// Export name (`default` for default-exported classes).
1602    pub export_name: String,
1603    /// Parent class name from the `extends` clause, if any.
1604    pub super_class: Option<String>,
1605    /// Interface names from the class `implements` clause.
1606    pub implements: Vec<String>,
1607    /// Ordered class type-parameter names used to compose concrete arguments
1608    /// through multi-hop inheritance.
1609    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1610    pub type_parameters: Vec<String>,
1611    /// Typed instance bindings used to resolve member-access chains in external templates.
1612    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1613    pub instance_bindings: Vec<(String, String)>,
1614    /// Positional type arguments on the `extends` clause (the `<DerivedClient>`
1615    /// in `extends BaseService<DerivedClient>`); an empty string marks a
1616    /// positional arg that is not a plain type reference. Lets the analyze layer
1617    /// substitute a base class's generic instance-binding field type with the
1618    /// subclass's concrete type argument (issue #1910).
1619    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1620    pub super_class_type_args: Vec<String>,
1621    /// Instance-binding fields whose annotation is exactly a class type
1622    /// parameter, as `(field_name, type_param_index)`. Lets an inherited generic
1623    /// property resolve to the subclass's concrete type argument rather than the
1624    /// constraint (issue #1910).
1625    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1626    pub generic_instance_bindings: Vec<(String, usize)>,
1627}
1628
1629/// An exported free-function factory proven to return one class instance.
1630///
1631/// `export function useApi() { return new RESTApi() }` records
1632/// `FactoryReturnExport { export_name: "useApi", class_local_name: "RESTApi" }`.
1633/// The `class_local_name` is the factory module's own LOCAL name, resolved at
1634/// analyze time through that module's imports/exports to the real class export,
1635/// so a cross-module `const x = useApi(); x.member` consumer credits the class
1636/// across the boundary. See issue #1441 (Part A).
1637#[derive(
1638    Debug,
1639    Clone,
1640    serde::Serialize,
1641    serde::Deserialize,
1642    bitcode::Encode,
1643    bitcode::Decode,
1644    PartialEq,
1645    Eq,
1646)]
1647pub struct FactoryReturnExport {
1648    /// Public export name (honors `export { useApi as useRestApi }`).
1649    pub export_name: String,
1650    /// The returned class's local name within the factory module.
1651    pub class_local_name: String,
1652}
1653
1654/// One resolved property of an object-literal factory return: a dotted property
1655/// path mapped to the class the value at that path is an instance of.
1656///
1657/// `return { invoke: { orders: factory.ordersPage } }` records
1658/// `{ property_path: "invoke.orders", class_local_name: "OrdersPage" }`. The class
1659/// name is the factory module's own LOCAL name, resolved at analyze time through the
1660/// factory module's imports to the real class export. See issue #1858.
1661#[derive(
1662    Debug,
1663    Clone,
1664    serde::Serialize,
1665    serde::Deserialize,
1666    bitcode::Encode,
1667    bitcode::Decode,
1668    PartialEq,
1669    Eq,
1670)]
1671pub struct FactoryReturnObjectProperty {
1672    /// Dotted property path from the returned object literal (`orders`, `invoke.orders`).
1673    pub property_path: String,
1674    /// The property value's class local name within the factory module.
1675    pub class_local_name: String,
1676}
1677
1678/// An exported factory function that returns an object literal whose property
1679/// values are class instances, joined to its public export name.
1680///
1681/// A cross-module `const ui = createUi(); ui.orders.member` consumer emits a
1682/// `FactoryReturnObjectPropertyAccess` fact; the analyze layer resolves `export_name`
1683/// through the consumer's imports to this module, matches `property_path`, and credits
1684/// `member` on the resolved class (gated on it being a class with members). See issue #1858.
1685#[derive(
1686    Debug,
1687    Clone,
1688    serde::Serialize,
1689    serde::Deserialize,
1690    bitcode::Encode,
1691    bitcode::Decode,
1692    PartialEq,
1693    Eq,
1694)]
1695pub struct FactoryReturnObjectShapeExport {
1696    /// Public export name (honors `export { createUi as createOrdersUi }`).
1697    pub export_name: String,
1698    /// Resolved `(property_path -> class_local_name)` entries for the returned literal.
1699    pub properties: Box<[FactoryReturnObjectProperty]>,
1700}
1701
1702/// A named-type property whose declared type is a named type reference.
1703///
1704/// `interface Opts { c: OptDep }` (or `type Opts = { c: OptDep }`) records
1705/// `TypeMemberTypeEntry { type_name: "Opts", property: "c", property_type: "OptDep" }`.
1706/// Both `type_name` and `property_type` are the DECLARING module's own local
1707/// names; resolution through that module's imports/exports is deferred to
1708/// analyze time, mirroring `FactoryReturnExport.class_local_name`. Consumed by
1709/// the `unused-class-member` typed-property-hop join so a consumer's
1710/// `this.opts.c.optM()` credits `OptDep.optM` across module boundaries.
1711/// See issue #1785.
1712#[derive(
1713    Debug,
1714    Clone,
1715    serde::Serialize,
1716    serde::Deserialize,
1717    bitcode::Encode,
1718    bitcode::Decode,
1719    PartialEq,
1720    Eq,
1721)]
1722pub struct TypeMemberTypeEntry {
1723    /// Local interface or type-alias name declaring the property.
1724    pub type_name: String,
1725    /// Property name declared on the type.
1726    pub property: String,
1727    /// The property's declared type name (local to the declaring module).
1728    pub property_type: String,
1729}
1730
1731/// A module-scope declaration that can be used as a TypeScript type.
1732#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
1733pub struct LocalTypeDeclaration {
1734    /// Local declaration name.
1735    pub name: String,
1736    /// Declaration identifier span.
1737    #[serde(serialize_with = "serialize_span")]
1738    pub span: Span,
1739}
1740
1741/// A reference from an exported symbol's public signature to a type name.
1742#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
1743pub struct PublicSignatureTypeReference {
1744    /// Exported symbol whose signature contains the reference.
1745    pub export_name: String,
1746    /// Referenced type name. Qualified names are reduced to their root identifier.
1747    pub type_name: String,
1748    /// Reference span.
1749    #[serde(serialize_with = "serialize_span")]
1750    pub span: Span,
1751}
1752
1753/// A member of an enum, class, or namespace.
1754#[derive(Debug, Clone, serde::Serialize)]
1755pub struct MemberInfo {
1756    /// Member name.
1757    pub name: String,
1758    /// The kind of member (enum, class method/property, or namespace member).
1759    pub kind: MemberKind,
1760    /// Source span of the member declaration.
1761    #[serde(serialize_with = "serialize_span")]
1762    pub span: Span,
1763    /// Whether this member has decorators (e.g., `@Column()`, `@Inject()`).
1764    /// Decorated members are used by frameworks at runtime and should not be
1765    /// flagged as unused class members, unless every decorator on the member
1766    /// is opted out via `FallowConfig.ignore_decorators`.
1767    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1768    pub has_decorator: bool,
1769    /// Full dotted path of each decorator on this member, in source order.
1770    /// `@step("x")` stores `"step"`; `@ns.foo` stores `"ns.foo"`. Empty for
1771    /// undecorated members, Angular signal-initializer properties (which set
1772    /// `has_decorator` without a literal decorator AST node), and decorators
1773    /// whose expression is not an identifier ladder (the entry is the empty
1774    /// string in that case, treated as never-matching by the predicate).
1775    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1776    pub decorator_names: Vec<String>,
1777    /// True when this is a static class method that returns a fresh instance
1778    /// of the same class: either via `return new this()` / `return new
1779    /// <SameClassName>()` in the body's last statement, or via a declared
1780    /// return type matching the class name. Consumers calling such a static
1781    /// method receive an instance, so the call result's member accesses are
1782    /// credited against the class. See issues #346, #387.
1783    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1784    pub is_instance_returning_static: bool,
1785    /// True when this is an instance class method whose call result is an
1786    /// instance of the same class. Qualifies when the declared return type
1787    /// matches the class name (`setX(): EventBuilder { ... }`) or when the
1788    /// body's last statement is `return this`. The analyze layer walks fluent
1789    /// chains (`Class.factory().setX().setY()`) only through methods carrying
1790    /// this flag, so the chain stops at a non-self-returning method like
1791    /// `.build()`. See issue #387.
1792    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1793    pub is_self_returning: bool,
1794}
1795
1796/// The kind of member.
1797#[derive(
1798    Debug,
1799    Clone,
1800    Copy,
1801    PartialEq,
1802    Eq,
1803    serde::Serialize,
1804    serde::Deserialize,
1805    bitcode::Encode,
1806    bitcode::Decode,
1807)]
1808#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1809#[serde(rename_all = "snake_case")]
1810pub enum MemberKind {
1811    /// A TypeScript enum member.
1812    EnumMember,
1813    /// A class method.
1814    ClassMethod,
1815    /// A class property.
1816    ClassProperty,
1817    /// A member exported from a TypeScript namespace.
1818    NamespaceMember,
1819    /// A member declared by a store object (Pinia `state` / `getters` /
1820    /// `actions` key, or a setup-store returned key). Cross-graph dead-member
1821    /// detection: a store member never accessed by any consumer project-wide.
1822    StoreMember,
1823}
1824
1825/// A static member access expression (e.g., `Status.Active`, `MyClass.create()`).
1826#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
1827pub struct MemberAccess {
1828    /// The identifier being accessed (the import name).
1829    pub object: String,
1830    /// The member being accessed.
1831    pub member: String,
1832}
1833
1834/// Direct export declarations that TypeScript treats as one merged symbol.
1835///
1836/// Spans identify the exact declaration slots without conflating unrelated
1837/// type/value declarations that happen to share a name.
1838#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
1839#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1840pub struct DeclarationMergeFact {
1841    /// Identifier spans for the declarations in this merge group.
1842    pub export_spans: Vec<(u32, u32)>,
1843}
1844
1845/// A default import binding consumed outside a statically known member access.
1846///
1847/// Resolution decides whether the target is an object-shaped module such as a
1848/// CSS Module or a proven static CommonJS object map. Keeping this fact
1849/// target-agnostic lets aliases and extensionless specifiers use the resolved
1850/// target path without broadening ordinary default-import behavior.
1851#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
1852#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1853pub struct DefaultImportWholeObjectUseFact {
1854    /// Local binding name in the importing module.
1855    pub local_name: String,
1856}
1857
1858/// A typed extraction fact for cross-layer analysis.
1859#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
1860#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1861#[serde(tag = "kind", rename_all = "snake_case")]
1862pub enum SemanticFact {
1863    /// A class member referenced from an Angular template, host binding, or
1864    /// component metadata entry.
1865    AngularTemplateMemberAccess(AngularTemplateMemberAccessFact),
1866    /// An Angular component field whose value is an array of a class.
1867    AngularComponentFieldArrayType(AngularComponentFieldArrayTypeFact),
1868    /// An Angular component spreads `this` into an object literal, so component
1869    /// input/output usage is opaque.
1870    AngularThisSpread(AngularThisSpreadFact),
1871    /// A member access on a value returned by an imported static factory call.
1872    FactoryCallMemberAccess(FactoryCallMemberAccessFact),
1873    /// A member access on a value returned by an imported free-function factory
1874    /// (`const x = importedFactory(); x.member`). See issue #1441 (Part A).
1875    FactoryFnMemberAccess(FactoryFnMemberAccessFact),
1876    /// A member access reached through a property of a value whose declared
1877    /// type is an imported named type (`this.opts.c.optM()` where `opts` is
1878    /// typed by an imported interface). See issue #1785.
1879    TypedPropertyMemberAccess(TypedPropertyMemberAccessFact),
1880    /// A member access on a fluent chain rooted at an imported static factory.
1881    FluentChainMemberAccess(FluentChainMemberAccessFact),
1882    /// A member access on a fluent chain rooted at a `new` expression.
1883    FluentChainNewMemberAccess(FluentChainNewMemberAccessFact),
1884    /// A member access on a Playwright fixture object inside a test callback.
1885    PlaywrightFixtureUse(PlaywrightFixtureUseFact),
1886    /// A Playwright fixture definition declared by a typed `test.extend<T>()`.
1887    PlaywrightFixtureDefinition(PlaywrightFixtureDefinitionFact),
1888    /// A Playwright fixture wrapper alias declared by `mergeTests` or `.extend`.
1889    PlaywrightFixtureAlias(PlaywrightFixtureAliasFact),
1890    /// A nested Playwright fixture binding declared by a fixture type alias.
1891    PlaywrightFixtureType(PlaywrightFixtureTypeFact),
1892    /// An exported value whose runtime instance targets a local class or interface.
1893    InstanceExportBinding(InstanceExportBindingFact),
1894    /// A dynamic custom-element tag render that makes static Lit tag credit opaque.
1895    DynamicCustomElementRender(DynamicCustomElementRenderFact),
1896    /// A factory-returned value consumed in a way that can expose ANY property
1897    /// (`const { a, ...rest } = importedFactory()`, a computed destructure key).
1898    /// The returned class must be treated as wholly used: crediting only the
1899    /// visible keys would leave a live member reported as dead.
1900    ///
1901    /// Appended, never inserted: `bitcode` encodes an enum by ordinal, so moving an
1902    /// existing variant would make an old cache decode one fact as another.
1903    FactoryFnWholeObject(FactoryFnWholeObjectFact),
1904    /// A member access reached through a property of a value returned by an
1905    /// imported factory that returns an object literal (`const ui = createUi();
1906    /// ui.orders.member`). Appended after `FactoryFnWholeObject`, never inserted
1907    /// (bitcode encodes by ordinal). See issue #1858.
1908    FactoryReturnObjectPropertyAccess(FactoryReturnObjectPropertyAccessFact),
1909    /// A `this.<field>.<member>` access tied to its exact enclosing class.
1910    /// Appended because bitcode encodes enum variants by ordinal.
1911    ClassThisMemberAccess(ClassThisMemberAccessFact),
1912    /// A whole-object use of `this.<field>...` tied to its exact enclosing class.
1913    /// Appended because bitcode encodes enum variants by ordinal.
1914    ClassThisWholeObjectUse(ClassThisWholeObjectUseFact),
1915    /// An ordered Vitest module-mock operation with direct imported-`vi`
1916    /// provenance.
1917    ///
1918    /// Appended because bitcode encodes enum variants by ordinal. The ordinary
1919    /// dynamic-import fact for the same source remains authoritative for graph
1920    /// reachability and unresolved-import diagnostics.
1921    VitestModuleMockOperation(VitestModuleMockOperationFact),
1922    /// Direct declarations that form one legal TypeScript declaration merge.
1923    /// Appended because bitcode encodes enum variants by ordinal.
1924    DeclarationMerge(DeclarationMergeFact),
1925    /// A named type whose direct receiver surface is contributed by another
1926    /// named type (for example `Pick<Base, ...>` or a union constituent).
1927    /// Appended because bitcode encodes enum variants by ordinal.
1928    TypeAliasSurfaceTarget(TypeAliasSurfaceTargetFact),
1929    /// A string-valued TypeScript enum member available as a static property key.
1930    /// Appended because bitcode encodes enum variants by ordinal.
1931    StringEnumMemberValue(StringEnumMemberValueFact),
1932    /// A computed property access keyed by a static enum member.
1933    /// Appended because bitcode encodes enum variants by ordinal.
1934    ComputedEnumKeyUse(ComputedEnumKeyUseFact),
1935    /// A directly declared non-optional member required by a named interface
1936    /// or object type.
1937    /// Appended because bitcode encodes enum variants by ordinal.
1938    RequiredTypeMember(RequiredTypeMemberFact),
1939    /// The file's complete CommonJS assignment surface is exactly one
1940    /// top-level `module.exports = { ... }` object literal whose keys are all
1941    /// static and which has no transpilation marker or competing assignment.
1942    /// Appended because bitcode encodes enum variants by ordinal.
1943    CjsSingleStaticObjectMap,
1944    /// A default import binding was handed on as a whole object.
1945    /// Appended because bitcode encodes enum variants by ordinal.
1946    DefaultImportWholeObjectUse(DefaultImportWholeObjectUseFact),
1947    /// A property of an exported object contains a local class instance.
1948    /// Appended because bitcode encodes enum variants by ordinal.
1949    ExportedObjectInstanceProperty(ExportedObjectInstancePropertyFact),
1950    /// A member read on an instance from a namespace-qualified constructor.
1951    /// Appended because bitcode encodes enum variants by ordinal.
1952    QualifiedClassMemberAccess(QualifiedClassMemberAccessFact),
1953}
1954
1955/// Iterate Angular template member names from typed semantic facts.
1956fn angular_template_member_names_from_parts(
1957    semantic_facts: &[SemanticFact],
1958) -> impl Iterator<Item = &str> {
1959    semantic_facts.iter().filter_map(|fact| {
1960        if let SemanticFact::AngularTemplateMemberAccess(access) = fact {
1961            Some(access.member.as_str())
1962        } else {
1963            None
1964        }
1965    })
1966}
1967
1968/// Iterate Angular template member names from a module's typed facts.
1969pub fn angular_template_member_names(module: &ModuleInfo) -> impl Iterator<Item = &str> {
1970    angular_template_member_names_from_parts(&module.semantic_facts)
1971}
1972
1973/// Return true when the fact slice contains any Angular template member
1974/// reference.
1975#[must_use]
1976fn has_angular_template_members_from_parts(semantic_facts: &[SemanticFact]) -> bool {
1977    angular_template_member_names_from_parts(semantic_facts)
1978        .next()
1979        .is_some()
1980}
1981
1982/// Return true when the module contains any Angular template member reference.
1983#[must_use]
1984pub fn has_angular_template_members(module: &ModuleInfo) -> bool {
1985    has_angular_template_members_from_parts(&module.semantic_facts)
1986}
1987
1988/// Return true when a module spreads `this` in Angular template context.
1989#[must_use]
1990pub fn has_angular_this_spread(module: &ModuleInfo) -> bool {
1991    SemanticFactView::new(&module.semantic_facts, &module.member_accesses).has_angular_this_spread()
1992}
1993
1994/// Return true when a module contains a dynamic custom-element render.
1995#[must_use]
1996pub fn has_dynamic_custom_element_render(module: &ModuleInfo) -> bool {
1997    module
1998        .semantic_facts
1999        .iter()
2000        .any(|fact| matches!(fact, SemanticFact::DynamicCustomElementRender(_)))
2001}
2002
2003/// Typed-first view over semantic extraction facts.
2004///
2005/// Extraction populates `semantic_facts` directly. The `member_accesses` slice
2006/// remains available for consumers that need ordinary source member accesses,
2007/// but it is no longer decoded as a string protocol for semantic facts.
2008#[derive(Debug, Clone, Copy)]
2009pub struct SemanticFactView<'a> {
2010    semantic_facts: &'a [SemanticFact],
2011    member_accesses: &'a [MemberAccess],
2012}
2013
2014impl<'a> SemanticFactView<'a> {
2015    /// Create a typed semantic fact view from current semantic facts plus
2016    /// ordinary source member accesses.
2017    #[must_use]
2018    pub const fn new(
2019        semantic_facts: &'a [SemanticFact],
2020        member_accesses: &'a [MemberAccess],
2021    ) -> Self {
2022        Self {
2023            semantic_facts,
2024            member_accesses,
2025        }
2026    }
2027
2028    /// Iterate typed semantic facts.
2029    pub fn facts(self) -> impl Iterator<Item = &'a SemanticFact> + 'a {
2030        self.semantic_facts.iter()
2031    }
2032
2033    /// Iterate Angular template member references.
2034    pub fn angular_template_member_names(self) -> impl Iterator<Item = &'a str> + 'a {
2035        angular_template_member_names_from_parts(self.semantic_facts)
2036    }
2037
2038    /// Collect Angular component field array-type facts.
2039    pub fn angular_component_field_array_types(self) -> Vec<AngularComponentFieldArrayTypeFact> {
2040        angular_component_field_array_type_facts(self.semantic_facts)
2041            .cloned()
2042            .collect()
2043    }
2044
2045    /// Return true when any Angular template member reference exists.
2046    #[must_use]
2047    pub fn has_angular_template_members(self) -> bool {
2048        self.angular_template_member_names().next().is_some()
2049    }
2050
2051    /// Return true when a module spreads `this` in Angular template context.
2052    #[must_use]
2053    pub fn has_angular_this_spread(self) -> bool {
2054        self.semantic_facts
2055            .iter()
2056            .any(|fact| matches!(fact, SemanticFact::AngularThisSpread(_)))
2057    }
2058
2059    /// Iterate ordinary source member accesses.
2060    pub fn ordinary_member_accesses(self) -> impl Iterator<Item = &'a MemberAccess> + 'a {
2061        self.member_accesses.iter()
2062    }
2063
2064    /// Collect class-scoped `this` member-access facts.
2065    pub fn class_this_member_accesses(self) -> Vec<ClassThisMemberAccessFact> {
2066        class_this_member_access_facts(self.semantic_facts)
2067            .cloned()
2068            .collect()
2069    }
2070
2071    /// Collect class-scoped `this` whole-object-use facts.
2072    pub fn class_this_whole_object_uses(self) -> Vec<ClassThisWholeObjectUseFact> {
2073        class_this_whole_object_use_facts(self.semantic_facts)
2074            .cloned()
2075            .collect()
2076    }
2077
2078    /// Collect instance-export binding facts.
2079    pub fn instance_export_bindings(self) -> Vec<InstanceExportBindingFact> {
2080        instance_export_binding_facts(self.semantic_facts)
2081            .cloned()
2082            .collect()
2083    }
2084
2085    /// Iterate exported object properties that contain class instances.
2086    pub fn exported_object_instance_properties(
2087        self,
2088    ) -> impl Iterator<Item = &'a ExportedObjectInstancePropertyFact> + 'a {
2089        exported_object_instance_property_facts(self.semantic_facts)
2090    }
2091
2092    /// Iterate proven namespace-qualified class instance member accesses.
2093    pub fn qualified_class_member_accesses(
2094        self,
2095    ) -> impl Iterator<Item = &'a QualifiedClassMemberAccessFact> + 'a {
2096        qualified_class_member_access_facts(self.semantic_facts)
2097    }
2098
2099    /// Collect static factory call member facts.
2100    pub fn factory_call_member_accesses(self) -> Vec<FactoryCallMemberAccessFact> {
2101        factory_call_member_access_facts(self.semantic_facts)
2102            .cloned()
2103            .collect()
2104    }
2105
2106    /// Collect free-function factory-return member facts.
2107    pub fn factory_fn_member_accesses(self) -> Vec<FactoryFnMemberAccessFact> {
2108        factory_fn_member_access_facts(self.semantic_facts)
2109            .cloned()
2110            .collect()
2111    }
2112
2113    /// Collect factory-return whole-object consumption facts.
2114    pub fn factory_fn_whole_objects(self) -> Vec<FactoryFnWholeObjectFact> {
2115        factory_fn_whole_object_facts(self.semantic_facts)
2116            .cloned()
2117            .collect()
2118    }
2119
2120    /// Collect object-literal factory-return property member facts.
2121    pub fn factory_return_object_property_accesses(
2122        self,
2123    ) -> Vec<FactoryReturnObjectPropertyAccessFact> {
2124        factory_return_object_property_access_facts(self.semantic_facts)
2125            .cloned()
2126            .collect()
2127    }
2128
2129    /// Collect typed-property-hop member facts.
2130    pub fn typed_property_member_accesses(self) -> Vec<TypedPropertyMemberAccessFact> {
2131        typed_property_member_access_facts(self.semantic_facts)
2132            .cloned()
2133            .collect()
2134    }
2135
2136    /// Collect members whose presence is required by a named structural type.
2137    pub fn required_type_members(self) -> impl Iterator<Item = &'a RequiredTypeMemberFact> + 'a {
2138        required_type_member_facts(self.semantic_facts)
2139    }
2140
2141    /// Collect type-alias receiver-surface edges.
2142    pub fn type_alias_surface_targets(self) -> Vec<TypeAliasSurfaceTargetFact> {
2143        type_alias_surface_target_facts(self.semantic_facts)
2144            .cloned()
2145            .collect()
2146    }
2147
2148    /// Collect string-valued enum member definitions.
2149    pub fn string_enum_member_values(self) -> Vec<StringEnumMemberValueFact> {
2150        string_enum_member_value_facts(self.semantic_facts)
2151            .cloned()
2152            .collect()
2153    }
2154
2155    /// Collect computed property accesses keyed by enum members.
2156    pub fn computed_enum_key_uses(self) -> Vec<ComputedEnumKeyUseFact> {
2157        computed_enum_key_use_facts(self.semantic_facts)
2158            .cloned()
2159            .collect()
2160    }
2161
2162    /// Collect static factory fluent-chain member facts.
2163    pub fn fluent_chain_member_accesses(self) -> Vec<FluentChainMemberAccessFact> {
2164        fluent_chain_member_access_facts(self.semantic_facts)
2165            .cloned()
2166            .collect()
2167    }
2168
2169    /// Collect constructor-rooted fluent-chain member facts.
2170    pub fn fluent_chain_new_member_accesses(self) -> Vec<FluentChainNewMemberAccessFact> {
2171        fluent_chain_new_member_access_facts(self.semantic_facts)
2172            .cloned()
2173            .collect()
2174    }
2175
2176    /// Collect Playwright fixture-use facts.
2177    pub fn playwright_fixture_uses(self) -> Vec<PlaywrightFixtureUseFact> {
2178        playwright_fixture_use_facts(self.semantic_facts)
2179            .cloned()
2180            .collect()
2181    }
2182
2183    /// Collect Playwright fixture-definition facts.
2184    pub fn playwright_fixture_definitions(self) -> Vec<PlaywrightFixtureDefinitionFact> {
2185        playwright_fixture_definition_facts(self.semantic_facts)
2186            .cloned()
2187            .collect()
2188    }
2189
2190    /// Collect Playwright fixture-alias facts.
2191    pub fn playwright_fixture_aliases(self) -> Vec<PlaywrightFixtureAliasFact> {
2192        playwright_fixture_alias_facts(self.semantic_facts)
2193            .cloned()
2194            .collect()
2195    }
2196
2197    /// Collect Playwright fixture-type facts.
2198    pub fn playwright_fixture_types(self) -> Vec<PlaywrightFixtureTypeFact> {
2199        playwright_fixture_type_facts(self.semantic_facts)
2200            .cloned()
2201            .collect()
2202    }
2203}
2204
2205/// Iterate ordinary whole-object uses.
2206pub fn ordinary_whole_object_uses(whole_object_uses: &[String]) -> impl Iterator<Item = &str> {
2207    whole_object_uses.iter().map(String::as_str)
2208}
2209
2210/// Iterate typed instance-export binding facts.
2211fn instance_export_binding_facts(
2212    semantic_facts: &[SemanticFact],
2213) -> impl Iterator<Item = &InstanceExportBindingFact> {
2214    semantic_facts.iter().filter_map(|fact| {
2215        if let SemanticFact::InstanceExportBinding(access) = fact {
2216            Some(access)
2217        } else {
2218            None
2219        }
2220    })
2221}
2222
2223fn exported_object_instance_property_facts(
2224    semantic_facts: &[SemanticFact],
2225) -> impl Iterator<Item = &ExportedObjectInstancePropertyFact> {
2226    semantic_facts.iter().filter_map(|fact| {
2227        if let SemanticFact::ExportedObjectInstanceProperty(property) = fact {
2228            Some(property)
2229        } else {
2230            None
2231        }
2232    })
2233}
2234
2235fn qualified_class_member_access_facts(
2236    semantic_facts: &[SemanticFact],
2237) -> impl Iterator<Item = &QualifiedClassMemberAccessFact> {
2238    semantic_facts.iter().filter_map(|fact| {
2239        if let SemanticFact::QualifiedClassMemberAccess(access) = fact {
2240            Some(access)
2241        } else {
2242            None
2243        }
2244    })
2245}
2246
2247fn class_this_member_access_facts(
2248    semantic_facts: &[SemanticFact],
2249) -> impl Iterator<Item = &ClassThisMemberAccessFact> {
2250    semantic_facts.iter().filter_map(|fact| {
2251        if let SemanticFact::ClassThisMemberAccess(access) = fact {
2252            Some(access)
2253        } else {
2254            None
2255        }
2256    })
2257}
2258
2259fn class_this_whole_object_use_facts(
2260    semantic_facts: &[SemanticFact],
2261) -> impl Iterator<Item = &ClassThisWholeObjectUseFact> {
2262    semantic_facts.iter().filter_map(|fact| {
2263        if let SemanticFact::ClassThisWholeObjectUse(access) = fact {
2264            Some(access)
2265        } else {
2266            None
2267        }
2268    })
2269}
2270
2271fn angular_component_field_array_type_facts(
2272    semantic_facts: &[SemanticFact],
2273) -> impl Iterator<Item = &AngularComponentFieldArrayTypeFact> {
2274    semantic_facts.iter().filter_map(|fact| {
2275        if let SemanticFact::AngularComponentFieldArrayType(access) = fact {
2276            Some(access)
2277        } else {
2278            None
2279        }
2280    })
2281}
2282
2283/// Iterate typed factory-call member facts.
2284fn factory_call_member_access_facts(
2285    semantic_facts: &[SemanticFact],
2286) -> impl Iterator<Item = &FactoryCallMemberAccessFact> {
2287    semantic_facts.iter().filter_map(|fact| {
2288        if let SemanticFact::FactoryCallMemberAccess(access) = fact {
2289            Some(access)
2290        } else {
2291            None
2292        }
2293    })
2294}
2295
2296/// Iterate typed free-function factory-return member facts.
2297fn factory_fn_member_access_facts(
2298    semantic_facts: &[SemanticFact],
2299) -> impl Iterator<Item = &FactoryFnMemberAccessFact> {
2300    semantic_facts.iter().filter_map(|fact| {
2301        if let SemanticFact::FactoryFnMemberAccess(access) = fact {
2302            Some(access)
2303        } else {
2304            None
2305        }
2306    })
2307}
2308
2309fn factory_fn_whole_object_facts(
2310    semantic_facts: &[SemanticFact],
2311) -> impl Iterator<Item = &FactoryFnWholeObjectFact> {
2312    semantic_facts.iter().filter_map(|fact| {
2313        if let SemanticFact::FactoryFnWholeObject(fact) = fact {
2314            Some(fact)
2315        } else {
2316            None
2317        }
2318    })
2319}
2320
2321/// Iterate object-literal factory-return property member facts.
2322fn factory_return_object_property_access_facts(
2323    semantic_facts: &[SemanticFact],
2324) -> impl Iterator<Item = &FactoryReturnObjectPropertyAccessFact> {
2325    semantic_facts.iter().filter_map(|fact| {
2326        if let SemanticFact::FactoryReturnObjectPropertyAccess(access) = fact {
2327            Some(access)
2328        } else {
2329            None
2330        }
2331    })
2332}
2333
2334/// Iterate typed fluent-chain member facts.
2335fn fluent_chain_member_access_facts(
2336    semantic_facts: &[SemanticFact],
2337) -> impl Iterator<Item = &FluentChainMemberAccessFact> {
2338    semantic_facts.iter().filter_map(|fact| {
2339        if let SemanticFact::FluentChainMemberAccess(access) = fact {
2340            Some(access)
2341        } else {
2342            None
2343        }
2344    })
2345}
2346
2347/// Iterate typed-property-hop member facts.
2348fn typed_property_member_access_facts(
2349    semantic_facts: &[SemanticFact],
2350) -> impl Iterator<Item = &TypedPropertyMemberAccessFact> {
2351    semantic_facts.iter().filter_map(|fact| {
2352        if let SemanticFact::TypedPropertyMemberAccess(access) = fact {
2353            Some(access)
2354        } else {
2355            None
2356        }
2357    })
2358}
2359
2360fn required_type_member_facts(
2361    semantic_facts: &[SemanticFact],
2362) -> impl Iterator<Item = &RequiredTypeMemberFact> {
2363    semantic_facts.iter().filter_map(|fact| {
2364        if let SemanticFact::RequiredTypeMember(required) = fact {
2365            Some(required)
2366        } else {
2367            None
2368        }
2369    })
2370}
2371
2372fn type_alias_surface_target_facts(
2373    semantic_facts: &[SemanticFact],
2374) -> impl Iterator<Item = &TypeAliasSurfaceTargetFact> {
2375    semantic_facts.iter().filter_map(|fact| {
2376        if let SemanticFact::TypeAliasSurfaceTarget(fact) = fact {
2377            Some(fact)
2378        } else {
2379            None
2380        }
2381    })
2382}
2383
2384fn string_enum_member_value_facts(
2385    semantic_facts: &[SemanticFact],
2386) -> impl Iterator<Item = &StringEnumMemberValueFact> {
2387    semantic_facts.iter().filter_map(|fact| {
2388        if let SemanticFact::StringEnumMemberValue(fact) = fact {
2389            Some(fact)
2390        } else {
2391            None
2392        }
2393    })
2394}
2395
2396fn computed_enum_key_use_facts(
2397    semantic_facts: &[SemanticFact],
2398) -> impl Iterator<Item = &ComputedEnumKeyUseFact> {
2399    semantic_facts.iter().filter_map(|fact| {
2400        if let SemanticFact::ComputedEnumKeyUse(fact) = fact {
2401            Some(fact)
2402        } else {
2403            None
2404        }
2405    })
2406}
2407
2408/// Iterate typed constructor-rooted fluent-chain member facts.
2409fn fluent_chain_new_member_access_facts(
2410    semantic_facts: &[SemanticFact],
2411) -> impl Iterator<Item = &FluentChainNewMemberAccessFact> {
2412    semantic_facts.iter().filter_map(|fact| {
2413        if let SemanticFact::FluentChainNewMemberAccess(access) = fact {
2414            Some(access)
2415        } else {
2416            None
2417        }
2418    })
2419}
2420
2421/// Iterate typed Playwright fixture-use facts.
2422fn playwright_fixture_use_facts(
2423    semantic_facts: &[SemanticFact],
2424) -> impl Iterator<Item = &PlaywrightFixtureUseFact> {
2425    semantic_facts.iter().filter_map(|fact| {
2426        if let SemanticFact::PlaywrightFixtureUse(access) = fact {
2427            Some(access)
2428        } else {
2429            None
2430        }
2431    })
2432}
2433
2434/// Iterate typed Playwright fixture-definition facts.
2435fn playwright_fixture_definition_facts(
2436    semantic_facts: &[SemanticFact],
2437) -> impl Iterator<Item = &PlaywrightFixtureDefinitionFact> {
2438    semantic_facts.iter().filter_map(|fact| {
2439        if let SemanticFact::PlaywrightFixtureDefinition(access) = fact {
2440            Some(access)
2441        } else {
2442            None
2443        }
2444    })
2445}
2446
2447/// Iterate typed Playwright fixture-alias facts.
2448fn playwright_fixture_alias_facts(
2449    semantic_facts: &[SemanticFact],
2450) -> impl Iterator<Item = &PlaywrightFixtureAliasFact> {
2451    semantic_facts.iter().filter_map(|fact| {
2452        if let SemanticFact::PlaywrightFixtureAlias(access) = fact {
2453            Some(access)
2454        } else {
2455            None
2456        }
2457    })
2458}
2459
2460/// Iterate typed Playwright fixture-type facts.
2461fn playwright_fixture_type_facts(
2462    semantic_facts: &[SemanticFact],
2463) -> impl Iterator<Item = &PlaywrightFixtureTypeFact> {
2464    semantic_facts.iter().filter_map(|fact| {
2465        if let SemanticFact::PlaywrightFixtureType(access) = fact {
2466            Some(access)
2467        } else {
2468            None
2469        }
2470    })
2471}
2472
2473/// A member name referenced from an Angular template surface.
2474#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2475#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2476pub struct AngularTemplateMemberAccessFact {
2477    /// Referenced class member name.
2478    pub member: String,
2479}
2480
2481/// A typed Angular component field that exposes array elements to templates.
2482#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2483#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2484pub struct AngularComponentFieldArrayTypeFact {
2485    /// Component field name used as the template iterable.
2486    pub field: String,
2487    /// Array element class name.
2488    pub element_class: String,
2489}
2490
2491/// Opaque Angular `{ ...this }` forwarding marker.
2492#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2493#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2494pub struct AngularThisSpreadFact;
2495
2496/// A member access on a static factory call result.
2497#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2498#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2499pub struct FactoryCallMemberAccessFact {
2500    /// Local imported class or namespace object used as the factory callee.
2501    pub callee_object: String,
2502    /// Static factory method invoked on the callee object.
2503    pub callee_method: String,
2504    /// Member accessed on the returned instance-like object.
2505    pub member: String,
2506}
2507
2508/// A member access on a value returned by an imported free-function factory.
2509///
2510/// `const x = importedFactory(); x.member` emits one fact per first-level read
2511/// on `x`. The analyze layer resolves `callee_name` through the consumer's
2512/// imports to the factory's origin module, reads that module's
2513/// `exported_factory_returns` to learn the returned class's local name, resolves
2514/// THAT through the factory module's own imports to the class export, and
2515/// credits `member` on the class. See issue #1441 (Part A).
2516#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2517#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2518pub struct FactoryFnMemberAccessFact {
2519    /// Local imported function used as the factory callee.
2520    pub callee_name: String,
2521    /// Member accessed on the returned instance-like object.
2522    pub member: String,
2523}
2524
2525/// A factory-returned value consumed opaquely, so every member of the class it
2526/// returns must be treated as used.
2527#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2528#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2529pub struct FactoryFnWholeObjectFact {
2530    /// Local imported function used as the factory callee.
2531    pub callee_name: String,
2532}
2533
2534/// A member access reached through a property of a value returned by an imported
2535/// factory that returns an object literal.
2536///
2537/// `const ui = createUi(); ui.orders.member` emits one fact per member read on a
2538/// factory-result property. The analyze layer resolves `callee_name` through the
2539/// consumer's imports to the factory's origin module, reads that module's
2540/// `exported_factory_return_object_shapes` to find the property whose path equals
2541/// `property_path` and its class local name, resolves THAT through the factory
2542/// module's own imports to the class export, and credits `member` on the class
2543/// (gated on the export actually being a class with members). See issue #1858.
2544#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2545#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2546pub struct FactoryReturnObjectPropertyAccessFact {
2547    /// Local imported function used as the factory callee.
2548    pub callee_name: String,
2549    /// Dotted property path between the factory-result local and the final member
2550    /// (e.g. `"orders"` for `ui.orders.member`, `"invoke.orders"` for `ui.invoke.orders.member`).
2551    pub property_path: String,
2552    /// Member accessed on the terminal property's instance.
2553    pub member: String,
2554}
2555
2556/// A member access reached through a typed property hop that the extraction
2557/// layer could not resolve locally.
2558///
2559/// `constructor(private opts: Opts) { ... this.opts.c.optM() }` where `Opts`
2560/// is NOT declared in this file emits
2561/// `TypedPropertyMemberAccessFact { type_name: "Opts", property_path: "c", member: "optM" }`.
2562/// The analyze layer resolves `type_name` through the consumer's imports to the
2563/// declaring module, walks `property_path` through that module's
2564/// `type_member_types`, resolves the terminal type name through the declaring
2565/// module's own imports, and credits `member` on the resolved class (gated on
2566/// the export actually being a class with members). See issue #1785.
2567#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2568#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2569pub struct TypedPropertyMemberAccessFact {
2570    /// Local (usually imported) named-type symbol the receiver is typed by.
2571    pub type_name: String,
2572    /// Remaining dotted property segments between the typed binding and the
2573    /// final member (e.g. `"c"` for `this.opts.c.optM()`).
2574    pub property_path: String,
2575    /// Member accessed on the terminal property's instance.
2576    pub member: String,
2577}
2578
2579/// A class member directly required by a named structural type.
2580///
2581/// Optional properties and methods are excluded: removing an optional
2582/// implementation can preserve assignability, while removing a required one
2583/// from an explicit `implements` contract cannot.
2584#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2585#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2586pub struct RequiredTypeMemberFact {
2587    /// Module-local interface or object-type alias name.
2588    pub type_name: String,
2589    /// Static required property or method name.
2590    pub member: String,
2591}
2592
2593/// One direct contributor to a named type alias's receiver surface.
2594///
2595/// Nested property types are deliberately excluded: in
2596/// `{ nested: Nested }`, `Alias.member` does not access `Nested.member`.
2597#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2598#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2599pub struct TypeAliasSurfaceTargetFact {
2600    /// Module-local alias declaration name.
2601    pub alias_name: String,
2602    /// Module-local or imported named type contributing the direct surface.
2603    pub target_name: String,
2604}
2605
2606/// A statically known string value of a TypeScript enum member.
2607#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2608#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2609pub struct StringEnumMemberValueFact {
2610    /// Module-local enum declaration name.
2611    pub enum_name: String,
2612    /// Static enum member name.
2613    pub member_name: String,
2614    /// Exact string initializer value.
2615    pub value: String,
2616}
2617
2618/// A computed property access whose key is a static enum member.
2619#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2620#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2621pub struct ComputedEnumKeyUseFact {
2622    /// Local enum object used as the key source.
2623    pub key_object: String,
2624    /// Static member selected from the enum object.
2625    pub key_member: String,
2626}
2627
2628/// A member access on a fluent chain rooted at a static factory call.
2629#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2630#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2631pub struct FluentChainMemberAccessFact {
2632    /// Local imported class or namespace object used as the chain root.
2633    pub root_object: String,
2634    /// Static factory method that starts the fluent chain.
2635    pub root_method: String,
2636    /// Intermediate fluent methods between the root method and final member.
2637    pub chain: Vec<String>,
2638    /// Member accessed at this chain step.
2639    pub member: String,
2640}
2641
2642/// A member access on a fluent chain rooted at a `new` expression.
2643#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2644#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2645pub struct FluentChainNewMemberAccessFact {
2646    /// Local imported class constructed by the `new` expression.
2647    pub class_name: String,
2648    /// Intermediate fluent methods between construction and final member.
2649    pub chain: Vec<String>,
2650    /// Member accessed at this chain step.
2651    pub member: String,
2652}
2653
2654/// A member access on a Playwright fixture object inside a test callback.
2655#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2656#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2657pub struct PlaywrightFixtureUseFact {
2658    /// Local test function or wrapper used as the callback callee.
2659    pub test_name: String,
2660    /// Fixture name or dotted fixture path referenced in the callback.
2661    pub fixture_name: String,
2662    /// Member accessed on the fixture target.
2663    pub member: String,
2664}
2665
2666/// A Playwright fixture definition declared by a typed `test.extend<T>()`.
2667#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2668#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2669pub struct PlaywrightFixtureDefinitionFact {
2670    /// Local test function or wrapper receiving the fixture definition.
2671    pub test_name: String,
2672    /// Fixture name or dotted fixture path declared by the fixture type.
2673    pub fixture_name: String,
2674    /// Local type symbol used as the fixture target.
2675    pub type_name: String,
2676}
2677
2678/// A Playwright fixture wrapper alias declared by `mergeTests` or `.extend`.
2679#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2680#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2681pub struct PlaywrightFixtureAliasFact {
2682    /// Local test function or wrapper that inherits fixture definitions.
2683    pub test_name: String,
2684    /// Local test function or wrapper inherited by `test_name`.
2685    pub base_name: String,
2686}
2687
2688/// A nested Playwright fixture binding declared by a fixture type alias.
2689#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2690#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2691pub struct PlaywrightFixtureTypeFact {
2692    /// Local type alias containing the nested fixture binding.
2693    pub alias_name: String,
2694    /// Fixture name or dotted fixture path declared inside the type alias.
2695    pub fixture_name: String,
2696    /// Local type symbol used as the nested fixture target.
2697    pub type_name: String,
2698}
2699
2700/// An exported value whose runtime instance targets a local class or interface.
2701#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2702#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2703pub struct InstanceExportBindingFact {
2704    /// Exported binding name.
2705    pub export_name: String,
2706    /// Local class or interface symbol used as the instance target.
2707    pub target_name: String,
2708}
2709
2710/// A property of an exported object whose value is an instance of a local class.
2711#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2712#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2713pub struct ExportedObjectInstancePropertyFact {
2714    /// Public export name of the containing object.
2715    pub export_name: String,
2716    /// Dotted path from the exported object to the instance property.
2717    pub property_path: String,
2718    /// Local class symbol instantiated at that property.
2719    pub class_local_name: String,
2720}
2721
2722/// A member read on an instance created by a namespace-qualified constructor.
2723#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2724#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2725pub struct QualifiedClassMemberAccessFact {
2726    /// Local namespace-import binding.
2727    pub namespace_local: String,
2728    /// Class export selected from that namespace.
2729    pub class_export_name: String,
2730    /// Instance member accessed through the proven object path.
2731    pub member: String,
2732}
2733
2734/// Opaque marker for a dynamic custom-element render site.
2735#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2736#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2737pub struct DynamicCustomElementRenderFact;
2738
2739/// The action performed by a Vitest module-mock operation.
2740#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2741#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2742#[serde(rename_all = "snake_case")]
2743pub enum VitestModuleMockAction {
2744    /// Register a mock. `factory_replaces_original` is true only when the
2745    /// factory is structurally closed and cannot load the original module.
2746    ///
2747    /// Automock (`vi.mock` / `jest.mock` without a factory) is always
2748    /// `factory_replaces_original: false` by decision (issue #2082). For
2749    /// Vitest, the runner derives the mocked shape by importing the original
2750    /// module, so its top-level code executes at collection time, and
2751    /// file-level masking cannot express "module evaluated but exports
2752    /// stubbed". For Jest, a `__mocks__` sibling takes precedence and the
2753    /// original is genuinely not required, but the manual mock itself may
2754    /// load the original (`jest.requireActual`), and proving it never does
2755    /// would need a cross-file factory proof. Both runners therefore keep
2756    /// coverage credit for the automock form.
2757    Mock {
2758        /// Whether the factory provably replaces the original module.
2759        factory_replaces_original: bool,
2760    },
2761    /// Remove a registered mock and restore the original module.
2762    Unmock,
2763}
2764
2765impl VitestModuleMockAction {
2766    /// Whether this operation registers a proven complete replacement.
2767    #[must_use]
2768    pub const fn replaces_original(self) -> bool {
2769        matches!(
2770            self,
2771            Self::Mock {
2772                factory_replaces_original: true
2773            }
2774        )
2775    }
2776}
2777
2778/// Ordered Vitest module-mock operation with a static source target.
2779///
2780/// The declaring [`ModuleInfo::file_id`] owns the test-root provenance. The
2781/// resolver consumes `source` through its canonical specifier pipeline; this
2782/// fact deliberately carries no resolved path or duplicate diagnostic span.
2783#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2784#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2785pub struct VitestModuleMockOperationFact {
2786    /// Static module specifier passed to `vi.mock` or `vi.unmock`.
2787    pub source: String,
2788    /// Source-order position of the call within the declaring module.
2789    pub call_start: u32,
2790    /// Typed mock or unmock action.
2791    pub action: VitestModuleMockAction,
2792}
2793
2794/// A `this`-rooted member access with exact enclosing-class provenance.
2795#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2796#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2797pub struct ClassThisMemberAccessFact {
2798    /// Enclosing class local name, or `default` for an anonymous default class.
2799    pub class_local_name: String,
2800    /// Dotted receiver spelling beginning with `this.`.
2801    pub object: String,
2802    /// Terminal member being accessed.
2803    pub member: String,
2804}
2805
2806/// A whole-object use of a `this`-rooted chain with enclosing-class provenance.
2807#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
2808#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2809pub struct ClassThisWholeObjectUseFact {
2810    /// Enclosing class local name, or `default` for an anonymous default class.
2811    pub class_local_name: String,
2812    /// Dotted receiver spelling beginning with `this.`.
2813    pub object: String,
2814}
2815
2816/// A statically flattenable callee path invoked in a module (e.g. `execSync`,
2817/// `child_process.exec`, `console.log`). One entry per unique `callee_path`
2818/// per module; the span anchors the first occurrence. Consumed by the
2819/// `boundaries.calls.forbidden` detector.
2820#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
2821pub struct CalleeUse {
2822    /// The dotted or bare callee path as written at the call site.
2823    pub callee_path: String,
2824    /// Start byte offset of the first call site using this path.
2825    pub span_start: u32,
2826}
2827
2828/// A `"use client"` / `"use server"` directive string written as an expression
2829/// statement in `program.body` (NOT the leading prologue), so the RSC bundler
2830/// silently ignores it. One entry per offending occurrence. Consumed by the
2831/// `misplaced-directive` detector.
2832#[derive(Debug, Clone, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
2833pub struct MisplacedDirectiveSite {
2834    /// `true` for `"use server"`, `false` for `"use client"`.
2835    pub is_server: bool,
2836    /// Start byte offset of the misplaced directive statement.
2837    pub span_start: u32,
2838}
2839
2840/// Which side of a dependency-injection link a call site represents.
2841#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
2842pub enum DiRole {
2843    /// `provide(KEY, value)` / `app.provide(KEY, value)` / `setContext(KEY, value)`.
2844    Provide,
2845    /// `inject(KEY)` / `getContext(KEY)`.
2846    Inject,
2847}
2848
2849/// Which framework's DI API a call site came from (drives the finding message).
2850#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
2851pub enum DiFramework {
2852    /// Vue `provide` / `inject` (from `vue` / `@vue/runtime-core`).
2853    Vue,
2854    /// Svelte `setContext` / `getContext` (from `svelte`).
2855    Svelte,
2856    /// Angular `inject(TOKEN)` / `@Inject(TOKEN)` (from `@angular/core`),
2857    /// matched against `{ provide: TOKEN, ... }` provider objects.
2858    Angular,
2859}
2860
2861/// A Vue `provide`/`inject` or Svelte `setContext`/`getContext` call site keyed
2862/// by an identifier symbol. The `key_local` is resolved at analyze time through
2863/// the consuming module's import/export tables to a canonical defining-site
2864/// export key, so a provide and an inject of the same shared symbol unify even
2865/// across barrel re-exports. Consumed by the `unprovided-inject` detector.
2866#[derive(Debug, Clone, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
2867pub struct DiKeySite {
2868    /// The key identifier as written at the call site.
2869    pub key_local: String,
2870    /// Whether this is a provide or an inject.
2871    pub role: DiRole,
2872    /// Which framework's API this came from.
2873    pub framework: DiFramework,
2874    /// Start byte offset of the call expression (anchors the finding).
2875    pub span_start: u32,
2876}
2877
2878/// A component prop declared by Vue `<script setup>` `defineProps` or Svelte 5
2879/// `$props()`. `used_in_script` / `used_in_template` are set during extraction;
2880/// the `unused-component-prop` detector flags a prop where neither is true. See
2881/// `harvest_define_props` and `harvest_svelte_props` in `sfc_props.rs`.
2882#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
2883pub struct ComponentProp {
2884    /// The declared prop name.
2885    pub name: String,
2886    /// The template/script-visible local binding name: the destructure alias for
2887    /// `const { name: alias } = defineProps()` or
2888    /// `let { name: alias } = $props()`, otherwise the prop name itself. A
2889    /// renamed prop is read through this local, so usage must be checked against
2890    /// it, not the declared name.
2891    pub local: String,
2892    /// Start byte offset of the prop declaration (anchors the finding).
2893    pub span_start: u32,
2894    /// Whether this prop is referenced in the component's `<script>` (a
2895    /// destructured local binding with a resolved reference, or a `props.<name>`
2896    /// member access). For React, this is set-in-body: a resolved reference to the
2897    /// destructured local anywhere in the component function body.
2898    pub used_in_script: bool,
2899    /// Whether this prop name is referenced in the component's `<template>`.
2900    /// Set by `apply_template_usage` when the template scanner credits the name.
2901    /// Always false for React (no template; React uses `used_in_script`).
2902    pub used_in_template: bool,
2903    /// The enclosing component name. Empty for Vue SFCs (one component per file,
2904    /// the file stem is the component, set by the detector). For React this is the
2905    /// component function/arrow name a prop was declared on, so the detector can
2906    /// emit the right `component_name` and apply the per-component abstain ladder
2907    /// (a file can declare several React components).
2908    pub component: String,
2909    /// React-only: `true` when the destructured prop local is referenced at least
2910    /// once OUTSIDE a child-JSX attribute value expression (a substantive
2911    /// consumption: a hook arg, a host-element child, a non-JSX-attr read). When
2912    /// `used_in_script` is true but this is false, the prop is referenced ONLY as
2913    /// the root of forwarded child attribute values, i.e. a pure pass-through.
2914    /// Always `false` for Vue (no forward-vs-consume distinction is computed).
2915    pub used_outside_forward: bool,
2916}
2917
2918/// A Vue `<script setup>` `defineEmits` declared event, harvested from the type
2919/// tuple-call form (`defineEmits<{ (e: 'foo'): void }>()`), the type object form
2920/// (`defineEmits<{ foo: [x: string] }>()`), or the runtime array form
2921/// (`defineEmits(['foo'])`). `used` is set during extraction when the bound emit
2922/// name is called as `emit('<name>')`. The `unused-component-emit` detector flags
2923/// an event where `used` is false. See `harvest_define_emits` in `sfc_props.rs`.
2924#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode, PartialEq, Eq)]
2925pub struct ComponentEmit {
2926    /// The declared emit event name.
2927    pub name: String,
2928    /// Start byte offset of the emit declaration (anchors the finding).
2929    pub span_start: u32,
2930    /// Whether this event is emitted via `emit('<name>')` somewhere in the
2931    /// component's `<script>`.
2932    pub used: bool,
2933}
2934
2935/// A Svelte custom event dispatched via `dispatch('<name>')`, where `dispatch`
2936/// is the binding from a `const dispatch = createEventDispatcher()` call. Only
2937/// literal-first-arg dispatches are recorded; a `dispatch(<nonLiteral>)` sets
2938/// `ModuleInfo::has_dynamic_dispatch` instead. Consumed by the
2939/// `unused-svelte-event` detector, which flags an event dispatched here but
2940/// listened to nowhere project-wide (the cross-file dead-output direction). The
2941/// span is a byte offset (not an `oxc_span::Span`) so the type round-trips
2942/// through the bitcode cache directly, mirroring `ComponentEmit::span_start`.
2943#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode, PartialEq, Eq)]
2944pub struct DispatchedEvent {
2945    /// The dispatched event name (the literal first argument).
2946    pub name: String,
2947    /// Start byte offset of the `dispatch(...)` call (anchors the finding).
2948    pub span_start: u32,
2949}
2950
2951/// A declared Angular component/directive input, harvested from an `@Input()`
2952/// decorator or a signal `input()` / `input.required()` / `model()` initializer
2953/// on an Angular-decorated class. Consumed by the `unused-component-input`
2954/// detector, which flags an input read nowhere in its own component (neither the
2955/// template nor the class body). The span is stored as a byte offset (not an
2956/// `oxc_span::Span`) so the type is cheap to mirror onto the cache, matching
2957/// `ComponentEmit::span_start`. `ModuleInfo` is not serialized, so no serde
2958/// attrs are derived here. `bitcode` derives let the type be mirrored directly
2959/// onto `CachedModule` (the same pattern as `ComponentEmit`).
2960#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode, PartialEq, Eq)]
2961pub struct AngularInputMember {
2962    /// The declared input name (the property key).
2963    pub name: String,
2964    /// Start byte offset of the property key (anchors the finding).
2965    pub span_start: u32,
2966}
2967
2968/// A declared Angular component/directive output, harvested from an `@Output()`
2969/// decorator or a signal `output()` / `outputFromObservable()` initializer on an
2970/// Angular-decorated class. Consumed by the `unused-component-output` detector,
2971/// which flags an output emitted nowhere in its own component. A `model()` is an
2972/// input and a framework-driven output, so it is recorded ONLY as an input and
2973/// never appears here (the implicit `update:` emit is framework-managed). The
2974/// span is a byte offset for the same reason as `AngularInputMember`.
2975#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode, PartialEq, Eq)]
2976pub struct AngularOutputMember {
2977    /// The declared output name (the property key).
2978    pub name: String,
2979    /// Start byte offset of the property key (anchors the finding).
2980    pub span_start: u32,
2981}
2982
2983/// A declared Angular `@Component` and its `selector` value(s), harvested from a
2984/// `@Component({ selector: '...' })` decorator. Consumed by the Angular arm of
2985/// the `unrendered-component` detector, which flags a component whose every
2986/// element selector is used in NO template project-wide (and that is not
2987/// referenced by class name anywhere, e.g. routed / bootstrapped / dynamically
2988/// rendered). A multi-selector string (`'app-foo, [appBar]'`) is split into the
2989/// `selectors` list. The span is stored as a byte offset (not an
2990/// `oxc_span::Span`) so the type round-trips through the bitcode cache directly,
2991/// mirroring `AngularInputMember::span_start`. `@Directive` is intentionally NOT
2992/// harvested here (directives have no template render). `ModuleInfo` is not
2993/// serialized, so no serde attrs are derived.
2994#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode, PartialEq, Eq)]
2995pub struct AngularComponentSelector {
2996    /// The declared selector strings for this component, split on `,`. A purely
2997    /// element-selector component has only `app-foo`-shaped entries; attribute
2998    /// (`[appFoo]`) and class (`.foo`) selectors are retained verbatim so the
2999    /// detector can abstain when ANY non-element selector is present.
3000    pub selectors: Vec<String>,
3001    /// Start byte offset of the component class declaration (anchors the
3002    /// finding).
3003    pub span_start: u32,
3004    /// The component class name (used to credit routed / bootstrapped / dynamic
3005    /// class-name references project-wide).
3006    pub class_name: String,
3007}
3008
3009/// A Lit / web-component custom element registered in a module via
3010/// `@customElement('x-foo')` or `customElements.define('x-foo', C)`. Consumed by
3011/// the Lit arm of the `unrendered-component` detector. The span is stored as a
3012/// byte offset (not an `oxc_span::Span`) so the type round-trips through the
3013/// bitcode cache directly, mirroring `AngularComponentSelector::span_start`.
3014#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode, PartialEq, Eq)]
3015pub struct RegisteredCustomElement {
3016    /// The registered custom-element tag name (`x-foo`).
3017    pub tag: String,
3018    /// The registering class's local name, used for the public-API / export
3019    /// abstain (an exported / published element is rendered by a downstream
3020    /// consumer the scan cannot see). Empty for an anonymous
3021    /// `export default @customElement('x-foo') class extends LitElement {}`.
3022    pub class_local_name: String,
3023    /// Start byte offset of the registering class declaration (anchors the
3024    /// finding at the element, NOT line 1, since a `.ts` file can register
3025    /// several custom elements).
3026    pub span_start: u32,
3027}
3028
3029/// A key returned from a SvelteKit route `load()` function's terminal return
3030/// object literal. Harvested from `+page.{ts,server.ts,js,server.js}` files
3031/// exporting a `load` function. Consumed by the `unused-load-data-key` detector,
3032/// which flags a key read by no consumer. The span is stored as byte offsets
3033/// (not an `oxc_span::Span`) so the type round-trips through the bitcode cache
3034/// directly, mirroring `DiKeySite::span_start` / `ComponentEmit::span_start`.
3035#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode, PartialEq, Eq)]
3036pub struct LoadReturnKey {
3037    /// The returned-object property key name.
3038    pub name: String,
3039    /// Start byte offset of the key (anchors the finding).
3040    pub span_start: u32,
3041    /// End byte offset of the key.
3042    pub span_end: u32,
3043}
3044
3045/// The syntactic shape of an identified React component definition. Drives the
3046/// abstain ladder later phases apply: a `forwardRef` / `memo` wrapper whose
3047/// props come from an imported interface fallow cannot resolve must abstain
3048/// (ADR-001), not guess.
3049#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
3050pub enum ComponentFunctionKind {
3051    /// A `function Foo() { return <.../> }` declaration.
3052    FnDecl,
3053    /// A `const Foo = () => <.../>` arrow (or function-expression) binding.
3054    Arrow,
3055    /// A `const Foo = forwardRef((props, ref) => <.../>)` wrapper.
3056    ForwardRefWrapper,
3057    /// A `const Foo = memo((props) => <.../>)` wrapper.
3058    MemoWrapper,
3059}
3060
3061/// An identified React component: a function/arrow whose body returns JSX.
3062/// Captured by `visit_jsx_element`'s enclosing-component tracking. The
3063/// `unused-component-prop` (React arm) and complexity-fold phases consume this;
3064/// the abstain flags keep zero-FP on the cases ADR-001 cannot resolve.
3065#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
3066pub struct ComponentFunction {
3067    /// The component name (the binding or declaration identifier).
3068    pub name: String,
3069    /// Start byte offset of the component definition (anchors findings).
3070    pub span_start: u32,
3071    /// The syntactic shape of the definition.
3072    pub kind: ComponentFunctionKind,
3073    /// Whether the component is exported from its module (a named export, a
3074    /// `export default`, or re-exported in the same module). Public-API
3075    /// components abstain in the prop phase.
3076    pub is_exported: bool,
3077    /// `true` when the component's props are not statically harvestable: a
3078    /// rest/spread in the signature (`{ ...rest }`), props passed wholesale to a
3079    /// hook/helper, or a `forwardRef` / `memo` wrapper whose props come from an
3080    /// imported interface generic fallow cannot resolve (ADR-001). The prop
3081    /// phase abstains on the whole component when set.
3082    pub has_unharvestable_props: bool,
3083    /// `true` when the component body calls `cloneElement` / `React.cloneElement`.
3084    /// `cloneElement` injects props by reflection, so the static forward-set is
3085    /// incomplete; the prop-drilling phase abstains on any chain through this
3086    /// component (ADR-001, zero-FP).
3087    pub uses_clone_element: bool,
3088    /// `true` when the component renders a `*.Provider` member-expression tag
3089    /// (`<FooContext.Provider>`). A context provider in the subtree means the
3090    /// drilling may be a deliberate non-context choice (or the prop is about to
3091    /// be provided); the prop-drilling phase downgrades/abstains.
3092    pub renders_provider: bool,
3093    /// `true` when the component passes a function as a child render value
3094    /// (render-props / children-as-function: `<Foo>{() => ...}</Foo>` or
3095    /// `<Foo render={() => ...}/>`). The forwarded shape is dynamic; the
3096    /// prop-drilling phase abstains on chains through this component.
3097    pub has_children_as_function: bool,
3098    /// `true` when the component body is pure structural indirection: a single
3099    /// statement returning exactly one capitalized/member-expression JSX element
3100    /// (no host wrapper, no extra children, optionally a fragment wrapping a
3101    /// single element) that forwards props via a bare spread of the component's
3102    /// own props binding / rest local (`<Child {...props}/>`), with NO named
3103    /// attributes alongside the spread and NO self-render. The cross-component
3104    /// `thin-wrapper` phase joins this with hook-density / cyclomatic checks and
3105    /// the resolved single render edge to flag a component that is a candidate
3106    /// for inlining. Computed from the component's own AST only, so it caches
3107    /// byte-identity-safe (ADR-001).
3108    pub is_pure_passthrough: bool,
3109}
3110
3111/// The kind of a React hook call. `Custom` covers any `use*`-named call that is
3112/// not one of the built-in hooks.
3113#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
3114pub enum HookUseKind {
3115    /// `useState(...)`.
3116    UseState,
3117    /// `useEffect(...)`.
3118    UseEffect,
3119    /// `useMemo(...)`.
3120    UseMemo,
3121    /// `useCallback(...)`.
3122    UseCallback,
3123    /// Any other `use*`-named call (a custom hook).
3124    Custom,
3125}
3126
3127/// A React hook call site inside a component. Consumed by the complexity-fold
3128/// phase (hook density) and surfaced as descriptive hotspot context.
3129#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
3130pub struct HookUse {
3131    /// The hook kind.
3132    pub kind: HookUseKind,
3133    /// The dependency-array arity, recorded ONLY when a literal array is present
3134    /// at the dependency-array position (`[a, b]` -> `Some(2)`, `[]` ->
3135    /// `Some(0)`). `None` when the call has no dependency array argument or the
3136    /// argument is not a literal array (ADR-001: do not guess).
3137    pub dep_array_arity: Option<u32>,
3138    /// Start byte offset of the hook call (anchors findings).
3139    pub span_start: u32,
3140    /// The enclosing component name (the top of the visitor's component stack
3141    /// when the hook call was recorded). Lets the descriptive per-component hook
3142    /// summary attribute hooks exactly even when a file declares several
3143    /// components. A hook recorded outside any component carries an empty string
3144    /// (the visitor only records hooks inside a component, so this is the
3145    /// rare top-level / unattributed case).
3146    pub component: String,
3147}
3148
3149/// A render edge: one component rendering another (a capitalized or
3150/// member-expression JSX tag). Captured at extraction time with the child's
3151/// written name; resolution of `child_component_name` to a `FileId`/export is
3152/// deferred to graph build via the existing import map.
3153#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
3154pub struct RenderEdge {
3155    /// The name of the component that renders the child (the enclosing
3156    /// component). Empty when the JSX is not inside an identified component (a
3157    /// top-level render expression).
3158    pub parent_component: String,
3159    /// The rendered child component name as written (`Foo` or the full
3160    /// member-expression path `Foo.Bar`).
3161    pub child_component_name: String,
3162    /// The attribute (prop) names passed at the render site, in source order.
3163    pub attr_names: Vec<String>,
3164    /// `true` when the render site contains a JSX spread (`{...x}`), so the
3165    /// passed-prop set is not statically complete.
3166    pub has_spread: bool,
3167    /// The forwarded attributes at this render site: each pairs the child
3168    /// attribute NAME with the identifier ROOT of its value expression
3169    /// (`userName={user.name}` -> `{ attr: "userName", root: "user" }`;
3170    /// `value={x}` -> `{ attr: "value", root: "x" }`). ONLY plain identifier or
3171    /// member-root access values are recorded (`{x}`, `{x.y}`, `{x.y.z}`); a value
3172    /// that is a call, an arrow/function, a conditional, a JSX element, or any
3173    /// other complex expression is NOT recorded here (its root would not be a pure
3174    /// forward) and sets `has_complex_forward` instead. The prop-drilling chain
3175    /// walk uses this pairing to map "this component forwards prop P" to "the
3176    /// child receives it as attribute A".
3177    pub forward_attrs: Vec<ForwardAttr>,
3178    /// `true` when at least one attribute value at this render site is a complex
3179    /// expression (a call, an arrow/function render-prop, a conditional, a JSX
3180    /// element-as-prop, a template literal, etc.) whose identifier root was NOT
3181    /// recorded in `forward_attrs`. The prop-drilling phase abstains on a chain
3182    /// whose forwarded prop flows through such a value (ADR-001, zero-FP).
3183    pub has_complex_forward: bool,
3184}
3185
3186/// One forwarded JSX attribute: the child attribute name plus the identifier
3187/// root of its value expression. See [`RenderEdge::forward_attrs`].
3188#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
3189pub struct ForwardAttr {
3190    /// The child attribute (prop) name as written (`userName`).
3191    pub attr: String,
3192    /// The identifier root of the attribute value expression (`user` for
3193    /// `userName={user.name}`).
3194    pub root: String,
3195}
3196
3197#[expect(
3198    clippy::trivially_copy_pass_by_ref,
3199    reason = "serde serialize_with requires &T"
3200)]
3201fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
3202    use serde::ser::SerializeMap;
3203    let mut map = serializer.serialize_map(Some(2))?;
3204    map.serialize_entry("start", &span.start)?;
3205    map.serialize_entry("end", &span.end)?;
3206    map.end()
3207}
3208
3209/// Export identifier.
3210#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
3211pub enum ExportName {
3212    /// A named export (e.g., `export const foo`).
3213    Named(String),
3214    /// The default export.
3215    Default,
3216}
3217
3218impl ExportName {
3219    /// Compare against a string without allocating (avoids `to_string()`).
3220    #[must_use]
3221    pub fn matches_str(&self, s: &str) -> bool {
3222        match self {
3223            Self::Named(n) => n == s,
3224            Self::Default => s == "default",
3225        }
3226    }
3227}
3228
3229impl std::fmt::Display for ExportName {
3230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3231        match self {
3232            Self::Named(n) => write!(f, "{n}"),
3233            Self::Default => write!(f, "default"),
3234        }
3235    }
3236}
3237
3238/// An import declaration.
3239#[derive(Debug, Clone)]
3240pub struct ImportInfo {
3241    /// The import specifier (e.g., `./utils` or `react`).
3242    pub source: String,
3243    /// How the symbol is imported (named, default, namespace, or side-effect).
3244    pub imported_name: ImportedName,
3245    /// The local binding name in the importing module.
3246    pub local_name: String,
3247    /// Whether this is a type-only import (`import type`).
3248    pub is_type_only: bool,
3249    /// Whether this whole-module import forwards type meanings only.
3250    ///
3251    /// Set for `export type *` and `export type * as ns` inside a
3252    /// `declare module '...'` body (issue #2375). Those forms record the same
3253    /// bindingless whole-module shape the plain ambient star records
3254    /// (issue #2357), but the star they stand for erases every value meaning,
3255    /// so the graph credits the target's star surface in the type namespace
3256    /// alone. Every other import leaves this false: `is_type_only` already
3257    /// decides their namespace on its own.
3258    pub is_type_only_star: bool,
3259    /// Whether this import originated from a CSS-context.
3260    pub from_style: bool,
3261    /// Source span of the import declaration.
3262    pub span: Span,
3263    /// Span of the source string literal used by the LSP to highlight the specifier.
3264    pub source_span: Span,
3265}
3266
3267/// How a symbol is imported.
3268#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
3269pub enum ImportedName {
3270    /// A named import (e.g., `import { foo }`).
3271    Named(String),
3272    /// A default import (e.g., `import React`).
3273    Default,
3274    /// A namespace import (e.g., `import * as utils`).
3275    Namespace,
3276    /// A side-effect import (e.g., `import './styles.css'`).
3277    SideEffect,
3278}
3279
3280#[cfg(target_pointer_width = "64")]
3281const _: () = assert!(std::mem::size_of::<ExportInfo>() == 136);
3282#[cfg(target_pointer_width = "64")]
3283const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
3284#[cfg(target_pointer_width = "64")]
3285const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
3286#[cfg(target_pointer_width = "64")]
3287const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
3288#[cfg(target_pointer_width = "64")]
3289const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
3290#[cfg(target_pointer_width = "64")]
3291const _: () = assert!(std::mem::size_of::<SemanticFact>() == 96);
3292#[cfg(target_pointer_width = "64")]
3293const _: () = assert!(std::mem::size_of::<SinkSite>() == 216);
3294#[cfg(target_pointer_width = "64")]
3295const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 1336);
3296#[cfg(target_pointer_width = "64")]
3297const _: () = assert!(std::mem::size_of::<TypeMemberTypeEntry>() == 72);
3298
3299/// A re-export declaration.
3300#[derive(Debug, Clone)]
3301pub struct ReExportInfo {
3302    /// The module being re-exported from.
3303    pub source: String,
3304    /// The name imported from the source module (or `*` for star re-exports).
3305    pub imported_name: String,
3306    /// The name exported from this module.
3307    pub exported_name: String,
3308    /// Whether this is a type-only re-export.
3309    pub is_type_only: bool,
3310    /// Source span of the re-export declaration on this module.
3311    pub span: oxc_span::Span,
3312    /// Span of the whole re-export statement. A multi-binding statement
3313    /// yields one `ReExportInfo` per binding, each with a per-binding
3314    /// `span`; this field lets consumers reason about the enclosing
3315    /// statement (e.g. suppression coverage). Empty (`start == end`) for
3316    /// synthesized re-exports that have no single owning statement.
3317    pub statement_span: oxc_span::Span,
3318    /// Span of the source string literal (the specifier in quotes), used to
3319    /// anchor unresolved-import findings on the specifier. Empty
3320    /// (`start == end`) when no literal exists in the statement.
3321    pub source_span: oxc_span::Span,
3322}
3323
3324/// A dynamic `import()` call.
3325#[derive(Debug, Clone)]
3326pub struct DynamicImportInfo {
3327    /// The import specifier.
3328    pub source: String,
3329    /// Source span of the `import()` expression.
3330    pub span: Span,
3331    /// Names destructured from the dynamic import result.
3332    /// Non-empty means `const { a, b } = await import(...)` -> Named imports.
3333    /// Empty means simple `import(...)` or `const x = await import(...)` -> Namespace.
3334    pub destructured_names: Vec<String>,
3335    /// The local variable name for `const x = await import(...)`.
3336    /// Used for namespace import narrowing via member access tracking.
3337    pub local_name: Option<String>,
3338    /// True when this dynamic import was synthesised by fallow rather than appearing in user source.
3339    pub is_speculative: bool,
3340}
3341
3342/// A `require()` call.
3343#[derive(Debug, Clone)]
3344pub struct RequireCallInfo {
3345    /// The require specifier.
3346    pub source: String,
3347    /// Source span of the `require()` call.
3348    pub span: Span,
3349    /// Source span of the specifier string-literal argument (including its
3350    /// quotes), e.g. the `'./x'` in `require('./x')`. Used to anchor an
3351    /// `unresolved-import` diagnostic squiggly under the specifier rather than
3352    /// the `require` keyword. `Span::default()` when the argument is not a
3353    /// plain string literal.
3354    pub source_span: Span,
3355    /// Names destructured from the `require()` result.
3356    pub destructured_names: Vec<String>,
3357    /// The local variable name for `const x = require(...)`.
3358    pub local_name: Option<String>,
3359    /// `true` for `import type X = require('...')`, the one require spelling
3360    /// TypeScript erases entirely: the emitted JavaScript contains no
3361    /// `require` call, so the target is a type-space reference and never a
3362    /// runtime dependency. Always `false` for `const x = require(...)`, which
3363    /// has no type-only spelling. Read by dependency classification so a
3364    /// type-only devDependency is not reported as production usage.
3365    pub is_type_only: bool,
3366}
3367
3368/// Result of parsing all files, including incremental cache statistics.
3369pub struct ParseResult {
3370    /// Extracted module information for all successfully parsed files.
3371    pub modules: Vec<ModuleInfo>,
3372    /// Files discovered with stable IDs but unreadable by the parser.
3373    pub read_failures: Vec<SourceReadFailure>,
3374    /// Number of files whose parse results were loaded from cache (unchanged).
3375    pub cache_hits: usize,
3376    /// Number of files that required a full parse (new or changed).
3377    pub cache_misses: usize,
3378    /// Summed wall-clock time of the actual AST parses across all rayon workers.
3379    pub parse_cpu_ms: f64,
3380}
3381
3382/// A discovered source that could not be read as UTF-8 text.
3383#[derive(Debug, Clone, PartialEq, Eq)]
3384pub struct SourceReadFailure {
3385    /// Stable discovery identity retained even though no module was produced.
3386    pub file_id: FileId,
3387    /// Absolute discovered source path.
3388    pub path: PathBuf,
3389    /// Underlying filesystem or UTF-8 decoding error.
3390    pub error: String,
3391}
3392
3393#[cfg(test)]
3394mod tests {
3395    use super::*;
3396
3397    fn span() -> Span {
3398        Span::new(0, 1)
3399    }
3400
3401    macro_rules! assert_released {
3402        ($values:expr) => {{
3403            assert!($values.is_empty());
3404        }};
3405    }
3406
3407    #[test]
3408    fn public_env_var_includes_public_ci_metadata() {
3409        for name in ["TAG_REF", "GITHUB_SHA", "CI_COMMIT_BRANCH", "APP_MODE"] {
3410            assert!(is_public_env_var(name), "{name} should be public metadata");
3411        }
3412    }
3413
3414    #[test]
3415    fn public_env_var_keeps_secret_shaped_names_source_backed() {
3416        for name in ["GITHUB_TOKEN", "REFRESH_TOKEN", "API_KEY", "SECRET_SHA"] {
3417            assert!(
3418                !is_public_env_var(name),
3419                "{name} should remain secret-shaped"
3420            );
3421        }
3422    }
3423
3424    #[test]
3425    fn ordinary_access_helpers_keep_source_accesses() {
3426        let member_accesses = vec![
3427            MemberAccess {
3428                object: "this".to_string(),
3429                member: "render".to_string(),
3430            },
3431            MemberAccess {
3432                object: "service".to_string(),
3433                member: "run".to_string(),
3434            },
3435        ];
3436        let ordinary = SemanticFactView::new(&[], &member_accesses)
3437            .ordinary_member_accesses()
3438            .map(|access| (access.object.as_str(), access.member.as_str()))
3439            .collect::<Vec<_>>();
3440
3441        assert_eq!(ordinary, vec![("this", "render"), ("service", "run")]);
3442
3443        let whole_object_uses = vec!["model".to_string(), "service".to_string()];
3444
3445        assert_eq!(
3446            ordinary_whole_object_uses(&whole_object_uses).collect::<Vec<_>>(),
3447            vec!["model", "service"]
3448        );
3449    }
3450
3451    #[test]
3452    fn angular_template_member_names_use_typed_facts() {
3453        let mut module = minimal_module_info();
3454        push_semantic_fact(
3455            &mut module,
3456            SemanticFact::AngularTemplateMemberAccess(AngularTemplateMemberAccessFact {
3457                member: "typed".to_string(),
3458            }),
3459        );
3460
3461        let names: Vec<&str> = angular_template_member_names(&module).collect();
3462
3463        assert_eq!(names, vec!["typed"]);
3464        assert!(has_angular_template_members(&module));
3465    }
3466
3467    #[test]
3468    fn angular_this_spread_uses_typed_fact() {
3469        let mut typed = minimal_module_info();
3470        push_semantic_fact(
3471            &mut typed,
3472            SemanticFact::AngularThisSpread(AngularThisSpreadFact),
3473        );
3474
3475        assert!(has_angular_this_spread(&typed));
3476        assert!(!has_angular_this_spread(&minimal_module_info()));
3477    }
3478
3479    #[test]
3480    fn semantic_fact_view_iterates_typed_facts() {
3481        let mut module = minimal_module_info();
3482        push_semantic_fact(
3483            &mut module,
3484            SemanticFact::FactoryCallMemberAccess(FactoryCallMemberAccessFact {
3485                callee_object: "Svc".to_string(),
3486                callee_method: "make".to_string(),
3487                member: "run".to_string(),
3488            }),
3489        );
3490
3491        let facts = SemanticFactView::new(&module.semantic_facts, &module.member_accesses)
3492            .facts()
3493            .collect::<Vec<_>>();
3494
3495        assert_eq!(
3496            facts[0],
3497            &SemanticFact::FactoryCallMemberAccess(FactoryCallMemberAccessFact {
3498                callee_object: "Svc".to_string(),
3499                callee_method: "make".to_string(),
3500                member: "run".to_string(),
3501            })
3502        );
3503    }
3504
3505    #[test]
3506    fn typed_fact_helpers_collect_each_family() {
3507        let mut module = minimal_module_info();
3508        push_semantic_fact(
3509            &mut module,
3510            SemanticFact::InstanceExportBinding(InstanceExportBindingFact {
3511                export_name: "exported".to_string(),
3512                target_name: "target".to_string(),
3513            }),
3514        );
3515        push_semantic_fact(
3516            &mut module,
3517            SemanticFact::FactoryCallMemberAccess(FactoryCallMemberAccessFact {
3518                callee_object: "Svc".to_string(),
3519                callee_method: "create".to_string(),
3520                member: "run".to_string(),
3521            }),
3522        );
3523        push_semantic_fact(
3524            &mut module,
3525            SemanticFact::FluentChainMemberAccess(FluentChainMemberAccessFact {
3526                root_object: "Builder".to_string(),
3527                root_method: "start".to_string(),
3528                chain: vec!["next".to_string()],
3529                member: "value".to_string(),
3530            }),
3531        );
3532        push_semantic_fact(
3533            &mut module,
3534            SemanticFact::FluentChainNewMemberAccess(FluentChainNewMemberAccessFact {
3535                class_name: "Builder".to_string(),
3536                chain: vec!["next".to_string(), "finish".to_string()],
3537                member: "done".to_string(),
3538            }),
3539        );
3540
3541        assert_eq!(
3542            SemanticFactView::new(&module.semantic_facts, &module.member_accesses)
3543                .instance_export_bindings(),
3544            vec![InstanceExportBindingFact {
3545                export_name: "exported".to_string(),
3546                target_name: "target".to_string(),
3547            }]
3548        );
3549        assert_eq!(
3550            SemanticFactView::new(&module.semantic_facts, &module.member_accesses)
3551                .factory_call_member_accesses(),
3552            vec![FactoryCallMemberAccessFact {
3553                callee_object: "Svc".to_string(),
3554                callee_method: "create".to_string(),
3555                member: "run".to_string(),
3556            }]
3557        );
3558        assert_eq!(
3559            SemanticFactView::new(&module.semantic_facts, &module.member_accesses)
3560                .fluent_chain_member_accesses(),
3561            vec![FluentChainMemberAccessFact {
3562                root_object: "Builder".to_string(),
3563                root_method: "start".to_string(),
3564                chain: vec!["next".to_string()],
3565                member: "value".to_string(),
3566            }]
3567        );
3568        assert_eq!(
3569            SemanticFactView::new(&module.semantic_facts, &module.member_accesses)
3570                .fluent_chain_new_member_accesses(),
3571            vec![FluentChainNewMemberAccessFact {
3572                class_name: "Builder".to_string(),
3573                chain: vec!["next".to_string(), "finish".to_string()],
3574                member: "done".to_string(),
3575            }]
3576        );
3577    }
3578
3579    #[test]
3580    fn semantic_fact_view_exposes_typed_first_contract() {
3581        let mut module = minimal_module_info();
3582        push_semantic_fact(
3583            &mut module,
3584            SemanticFact::FactoryCallMemberAccess(FactoryCallMemberAccessFact {
3585                callee_object: "Svc".to_string(),
3586                callee_method: "create".to_string(),
3587                member: "run".to_string(),
3588            }),
3589        );
3590        push_semantic_fact(
3591            &mut module,
3592            SemanticFact::PlaywrightFixtureUse(PlaywrightFixtureUseFact {
3593                test_name: "test".to_string(),
3594                fixture_name: "page".to_string(),
3595                member: "goto".to_string(),
3596            }),
3597        );
3598        push_semantic_fact(
3599            &mut module,
3600            SemanticFact::InstanceExportBinding(InstanceExportBindingFact {
3601                export_name: "exported".to_string(),
3602                target_name: "target".to_string(),
3603            }),
3604        );
3605
3606        let view = SemanticFactView::new(&module.semantic_facts, &module.member_accesses);
3607
3608        assert_eq!(
3609            view.factory_call_member_accesses(),
3610            vec![FactoryCallMemberAccessFact {
3611                callee_object: "Svc".to_string(),
3612                callee_method: "create".to_string(),
3613                member: "run".to_string(),
3614            }]
3615        );
3616        assert_eq!(
3617            view.playwright_fixture_uses(),
3618            vec![PlaywrightFixtureUseFact {
3619                test_name: "test".to_string(),
3620                fixture_name: "page".to_string(),
3621                member: "goto".to_string(),
3622            }]
3623        );
3624        assert_eq!(
3625            view.instance_export_bindings(),
3626            vec![InstanceExportBindingFact {
3627                export_name: "exported".to_string(),
3628                target_name: "target".to_string(),
3629            }]
3630        );
3631    }
3632
3633    #[test]
3634    fn playwright_fixture_fact_helpers_select_each_fact_family() {
3635        let mut module = minimal_module_info();
3636        push_semantic_fact(
3637            &mut module,
3638            SemanticFact::PlaywrightFixtureUse(PlaywrightFixtureUseFact {
3639                test_name: "test".to_string(),
3640                fixture_name: "page".to_string(),
3641                member: "goto".to_string(),
3642            }),
3643        );
3644        push_semantic_fact(
3645            &mut module,
3646            SemanticFact::PlaywrightFixtureDefinition(PlaywrightFixtureDefinitionFact {
3647                test_name: "test".to_string(),
3648                fixture_name: "adminPage".to_string(),
3649                type_name: "AdminPage".to_string(),
3650            }),
3651        );
3652        push_semantic_fact(
3653            &mut module,
3654            SemanticFact::PlaywrightFixtureAlias(PlaywrightFixtureAliasFact {
3655                test_name: "mergedTest".to_string(),
3656                base_name: "test".to_string(),
3657            }),
3658        );
3659        push_semantic_fact(
3660            &mut module,
3661            SemanticFact::PlaywrightFixtureType(PlaywrightFixtureTypeFact {
3662                alias_name: "Pages".to_string(),
3663                fixture_name: "adminPage".to_string(),
3664                type_name: "AdminPage".to_string(),
3665            }),
3666        );
3667
3668        assert_eq!(
3669            playwright_fixture_use_facts(&module.semantic_facts)
3670                .map(|fact| fact.member.as_str())
3671                .collect::<Vec<_>>(),
3672            vec!["goto"]
3673        );
3674        assert_eq!(
3675            playwright_fixture_definition_facts(&module.semantic_facts)
3676                .map(|fact| fact.type_name.as_str())
3677                .collect::<Vec<_>>(),
3678            vec!["AdminPage"]
3679        );
3680        assert_eq!(
3681            playwright_fixture_alias_facts(&module.semantic_facts)
3682                .map(|fact| fact.base_name.as_str())
3683                .collect::<Vec<_>>(),
3684            vec!["test"]
3685        );
3686        assert_eq!(
3687            playwright_fixture_type_facts(&module.semantic_facts)
3688                .map(|fact| fact.fixture_name.as_str())
3689                .collect::<Vec<_>>(),
3690            vec!["adminPage"]
3691        );
3692    }
3693
3694    #[test]
3695    fn line_offsets_empty_string() {
3696        assert_eq!(compute_line_offsets(""), vec![0]);
3697    }
3698
3699    #[test]
3700    #[expect(
3701        clippy::too_many_lines,
3702        reason = "exhaustive field-by-field construction + release assertions for every ModuleInfo field"
3703    )]
3704    fn release_resolution_payload_drops_copied_vectors_only() {
3705        let mut module = ModuleInfo {
3706            file_id: FileId(7),
3707            exports: vec![ExportInfo {
3708                name: ExportName::Named("kept".to_string()),
3709                local_name: None,
3710                is_type_only: false,
3711                is_side_effect_used: false,
3712                visibility: VisibilityTag::None,
3713                expected_unused_reason: None,
3714                span: span(),
3715                members: Vec::new(),
3716                super_class: None,
3717            }]
3718            .into(),
3719            imports: vec![ImportInfo {
3720                source: "node:child_process".to_string(),
3721                imported_name: ImportedName::Default,
3722                local_name: "childProcess".to_string(),
3723                is_type_only: false,
3724                is_type_only_star: false,
3725                from_style: false,
3726                span: span(),
3727                source_span: span(),
3728            }],
3729            re_exports: vec![ReExportInfo {
3730                source: "./kept".to_string(),
3731                imported_name: "kept".to_string(),
3732                exported_name: "kept".to_string(),
3733                is_type_only: false,
3734                span: span(),
3735                statement_span: span(),
3736                source_span: span(),
3737            }],
3738            dynamic_imports: vec![DynamicImportInfo {
3739                source: "./dynamic".to_string(),
3740                span: span(),
3741                destructured_names: vec!["value".to_string()],
3742                local_name: None,
3743                is_speculative: false,
3744            }],
3745            dynamic_import_patterns: vec![DynamicImportPattern {
3746                prefix: "./pages/".to_string(),
3747                suffix: Some(".tsx".to_string()),
3748                span: span(),
3749                mechanism: ModuleLoadMechanism::EsModule,
3750            }],
3751            require_calls: vec![RequireCallInfo {
3752                source: "./required".to_string(),
3753                span: span(),
3754                source_span: span(),
3755                destructured_names: Vec::new(),
3756                local_name: Some("required".to_string()),
3757                is_type_only: false,
3758            }],
3759            package_path_references: vec!["react".to_string()].into(),
3760            member_accesses: vec![MemberAccess {
3761                object: "Status".to_string(),
3762                member: "Active".to_string(),
3763            }]
3764            .into(),
3765            semantic_facts: std::sync::Arc::default(),
3766            whole_object_uses: vec!["Status".to_string()].into(),
3767            has_cjs_exports: true,
3768            has_angular_component_template_url: true,
3769            content_hash: 42,
3770            suppressions: Vec::new(),
3771            unknown_suppression_kinds: Vec::new(),
3772            unused_import_bindings: vec!["unused".to_string()],
3773            type_referenced_import_bindings: vec!["TypeOnly".to_string()],
3774            value_referenced_import_bindings: vec!["Value".to_string()],
3775            line_offsets: vec![0, 8],
3776            complexity: vec![FunctionComplexity {
3777                name: "work".to_string(),
3778                is_private_member: false,
3779                line: 1,
3780                col: 0,
3781                cyclomatic: 2,
3782                cognitive: 3,
3783                line_count: 4,
3784                param_count: 1,
3785                react_hook_count: 0,
3786                react_jsx_max_depth: 0,
3787                react_prop_count: 0,
3788                source_hash: Some("hash".to_string()),
3789                contributions: Vec::new(),
3790            }],
3791            flag_uses: vec![FlagUse {
3792                flag_name: "FEATURE_X".to_string(),
3793                kind: FlagUseKind::EnvVar,
3794                line: 1,
3795                col: 0,
3796                guard_span_start: None,
3797                guard_span_end: None,
3798                sdk_name: None,
3799            }],
3800            class_heritage: vec![ClassHeritageInfo {
3801                export_name: "Child".to_string(),
3802                super_class: Some("Parent".to_string()),
3803                implements: vec!["Contract".to_string()],
3804                type_parameters: Vec::new(),
3805                instance_bindings: Vec::new(),
3806                super_class_type_args: Vec::new(),
3807                generic_instance_bindings: Vec::new(),
3808            }],
3809            exported_factory_returns: std::sync::Arc::from([FactoryReturnExport {
3810                export_name: "useApi".to_string(),
3811                class_local_name: "RESTApi".to_string(),
3812            }]),
3813            exported_factory_return_object_shapes: std::sync::Arc::from([
3814                FactoryReturnObjectShapeExport {
3815                    export_name: "createUi".to_string(),
3816                    properties: Box::from([FactoryReturnObjectProperty {
3817                        property_path: "orders".to_string(),
3818                        class_local_name: "OrdersPage".to_string(),
3819                    }]),
3820                },
3821            ]),
3822            type_member_types: std::sync::Arc::from([TypeMemberTypeEntry {
3823                type_name: "Opts".to_string(),
3824                property: "c".to_string(),
3825                property_type: "OptDep".to_string(),
3826            }]),
3827            injection_tokens: vec![("TOKEN".to_string(), "Contract".to_string())],
3828            local_type_declarations: vec![LocalTypeDeclaration {
3829                name: "Contract".to_string(),
3830                span: span(),
3831            }],
3832            public_signature_type_references: vec![PublicSignatureTypeReference {
3833                export_name: "kept".to_string(),
3834                type_name: "Contract".to_string(),
3835                span: span(),
3836            }],
3837            namespace_object_aliases: vec![NamespaceObjectAlias {
3838                via_export_name: "api".to_string(),
3839                suffix: "read".to_string(),
3840                namespace_local: "ns".to_string(),
3841            }],
3842            iconify_prefixes: vec!["hero".to_string()],
3843            iconify_icon_names: vec!["hero-home".to_string()],
3844            auto_import_candidates: vec!["useState".to_string()],
3845            directives: vec!["use client".to_string()],
3846            client_only_dynamic_import_spans: Vec::new(),
3847            security_sinks: Vec::new(),
3848            security_sinks_skipped: 1,
3849            security_unresolved_callee_sites: Vec::new(),
3850            tainted_bindings: Vec::new(),
3851            sanitized_sink_args: Vec::new(),
3852            security_control_sites: Vec::new(),
3853            callee_uses: Vec::new(),
3854            misplaced_directives: Vec::new(),
3855            inline_server_action_exports: Vec::new(),
3856            di_key_sites: Vec::new(),
3857            has_dynamic_provide: false,
3858            referenced_import_bindings: Vec::new(),
3859            component_props: Vec::new(),
3860            has_props_attrs_fallthrough: false,
3861            has_define_expose: false,
3862            has_define_model: false,
3863            has_unharvestable_props: false,
3864            component_emits: Vec::new(),
3865            angular_inputs: Vec::new(),
3866            angular_outputs: Vec::new(),
3867            angular_component_selectors: Vec::new(),
3868            registered_custom_elements: Vec::new(),
3869            used_custom_element_tags: Vec::new(),
3870            angular_used_selectors: Vec::new(),
3871            angular_entry_component_refs: Vec::new(),
3872            has_dynamic_component_render: false,
3873            has_unharvestable_emits: false,
3874            has_dynamic_emit: false,
3875            has_emit_whole_object_use: false,
3876            load_return_keys: Vec::new(),
3877            has_unharvestable_load: false,
3878            has_load_data_whole_use: false,
3879            has_page_data_store_whole_use: false,
3880            has_route_loader_data_whole_use: false,
3881            component_functions: Vec::new(),
3882            react_props: Vec::new(),
3883            hook_uses: Vec::new(),
3884            render_edges: Vec::new(),
3885            svelte_dispatched_events: Vec::new(),
3886            svelte_listened_events: Vec::new(),
3887            has_dynamic_dispatch: false,
3888        };
3889
3890        module.release_resolution_payload();
3891
3892        assert_eq!(module.file_id, FileId(7));
3893        assert_eq!(module.content_hash, 42);
3894        assert_eq!(module.line_offsets, vec![0, 8]);
3895        assert_eq!(module.imports.len(), 1);
3896        assert_eq!(module.exports.len(), 1);
3897        assert_eq!(module.re_exports.len(), 1);
3898        assert_eq!(module.dynamic_import_patterns.len(), 1);
3899        assert_eq!(module.member_accesses.len(), 1);
3900        assert_eq!(module.complexity.len(), 1);
3901        assert_eq!(module.flag_uses.len(), 1);
3902        assert_eq!(module.class_heritage.len(), 1);
3903        assert_eq!(module.exported_factory_returns.len(), 1);
3904        assert_eq!(module.injection_tokens.len(), 1);
3905        assert_eq!(module.local_type_declarations.len(), 1);
3906        assert_eq!(module.public_signature_type_references.len(), 1);
3907        assert_eq!(module.iconify_prefixes.len(), 1);
3908        assert_eq!(module.iconify_icon_names.len(), 1);
3909        assert_eq!(module.directives.len(), 1);
3910        assert_eq!(module.security_sinks_skipped, 1);
3911        assert_released!(module.dynamic_imports);
3912        assert_released!(module.require_calls);
3913        assert_released!(module.package_path_references);
3914        assert_released!(module.whole_object_uses);
3915        assert_released!(module.unused_import_bindings);
3916        assert_released!(module.type_referenced_import_bindings);
3917        assert_released!(module.value_referenced_import_bindings);
3918        assert_released!(module.namespace_object_aliases);
3919        assert_released!(module.auto_import_candidates);
3920        assert_eq!(
3921            module.referenced_import_bindings,
3922            vec!["childProcess".to_string()]
3923        );
3924    }
3925
3926    #[test]
3927    fn sink_shape_bitcode_roundtrip() {
3928        for shape in [
3929            SinkShape::Call,
3930            SinkShape::MemberCall,
3931            SinkShape::MemberAssign,
3932            SinkShape::TaggedTemplate,
3933            SinkShape::JsxAttr,
3934            SinkShape::NewExpression,
3935            SinkShape::SecretLiteral,
3936        ] {
3937            let encoded = bitcode::encode(&shape);
3938            let decoded: SinkShape = bitcode::decode(&encoded).expect("decode sink shape");
3939            assert_eq!(shape, decoded);
3940        }
3941    }
3942
3943    #[test]
3944    fn sink_arg_kind_bitcode_roundtrip() {
3945        for kind in [
3946            SinkArgKind::TemplateWithSubst,
3947            SinkArgKind::Concat,
3948            SinkArgKind::Object,
3949            SinkArgKind::Call,
3950            SinkArgKind::Literal,
3951            SinkArgKind::NoArg,
3952            SinkArgKind::Other,
3953        ] {
3954            let encoded = bitcode::encode(&kind);
3955            let decoded: SinkArgKind = bitcode::decode(&encoded).expect("decode sink arg kind");
3956            assert_eq!(kind, decoded);
3957        }
3958    }
3959
3960    #[test]
3961    fn security_url_shape_bitcode_roundtrip() {
3962        for shape in [
3963            SecurityUrlShape::FixedOriginDynamicPath,
3964            SecurityUrlShape::DynamicOrigin,
3965        ] {
3966            let encoded = bitcode::encode(&shape);
3967            let decoded: SecurityUrlShape =
3968                bitcode::decode(&encoded).expect("decode security url shape");
3969            assert_eq!(shape, decoded);
3970        }
3971    }
3972
3973    #[test]
3974    fn sink_site_bitcode_roundtrip() {
3975        let site = SinkSite {
3976            sink_shape: SinkShape::MemberAssign,
3977            callee_path: "el.innerHTML".to_string(),
3978            arg_index: 0,
3979            arg_is_non_literal: true,
3980            arg_kind: SinkArgKind::Other,
3981            arg_literal: Some(SinkLiteralValue::Integer(511)),
3982            regex_pattern: None,
3983            object_properties: vec![SinkObjectProperty {
3984                key: "origin".to_string(),
3985                value: SinkLiteralValue::String("*".to_string()),
3986            }],
3987            object_property_keys: vec!["origin".to_string()],
3988            object_property_keys_complete: true,
3989            arg_idents: vec!["userInput".to_string()],
3990            arg_source_paths: vec!["req.body.email".to_string(), "req.body".to_string()],
3991            span_start: 10,
3992            span_end: 20,
3993            url_arg_literal: Some("https://api.example.com".to_string()),
3994            url_shape: Some(SecurityUrlShape::FixedOriginDynamicPath),
3995        };
3996        let encoded = bitcode::encode(&site);
3997        let decoded: SinkSite = bitcode::decode(&encoded).expect("decode sink site");
3998        assert_eq!(decoded.sink_shape, site.sink_shape);
3999        assert_eq!(decoded.callee_path, site.callee_path);
4000        assert_eq!(decoded.arg_index, site.arg_index);
4001        assert_eq!(decoded.arg_is_non_literal, site.arg_is_non_literal);
4002        assert_eq!(decoded.arg_kind, site.arg_kind);
4003        assert_eq!(decoded.arg_literal, site.arg_literal);
4004        assert_eq!(decoded.object_properties, site.object_properties);
4005        assert_eq!(decoded.object_property_keys, site.object_property_keys);
4006        assert_eq!(
4007            decoded.object_property_keys_complete,
4008            site.object_property_keys_complete
4009        );
4010        assert_eq!(decoded.arg_idents, site.arg_idents);
4011        assert_eq!(decoded.arg_source_paths, site.arg_source_paths);
4012        assert_eq!(decoded.url_shape, site.url_shape);
4013        assert_eq!(decoded.span(), site.span());
4014    }
4015
4016    #[test]
4017    fn line_offsets_single_line_no_newline() {
4018        assert_eq!(compute_line_offsets("hello"), vec![0]);
4019    }
4020
4021    #[test]
4022    fn line_offsets_single_line_with_newline() {
4023        assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
4024    }
4025
4026    #[test]
4027    fn line_offsets_multiple_lines() {
4028        assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
4029    }
4030
4031    #[test]
4032    fn line_offsets_trailing_newline() {
4033        assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
4034    }
4035
4036    #[test]
4037    fn line_offsets_consecutive_newlines() {
4038        assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
4039    }
4040
4041    #[test]
4042    fn line_offsets_multibyte_utf8() {
4043        assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
4044    }
4045
4046    #[test]
4047    fn line_col_offset_zero() {
4048        let offsets = compute_line_offsets("abc\ndef\nghi");
4049        let (line, col) = byte_offset_to_line_col(&offsets, 0);
4050        assert_eq!((line, col), (1, 0));
4051    }
4052
4053    #[test]
4054    fn line_col_middle_of_first_line() {
4055        let offsets = compute_line_offsets("abc\ndef\nghi");
4056        let (line, col) = byte_offset_to_line_col(&offsets, 2);
4057        assert_eq!((line, col), (1, 2));
4058    }
4059
4060    #[test]
4061    fn line_col_start_of_second_line() {
4062        let offsets = compute_line_offsets("abc\ndef\nghi");
4063        let (line, col) = byte_offset_to_line_col(&offsets, 4);
4064        assert_eq!((line, col), (2, 0));
4065    }
4066
4067    #[test]
4068    fn line_col_middle_of_second_line() {
4069        let offsets = compute_line_offsets("abc\ndef\nghi");
4070        let (line, col) = byte_offset_to_line_col(&offsets, 5);
4071        assert_eq!((line, col), (2, 1));
4072    }
4073
4074    #[test]
4075    fn line_col_start_of_third_line() {
4076        let offsets = compute_line_offsets("abc\ndef\nghi");
4077        let (line, col) = byte_offset_to_line_col(&offsets, 8);
4078        assert_eq!((line, col), (3, 0));
4079    }
4080
4081    #[test]
4082    fn line_col_end_of_file() {
4083        let offsets = compute_line_offsets("abc\ndef\nghi");
4084        let (line, col) = byte_offset_to_line_col(&offsets, 10);
4085        assert_eq!((line, col), (3, 2));
4086    }
4087
4088    #[test]
4089    fn line_col_single_line() {
4090        let offsets = compute_line_offsets("hello");
4091        let (line, col) = byte_offset_to_line_col(&offsets, 3);
4092        assert_eq!((line, col), (1, 3));
4093    }
4094
4095    #[test]
4096    fn line_col_at_newline_byte() {
4097        let offsets = compute_line_offsets("abc\ndef");
4098        let (line, col) = byte_offset_to_line_col(&offsets, 3);
4099        assert_eq!((line, col), (1, 3));
4100    }
4101
4102    #[test]
4103    fn export_name_matches_str_named() {
4104        let name = ExportName::Named("foo".to_string());
4105        assert!(name.matches_str("foo"));
4106        assert!(!name.matches_str("bar"));
4107        assert!(!name.matches_str("default"));
4108    }
4109
4110    #[test]
4111    fn export_name_matches_str_default() {
4112        let name = ExportName::Default;
4113        assert!(name.matches_str("default"));
4114        assert!(!name.matches_str("foo"));
4115    }
4116
4117    #[test]
4118    fn export_name_display_named() {
4119        let name = ExportName::Named("myExport".to_string());
4120        assert_eq!(name.to_string(), "myExport");
4121    }
4122
4123    #[test]
4124    fn export_name_display_default() {
4125        let name = ExportName::Default;
4126        assert_eq!(name.to_string(), "default");
4127    }
4128
4129    #[test]
4130    fn export_name_equality_named() {
4131        let a = ExportName::Named("foo".to_string());
4132        let b = ExportName::Named("foo".to_string());
4133        let c = ExportName::Named("bar".to_string());
4134        assert_eq!(a, b);
4135        assert_ne!(a, c);
4136    }
4137
4138    #[test]
4139    fn export_name_equality_default() {
4140        let a = ExportName::Default;
4141        let b = ExportName::Default;
4142        assert_eq!(a, b);
4143    }
4144
4145    #[test]
4146    fn export_name_named_not_equal_to_default() {
4147        let named = ExportName::Named("default".to_string());
4148        let default = ExportName::Default;
4149        assert_ne!(named, default);
4150    }
4151
4152    #[test]
4153    fn export_name_hash_consistency() {
4154        use std::collections::hash_map::DefaultHasher;
4155        use std::hash::{Hash, Hasher};
4156
4157        let mut h1 = DefaultHasher::new();
4158        let mut h2 = DefaultHasher::new();
4159        ExportName::Named("foo".to_string()).hash(&mut h1);
4160        ExportName::Named("foo".to_string()).hash(&mut h2);
4161        assert_eq!(h1.finish(), h2.finish());
4162    }
4163
4164    #[test]
4165    fn export_name_matches_str_empty_string() {
4166        let name = ExportName::Named(String::new());
4167        assert!(name.matches_str(""));
4168        assert!(!name.matches_str("foo"));
4169    }
4170
4171    #[test]
4172    fn export_name_default_does_not_match_empty() {
4173        let name = ExportName::Default;
4174        assert!(!name.matches_str(""));
4175    }
4176
4177    #[test]
4178    fn imported_name_equality() {
4179        assert_eq!(
4180            ImportedName::Named("foo".to_string()),
4181            ImportedName::Named("foo".to_string())
4182        );
4183        assert_ne!(
4184            ImportedName::Named("foo".to_string()),
4185            ImportedName::Named("bar".to_string())
4186        );
4187        assert_eq!(ImportedName::Default, ImportedName::Default);
4188        assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
4189        assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
4190        assert_ne!(ImportedName::Default, ImportedName::Namespace);
4191        assert_ne!(
4192            ImportedName::Named("default".to_string()),
4193            ImportedName::Default
4194        );
4195    }
4196
4197    #[test]
4198    fn member_kind_equality() {
4199        assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
4200        assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
4201        assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
4202        assert_eq!(MemberKind::NamespaceMember, MemberKind::NamespaceMember);
4203        assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
4204        assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
4205        assert_ne!(MemberKind::NamespaceMember, MemberKind::EnumMember);
4206    }
4207
4208    #[test]
4209    fn member_kind_bitcode_roundtrip() {
4210        let kinds = [
4211            MemberKind::EnumMember,
4212            MemberKind::ClassMethod,
4213            MemberKind::ClassProperty,
4214            MemberKind::NamespaceMember,
4215        ];
4216        for kind in &kinds {
4217            let bytes = bitcode::encode(kind);
4218            let decoded: MemberKind = bitcode::decode(&bytes).unwrap();
4219            assert_eq!(&decoded, kind);
4220        }
4221    }
4222
4223    #[test]
4224    fn member_access_bitcode_roundtrip() {
4225        let access = MemberAccess {
4226            object: "Status".to_string(),
4227            member: "Active".to_string(),
4228        };
4229        let bytes = bitcode::encode(&access);
4230        let decoded: MemberAccess = bitcode::decode(&bytes).unwrap();
4231        assert_eq!(decoded.object, "Status");
4232        assert_eq!(decoded.member, "Active");
4233    }
4234
4235    #[test]
4236    fn line_offsets_crlf_only_counts_lf() {
4237        let offsets = compute_line_offsets("ab\r\ncd");
4238        assert_eq!(offsets, vec![0, 4]);
4239    }
4240
4241    #[test]
4242    fn line_col_empty_file_offset_zero() {
4243        let offsets = compute_line_offsets("");
4244        let (line, col) = byte_offset_to_line_col(&offsets, 0);
4245        assert_eq!((line, col), (1, 0));
4246    }
4247
4248    // --- VisibilityTag ---
4249
4250    #[test]
4251    fn visibility_tag_default_is_none_variant() {
4252        assert_eq!(VisibilityTag::default(), VisibilityTag::None);
4253    }
4254
4255    #[test]
4256    fn visibility_tag_is_none_only_for_none_variant() {
4257        assert!(VisibilityTag::None.is_none());
4258        assert!(!VisibilityTag::Public.is_none());
4259        assert!(!VisibilityTag::Internal.is_none());
4260        assert!(!VisibilityTag::Beta.is_none());
4261        assert!(!VisibilityTag::Alpha.is_none());
4262        assert!(!VisibilityTag::ExpectedUnused.is_none());
4263    }
4264
4265    #[test]
4266    fn visibility_tag_suppresses_unused_for_api_tags() {
4267        assert!(VisibilityTag::Public.suppresses_unused());
4268        assert!(VisibilityTag::Internal.suppresses_unused());
4269        assert!(VisibilityTag::Beta.suppresses_unused());
4270        assert!(VisibilityTag::Alpha.suppresses_unused());
4271    }
4272
4273    #[test]
4274    fn visibility_tag_does_not_suppress_none_or_expected_unused() {
4275        assert!(!VisibilityTag::None.suppresses_unused());
4276        assert!(!VisibilityTag::ExpectedUnused.suppresses_unused());
4277    }
4278
4279    // --- is_public_env_path ---
4280
4281    #[test]
4282    fn is_public_env_path_process_env_public_prefix() {
4283        assert!(is_public_env_path("process.env.NEXT_PUBLIC_API_URL"));
4284        assert!(is_public_env_path("process.env.VITE_APP_KEY"));
4285        assert!(is_public_env_path("process.env.REACT_APP_TITLE"));
4286        assert!(is_public_env_path("process.env.NODE_ENV"));
4287    }
4288
4289    #[test]
4290    fn is_public_env_path_import_meta_env_public_prefix() {
4291        assert!(is_public_env_path("import.meta.env.VITE_BASE_URL"));
4292        assert!(is_public_env_path("import.meta.env.PUBLIC_API"));
4293    }
4294
4295    #[test]
4296    fn is_public_env_path_secret_env_vars_are_not_public() {
4297        assert!(!is_public_env_path("process.env.SECRET_KEY"));
4298        assert!(!is_public_env_path("process.env.DATABASE_PASSWORD"));
4299        assert!(!is_public_env_path("import.meta.env.API_TOKEN"));
4300    }
4301
4302    #[test]
4303    fn is_public_env_path_non_env_paths_are_not_public() {
4304        assert!(!is_public_env_path("req.query.id"));
4305        assert!(!is_public_env_path("process.argv"));
4306        assert!(!is_public_env_path("window.location.href"));
4307    }
4308
4309    // --- is_public_env_var edge cases ---
4310
4311    #[test]
4312    fn is_public_env_var_exact_matches() {
4313        assert!(is_public_env_var("NODE_ENV"));
4314    }
4315
4316    #[test]
4317    fn is_public_env_var_all_known_prefixes() {
4318        assert!(is_public_env_var("NUXT_PUBLIC_API_URL"));
4319        assert!(is_public_env_var("PUBLIC_API_KEY"));
4320        assert!(is_public_env_var("GATSBY_APP_ID"));
4321        assert!(is_public_env_var("EXPO_PUBLIC_SENTRY_DSN"));
4322        assert!(is_public_env_var("STORYBOOK_ENV"));
4323    }
4324
4325    #[test]
4326    fn is_public_env_var_secret_token_beats_metadata_token() {
4327        // "SECRET_SHA": has SECRET (wins) and SHA (metadata); should NOT be public
4328        assert!(!is_public_env_var("SECRET_SHA"));
4329        // "REF_TOKEN": has TOKEN (secret) and REF (metadata); should NOT be public
4330        assert!(!is_public_env_var("REF_TOKEN"));
4331    }
4332
4333    #[test]
4334    fn is_public_env_var_plain_unknown_names_are_not_public() {
4335        assert!(!is_public_env_var("MY_SERVICE_URL"));
4336        assert!(!is_public_env_var("FEATURE_FLAG"));
4337        assert!(!is_public_env_var("DATABASE_URL"));
4338    }
4339
4340    // --- SinkSite::span ---
4341
4342    #[test]
4343    fn sink_site_span_reconstructs_from_offsets() {
4344        let site = SinkSite {
4345            sink_shape: SinkShape::Call,
4346            callee_path: "eval".to_string(),
4347            arg_index: 0,
4348            arg_is_non_literal: true,
4349            arg_kind: SinkArgKind::Other,
4350            arg_literal: None,
4351            regex_pattern: None,
4352            object_properties: Vec::new(),
4353            object_property_keys: Vec::new(),
4354            object_property_keys_complete: false,
4355            arg_idents: Vec::new(),
4356            arg_source_paths: Vec::new(),
4357            span_start: 5,
4358            span_end: 15,
4359            url_arg_literal: None,
4360            url_shape: None,
4361        };
4362        let s = site.span();
4363        assert_eq!(s.start, 5);
4364        assert_eq!(s.end, 15);
4365    }
4366
4367    // --- SecurityControlKind ---
4368
4369    #[test]
4370    fn security_control_kind_equality_and_ordering() {
4371        assert_eq!(
4372            SecurityControlKind::Sanitization,
4373            SecurityControlKind::Sanitization
4374        );
4375        assert_eq!(
4376            SecurityControlKind::Validation,
4377            SecurityControlKind::Validation
4378        );
4379        assert_ne!(
4380            SecurityControlKind::Sanitization,
4381            SecurityControlKind::Validation
4382        );
4383        assert!(SecurityControlKind::Sanitization < SecurityControlKind::Validation);
4384        assert!(SecurityControlKind::Authentication < SecurityControlKind::Authorization);
4385    }
4386
4387    // --- SanitizerScope ---
4388
4389    #[test]
4390    fn sanitizer_scope_equality_and_ordering() {
4391        assert_eq!(SanitizerScope::Html, SanitizerScope::Html);
4392        assert_eq!(SanitizerScope::Url, SanitizerScope::Url);
4393        assert_eq!(SanitizerScope::Path, SanitizerScope::Path);
4394        assert_eq!(SanitizerScope::SqlIdentifier, SanitizerScope::SqlIdentifier);
4395        assert_ne!(SanitizerScope::Html, SanitizerScope::Url);
4396        assert!(SanitizerScope::Html < SanitizerScope::Url);
4397    }
4398
4399    // --- SkippedSecurityCalleeReason ---
4400
4401    #[test]
4402    fn skipped_security_callee_reason_equality() {
4403        assert_eq!(
4404            SkippedSecurityCalleeReason::ComputedMember,
4405            SkippedSecurityCalleeReason::ComputedMember
4406        );
4407        assert_ne!(
4408            SkippedSecurityCalleeReason::ComputedMember,
4409            SkippedSecurityCalleeReason::DynamicDispatch
4410        );
4411        assert_ne!(
4412            SkippedSecurityCalleeReason::DynamicDispatch,
4413            SkippedSecurityCalleeReason::UnsupportedAssignmentObject
4414        );
4415    }
4416
4417    // --- SkippedSecurityCalleeExpressionKind ---
4418
4419    #[test]
4420    fn skipped_security_callee_expression_kind_equality() {
4421        use SkippedSecurityCalleeExpressionKind as K;
4422        assert_eq!(K::StaticMemberExpression, K::StaticMemberExpression);
4423        assert_eq!(K::ComputedMemberExpression, K::ComputedMemberExpression);
4424        assert_eq!(K::Identifier, K::Identifier);
4425        assert_eq!(K::Other, K::Other);
4426        assert_ne!(K::StaticMemberExpression, K::ComputedMemberExpression);
4427        assert_ne!(K::Identifier, K::Other);
4428    }
4429
4430    // --- SinkLiteralValue ---
4431
4432    #[test]
4433    fn sink_literal_value_equality() {
4434        assert_eq!(
4435            SinkLiteralValue::String("x".to_string()),
4436            SinkLiteralValue::String("x".to_string())
4437        );
4438        assert_ne!(
4439            SinkLiteralValue::String("x".to_string()),
4440            SinkLiteralValue::String("y".to_string())
4441        );
4442        assert_eq!(SinkLiteralValue::Integer(42), SinkLiteralValue::Integer(42));
4443        assert_ne!(SinkLiteralValue::Integer(1), SinkLiteralValue::Integer(2));
4444        assert_eq!(
4445            SinkLiteralValue::Boolean(true),
4446            SinkLiteralValue::Boolean(true)
4447        );
4448        assert_ne!(
4449            SinkLiteralValue::Boolean(true),
4450            SinkLiteralValue::Boolean(false)
4451        );
4452        assert_eq!(SinkLiteralValue::Null, SinkLiteralValue::Null);
4453        assert_ne!(SinkLiteralValue::Null, SinkLiteralValue::Boolean(false));
4454    }
4455
4456    // --- SecurityUrlShape ---
4457
4458    #[test]
4459    fn security_url_shape_equality() {
4460        assert_eq!(
4461            SecurityUrlShape::FixedOriginDynamicPath,
4462            SecurityUrlShape::FixedOriginDynamicPath
4463        );
4464        assert_eq!(
4465            SecurityUrlShape::DynamicOrigin,
4466            SecurityUrlShape::DynamicOrigin
4467        );
4468        assert_ne!(
4469            SecurityUrlShape::FixedOriginDynamicPath,
4470            SecurityUrlShape::DynamicOrigin
4471        );
4472    }
4473
4474    // --- FlagUseKind ---
4475
4476    #[test]
4477    fn flag_use_kind_equality() {
4478        assert_eq!(FlagUseKind::EnvVar, FlagUseKind::EnvVar);
4479        assert_eq!(FlagUseKind::SdkCall, FlagUseKind::SdkCall);
4480        assert_eq!(FlagUseKind::ConfigObject, FlagUseKind::ConfigObject);
4481        assert_ne!(FlagUseKind::EnvVar, FlagUseKind::SdkCall);
4482        assert_ne!(FlagUseKind::SdkCall, FlagUseKind::ConfigObject);
4483    }
4484
4485    // --- ComplexityMetric ---
4486
4487    #[test]
4488    fn complexity_metric_equality() {
4489        assert_eq!(ComplexityMetric::Cyclomatic, ComplexityMetric::Cyclomatic);
4490        assert_eq!(ComplexityMetric::Cognitive, ComplexityMetric::Cognitive);
4491        assert_ne!(ComplexityMetric::Cyclomatic, ComplexityMetric::Cognitive);
4492    }
4493
4494    // --- ComplexityContributionKind ---
4495
4496    #[test]
4497    fn complexity_contribution_kind_equality_spot_check() {
4498        use ComplexityContributionKind as K;
4499        assert_eq!(K::If, K::If);
4500        assert_eq!(K::Else, K::Else);
4501        assert_eq!(K::ElseIf, K::ElseIf);
4502        assert_eq!(K::Ternary, K::Ternary);
4503        assert_eq!(K::LogicalAnd, K::LogicalAnd);
4504        assert_eq!(K::LogicalOr, K::LogicalOr);
4505        assert_eq!(K::NullishCoalescing, K::NullishCoalescing);
4506        assert_eq!(K::LogicalAssignment, K::LogicalAssignment);
4507        assert_eq!(K::OptionalChain, K::OptionalChain);
4508        assert_eq!(K::For, K::For);
4509        assert_eq!(K::ForIn, K::ForIn);
4510        assert_eq!(K::ForOf, K::ForOf);
4511        assert_eq!(K::While, K::While);
4512        assert_eq!(K::DoWhile, K::DoWhile);
4513        assert_eq!(K::Switch, K::Switch);
4514        assert_eq!(K::Case, K::Case);
4515        assert_eq!(K::Catch, K::Catch);
4516        assert_eq!(K::LabeledBreak, K::LabeledBreak);
4517        assert_eq!(K::LabeledContinue, K::LabeledContinue);
4518        assert_eq!(K::JsxDepth, K::JsxDepth);
4519        assert_eq!(K::HookDensity, K::HookDensity);
4520        assert_eq!(K::PropCount, K::PropCount);
4521        assert_eq!(K::Await, K::Await);
4522        assert_eq!(K::Then, K::Then);
4523        assert_ne!(K::If, K::Else);
4524        assert_ne!(K::For, K::While);
4525        assert_ne!(K::Switch, K::Case);
4526    }
4527
4528    // --- MisplacedDirectiveSite ---
4529
4530    #[test]
4531    fn misplaced_directive_site_equality() {
4532        let client = MisplacedDirectiveSite {
4533            is_server: false,
4534            span_start: 10,
4535        };
4536        let server = MisplacedDirectiveSite {
4537            is_server: true,
4538            span_start: 10,
4539        };
4540        let client2 = MisplacedDirectiveSite {
4541            is_server: false,
4542            span_start: 10,
4543        };
4544        assert_eq!(client, client2);
4545        assert_ne!(client, server);
4546    }
4547
4548    #[test]
4549    fn misplaced_directive_site_is_server_flag() {
4550        let site = MisplacedDirectiveSite {
4551            is_server: true,
4552            span_start: 42,
4553        };
4554        assert!(site.is_server);
4555        assert_eq!(site.span_start, 42);
4556
4557        let client_site = MisplacedDirectiveSite {
4558            is_server: false,
4559            span_start: 0,
4560        };
4561        assert!(!client_site.is_server);
4562    }
4563
4564    // --- DiRole / DiFramework ---
4565
4566    #[test]
4567    fn di_role_equality() {
4568        assert_eq!(DiRole::Provide, DiRole::Provide);
4569        assert_eq!(DiRole::Inject, DiRole::Inject);
4570        assert_ne!(DiRole::Provide, DiRole::Inject);
4571    }
4572
4573    #[test]
4574    fn di_framework_equality() {
4575        assert_eq!(DiFramework::Vue, DiFramework::Vue);
4576        assert_eq!(DiFramework::Svelte, DiFramework::Svelte);
4577        assert_eq!(DiFramework::Angular, DiFramework::Angular);
4578        assert_ne!(DiFramework::Vue, DiFramework::Svelte);
4579        assert_ne!(DiFramework::Svelte, DiFramework::Angular);
4580    }
4581
4582    // --- ComponentEmit ---
4583
4584    #[test]
4585    fn component_emit_equality() {
4586        let a = ComponentEmit {
4587            name: "close".to_string(),
4588            span_start: 10,
4589            used: true,
4590        };
4591        let b = ComponentEmit {
4592            name: "close".to_string(),
4593            span_start: 10,
4594            used: true,
4595        };
4596        let different_used = ComponentEmit {
4597            name: "close".to_string(),
4598            span_start: 10,
4599            used: false,
4600        };
4601        let different_name = ComponentEmit {
4602            name: "open".to_string(),
4603            span_start: 10,
4604            used: true,
4605        };
4606        assert_eq!(a, b);
4607        assert_ne!(a, different_used);
4608        assert_ne!(a, different_name);
4609    }
4610
4611    // --- DispatchedEvent ---
4612
4613    #[test]
4614    fn dispatched_event_equality() {
4615        let a = DispatchedEvent {
4616            name: "myEvent".to_string(),
4617            span_start: 20,
4618        };
4619        let b = DispatchedEvent {
4620            name: "myEvent".to_string(),
4621            span_start: 20,
4622        };
4623        let c = DispatchedEvent {
4624            name: "otherEvent".to_string(),
4625            span_start: 20,
4626        };
4627        let d = DispatchedEvent {
4628            name: "myEvent".to_string(),
4629            span_start: 99,
4630        };
4631        assert_eq!(a, b);
4632        assert_ne!(a, c);
4633        assert_ne!(a, d);
4634    }
4635
4636    // --- AngularInputMember / AngularOutputMember ---
4637
4638    #[test]
4639    fn angular_input_member_equality() {
4640        let a = AngularInputMember {
4641            name: "title".to_string(),
4642            span_start: 5,
4643        };
4644        let b = AngularInputMember {
4645            name: "title".to_string(),
4646            span_start: 5,
4647        };
4648        let c = AngularInputMember {
4649            name: "label".to_string(),
4650            span_start: 5,
4651        };
4652        assert_eq!(a, b);
4653        assert_ne!(a, c);
4654    }
4655
4656    #[test]
4657    fn angular_output_member_equality() {
4658        let a = AngularOutputMember {
4659            name: "clicked".to_string(),
4660            span_start: 8,
4661        };
4662        let b = AngularOutputMember {
4663            name: "clicked".to_string(),
4664            span_start: 8,
4665        };
4666        let c = AngularOutputMember {
4667            name: "hovered".to_string(),
4668            span_start: 8,
4669        };
4670        assert_eq!(a, b);
4671        assert_ne!(a, c);
4672    }
4673
4674    // --- AngularComponentSelector ---
4675
4676    #[test]
4677    fn angular_component_selector_fields() {
4678        let s = AngularComponentSelector {
4679            selectors: vec!["app-foo".to_string(), "[appFoo]".to_string()],
4680            span_start: 100,
4681            class_name: "FooComponent".to_string(),
4682        };
4683        assert_eq!(s.selectors.len(), 2);
4684        assert_eq!(s.selectors[0], "app-foo");
4685        assert_eq!(s.selectors[1], "[appFoo]");
4686        assert_eq!(s.class_name, "FooComponent");
4687    }
4688
4689    #[test]
4690    fn angular_component_selector_equality() {
4691        let a = AngularComponentSelector {
4692            selectors: vec!["app-bar".to_string()],
4693            span_start: 0,
4694            class_name: "BarComponent".to_string(),
4695        };
4696        let b = AngularComponentSelector {
4697            selectors: vec!["app-bar".to_string()],
4698            span_start: 0,
4699            class_name: "BarComponent".to_string(),
4700        };
4701        let c = AngularComponentSelector {
4702            selectors: vec!["app-baz".to_string()],
4703            span_start: 0,
4704            class_name: "BazComponent".to_string(),
4705        };
4706        assert_eq!(a, b);
4707        assert_ne!(a, c);
4708    }
4709
4710    // --- LoadReturnKey ---
4711
4712    #[test]
4713    fn load_return_key_equality() {
4714        let a = LoadReturnKey {
4715            name: "user".to_string(),
4716            span_start: 50,
4717            span_end: 54,
4718        };
4719        let b = LoadReturnKey {
4720            name: "user".to_string(),
4721            span_start: 50,
4722            span_end: 54,
4723        };
4724        let c = LoadReturnKey {
4725            name: "posts".to_string(),
4726            span_start: 50,
4727            span_end: 55,
4728        };
4729        assert_eq!(a, b);
4730        assert_ne!(a, c);
4731    }
4732
4733    #[test]
4734    fn load_return_key_span_fields() {
4735        let key = LoadReturnKey {
4736            name: "data".to_string(),
4737            span_start: 10,
4738            span_end: 14,
4739        };
4740        assert_eq!(key.span_start, 10);
4741        assert_eq!(key.span_end, 14);
4742        assert_eq!(key.name, "data");
4743    }
4744
4745    // --- ComponentFunctionKind ---
4746
4747    #[test]
4748    fn component_function_kind_equality() {
4749        assert_eq!(ComponentFunctionKind::FnDecl, ComponentFunctionKind::FnDecl);
4750        assert_eq!(ComponentFunctionKind::Arrow, ComponentFunctionKind::Arrow);
4751        assert_eq!(
4752            ComponentFunctionKind::ForwardRefWrapper,
4753            ComponentFunctionKind::ForwardRefWrapper
4754        );
4755        assert_eq!(
4756            ComponentFunctionKind::MemoWrapper,
4757            ComponentFunctionKind::MemoWrapper
4758        );
4759        assert_ne!(ComponentFunctionKind::FnDecl, ComponentFunctionKind::Arrow);
4760        assert_ne!(
4761            ComponentFunctionKind::ForwardRefWrapper,
4762            ComponentFunctionKind::MemoWrapper
4763        );
4764    }
4765
4766    // --- HookUseKind ---
4767
4768    #[test]
4769    fn hook_use_kind_equality() {
4770        assert_eq!(HookUseKind::UseState, HookUseKind::UseState);
4771        assert_eq!(HookUseKind::UseEffect, HookUseKind::UseEffect);
4772        assert_eq!(HookUseKind::UseMemo, HookUseKind::UseMemo);
4773        assert_eq!(HookUseKind::UseCallback, HookUseKind::UseCallback);
4774        assert_eq!(HookUseKind::Custom, HookUseKind::Custom);
4775        assert_ne!(HookUseKind::UseState, HookUseKind::UseEffect);
4776        assert_ne!(HookUseKind::UseMemo, HookUseKind::Custom);
4777    }
4778
4779    // --- HookUse ---
4780
4781    #[test]
4782    fn hook_use_fields() {
4783        let h = HookUse {
4784            kind: HookUseKind::UseEffect,
4785            dep_array_arity: Some(2),
4786            span_start: 30,
4787            component: "Widget".to_string(),
4788        };
4789        assert_eq!(h.kind, HookUseKind::UseEffect);
4790        assert_eq!(h.dep_array_arity, Some(2));
4791        assert_eq!(h.span_start, 30);
4792        assert_eq!(h.component, "Widget");
4793    }
4794
4795    #[test]
4796    fn hook_use_no_dep_array() {
4797        let h = HookUse {
4798            kind: HookUseKind::UseCallback,
4799            dep_array_arity: None,
4800            span_start: 0,
4801            component: String::new(),
4802        };
4803        assert!(h.dep_array_arity.is_none());
4804    }
4805
4806    // --- MemberKind::StoreMember (missed in existing bitcode test) ---
4807
4808    #[test]
4809    fn member_kind_store_member_bitcode_roundtrip() {
4810        let kind = MemberKind::StoreMember;
4811        let bytes = bitcode::encode(&kind);
4812        let decoded: MemberKind = bitcode::decode(&bytes).unwrap();
4813        assert_eq!(decoded, kind);
4814    }
4815
4816    // --- RenderEdge / ForwardAttr ---
4817
4818    #[test]
4819    fn render_edge_fields() {
4820        let edge = RenderEdge {
4821            parent_component: "Parent".to_string(),
4822            child_component_name: "Child".to_string(),
4823            attr_names: vec!["title".to_string(), "onClick".to_string()],
4824            has_spread: false,
4825            forward_attrs: vec![ForwardAttr {
4826                attr: "title".to_string(),
4827                root: "props".to_string(),
4828            }],
4829            has_complex_forward: false,
4830        };
4831        assert_eq!(edge.parent_component, "Parent");
4832        assert_eq!(edge.child_component_name, "Child");
4833        assert_eq!(edge.attr_names.len(), 2);
4834        assert!(!edge.has_spread);
4835        assert_eq!(edge.forward_attrs.len(), 1);
4836        assert_eq!(edge.forward_attrs[0].attr, "title");
4837        assert_eq!(edge.forward_attrs[0].root, "props");
4838        assert!(!edge.has_complex_forward);
4839    }
4840
4841    #[test]
4842    fn render_edge_with_spread() {
4843        let edge = RenderEdge {
4844            parent_component: "Wrapper".to_string(),
4845            child_component_name: "Inner".to_string(),
4846            attr_names: Vec::new(),
4847            has_spread: true,
4848            forward_attrs: Vec::new(),
4849            has_complex_forward: true,
4850        };
4851        assert!(edge.has_spread);
4852        assert!(edge.has_complex_forward);
4853    }
4854
4855    // --- ComponentFunction ---
4856
4857    #[test]
4858    fn component_function_fields() {
4859        let cf = ComponentFunction {
4860            name: "MyButton".to_string(),
4861            span_start: 0,
4862            kind: ComponentFunctionKind::Arrow,
4863            is_exported: true,
4864            has_unharvestable_props: false,
4865            uses_clone_element: false,
4866            renders_provider: false,
4867            has_children_as_function: false,
4868            is_pure_passthrough: false,
4869        };
4870        assert_eq!(cf.name, "MyButton");
4871        assert_eq!(cf.kind, ComponentFunctionKind::Arrow);
4872        assert!(cf.is_exported);
4873        assert!(!cf.has_unharvestable_props);
4874        assert!(!cf.is_pure_passthrough);
4875    }
4876
4877    #[test]
4878    fn component_function_passthrough_flag() {
4879        let cf = ComponentFunction {
4880            name: "Passthrough".to_string(),
4881            span_start: 5,
4882            kind: ComponentFunctionKind::FnDecl,
4883            is_exported: false,
4884            has_unharvestable_props: false,
4885            uses_clone_element: false,
4886            renders_provider: false,
4887            has_children_as_function: false,
4888            is_pure_passthrough: true,
4889        };
4890        assert!(cf.is_pure_passthrough);
4891        assert!(!cf.is_exported);
4892    }
4893
4894    // --- DiKeySite ---
4895
4896    #[test]
4897    fn di_key_site_fields() {
4898        let site = DiKeySite {
4899            key_local: "MY_KEY".to_string(),
4900            role: DiRole::Provide,
4901            framework: DiFramework::Vue,
4902            span_start: 77,
4903        };
4904        assert_eq!(site.key_local, "MY_KEY");
4905        assert_eq!(site.role, DiRole::Provide);
4906        assert_eq!(site.framework, DiFramework::Vue);
4907        assert_eq!(site.span_start, 77);
4908    }
4909
4910    #[test]
4911    fn di_key_site_inject_svelte() {
4912        let site = DiKeySite {
4913            key_local: "ctx_key".to_string(),
4914            role: DiRole::Inject,
4915            framework: DiFramework::Svelte,
4916            span_start: 0,
4917        };
4918        assert_eq!(site.role, DiRole::Inject);
4919        assert_eq!(site.framework, DiFramework::Svelte);
4920    }
4921
4922    // --- release_resolution_payload: page data store whole-use derivation ---
4923
4924    #[test]
4925    fn release_payload_derives_page_data_store_whole_use_from_page_data() {
4926        let mut m = minimal_module_info();
4927        m.whole_object_uses = vec!["page.data".to_string()].into();
4928        m.release_resolution_payload();
4929        assert!(m.has_page_data_store_whole_use);
4930    }
4931
4932    #[test]
4933    fn release_payload_derives_page_data_store_whole_use_from_dollar_page_data() {
4934        let mut m = minimal_module_info();
4935        m.whole_object_uses = vec!["$page.data".to_string()].into();
4936        m.release_resolution_payload();
4937        assert!(m.has_page_data_store_whole_use);
4938    }
4939
4940    #[test]
4941    fn release_payload_does_not_set_page_data_store_whole_use_for_other_names() {
4942        let mut m = minimal_module_info();
4943        m.whole_object_uses = vec!["data".to_string(), "page".to_string()].into();
4944        m.release_resolution_payload();
4945        assert!(!m.has_page_data_store_whole_use);
4946    }
4947
4948    #[test]
4949    fn release_payload_derives_route_loader_data_whole_use() {
4950        let mut m = minimal_module_info();
4951        m.whole_object_uses = vec!["$fallow.routeLoaderData".to_string()].into();
4952        m.release_resolution_payload();
4953        assert!(m.has_route_loader_data_whole_use);
4954    }
4955
4956    // --- release_resolution_payload: referenced_import_bindings derivation ---
4957
4958    #[test]
4959    fn release_payload_referenced_bindings_excludes_empty_local_names() {
4960        let mut m = minimal_module_info();
4961        m.imports = vec![
4962            ImportInfo {
4963                source: "./styles.css".to_string(),
4964                imported_name: ImportedName::SideEffect,
4965                local_name: String::new(), // empty = side-effect import
4966                is_type_only: false,
4967                is_type_only_star: false,
4968                from_style: true,
4969                span: span(),
4970                source_span: span(),
4971            },
4972            ImportInfo {
4973                source: "react".to_string(),
4974                imported_name: ImportedName::Default,
4975                local_name: "React".to_string(),
4976                is_type_only: false,
4977                is_type_only_star: false,
4978                from_style: false,
4979                span: span(),
4980                source_span: span(),
4981            },
4982        ];
4983        m.unused_import_bindings = vec!["React".to_string()];
4984        m.release_resolution_payload();
4985        // "React" was unused, empty local is filtered; result should be empty
4986        assert!(m.referenced_import_bindings.is_empty());
4987    }
4988
4989    #[test]
4990    fn release_payload_referenced_bindings_sorted_and_deduped() {
4991        let mut m = minimal_module_info();
4992        // Two imports with the same local name (unusual but possible via re-exports)
4993        m.imports = vec![
4994            ImportInfo {
4995                source: "a".to_string(),
4996                imported_name: ImportedName::Named("foo".to_string()),
4997                local_name: "foo".to_string(),
4998                is_type_only: false,
4999                is_type_only_star: false,
5000                from_style: false,
5001                span: span(),
5002                source_span: span(),
5003            },
5004            ImportInfo {
5005                source: "b".to_string(),
5006                imported_name: ImportedName::Named("bar".to_string()),
5007                local_name: "bar".to_string(),
5008                is_type_only: false,
5009                is_type_only_star: false,
5010                from_style: false,
5011                span: span(),
5012                source_span: span(),
5013            },
5014            ImportInfo {
5015                source: "c".to_string(),
5016                imported_name: ImportedName::Named("foo".to_string()),
5017                local_name: "foo".to_string(),
5018                is_type_only: false,
5019                is_type_only_star: false,
5020                from_style: false,
5021                span: span(),
5022                source_span: span(),
5023            },
5024        ];
5025        m.unused_import_bindings = Vec::new();
5026        m.release_resolution_payload();
5027        // sorted: ["bar", "foo"] with "foo" deduped
5028        assert_eq!(
5029            m.referenced_import_bindings,
5030            vec!["bar".to_string(), "foo".to_string()]
5031        );
5032    }
5033
5034    // --- CalleeUse ---
5035
5036    #[test]
5037    fn callee_use_fields() {
5038        let cu = CalleeUse {
5039            callee_path: "child_process.exec".to_string(),
5040            span_start: 100,
5041        };
5042        assert_eq!(cu.callee_path, "child_process.exec");
5043        assert_eq!(cu.span_start, 100);
5044    }
5045
5046    // --- Helper to build a minimal ModuleInfo for targeted tests ---
5047
5048    fn minimal_module_info() -> ModuleInfo {
5049        ModuleInfo::empty(FileId(0))
5050    }
5051
5052    fn push_semantic_fact(module: &mut ModuleInfo, fact: SemanticFact) {
5053        let mut facts = std::mem::take(&mut module.semantic_facts).to_vec();
5054        facts.push(fact);
5055        module.semantic_facts = facts.into();
5056    }
5057
5058    #[test]
5059    fn dynamic_custom_element_render_helper_prefers_typed_fact() {
5060        let mut module = minimal_module_info();
5061        push_semantic_fact(
5062            &mut module,
5063            SemanticFact::DynamicCustomElementRender(DynamicCustomElementRenderFact),
5064        );
5065
5066        assert!(has_dynamic_custom_element_render(&module));
5067    }
5068
5069    #[test]
5070    fn function_complexity_bitcode_roundtrip() {
5071        let fc = FunctionComplexity {
5072            name: "processData".to_string(),
5073            is_private_member: true,
5074            line: 42,
5075            col: 4,
5076            cyclomatic: 15,
5077            cognitive: 25,
5078            line_count: 80,
5079            param_count: 3,
5080            react_hook_count: 0,
5081            react_jsx_max_depth: 0,
5082            react_prop_count: 0,
5083            source_hash: Some("0123456789abcdef".to_string()),
5084            contributions: vec![
5085                ComplexityContribution {
5086                    line: 43,
5087                    col: 8,
5088                    metric: ComplexityMetric::Cyclomatic,
5089                    kind: ComplexityContributionKind::If,
5090                    weight: 1,
5091                    nesting: 0,
5092                },
5093                ComplexityContribution {
5094                    line: 45,
5095                    col: 12,
5096                    metric: ComplexityMetric::Cognitive,
5097                    kind: ComplexityContributionKind::ElseIf,
5098                    weight: 3,
5099                    nesting: 2,
5100                },
5101            ],
5102        };
5103        let bytes = bitcode::encode(&fc);
5104        let decoded: FunctionComplexity = bitcode::decode(&bytes).unwrap();
5105        assert_eq!(decoded.name, "processData");
5106        assert!(decoded.is_private_member);
5107        assert_eq!(decoded.line, 42);
5108        assert_eq!(decoded.col, 4);
5109        assert_eq!(decoded.cyclomatic, 15);
5110        assert_eq!(decoded.cognitive, 25);
5111        assert_eq!(decoded.line_count, 80);
5112        assert_eq!(decoded.source_hash.as_deref(), Some("0123456789abcdef"));
5113        assert_eq!(decoded.contributions.len(), 2);
5114        assert_eq!(
5115            decoded.contributions[1].kind,
5116            ComplexityContributionKind::ElseIf
5117        );
5118        assert_eq!(decoded.contributions[1].weight, 3);
5119        assert_eq!(decoded.contributions[1].nesting, 2);
5120        assert_eq!(decoded.contributions[1].metric, ComplexityMetric::Cognitive);
5121    }
5122}