fallow_types/trace.rs
1//! Shared trace output contracts for analysis and integration surfaces.
2
3use std::path::{Path, PathBuf};
4
5use serde::Serialize;
6
7use crate::cache_rejection::CacheRejection;
8use crate::duplicates::{CloneInstance, RefactoringSuggestion};
9use crate::semantic::SemanticNamespace;
10use crate::serde_path;
11use crate::trace_chain::StarExportAmbiguity;
12
13/// Result of tracing an export: why it is considered used or unused.
14#[derive(Debug, Serialize)]
15#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16pub struct ExportTrace {
17 /// The file containing the export.
18 #[serde(serialize_with = "serde_path::serialize")]
19 pub file: PathBuf,
20 /// The export name being traced.
21 pub export_name: String,
22 /// Namespace whose references are listed for the traced export. The
23 /// preferred lane wins whenever it carries a reference: `value` for a
24 /// value export, `type` for a type-only one. When the preferred lane
25 /// carries none and the other lane resolves to the same declaration, the
26 /// other lane's references are listed and this field names it, so a value
27 /// export whose only credit is a bound `import type` reports `type` with
28 /// `is_used: true`. `is_used` and `direct_references` follow the listed
29 /// lane only, and only reachable reference sources can credit it. Legal
30 /// declaration merges share one declaration group across lanes, including
31 /// an `interface` next to a same-name `class` and a `class` next to a
32 /// same-name `namespace`, so references to either lane credit the merged
33 /// declaration. Distinct same-name declarations outside a merge remain
34 /// separate and keep the preferred lane. `semantic.target.namespace`
35 /// names the lane the declaration itself occupies and can therefore differ
36 /// from this field. Producers always emit the field; the schema permits
37 /// omission by payloads created before namespaces were exposed.
38 #[cfg_attr(feature = "schema", schemars(default))]
39 pub namespace: crate::semantic::SemanticNamespace,
40 /// Whether the file is reachable from an entry point.
41 pub file_reachable: bool,
42 /// Whether the file is an entry point.
43 pub is_entry_point: bool,
44 /// Whether the export is considered used.
45 pub is_used: bool,
46 /// Files that reference this export directly.
47 pub direct_references: Vec<ExportReference>,
48 /// Reachable direct references grouped by namespace. This is additive to
49 /// `namespace` and `direct_references`, whose winning-lane meaning remains
50 /// unchanged for backwards compatibility.
51 #[serde(default, skip_serializing_if = "Vec::is_empty")]
52 pub direct_references_by_namespace: Vec<NamespacedExportReferences>,
53 /// A star-export collision that makes the traced name ambiguous. When
54 /// present, `is_used: false` is an abstention rather than an unused-code
55 /// verdict.
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub star_export_ambiguity: Option<StarExportAmbiguity>,
58 /// Re-export chains that pass through this export.
59 pub re_export_chains: Vec<ReExportChain>,
60 /// Human-readable reason summary.
61 pub reason: String,
62 /// Exact checker-backed references when type-aware tracing is enabled.
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub semantic: Option<crate::semantic::SemanticSymbolTrace>,
65}
66
67/// Result of tracing a class / enum / store MEMBER: the `--trace FILE:NAME`
68/// fallback when `NAME` is not a top-level export but a member declared on one
69/// (issue #1744). The trace runs on the module graph only, so it reports the
70/// OWNING export's reachability and usage (the gating precondition for
71/// member-level crediting) plus a pointer to the right `--unused-*-members`
72/// command, rather than per-member crediting provenance.
73#[derive(Debug, Serialize)]
74#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
75pub struct ClassMemberTrace {
76 /// The file containing the member.
77 #[serde(serialize_with = "serde_path::serialize")]
78 pub file: PathBuf,
79 /// The member name being traced.
80 pub member_name: String,
81 /// The member kind: `class-method`, `class-property`, `enum-member`,
82 /// `store-member`, or `namespace-member`.
83 pub member_kind: String,
84 /// The export that declares this member (the class / enum / store name).
85 pub owner_export: String,
86 /// Namespace whose references credit the owning export, mirroring
87 /// [`ExportTrace::namespace`] for the export this member is declared on.
88 /// `owner_is_used` and `owner_direct_references` describe that lane, so a
89 /// member of a value export credited only by a bound `import type` reports
90 /// `type` here. `semantic.target.namespace` names the lane the checker
91 /// proof covers and can therefore differ. Producers always emit the field;
92 /// the schema permits omission by payloads created before the owner
93 /// namespace was exposed.
94 #[cfg_attr(feature = "schema", schemars(default))]
95 pub owner_namespace: crate::semantic::SemanticNamespace,
96 /// Whether the owning export is considered used.
97 pub owner_is_used: bool,
98 /// Whether the file is reachable from an entry point.
99 pub owner_file_reachable: bool,
100 /// Whether the file is an entry point.
101 pub owner_is_entry_point: bool,
102 /// Files that reference the owning export directly.
103 pub owner_direct_references: Vec<ExportReference>,
104 /// Re-export chains through which the owning export is reachable. Populated
105 /// so a machine consumer can tell "used via a barrel" (empty direct refs but
106 /// non-empty chains) from "genuinely unreferenced".
107 pub owner_re_export_chains: Vec<ReExportChain>,
108 /// Human-readable reason summary plus the follow-up command to inspect the
109 /// member finding.
110 pub reason: String,
111 /// Exact checker-backed member references when type-aware tracing is enabled.
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub semantic: Option<crate::semantic::SemanticSymbolTrace>,
114}
115
116/// A direct reference to an export.
117#[derive(Debug, Clone, Serialize)]
118#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
119pub struct ExportReference {
120 /// File that contains the reference.
121 #[serde(serialize_with = "serde_path::serialize")]
122 pub from_file: PathBuf,
123 /// Reference kind, such as named import, default import, or re-export.
124 pub kind: String,
125}
126
127/// Direct references that credit one namespace of an export binding.
128#[derive(Debug, Serialize)]
129#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
130pub struct NamespacedExportReferences {
131 /// Credited namespace.
132 pub namespace: SemanticNamespace,
133 /// Number of reachable references in this namespace.
134 pub reference_count: usize,
135 /// Reachable references in deterministic graph order.
136 pub references: Vec<ExportReference>,
137}
138
139/// A re-export chain showing how an export is propagated.
140#[derive(Debug, Serialize)]
141#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
142pub struct ReExportChain {
143 /// The barrel file that re-exports this symbol.
144 #[serde(serialize_with = "serde_path::serialize")]
145 pub barrel_file: PathBuf,
146 /// The name it is re-exported as.
147 pub exported_as: String,
148 /// Number of references on the barrel's re-exported symbol.
149 pub reference_count: usize,
150}
151
152/// Result of tracing all edges for a file.
153#[derive(Debug, Serialize)]
154#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
155pub struct FileTrace {
156 /// The traced file.
157 #[serde(serialize_with = "serde_path::serialize")]
158 pub file: PathBuf,
159 /// Whether this file is reachable from entry points.
160 pub is_reachable: bool,
161 /// Whether this file is an entry point.
162 pub is_entry_point: bool,
163 /// Exports declared by this file.
164 pub exports: Vec<TracedExport>,
165 /// Files that this file imports from.
166 #[serde(serialize_with = "serde_path::serialize_vec")]
167 pub imports_from: Vec<PathBuf>,
168 /// Files that import from this file.
169 #[serde(serialize_with = "serde_path::serialize_vec")]
170 pub imported_by: Vec<PathBuf>,
171 /// Re-exports declared by this file.
172 pub re_exports: Vec<TracedReExport>,
173 /// The configs that make this file an entry point through Module
174 /// Federation `exposes`, one per config. Absent when no Federation config
175 /// exposes the file (issue #2796).
176 #[serde(default, skip_serializing_if = "Vec::is_empty")]
177 pub sources: Vec<TraceSource>,
178}
179
180/// Which configs name which files and which dependency names, collected by one
181/// analysis for the trace output.
182///
183/// It holds plain data, so a trace looks a file up without the rules that
184/// produced it.
185#[derive(Debug, Clone, Default, PartialEq, Eq)]
186pub struct TraceProvenance {
187 /// Root-relative file path and the config that names it.
188 files: Vec<(PathBuf, TraceSource)>,
189 /// Dependency name and the config that names it.
190 dependencies: Vec<(String, TraceSource)>,
191 /// Dependency name and why the unused devDependency check credits it as
192 /// tooling.
193 tooling_credits: Vec<(String, ToolingCredit)>,
194}
195
196impl TraceProvenance {
197 /// Record that `source` names the root-relative `file`.
198 pub fn push_file(&mut self, file: PathBuf, source: TraceSource) {
199 if !self
200 .files
201 .iter()
202 .any(|(known, known_source)| *known == file && *known_source == source)
203 {
204 self.files.push((file, source));
205 }
206 }
207
208 /// Record that `source` names the dependency `name`.
209 pub fn push_dependency(&mut self, name: String, source: TraceSource) {
210 if !self
211 .dependencies
212 .iter()
213 .any(|(known, known_source)| *known == name && *known_source == source)
214 {
215 self.dependencies.push((name, source));
216 }
217 }
218
219 /// The configs that name `file`, a root-relative path.
220 #[must_use]
221 pub fn file_sources(&self, file: &std::path::Path) -> Vec<TraceSource> {
222 self.files
223 .iter()
224 .filter(|(known, _)| known == file)
225 .map(|(_, source)| source.clone())
226 .collect()
227 }
228
229 /// The configs that name the dependency `name`.
230 #[must_use]
231 pub fn dependency_sources(&self, name: &str) -> Vec<TraceSource> {
232 self.dependencies
233 .iter()
234 .filter(|(known, _)| known == name)
235 .map(|(_, source)| source.clone())
236 .collect()
237 }
238
239 /// Record why the dependency `name` is credited as tooling. The first
240 /// credit recorded for a name wins.
241 pub fn push_tooling_credit(&mut self, name: String, credit: ToolingCredit) {
242 if !self.tooling_credits.iter().any(|(known, _)| *known == name) {
243 self.tooling_credits.push((name, credit));
244 }
245 }
246
247 /// Why the dependency `name` is credited as tooling, if it is.
248 #[must_use]
249 pub fn tooling_credit(&self, name: &str) -> Option<ToolingCredit> {
250 self.tooling_credits
251 .iter()
252 .find(|(known, _)| known == name)
253 .map(|(_, credit)| credit.clone())
254 }
255}
256
257/// Why the unused devDependency check counts a dependency as used tooling
258/// although no source file imports it.
259#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
260#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
261pub struct ToolingCredit {
262 /// The evidence: `plugin-config` when the plugin that declares the
263 /// dependency found its own config file, `plugin-reference` when a
264 /// package.json script, a CI workflow or a git hook runs one of that
265 /// plugin's packages, `ambient-types` for a type package that declares
266 /// globals, `types-target` when the project declares or imports the
267 /// package that a `@types/` package types, `types-config` when a config
268 /// file, such as a tsconfig `types` entry, names the type package,
269 /// `known-tooling` for a library from the tooling catalogue,
270 /// `known-tooling-config` when a command-line tool from the catalogue has
271 /// its own config file, and `own-peer` when the same manifest lists the
272 /// devDependency in `peerDependencies`. The set is open.
273 pub reason: String,
274 /// The plugin that declares the dependency as tooling.
275 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub plugin: Option<String>,
277 /// The config file found, relative to the project root, for
278 /// `plugin-config` and `known-tooling-config`. A `package.json` path when
279 /// the config is a package.json key. For `own-peer`, the manifest that
280 /// lists the dependency.
281 #[serde(
282 serialize_with = "serde_path::serialize_option",
283 default,
284 skip_serializing_if = "Option::is_none"
285 )]
286 pub config: Option<PathBuf>,
287 /// The package that a script, CI workflow or git hook runs, for
288 /// `plugin-reference`, or the package that a `@types/` package types,
289 /// for `types-target`.
290 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub reference: Option<String>,
292}
293
294/// A config that names a traced file or a traced dependency, and the key that
295/// names it.
296#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
297#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
298pub struct TraceSource {
299 /// The mechanism that names the file or the dependency:
300 /// `module-federation`. The set is open.
301 pub kind: String,
302 /// The plugin that read the config, as it labels itself:
303 /// `module-federation` for a standalone `module-federation.config.*`,
304 /// or the bundler plugin (`webpack`, `rspack`, `rsbuild`, `vite`,
305 /// `nextjs`) that read the same options inline from its own config.
306 pub plugin: String,
307 /// The file that names the file or the dependency, relative to the
308 /// project root: the config file, or the source file of a Module
309 /// Federation runtime call.
310 #[serde(serialize_with = "serde_path::serialize")]
311 pub config: PathBuf,
312 /// The config key or the runtime function that names the file or the
313 /// dependency: `exposes` for an exposed file, `remotes` for a remote
314 /// alias, and `registerRemotes`, `loadRemote`, `init` or `createInstance`
315 /// for a remote that a runtime call names. The set is open.
316 pub key: String,
317}
318
319/// An export with usage information.
320#[derive(Debug, Serialize)]
321#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
322pub struct TracedExport {
323 /// Export name.
324 pub name: String,
325 /// Whether the export is type-only.
326 pub is_type_only: bool,
327 /// Number of references to this export.
328 pub reference_count: usize,
329 /// Files that reference this export.
330 pub referenced_by: Vec<ExportReference>,
331}
332
333/// A re-export with source information.
334#[derive(Debug, Serialize)]
335#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
336pub struct TracedReExport {
337 /// Source file being re-exported from.
338 #[serde(serialize_with = "serde_path::serialize")]
339 pub source_file: PathBuf,
340 /// Imported symbol name.
341 pub imported_name: String,
342 /// Exported symbol name.
343 pub exported_name: String,
344}
345
346/// Result of tracing a dependency: where it is used.
347#[derive(Debug, Serialize)]
348#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
349pub struct DependencyTrace {
350 /// The dependency name being traced.
351 pub package_name: String,
352 /// Files that import this dependency.
353 #[serde(serialize_with = "serde_path::serialize_vec")]
354 pub imported_by: Vec<PathBuf>,
355 /// Files that import this dependency with type-only imports.
356 #[serde(serialize_with = "serde_path::serialize_vec")]
357 pub type_only_imported_by: Vec<PathBuf>,
358 /// Whether the dependency is invoked from package.json scripts, CI configs
359 /// or git hooks.
360 pub used_in_scripts: bool,
361 /// Whether the dependency is used at all: imported, invoked from scripts,
362 /// or listed as a peer by a used package (`peer_of`).
363 pub is_used: bool,
364 /// Total import count.
365 pub import_count: usize,
366 /// Used packages that list this dependency in their installed
367 /// `peerDependencies`, required or optional, sorted by name. The
368 /// unused-dependency check credits such a peer, because the package that
369 /// lists it loads it at runtime. Absent when no used package lists it.
370 #[serde(default, skip_serializing_if = "Vec::is_empty")]
371 pub peer_of: Vec<String>,
372 /// The configs that declare this name as a Module Federation remote alias
373 /// under `remotes`, one per config. A remote alias is provided by a
374 /// remote container at runtime, not by an npm package. Absent when no
375 /// Federation config declares the name (issue #2796).
376 #[serde(default, skip_serializing_if = "Vec::is_empty")]
377 pub sources: Vec<TraceSource>,
378 /// Why the unused devDependency check credits the dependency as tooling
379 /// when no file imports it and no script, CI workflow or git hook runs it.
380 /// When present, `is_used` is `true`. Absent otherwise.
381 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub tooling_credit: Option<ToolingCredit>,
383 /// The manifests that the unused-dependency check flags for this name,
384 /// relative to the project root and sorted. The check reads each
385 /// declaring manifest on its own. An import credits the nearest manifest
386 /// that installs the package, so a name that one workspace uses can still
387 /// be unused in the root manifest or in another workspace. Absent when no
388 /// manifest is flagged.
389 #[serde(
390 default,
391 serialize_with = "serde_path::serialize_vec",
392 skip_serializing_if = "Vec::is_empty"
393 )]
394 pub unused_in: Vec<PathBuf>,
395}
396
397impl DependencyTrace {
398 /// Record the manifests that the unused-dependency report flags for this
399 /// name, so the trace names each declaration that the report lists.
400 pub fn apply_unused_declarations(
401 &mut self,
402 results: &crate::results::AnalysisResults,
403 root: &Path,
404 ) {
405 let flagged = results
406 .unused_dependencies
407 .iter()
408 .map(|finding| &finding.dep)
409 .chain(
410 results
411 .unused_dev_dependencies
412 .iter()
413 .map(|finding| &finding.dep),
414 )
415 .chain(
416 results
417 .unused_optional_dependencies
418 .iter()
419 .map(|finding| &finding.dep),
420 )
421 .filter(|dep| dep.package_name == self.package_name)
422 .map(|dep| {
423 dep.path
424 .strip_prefix(root)
425 .unwrap_or(&dep.path)
426 .to_path_buf()
427 });
428 self.unused_in.extend(flagged);
429 self.unused_in.sort();
430 self.unused_in.dedup();
431 }
432
433 /// Attach the tooling credit of an otherwise unused dependency and count
434 /// the dependency as used, so the trace agrees with the unused-dependency
435 /// report. A dependency that an import or a script already uses keeps no
436 /// credit, because the credit does not decide its status.
437 pub fn apply_tooling_credit(&mut self, credit: Option<ToolingCredit>) {
438 if self.is_used {
439 return;
440 }
441 if let Some(credit) = credit {
442 self.is_used = true;
443 self.tooling_credit = Some(credit);
444 }
445 }
446}
447
448/// Sub-phase attribution inside the entry-point discovery stage.
449///
450/// `PipelineTimings::entry_points_ms` is a single opaque number; these are the
451/// consecutive wall-clock spans that make it up, so a slow discovery stage can
452/// be attributed instead of guessed at. The spans cover the discovery sections
453/// only, so they sum to slightly less than `entry_points_ms`: the summary and
454/// count that follow discovery are not attributed to any span.
455#[derive(Debug, Clone, Copy, Default, Serialize)]
456#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
457pub struct EntryPointSpans {
458 /// Root-package discovery: manual entry globs, root `package.json` fields,
459 /// and the nested `package.json` scan under the conventional monorepo
460 /// directories.
461 pub root_ms: f64,
462 /// Runtime script seed collection plus per-workspace discovery.
463 pub workspaces_ms: f64,
464 /// Plugin entry-point glob compilation and matching.
465 pub plugins_ms: f64,
466 /// The part of `plugins_ms` spent compiling plugin patterns into a glob
467 /// set. Scales with active pattern count, not with project size.
468 pub plugin_glob_build_ms: f64,
469 /// The part of `plugins_ms` spent matching the compiled set against every
470 /// discovered file. Scales with file count times pattern count.
471 pub plugin_glob_match_ms: f64,
472 /// Infrastructure config-file probing at the project root.
473 pub infrastructure_ms: f64,
474 /// Configured `dynamicallyLoaded` glob expansion. Zero when unconfigured.
475 pub dynamic_ms: f64,
476 /// Sorting and deduplicating the merged entry set.
477 pub dedup_ms: f64,
478}
479
480/// Deterministic work counts for one dead-code pipeline run.
481///
482/// Every count is exact for a given project, commit and cache state. It does
483/// not depend on the thread count, the machine or the load, so a regression
484/// test can compare it with exact equality where a millisecond value is too
485/// noisy. A count is zero when its stage did no work, for example the resolver
486/// counts on a run that reused the persisted module graph.
487#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
488#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
489pub struct PipelineCounters {
490 /// Source files whose bytes the parse stage read from disk. A warm cache
491 /// hit on file metadata reads no bytes. `cache_misses` counts the files
492 /// that were parsed.
493 pub files_read: u64,
494 /// Bytes of source read from disk by the parse stage.
495 pub source_bytes_read: u64,
496 /// Bytes of the persisted parse cache read from disk. Zero when the run
497 /// had no parse cache or used `--no-cache`.
498 pub parse_cache_bytes_read: u64,
499 /// Bytes of stylesheet source that the parse stage passed through the CSS
500 /// comment mask. The parse masks each stylesheet once, so this value is
501 /// the size of the parsed stylesheets. A higher value shows a repeated
502 /// mask. Zero when no stylesheet was parsed.
503 pub css_masked_bytes: u64,
504 /// Specifier resolutions that the import sites asked for: one for each
505 /// static import binding, re-export, `require()`, `import()` and module
506 /// mock. Internal retries inside the resolver are not counted here.
507 pub resolve_specifier_calls: u64,
508 /// Distinct `(specifier, from_style)` pairs for each importing file,
509 /// summed over all files. A ratio of `resolve_specifier_calls` to this
510 /// value above 1.0 shows bindings that share a specifier. The resolver
511 /// runs at most once for each of these pairs. A pair that returns before
512 /// the resolver, such as an external URL, makes no resolver call.
513 pub unique_specifiers: u64,
514 /// Calls into the module resolver, including fallback retries.
515 pub oxc_resolve_calls: u64,
516 /// Path canonicalize calls that import resolution needs: each direct call,
517 /// plus one for each distinct path that goes through the canonicalize
518 /// cache. Resolver setup is not counted.
519 pub canonicalize_calls: u64,
520}
521
522/// Pipeline performance timings.
523#[derive(Debug, Clone, Serialize)]
524#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
525pub struct PipelineTimings {
526 /// Time spent discovering files.
527 pub discover_files_ms: f64,
528 /// Number of discovered files.
529 pub file_count: usize,
530 /// Time spent discovering workspaces.
531 pub workspaces_ms: f64,
532 /// Number of discovered workspaces.
533 pub workspace_count: usize,
534 /// Time spent running plugin discovery.
535 pub plugins_ms: f64,
536 /// Time spent analyzing package scripts and CI configuration.
537 pub script_analysis_ms: f64,
538 /// Wall-clock time spent parsing and extracting modules.
539 pub parse_extract_ms: f64,
540 /// Summed parser CPU time across workers.
541 pub parse_cpu_ms: f64,
542 /// The part of `parse_extract_ms` that reads and decodes the persisted
543 /// parse cache. Zero with `--no-cache`.
544 pub parse_cache_load_ms: f64,
545 /// Number of extracted modules.
546 pub module_count: usize,
547 /// Number of files loaded from the parse cache.
548 pub cache_hits: usize,
549 /// Number of files parsed without a cache hit.
550 pub cache_misses: usize,
551 /// Why the persisted parse cache was not reused, when it was not. `None`
552 /// means the cache was loaded; the hit and miss counts then describe how
553 /// much of it applied.
554 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub cache_rejection: Option<CacheRejection>,
556 /// Why the persisted module-graph cache was not reused, when it was not.
557 #[serde(default, skip_serializing_if = "Option::is_none")]
558 pub graph_cache_rejection: Option<CacheRejection>,
559 /// Time spent updating the parse cache.
560 pub cache_update_ms: f64,
561 /// Time spent categorizing entry points.
562 pub entry_points_ms: f64,
563 /// Sub-phase attribution for `entry_points_ms`.
564 pub entry_point_spans: EntryPointSpans,
565 /// Number of entry points considered.
566 pub entry_point_count: usize,
567 /// Time spent resolving imports.
568 pub resolve_imports_ms: f64,
569 /// Time spent building the module graph.
570 pub build_graph_ms: f64,
571 /// Time spent running analysis.
572 pub analyze_ms: f64,
573 /// Time spent running duplicate-code analysis, when included.
574 #[serde(default, skip_serializing_if = "Option::is_none")]
575 pub duplication_ms: Option<f64>,
576 /// Total pipeline time.
577 pub total_ms: f64,
578 /// Deterministic work counts for this run.
579 pub counters: PipelineCounters,
580}
581
582/// Result of computing the impact closure for a single file as the seed.
583#[derive(Debug, Serialize)]
584#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
585pub struct ImpactClosureTrace {
586 /// The seed file, root-relative.
587 pub seed: String,
588 /// Root-relative paths transitively affected by the seed.
589 pub affected_not_shown: Vec<String>,
590 /// Coordination gaps between the seed and consumers.
591 pub coordination_gap: Vec<ImpactClosureGap>,
592}
593
594/// Wire-version discriminator for [`ImportPathTrace`]. Independent from the
595/// global `SchemaVersion`: the import-path payload versions on its own cadence,
596/// like the other independently-versioned envelopes. Serializes as a string
597/// `const` so JSON consumers can switch on it.
598#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
599#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
600pub enum ImportPathTraceSchemaVersion {
601 /// First release of the `fallow trace --path` shape.
602 #[serde(rename = "1")]
603 V1,
604}
605
606/// Result of asking how one module reaches another: the shortest import path.
607///
608/// `reachable` is the only field that separates "no route exists" from "the
609/// route is empty because both ends are the same module". Both report
610/// `hops: 0`, so a consumer must read `reachable`, never the hop count.
611#[derive(Debug, Serialize)]
612#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
613#[cfg_attr(feature = "schema", schemars(title = "fallow trace --path"))]
614pub struct ImportPathTrace {
615 /// Wire-shape version of this payload.
616 pub schema_version: ImportPathTraceSchemaVersion,
617 /// The module the walk started from, root-relative.
618 pub from: String,
619 /// The module the walk was looking for, root-relative.
620 pub to: String,
621 /// Whether `to` is reachable from `from` by following import edges.
622 pub reachable: bool,
623 /// Number of import edges on the reported route. `0` both when the two ends
624 /// are the same module and when there is no route at all.
625 pub hops: usize,
626 /// The route, in import order. Empty whenever `hops` is `0`.
627 pub path: Vec<ImportPathHop>,
628 /// Human-readable summary of the outcome.
629 pub reason: String,
630}
631
632/// One import edge on an [`ImportPathTrace`].
633#[derive(Debug, Serialize, PartialEq, Eq)]
634#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
635pub struct ImportPathHop {
636 /// The importing module, root-relative.
637 pub from: String,
638 /// The imported module, root-relative.
639 pub to: String,
640 /// Whether every symbol on this edge is type-only, so the hop is erased at
641 /// build time. Type-only hops are reported, never skipped: an `import type`
642 /// chain is a real compile-time coupling.
643 pub type_only: bool,
644 /// Whether the edge carries a runtime value but no static one: the target
645 /// loads only on demand (`import()`, a lazy glob or template pattern) or
646 /// on another thread (a worker URL, a worker loader request,
647 /// `child_process.fork`). False for a
648 /// static hop and for a type-only hop.
649 pub dynamic: bool,
650 /// 1-based line in `from` of the imported binding that creates this edge:
651 /// the first value-carrying symbol on the import, or the first symbol when
652 /// every symbol is type-only. On a multi-line import that is the binding's
653 /// own line, not the `import` keyword's. Absent when the edge carries no
654 /// span or the source could not be read.
655 #[serde(default, skip_serializing_if = "Option::is_none")]
656 pub import_line: Option<u32>,
657}
658
659/// One coordination-gap entry in an [`ImpactClosureTrace`].
660#[derive(Debug, Serialize)]
661#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
662pub struct ImpactClosureGap {
663 /// Root-relative path of the consumer module.
664 pub consumer_file: String,
665 /// Exported symbol names the consumer references.
666 pub consumed_symbols: Vec<String>,
667 /// Scope note for the syntactic trace.
668 pub note: String,
669}
670
671/// Result of tracing a clone: all groups containing the code at a source
672/// location or addressed by a stable clone fingerprint.
673#[derive(Debug, Serialize)]
674#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
675pub struct CloneTrace {
676 /// File passed to the trace request, root-relative when a group matches.
677 #[serde(serialize_with = "serde_path::serialize")]
678 pub file: PathBuf,
679 /// 1-based line passed to the trace request or representative group line.
680 pub line: usize,
681 /// The matched clone instance, if one exists.
682 pub matched_instance: Option<CloneInstance>,
683 /// Clone groups matched by the trace request.
684 pub clone_groups: Vec<TracedCloneGroup>,
685}
686
687/// One clone group returned from a clone trace request.
688#[derive(Debug, Serialize)]
689#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
690pub struct TracedCloneGroup {
691 /// Stable content fingerprint, usually `dup:<8hex>` and widened on rare
692 /// report collisions.
693 pub fingerprint: String,
694 /// Number of tokens in the duplicated block.
695 pub token_count: usize,
696 /// Number of lines in the duplicated block.
697 pub line_count: usize,
698 /// Maximum directory-tree or same-file line distance between instances.
699 pub spread: usize,
700 /// Lowest all-pairs similarity for a near-miss clone group.
701 #[serde(default, skip_serializing_if = "Option::is_none")]
702 #[cfg_attr(feature = "schema", schemars(with = "f64"))]
703 pub similarity: Option<f64>,
704 /// Root-relative clone instances in this group.
705 pub instances: Vec<CloneInstance>,
706 /// Group-level refactoring suggestion.
707 pub suggestion: RefactoringSuggestion,
708 /// Best-effort name for the extracted function. Advisory only.
709 #[serde(default, skip_serializing_if = "Option::is_none")]
710 pub suggested_name: Option<String>,
711}