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