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