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