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