Skip to main content

fallow_types/
extract.rs

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