Skip to main content

fallow_types/
trace.rs

1//! Shared trace output contracts for analysis and integration surfaces.
2
3use std::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}
174
175/// An export with usage information.
176#[derive(Debug, Serialize)]
177#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
178pub struct TracedExport {
179    /// Export name.
180    pub name: String,
181    /// Whether the export is type-only.
182    pub is_type_only: bool,
183    /// Number of references to this export.
184    pub reference_count: usize,
185    /// Files that reference this export.
186    pub referenced_by: Vec<ExportReference>,
187}
188
189/// A re-export with source information.
190#[derive(Debug, Serialize)]
191#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
192pub struct TracedReExport {
193    /// Source file being re-exported from.
194    #[serde(serialize_with = "serde_path::serialize")]
195    pub source_file: PathBuf,
196    /// Imported symbol name.
197    pub imported_name: String,
198    /// Exported symbol name.
199    pub exported_name: String,
200}
201
202/// Result of tracing a dependency: where it is used.
203#[derive(Debug, Serialize)]
204#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
205pub struct DependencyTrace {
206    /// The dependency name being traced.
207    pub package_name: String,
208    /// Files that import this dependency.
209    #[serde(serialize_with = "serde_path::serialize_vec")]
210    pub imported_by: Vec<PathBuf>,
211    /// Files that import this dependency with type-only imports.
212    #[serde(serialize_with = "serde_path::serialize_vec")]
213    pub type_only_imported_by: Vec<PathBuf>,
214    /// Whether the dependency is invoked from package.json scripts or CI configs.
215    pub used_in_scripts: bool,
216    /// Whether the dependency is used at all.
217    pub is_used: bool,
218    /// Total import count.
219    pub import_count: usize,
220}
221
222/// Sub-phase attribution inside the entry-point discovery stage.
223///
224/// `PipelineTimings::entry_points_ms` is a single opaque number; these are the
225/// consecutive wall-clock spans that make it up, so a slow discovery stage can
226/// be attributed instead of guessed at. The spans cover the discovery sections
227/// only, so they sum to slightly less than `entry_points_ms`: the summary and
228/// count that follow discovery are not attributed to any span.
229#[derive(Debug, Clone, Copy, Default, Serialize)]
230#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
231pub struct EntryPointSpans {
232    /// Root-package discovery: manual entry globs, root `package.json` fields,
233    /// and the nested `package.json` scan under the conventional monorepo
234    /// directories.
235    pub root_ms: f64,
236    /// Runtime script seed collection plus per-workspace discovery.
237    pub workspaces_ms: f64,
238    /// Plugin entry-point glob compilation and matching.
239    pub plugins_ms: f64,
240    /// The part of `plugins_ms` spent compiling plugin patterns into a glob
241    /// set. Scales with active pattern count, not with project size.
242    pub plugin_glob_build_ms: f64,
243    /// The part of `plugins_ms` spent matching the compiled set against every
244    /// discovered file. Scales with file count times pattern count.
245    pub plugin_glob_match_ms: f64,
246    /// Infrastructure config-file probing at the project root.
247    pub infrastructure_ms: f64,
248    /// Configured `dynamicallyLoaded` glob expansion. Zero when unconfigured.
249    pub dynamic_ms: f64,
250    /// Sorting and deduplicating the merged entry set.
251    pub dedup_ms: f64,
252}
253
254/// Pipeline performance timings.
255#[derive(Debug, Clone, Serialize)]
256#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
257pub struct PipelineTimings {
258    /// Time spent discovering files.
259    pub discover_files_ms: f64,
260    /// Number of discovered files.
261    pub file_count: usize,
262    /// Time spent discovering workspaces.
263    pub workspaces_ms: f64,
264    /// Number of discovered workspaces.
265    pub workspace_count: usize,
266    /// Time spent running plugin discovery.
267    pub plugins_ms: f64,
268    /// Time spent analyzing package scripts and CI configuration.
269    pub script_analysis_ms: f64,
270    /// Wall-clock time spent parsing and extracting modules.
271    pub parse_extract_ms: f64,
272    /// Summed parser CPU time across workers.
273    pub parse_cpu_ms: f64,
274    /// Number of extracted modules.
275    pub module_count: usize,
276    /// Number of files loaded from the parse cache.
277    pub cache_hits: usize,
278    /// Number of files parsed without a cache hit.
279    pub cache_misses: usize,
280    /// Why the persisted parse cache was not reused, when it was not. `None`
281    /// means the cache was loaded; the hit and miss counts then describe how
282    /// much of it applied.
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub cache_rejection: Option<CacheRejection>,
285    /// Why the persisted module-graph cache was not reused, when it was not.
286    #[serde(default, skip_serializing_if = "Option::is_none")]
287    pub graph_cache_rejection: Option<CacheRejection>,
288    /// Time spent updating the parse cache.
289    pub cache_update_ms: f64,
290    /// Time spent categorizing entry points.
291    pub entry_points_ms: f64,
292    /// Sub-phase attribution for `entry_points_ms`.
293    pub entry_point_spans: EntryPointSpans,
294    /// Number of entry points considered.
295    pub entry_point_count: usize,
296    /// Time spent resolving imports.
297    pub resolve_imports_ms: f64,
298    /// Time spent building the module graph.
299    pub build_graph_ms: f64,
300    /// Time spent running analysis.
301    pub analyze_ms: f64,
302    /// Time spent running duplicate-code analysis, when included.
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub duplication_ms: Option<f64>,
305    /// Total pipeline time.
306    pub total_ms: f64,
307}
308
309/// Result of computing the impact closure for a single file as the seed.
310#[derive(Debug, Serialize)]
311#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
312pub struct ImpactClosureTrace {
313    /// The seed file, root-relative.
314    pub seed: String,
315    /// Root-relative paths transitively affected by the seed.
316    pub affected_not_shown: Vec<String>,
317    /// Coordination gaps between the seed and consumers.
318    pub coordination_gap: Vec<ImpactClosureGap>,
319}
320
321/// Wire-version discriminator for [`ImportPathTrace`]. Independent from the
322/// global `SchemaVersion`: the import-path payload versions on its own cadence,
323/// like the other independently-versioned envelopes. Serializes as a string
324/// `const` so JSON consumers can switch on it.
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
326#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
327pub enum ImportPathTraceSchemaVersion {
328    /// First release of the `fallow trace --path` shape.
329    #[serde(rename = "1")]
330    V1,
331}
332
333/// Result of asking how one module reaches another: the shortest import path.
334///
335/// `reachable` is the only field that separates "no route exists" from "the
336/// route is empty because both ends are the same module". Both report
337/// `hops: 0`, so a consumer must read `reachable`, never the hop count.
338#[derive(Debug, Serialize)]
339#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
340#[cfg_attr(feature = "schema", schemars(title = "fallow trace --path"))]
341pub struct ImportPathTrace {
342    /// Wire-shape version of this payload.
343    pub schema_version: ImportPathTraceSchemaVersion,
344    /// The module the walk started from, root-relative.
345    pub from: String,
346    /// The module the walk was looking for, root-relative.
347    pub to: String,
348    /// Whether `to` is reachable from `from` by following import edges.
349    pub reachable: bool,
350    /// Number of import edges on the reported route. `0` both when the two ends
351    /// are the same module and when there is no route at all.
352    pub hops: usize,
353    /// The route, in import order. Empty whenever `hops` is `0`.
354    pub path: Vec<ImportPathHop>,
355    /// Human-readable summary of the outcome.
356    pub reason: String,
357}
358
359/// One import edge on an [`ImportPathTrace`].
360#[derive(Debug, Serialize, PartialEq, Eq)]
361#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
362pub struct ImportPathHop {
363    /// The importing module, root-relative.
364    pub from: String,
365    /// The imported module, root-relative.
366    pub to: String,
367    /// Whether every symbol on this edge is type-only, so the hop is erased at
368    /// build time. Type-only hops are reported, never skipped: an `import type`
369    /// chain is a real compile-time coupling.
370    pub type_only: bool,
371    /// 1-based line in `from` of the imported binding that creates this edge:
372    /// the first value-carrying symbol on the import, or the first symbol when
373    /// every symbol is type-only. On a multi-line import that is the binding's
374    /// own line, not the `import` keyword's. Absent when the edge carries no
375    /// span or the source could not be read.
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub import_line: Option<u32>,
378}
379
380/// One coordination-gap entry in an [`ImpactClosureTrace`].
381#[derive(Debug, Serialize)]
382#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
383pub struct ImpactClosureGap {
384    /// Root-relative path of the consumer module.
385    pub consumer_file: String,
386    /// Exported symbol names the consumer references.
387    pub consumed_symbols: Vec<String>,
388    /// Scope note for the syntactic trace.
389    pub note: String,
390}
391
392/// Result of tracing a clone: all groups containing the code at a source
393/// location or addressed by a stable clone fingerprint.
394#[derive(Debug, Serialize)]
395#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
396pub struct CloneTrace {
397    /// File passed to the trace request, root-relative when a group matches.
398    #[serde(serialize_with = "serde_path::serialize")]
399    pub file: PathBuf,
400    /// 1-based line passed to the trace request or representative group line.
401    pub line: usize,
402    /// The matched clone instance, if one exists.
403    pub matched_instance: Option<CloneInstance>,
404    /// Clone groups matched by the trace request.
405    pub clone_groups: Vec<TracedCloneGroup>,
406}
407
408/// One clone group returned from a clone trace request.
409#[derive(Debug, Serialize)]
410#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
411pub struct TracedCloneGroup {
412    /// Stable content fingerprint, usually `dup:<8hex>` and widened on rare
413    /// report collisions.
414    pub fingerprint: String,
415    /// Number of tokens in the duplicated block.
416    pub token_count: usize,
417    /// Number of lines in the duplicated block.
418    pub line_count: usize,
419    /// Maximum directory-tree or same-file line distance between instances.
420    pub spread: usize,
421    /// Lowest all-pairs similarity for a near-miss clone group.
422    #[serde(default, skip_serializing_if = "Option::is_none")]
423    #[cfg_attr(feature = "schema", schemars(with = "f64"))]
424    pub similarity: Option<f64>,
425    /// Root-relative clone instances in this group.
426    pub instances: Vec<CloneInstance>,
427    /// Group-level refactoring suggestion.
428    pub suggestion: RefactoringSuggestion,
429    /// Best-effort name for the extracted function. Advisory only.
430    #[serde(default, skip_serializing_if = "Option::is_none")]
431    pub suggested_name: Option<String>,
432}