Skip to main content

fallow_types/
extract.rs

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