Skip to main content

fallow_types/
trace_chain.rs

1//! Symbol-level call-chain output contracts.
2
3use std::path::PathBuf;
4
5use serde::Serialize;
6
7use crate::semantic::SemanticNamespace;
8use crate::serde_path;
9
10/// Default chain depth when `--depth` is unset.
11pub const DEFAULT_TRACE_DEPTH: u32 = 2;
12
13/// Which directions to walk.
14#[derive(Debug, Clone, Copy)]
15pub struct TraceDirections {
16    /// Walk up to callers.
17    pub callers: bool,
18    /// Walk down to callees.
19    pub callees: bool,
20}
21
22/// The result of a symbol-level call-chain trace. Its own surface (`kind:
23/// "trace"`), NOT folded into the ranked brief.
24#[derive(Debug, Clone, Serialize)]
25#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
26pub struct SymbolChainTrace {
27    /// The file containing the traced symbol (project-root-relative).
28    #[serde(serialize_with = "serde_path::serialize")]
29    pub file: PathBuf,
30    /// The traced symbol name.
31    pub symbol: String,
32    /// Whether the symbol's defining export was found in the graph. When
33    /// `false`, the chains are empty and `reason` explains why.
34    pub symbol_found: bool,
35    /// The chain depth applied to both directions.
36    pub depth: u32,
37    /// Whether this trace is best-effort (always `true`: symbol-level chains are
38    /// labeled best-effort, syntactic per ADR-001).
39    pub best_effort: bool,
40    /// Caller chain hops (UP). Present only when `--callers` was requested.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub callers: Option<Vec<ChainHop>>,
43    /// Callee chain hops (DOWN) resolved to an import-symbol edge. Present only
44    /// when `--callees` was requested.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub callees: Option<Vec<ChainHop>>,
47    /// Callees referenced at a call site in the symbol's module that the
48    /// syntactic walk could NOT resolve to an import-symbol edge (locals,
49    /// globals, dynamic dispatch, re-bound callees). Reported, never dropped.
50    /// Present only when `--callees` was requested.
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub unresolved_callees: Option<Vec<UnresolvedCallee>>,
53    /// Set when the name is unresolvable because two different `export *`
54    /// sources of this file supply it. `symbol_found` is `false` in that case
55    /// for the same reason it is false for an unknown name (the file exports
56    /// nothing under it per ECMA-262 ResolveExport), so this field is the only
57    /// thing that separates a barrel mistake from a typo.
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub star_export_ambiguity: Option<StarExportAmbiguity>,
60    /// A human-readable summary of the trace outcome.
61    pub reason: String,
62}
63
64/// The `export *` collision that keeps a name from being exported.
65#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
66#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67pub struct StarExportAmbiguity {
68    /// Files that each declare a colliding declaration under the traced name
69    /// (project-root-relative), sorted. These are the origins to fix: keep one,
70    /// rename or explicitly re-export the rest.
71    #[serde(serialize_with = "serde_path::serialize_vec")]
72    pub sources: Vec<PathBuf>,
73    /// The namespaces the collision occurs in, type before value. A name can
74    /// collide in type space, value space, or both.
75    pub namespaces: Vec<SemanticNamespace>,
76}
77
78/// One hop in a caller / callee chain.
79#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
81pub struct ChainHop {
82    /// The file at this hop (project-root-relative). For a caller hop this is
83    /// the importing module; for a callee hop the imported module.
84    #[serde(serialize_with = "serde_path::serialize")]
85    pub file: PathBuf,
86    /// The symbol name as imported across the edge (`default`, `*` for namespace,
87    /// the imported name otherwise).
88    pub imported_as: String,
89    /// The local binding name in the file at this hop.
90    pub local_name: String,
91    /// Whether the import edge is type-only (`import type { ... }`).
92    pub type_only: bool,
93    /// The hop's depth (1 = direct caller/callee of the symbol).
94    pub depth: u32,
95}
96
97/// A callee referenced at a call site that did not resolve to an import-symbol
98/// edge. Surfaced so a missing callee is never silently dropped.
99#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
100#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
101pub struct UnresolvedCallee {
102    /// The callee path as written at the call site (e.g. `helper`,
103    /// `obj.method`).
104    pub callee: String,
105    /// Why it is unresolved (best-effort classification).
106    pub reason: UnresolvedReason,
107}
108
109/// Best-effort classification of why a callee did not resolve to an edge.
110#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
111#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
112#[serde(rename_all = "kebab-case")]
113pub enum UnresolvedReason {
114    /// A bare identifier call with no matching import binding (a same-module
115    /// local function, a global, or a re-bound callee).
116    LocalOrGlobal,
117    /// A computed / member-expression callee (`obj.method`, dynamic dispatch).
118    MemberOrDynamic,
119}
120
121/// Target and traversal parameters for a symbol-chain trace.
122#[derive(Debug, Clone, Copy)]
123pub struct SymbolChainQuery<'a> {
124    /// File path of the target symbol, root-relative or absolute.
125    pub file: &'a str,
126    /// Exported symbol name to trace.
127    pub symbol: &'a str,
128    /// Maximum traversal depth in each direction.
129    pub depth: u32,
130    /// Which directions to walk.
131    pub directions: TraceDirections,
132}