Skip to main content

fallow_types/
extract.rs

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