Skip to main content

devscout_rs/query/
impact_why.rs

1// `impact`'s per-row `why`: which rule or tier best explains why a file was
2// reached, folded across every edge kind the walk saw for it. Split out of
3// `impact.rs` to keep that file under its size ceiling; `pub(super)`
4// throughout, so only `impact.rs` itself reaches in.
5
6use crate::graph;
7
8use super::impact::VisitedEntry;
9use super::seq::SeqSet;
10// Re-exported so `impact.rs` reaches both through this one module -- it
11// never needs `super::why` directly.
12use super::why::why_for_edge;
13pub(super) use super::why::Why;
14
15/// One representative referencing line PER EDGE KIND that reached this file,
16/// the lowest line per kind.
17///
18/// `0` means "this kind never contributed", which
19/// keeps a row a kind never touched free of that key in `--json`. `direct_amb`
20/// is the ambiguous half of the `direct` kind, kept apart only so
21/// `build_impact_model` can apply the resolved-over-ambiguous tie-break instead
22/// of letting an ambiguous line win by being numerically smaller.
23#[derive(Debug, Clone, Default, PartialEq)]
24pub struct KindLines {
25    /// The direct value.
26    pub direct: usize,
27    /// The direct amb value.
28    pub direct_amb: usize,
29    /// The ctor di value.
30    pub ctor_di: usize,
31    /// The heuristic value.
32    pub heuristic: usize,
33    /// The iface value.
34    pub iface: usize,
35    /// The lowest `bus-hop` line reaching this file, weakest of every slot.
36    pub bus: usize,
37}
38
39// The lowest-line-wins guard for `ctor_di`, whose `why` is always `ctor-di`;
40// every other `KindLines` slot routes through `Hit`'s `note_*` methods instead.
41pub(super) fn note_line(slot: &mut usize, line: usize) {
42    if line > 0 && (*slot == 0 || line < *slot) {
43        *slot = line;
44    }
45}
46
47/// One `bus-hop` edge's own identity: the route a possible-route row
48/// discloses, and what a later hop's downstream row inherits when it is
49/// reached only through this one.
50///
51/// Total order `(file, line, message, to)`
52/// byte order (the same route-tuple ordering a `bus-hop` edge's own identity
53/// uses elsewhere in this engine), `to_file` a final harmless tiebreak -- so
54/// "the lowest by (publisher file, line, message, handler)" is a single
55/// comparison, never a policy choice made ad hoc at each call site.
56#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
57pub struct BusOrigin {
58    /// The publish site's own file.
59    pub file: String,
60    /// The publish site's own line.
61    pub line: usize,
62    /// The resolved message identity.
63    pub message: String,
64    /// The handler def id.
65    pub to: String,
66    /// The handler's own file.
67    pub to_file: String,
68}
69
70// `pub(super)` throughout: `build_visited_entry` reads every field of a
71// finished `Hit` to assemble one `VisitedEntry`.
72#[derive(Debug, Clone, Default)]
73pub(super) struct Hit {
74    pub(super) via_count: u32,
75    pub(super) ambiguous_count: u32,
76    pub(super) heuristic_count: u32,
77    // How many of `heuristic_count` came from the EXTENSION tier. Counted
78    // rather than flagged so the walk keeps one shape for both tiers, and one
79    // is all the row needs to call itself an extension (see `row_tier`).
80    pub(super) ext_count: u32,
81    pub(super) symbols: SeqSet<String>,
82    // The `via` labels an interface-hop hit at this file carries
83    // (`"IFoo (ctor-di)"` or bare `"IFoo"`), first-seen order.
84    pub(super) iface_via: SeqSet<String>,
85    // One representative line per edge kind (see `KindLines`).
86    pub(super) lines: KindLines,
87    // Which edge explains each of `lines`' slots -- see `WhyTrack`.
88    pub(super) why_track: WhyTrack,
89    // Set by `note_taint` once an untainted edge reaches this file.
90    pub(super) has_non_bus_path: bool,
91    // The lowest-ordered bus-hop origin any path to this file crossed --
92    // present whenever at least one tainted edge (direct or inherited)
93    // reached it, regardless of whether an untainted edge also did; a row's
94    // own disclosure gates on `bus_only` (`!has_non_bus_path`) instead of on
95    // this being present, so a file with both kinds of path keeps its
96    // stronger `why` and drops the marker without needing this cleared too.
97    pub(super) bus_origin: Option<BusOrigin>,
98}
99
100/// Which edge (kind, and for `uses-member` its tier) explains each of a
101/// file's non-zero `KindLines` slots -- `direct`/`direct_amb`/`heuristic`/
102/// `iface`, in that order. `None` exactly when the paired line is `0`.
103/// `lines.ctor_di` needs no partner: every edge that sets it is a `ctor-di`
104/// edge, so `primary_why` names it directly.
105#[derive(Debug, Clone, Default)]
106pub(super) struct WhyTrack {
107    pub(super) direct: Option<Why>,
108    pub(super) direct_amb: Option<Why>,
109    pub(super) heuristic: Option<Why>,
110    pub(super) iface: Option<Why>,
111    /// The weakest slot: a `bus-hop` edge is a possible, runtime-unverified
112    /// route, so it names a file's `why` only when no other kind reached it
113    /// at all -- one further step below `heuristic`, which is at least a
114    /// confirmed edge the resolver merely guessed the target of.
115    pub(super) bus: Option<Why>,
116}
117
118// The lowest-line-wins guard, paired with the specific edge that explains the
119// winning line. Both slots are set together so they can never name two
120// different edges for the same line; the four `Hit` methods below are its
121// only callers, one per `KindLines`/`WhyTrack` slot pair.
122fn note_line_why(slot: &mut usize, why_slot: &mut Option<Why>, line: usize, why: Why) {
123    if line > 0 && (*slot == 0 || line < *slot) {
124        *slot = line;
125        *why_slot = Some(why);
126    }
127}
128
129impl Hit {
130    /// Records a resolved (precise) reference reaching this file. `origin`
131    /// is the bus-hop identity the frontier def this edge came from was
132    /// itself only reachable through, when it was reachable only that way;
133    /// every non-`note_bus` method takes it so taint threads through the
134    /// walk, carrying the origin along with it, without a separate call at
135    /// each site.
136    pub(super) fn note_direct(
137        &mut self,
138        line: usize,
139        edge: &graph::Edge,
140        origin: Option<&BusOrigin>,
141    ) {
142        let why = why_for_edge(edge);
143        note_line_why(
144            &mut self.lines.direct,
145            &mut self.why_track.direct,
146            line,
147            why,
148        );
149        self.note_taint(origin);
150    }
151    /// Records an ambiguous reference reaching this file.
152    pub(super) fn note_direct_amb(
153        &mut self,
154        line: usize,
155        edge: &graph::Edge,
156        origin: Option<&BusOrigin>,
157    ) {
158        let why = why_for_edge(edge);
159        note_line_why(
160            &mut self.lines.direct_amb,
161            &mut self.why_track.direct_amb,
162            line,
163            why,
164        );
165        self.note_taint(origin);
166    }
167    /// Records the interface hop reaching this file.
168    pub(super) fn note_iface(
169        &mut self,
170        line: usize,
171        edge: &graph::Edge,
172        origin: Option<&BusOrigin>,
173    ) {
174        let why = why_for_edge(edge);
175        note_line_why(&mut self.lines.iface, &mut self.why_track.iface, line, why);
176        self.note_taint(origin);
177    }
178    /// Records a heuristic (guessed) edge reaching this file.
179    pub(super) fn note_heuristic(
180        &mut self,
181        line: usize,
182        edge: &graph::Edge,
183        origin: Option<&BusOrigin>,
184    ) {
185        let why = why_for_edge(edge);
186        note_line_why(
187            &mut self.lines.heuristic,
188            &mut self.why_track.heuristic,
189            line,
190            why,
191        );
192        self.note_taint(origin);
193    }
194    /// Records a `bus-hop` edge reaching this file, in either direction --
195    /// always `Why::BusHop`, kept in its own slot so it never outranks a
196    /// stronger kind merely by sitting on an earlier line. Never clears the
197    /// taint: a bus hop is exactly what taints a path in the first place.
198    /// Records the edge's OWN identity as this hit's origin (the lowest
199    /// seen so far), distinct from `note_taint`'s inherited origin: this is
200    /// the hop itself, not a hop some upstream def merely carried forward.
201    pub(super) fn note_bus(&mut self, line: usize, edge: &graph::Edge) {
202        let why = why_for_edge(edge);
203        note_line_why(&mut self.lines.bus, &mut self.why_track.bus, line, why);
204        if let graph::Edge::BusHop {
205            from_file,
206            from_line,
207            message,
208            to,
209            to_file,
210            ..
211        } = edge
212        {
213            self.note_bus_origin(BusOrigin {
214                file: from_file.clone(),
215                line: *from_line,
216                message: message.clone(),
217                to: to.clone(),
218                to_file: to_file.clone(),
219            });
220        }
221    }
222    // Keeps the lowest-ordered `BusOrigin` seen so far for this hit.
223    fn note_bus_origin(&mut self, origin: BusOrigin) {
224        if self
225            .bus_origin
226            .as_ref()
227            .is_none_or(|existing| origin < *existing)
228        {
229            self.bus_origin = Some(origin);
230        }
231    }
232    /// Clears the possible-route taint the moment ANY edge reaching this
233    /// file is untainted (`origin` is `None`), and otherwise records the
234    /// inherited origin -- the same taint/untaint decision as before, now
235    /// carrying an identity rather than a bare flag. `ctor_di`'s own site
236    /// has no `note_*` method of its own to fold this into, so it calls
237    /// this directly.
238    pub(super) fn note_taint(&mut self, origin: Option<&BusOrigin>) {
239        match origin {
240            Some(o) => self.note_bus_origin(o.clone()),
241            None => self.has_non_bus_path = true,
242        }
243    }
244}
245
246// Assembles a row's per-kind representative lines. The resolved-over-ambiguous
247// tie-break decides the `direct` kind: a resolved site outranks an ambiguous
248// one, and the ambiguous line is used only when the resolved half never fired.
249pub(super) fn from_lines_of(lines: &KindLines) -> Vec<(&'static str, usize)> {
250    let mut out: Vec<(&'static str, usize)> = Vec::new();
251    let direct = if lines.direct != 0 {
252        lines.direct
253    } else {
254        lines.direct_amb
255    };
256    if direct != 0 {
257        out.push(("direct", direct));
258    }
259    if lines.ctor_di != 0 {
260        out.push(("ctor-di", lines.ctor_di));
261    }
262    if lines.heuristic != 0 {
263        out.push(("heuristic", lines.heuristic));
264    }
265    if lines.iface != 0 {
266        out.push(("iface", lines.iface));
267    }
268    if lines.bus != 0 {
269        out.push(("bus", lines.bus));
270    }
271    out
272}
273
274/// Routes one edge -- from either walk direction, reverse or forward -- to
275/// `note_bus` or `note_direct`, whichever it is: the one call site both
276/// kinds share, since a `bus-hop` edge always records its OWN identity via
277/// `note_bus` regardless of `origin`, while every other kind carries the
278/// frontier def's own inherited origin through to `note_direct`.
279pub(super) fn note_kind_edge(
280    h: &mut Hit,
281    line: usize,
282    edge: &graph::Edge,
283    origin: Option<&BusOrigin>,
284) {
285    if matches!(edge, graph::Edge::BusHop { .. }) {
286        h.note_bus(line, edge);
287    } else {
288        h.note_direct(line, edge, origin);
289    }
290}
291
292// The single `why` a row reports when more than one edge kind reached the
293// same file follows one priority order: ctor-di, direct, ambiguous, iface,
294// guess -- the most specific evidence about the seed definition wins; an
295// interface hop names the shared interface rather than the seed itself, and
296// a guess is named only when nothing else fired at all.
297fn primary_why(lines: &KindLines, track: &WhyTrack) -> Why {
298    if lines.ctor_di != 0 {
299        return Why::CtorDi;
300    }
301    if lines.direct != 0 {
302        return track.direct.unwrap_or(Why::UsesMemberPrecise);
303    }
304    if lines.direct_amb != 0 {
305        return track.direct_amb.unwrap_or(Why::UsesMemberPrecise);
306    }
307    if lines.iface != 0 {
308        return track.iface.unwrap_or(Why::UsesMemberPrecise);
309    }
310    if lines.heuristic != 0 || track.heuristic.is_some() {
311        return track.heuristic.unwrap_or(Why::UsesMemberGuess);
312    }
313    track.bus.unwrap_or(Why::BusHop)
314}
315
316/// Assembles one `VisitedEntry` from a hop's finished `Hit` bookkeeping,
317/// folding `why` in via `primary_why` alongside every other per-file summary
318/// field -- the one place `impact_walk` hands a `Hit` off as a resolved row.
319pub(super) fn build_visited_entry(hop: u32, infra: bool, h: &Hit) -> VisitedEntry {
320    VisitedEntry {
321        hop,
322        via_count: h.via_count,
323        ambiguous_count: h.ambiguous_count,
324        heuristic_count: h.heuristic_count,
325        ext_count: h.ext_count,
326        symbols: h.symbols.clone().into_vec(),
327        iface_via: h.iface_via.clone().into_vec(),
328        lines: h.lines.clone(),
329        infra,
330        why: primary_why(&h.lines, &h.why_track),
331        bus_only: !h.has_non_bus_path,
332        // Present only when `bus_only` is: a file also reached by a real
333        // path keeps its stronger `why` and discloses no origin at all.
334        bus_origin: if h.has_non_bus_path {
335            None
336        } else {
337            h.bus_origin.clone()
338        },
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    fn lines(ctor_di: usize, direct: usize, direct_amb: usize, iface: usize) -> KindLines {
347        KindLines {
348            ctor_di,
349            direct,
350            direct_amb,
351            iface,
352            heuristic: 0,
353            bus: 0,
354        }
355    }
356
357    #[test]
358    fn ctor_di_alone_wins() {
359        let l = lines(1, 0, 0, 0);
360        let track = WhyTrack::default();
361        assert_eq!(primary_why(&l, &track), Why::CtorDi);
362    }
363
364    #[test]
365    fn direct_alone_reports_its_tracked_why() {
366        let l = lines(0, 1, 0, 0);
367        let track = WhyTrack {
368            direct: Some(Why::UsesType),
369            ..WhyTrack::default()
370        };
371        assert_eq!(primary_why(&l, &track), Why::UsesType);
372    }
373
374    #[test]
375    fn direct_amb_alone_reports_its_tracked_why() {
376        let l = lines(0, 0, 1, 0);
377        let track = WhyTrack {
378            direct_amb: Some(Why::Inherits),
379            ..WhyTrack::default()
380        };
381        assert_eq!(primary_why(&l, &track), Why::Inherits);
382    }
383
384    #[test]
385    fn iface_alone_reports_its_tracked_why() {
386        let l = lines(0, 0, 0, 1);
387        let track = WhyTrack {
388            iface: Some(Why::UsesMemberExt),
389            ..WhyTrack::default()
390        };
391        assert_eq!(primary_why(&l, &track), Why::UsesMemberExt);
392    }
393
394    #[test]
395    fn heuristic_alone_reports_its_tracked_why() {
396        let l = lines(0, 0, 0, 0);
397        let track = WhyTrack {
398            heuristic: Some(Why::UsesMemberGuess),
399            ..WhyTrack::default()
400        };
401        assert_eq!(primary_why(&l, &track), Why::UsesMemberGuess);
402    }
403
404    #[test]
405    fn ctor_di_outranks_direct() {
406        let l = lines(1, 1, 0, 0);
407        let track = WhyTrack {
408            direct: Some(Why::UsesType),
409            ..WhyTrack::default()
410        };
411        assert_eq!(primary_why(&l, &track), Why::CtorDi);
412    }
413
414    #[test]
415    fn direct_outranks_direct_amb() {
416        let l = lines(0, 1, 1, 0);
417        let track = WhyTrack {
418            direct: Some(Why::UsesType),
419            direct_amb: Some(Why::Inherits),
420            ..WhyTrack::default()
421        };
422        assert_eq!(primary_why(&l, &track), Why::UsesType);
423    }
424
425    #[test]
426    fn direct_amb_outranks_iface() {
427        let l = lines(0, 0, 1, 1);
428        let track = WhyTrack {
429            direct_amb: Some(Why::Inherits),
430            iface: Some(Why::UsesMemberExt),
431            ..WhyTrack::default()
432        };
433        assert_eq!(primary_why(&l, &track), Why::Inherits);
434    }
435
436    #[test]
437    fn iface_outranks_heuristic() {
438        let l = lines(0, 0, 0, 1);
439        let track = WhyTrack {
440            iface: Some(Why::UsesMemberExt),
441            heuristic: Some(Why::UsesMemberGuess),
442            ..WhyTrack::default()
443        };
444        assert_eq!(primary_why(&l, &track), Why::UsesMemberExt);
445    }
446
447    #[test]
448    fn bus_alone_reports_its_tracked_why() {
449        let l = KindLines {
450            bus: 1,
451            ..lines(0, 0, 0, 0)
452        };
453        let track = WhyTrack {
454            bus: Some(Why::BusHop),
455            ..WhyTrack::default()
456        };
457        assert_eq!(primary_why(&l, &track), Why::BusHop);
458    }
459
460    #[test]
461    fn heuristic_outranks_bus() {
462        let l = KindLines {
463            heuristic: 1,
464            bus: 1,
465            ..lines(0, 0, 0, 0)
466        };
467        let track = WhyTrack {
468            heuristic: Some(Why::UsesMemberGuess),
469            bus: Some(Why::BusHop),
470            ..WhyTrack::default()
471        };
472        assert_eq!(primary_why(&l, &track), Why::UsesMemberGuess);
473    }
474
475    #[test]
476    fn all_six_slots_set_still_picks_ctor_di() {
477        let l = KindLines {
478            ctor_di: 1,
479            direct: 1,
480            direct_amb: 1,
481            iface: 1,
482            heuristic: 1,
483            bus: 1,
484        };
485        let track = WhyTrack {
486            direct: Some(Why::UsesType),
487            direct_amb: Some(Why::Inherits),
488            iface: Some(Why::UsesMemberExt),
489            heuristic: Some(Why::UsesMemberGuess),
490            bus: Some(Why::BusHop),
491        };
492        assert_eq!(primary_why(&l, &track), Why::CtorDi);
493    }
494}