Skip to main content

devscout_rs/graph/
edge.rs

1use serde::{Deserialize, Serialize};
2
3use super::fragment_types::is_false;
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6/// Represents `Candidate`.
7pub struct Candidate {
8    /// The id value.
9    pub id: String,
10    /// The file value.
11    pub file: String,
12}
13
14/// Which heuristic tier emitted a guess.
15///
16/// The two differ by an order of magnitude in precision and `heuristic: true`
17/// alone cannot tell them apart: `Ext` is C#'s own extension-method lookup run over a recorded
18/// `(member, this-type)` bucket -- a real rule, only unverifiable against an
19/// out-of-graph receiver -- while `Guess` is the scored tier picking by NAME
20/// among the defs that happen to declare a member so called.
21#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum HeuristicTier {
24    /// Tier (f): extension-method lookup.
25    Ext,
26    /// The scored tier: a name guess.
27    Guess,
28}
29
30/// The guess tag, appended LAST on every edge kind that can
31/// carry it. `heuristic: true` is set only on an edge a heuristic tier
32/// emitted and never writes `heuristic: false`, so this side pairs
33/// `skip_serializing_if` with `default`: absent == precise, and a precise
34/// edge's bytes are exactly what they were before the tag existed. Only
35/// the resolver's heuristic tiers set it today (both
36/// `uses-member`), but the flag lives on all three targeted kinds because
37/// the query layer's heuristic adjacency is kind-keyed, so a future tier
38/// tagging a `uses-type` edge needs no schema change.
39///
40/// `uses-member` carries two more appended keys, `tier` then `member`, in
41/// that order and NOT mirrored onto the other two kinds: neither has a
42/// member to name, and no tier tags one today. `tier` splits the umbrella
43/// flag into the two tiers a reader can act on differently; `member` names
44/// the member the reference reads or calls and is written on every
45/// uses-member edge, precise ones included, because "which member" is a fact
46/// about the reference rather than about the guess. Both are
47/// omit-when-`None`, so a reader of the old shape sees only added keys.
48///
49/// The next slot on this variant is reserved for `source: Option<Provenance>`
50/// (`"semantic"` for an edge a real compiler vouched for), appended after
51/// `member` and omitted when empty, on the same rule.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53#[serde(tag = "kind")]
54pub enum Edge {
55    #[serde(rename = "inherits")]
56    /// Represents `Inherits`.
57    Inherits {
58        /// The value value.
59        from_file: String,
60        /// The value value.
61        from_line: usize,
62        /// The value value.
63        to: String,
64        /// The value value.
65        to_file: String,
66        #[serde(default, skip_serializing_if = "is_false")]
67        /// The value value.
68        heuristic: bool,
69    },
70    #[serde(rename = "uses-type")]
71    /// Represents `UsesType`.
72    UsesType {
73        /// The value value.
74        from_file: String,
75        /// The value value.
76        from_line: usize,
77        /// The value value.
78        to: String,
79        /// The value value.
80        to_file: String,
81        #[serde(default, skip_serializing_if = "is_false")]
82        /// The value value.
83        heuristic: bool,
84    },
85    #[serde(rename = "uses-member")]
86    /// Represents `UsesMember`.
87    UsesMember {
88        /// The value value.
89        from_file: String,
90        /// The value value.
91        from_line: usize,
92        /// The value value.
93        to: String,
94        /// The value value.
95        to_file: String,
96        #[serde(default, skip_serializing_if = "is_false")]
97        /// The value value.
98        heuristic: bool,
99        #[serde(default, skip_serializing_if = "Option::is_none")]
100        /// Which guess tier emitted this edge -- `Some` exactly when
101        /// `heuristic` is true. Build the variant through
102        /// `Edge::uses_member`, which derives one from the other.
103        tier: Option<HeuristicTier>,
104        #[serde(default, skip_serializing_if = "Option::is_none")]
105        /// The member this reference reads or calls, on precise and
106        /// heuristic edges alike. `None` only for a reference the extractor
107        /// recorded no member name for.
108        member: Option<String>,
109    },
110    #[serde(rename = "imports")]
111    /// The value value.
112    Imports {
113        /// The source file.
114        from_file: String,
115        /// The source line.
116        from_line: usize,
117        /// The imported target.
118        target: String,
119    },
120    #[serde(rename = "ambiguous")]
121    /// Represents `Ambiguous`.
122    Ambiguous {
123        /// The value value.
124        origin: String,
125        /// The value value.
126        from_file: String,
127        /// The value value.
128        from_line: usize,
129        /// The value value.
130        raw: String,
131        /// The value value.
132        candidates: Vec<Candidate>,
133        /// The value value.
134        candidate_count: usize,
135    },
136    /// Constructor-parameter DI resolution (see resolve.rs's
137    /// `resolve_ctor_param`). Field order (`kind`, `from_file`, `from_line`,
138    /// `iface`, `resolution`, `args`, `to`, `candidates`) is significant --
139    /// it fixes the serialized field order.
140    /// `iface` is the injected type's bare identifier; `args` its closed
141    /// generic arguments, present only when it has any; `to` the resolved
142    /// implementation's def id, present only for 'plain'/'closed'/
143    /// 'open-generic'; `candidates` the capped, sorted tie list, present only
144    /// for 'ambiguous'.
145    #[serde(rename = "ctor-di")]
146    CtorDi {
147        /// The value value.
148        from_file: String,
149        /// The value value.
150        from_line: usize,
151        /// The value value.
152        iface: String,
153        /// The value value.
154        resolution: String,
155        #[serde(default, skip_serializing_if = "Option::is_none")]
156        /// The value value.
157        args: Option<Vec<String>>,
158        #[serde(default, skip_serializing_if = "Option::is_none")]
159        /// The value value.
160        to: Option<String>,
161        #[serde(default, skip_serializing_if = "Vec::is_empty")]
162        /// The value value.
163        candidates: Vec<Candidate>,
164    },
165    /// A TS/TSX module import. Distinct from `imports` (C#'s
166    /// `using` directive, which names a NAMESPACE and resolves to no file):
167    /// `target` is the specifier as written and `to_file` the file it
168    /// resolved to. `via` is appended LAST and present only on a
169    /// barrel-followed edge, naming the module the source literally imported
170    /// so a reader can tell the routing table apart from the dependency.
171    #[serde(rename = "import")]
172    Import {
173        /// The value value.
174        from_file: String,
175        /// The value value.
176        from_line: usize,
177        /// The value value.
178        target: String,
179        /// The value value.
180        to_file: String,
181        #[serde(default, skip_serializing_if = "Option::is_none")]
182        /// The value value.
183        via: Option<String>,
184    },
185    /// A call or `new` naming an imported or locally-exported
186    /// declaration.
187    #[serde(rename = "call")]
188    /// The value value.
189    Call {
190        /// The source file.
191        from_file: String,
192        /// The source line.
193        from_line: usize,
194        /// The target definition identifier.
195        to: String,
196        /// The target file.
197        to_file: String,
198    },
199    /// A JSX tag naming a component declaration.
200    #[serde(rename = "jsx-use")]
201    /// The value value.
202    JsxUse {
203        /// The source file.
204        from_file: String,
205        /// The source line.
206        from_line: usize,
207        /// The target definition identifier.
208        to: String,
209        /// The target file.
210        to_file: String,
211    },
212    /// An action creator or thunk handed to a dispatching call
213    /// (`dispatch(...)`, `ofType(...)`).
214    #[serde(rename = "dispatch")]
215    /// The value value.
216    Dispatch {
217        /// The source file.
218        from_file: String,
219        /// The source line.
220        from_line: usize,
221        /// The target definition identifier.
222        to: String,
223        /// The target file.
224        to_file: String,
225    },
226    /// The implementation of a service interface: either a type-level fact
227    /// (`member` absent) read off a two-type-argument DI registration call
228    /// (`services.AddScoped<IContract, Impl>()`), recorded against the
229    /// IMPLEMENTATION type's own declaring file and line -- the same
230    /// convention `inherits` uses for its own base-list reference, applied
231    /// here to a fact the resolver derived rather than one it read directly
232    /// off that type's own base list -- or a member-level fact (`member`
233    /// present, naming the satisfied interface member) connecting an
234    /// implementing member to the interface member it satisfies. Never a
235    /// guess: an ambiguous name-and-arity match emits nothing rather than
236    /// picking a candidate. `to`/`to_file` name the interface.
237    #[serde(rename = "implements")]
238    Implements {
239        /// The value value.
240        from_file: String,
241        /// The value value.
242        from_line: usize,
243        /// The value value.
244        to: String,
245        /// The value value.
246        to_file: String,
247        #[serde(default, skip_serializing_if = "Option::is_none")]
248        /// The interface member this implementation satisfies; absent for
249        /// the type-level, registration-driven fact.
250        member: Option<String>,
251    },
252    /// An `override` member connected to the nearest in-graph base member of
253    /// the same name and arity, matched the same way `implements` is: two or
254    /// more candidates at that arity emit nothing. `to`/`to_file` name the
255    /// base type declaring the overridden member; `member` names it.
256    #[serde(rename = "overrides")]
257    Overrides {
258        /// The value value.
259        from_file: String,
260        /// The value value.
261        from_line: usize,
262        /// The value value.
263        to: String,
264        /// The value value.
265        to_file: String,
266        #[serde(default, skip_serializing_if = "Option::is_none")]
267        /// The value value.
268        member: Option<String>,
269    },
270}
271
272impl Edge {
273    /// The guess tier that emitted this edge, or `None` on a precise one --
274    /// and on every kind that carries no tier at all, which is every kind but
275    /// `uses-member`.
276    pub fn tier(&self) -> Option<HeuristicTier> {
277        match self {
278            Edge::UsesMember { tier, .. } => *tier,
279            _ => None,
280        }
281    }
282
283    /// Whether the edge declares itself a guess, across the three kinds that
284    /// can carry the flag.
285    pub fn is_heuristic(&self) -> bool {
286        match self {
287            Edge::Inherits { heuristic, .. }
288            | Edge::UsesType { heuristic, .. }
289            | Edge::UsesMember { heuristic, .. } => *heuristic,
290            _ => false,
291        }
292    }
293
294    /// The one way to build a `uses-member` edge. `heuristic` is DERIVED from
295    /// `tier` rather than passed alongside it, so the two cannot disagree: a
296    /// tagged edge is always flagged, a flagged edge always names its tier,
297    /// and no emit site can grow a third state by forgetting a field.
298    pub fn uses_member(
299        from_file: String,
300        from_line: usize,
301        to: String,
302        to_file: String,
303        member: Option<String>,
304        tier: Option<HeuristicTier>,
305    ) -> Edge {
306        Edge::UsesMember {
307            from_file,
308            from_line,
309            to,
310            to_file,
311            heuristic: tier.is_some(),
312            tier,
313            member,
314        }
315    }
316}
317
318#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
319/// Represents `EdgesByKind`.
320pub struct EdgesByKind {
321    /// The inherits value.
322    pub inherits: usize,
323    #[serde(rename = "uses-type")]
324    /// The uses type value.
325    pub uses_type: usize,
326    /// The imports value.
327    pub imports: usize,
328    #[serde(rename = "uses-member")]
329    /// The uses member value.
330    pub uses_member: usize,
331    /// Appended LAST, matching the `ctor-di` slot in the fixed key order.
332    #[serde(rename = "ctor-di")]
333    pub ctor_di: usize,
334    /// The type-level and member-level `implements` edges, counted
335    /// together -- appended after `ctor-di`, before the optional TS keys.
336    #[serde(default)]
337    pub implements: usize,
338    /// The member-level `overrides` edges, appended right after
339    /// `implements`, same rule.
340    #[serde(default)]
341    pub overrides: usize,
342    /// The four TS edge counts, appended after `overrides` in this exact order
343    /// and ONLY when the repo carries a TS fragment at all. A C#-only repo's
344    /// stats block omits them entirely -- which is why these are `Option` and
345    /// not a plain `0`.
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    pub import: Option<usize>,
348    #[serde(default, skip_serializing_if = "Option::is_none")]
349    /// The call value.
350    pub call: Option<usize>,
351    #[serde(rename = "jsx-use", default, skip_serializing_if = "Option::is_none")]
352    /// The jsx use value.
353    pub jsx_use: Option<usize>,
354    #[serde(default, skip_serializing_if = "Option::is_none")]
355    /// The dispatch value.
356    pub dispatch: Option<usize>,
357}
358
359/// `heuristic_edge_count` split by the tier that emitted each edge, in the
360/// fixed key order every tier-keyed output uses (ext, then guess).
361///
362/// Both keys are always written, and `ext + guess` equals `heuristic_edge_count` --
363/// including after the heuristic-side dedup, which decrements the dropped
364/// edge's own tier.
365#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
366pub struct HeuristicByTier {
367    /// Edges tier (f) emitted.
368    pub ext: usize,
369    /// Edges the scored tier emitted.
370    pub guess: usize,
371}