Skip to main content

fallow_types/
extract.rs

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