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