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