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