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