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}