Skip to main content

devscout_rs/render/
refs.rs

1use crate::query;
2
3use super::blocks::{compact_block, ref_kind_block, ref_kind_block_if_any};
4use super::markers::{compact_marker, heuristic_suffix, source_suffix, BUS_HOP_UNVERIFIED};
5
6// The one line that splits an enum's inbound member edges by which MEMBER they
7// land on. `refs Toggles` already counted them all under `uses-member`; this
8// says how many of that total were member-level and which members carried them,
9// which is the half of the answer a caller otherwise has to run a second query
10// (or a grep) to get. Printed only for an enum with at least one member-level
11// reference, so no other symbol's output moves.
12fn member_refs_line(m: &query::MemberRefs) -> String {
13    let named = m
14        .members
15        .iter()
16        .map(|e| format!("{} {}", e.name, e.count))
17        .collect::<Vec<_>>()
18        .join(", ");
19    let more = if m.dropped != 0 {
20        format!(" +{} more", m.dropped)
21    } else {
22        String::new()
23    };
24    format!(
25        "member refs: {} across {} member(s): {named}{more}",
26        m.total, m.member_count
27    )
28}
29
30// One `bus-hop` provenance row: publisher (`file:line`), direction, the
31// resolved message, the handler (`to`/`to_file`), the evidence word, and how
32// many handlers the message reaches in all -- every fact a bus-hop row must
33// show, regardless of which side of the edge the queried symbol sits on (see
34// `BusHopRow`'s own doc comment). `handlers` is what separates one route
35// from one shared contract every publisher appears to reach. `pub(super)` so
36// `render::coverage`'s own `tests` bus table reuses this verbatim rather
37// than respelling the disclosure a second time.
38pub(super) fn bus_hop_line(r: &query::BusHopRow) -> String {
39    format!(
40        "{}:{}  {}  message={}  handler={}  handlerFile={}  evidence={}  handlers={}  \
41         ({BUS_HOP_UNVERIFIED})",
42        r.file,
43        r.line,
44        r.direction.as_str(),
45        r.message,
46        r.to,
47        r.to_file,
48        r.evidence,
49        r.message_handlers
50    )
51}
52
53/// Renders a reference-query model as human-readable text.
54#[allow(
55    clippy::too_many_lines,
56    reason = "one ordered pass emitting every section of the refs model in the fixed order the text output promises"
57)]
58pub fn render_refs_text(model: &query::RefsModel) -> String {
59    let mut out: Vec<String> = Vec::new();
60    out.push(format!("{}  ({})", model.id, model.kind));
61    out.push(format!(
62        "def: {}",
63        model
64            .sites
65            .iter()
66            .map(|s| format!("{}:{}", s.file, s.line))
67            .collect::<Vec<_>>()
68            .join("  ")
69    ));
70
71    out.push("inbound:".to_string());
72    ref_kind_block(
73        &mut out,
74        "inherits",
75        Some(&model.inbound.inherits),
76        |r: &query::InboundRow| {
77            format!(
78                "{}:{}  inherits{}{}",
79                r.file,
80                r.line,
81                heuristic_suffix(r.heuristic, r.tier),
82                source_suffix(&r.source)
83            )
84        },
85    );
86    ref_kind_block(
87        &mut out,
88        "uses-type",
89        Some(&model.inbound.uses_type),
90        |r: &query::InboundRow| {
91            format!(
92                "{}:{}  uses-type{}{}",
93                r.file,
94                r.line,
95                heuristic_suffix(r.heuristic, r.tier),
96                source_suffix(&r.source)
97            )
98        },
99    );
100    ref_kind_block(
101        &mut out,
102        "uses-member",
103        Some(&model.inbound.uses_member),
104        |r: &query::InboundRow| {
105            format!(
106                "{}:{}  uses-member{}{}",
107                r.file,
108                r.line,
109                heuristic_suffix(r.heuristic, r.tier),
110                source_suffix(&r.source)
111            )
112        },
113    );
114    ref_kind_block_if_any(
115        &mut out,
116        "implements",
117        &model.inbound.implements,
118        |r: &query::InboundRow| {
119            format!(
120                "{}:{}  implements{}",
121                r.file,
122                r.line,
123                source_suffix(&r.source)
124            )
125        },
126    );
127    ref_kind_block_if_any(
128        &mut out,
129        "overrides",
130        &model.inbound.overrides,
131        |r: &query::InboundRow| {
132            format!(
133                "{}:{}  overrides{}",
134                r.file,
135                r.line,
136                source_suffix(&r.source)
137            )
138        },
139    );
140    // One trailer for the five kinds, because they share one cap: the
141    // per-kind headers say how much each kind lost, this says what the call as
142    // a whole did not return. `--all` lifts this cap too, the same lever the
143    // outbound trailer below names, but the text here already reports the true
144    // drop count and is unchanged whether or not `--all` is set.
145    let inbound_dropped = model.inbound.inherits.dropped
146        + model.inbound.uses_type.dropped
147        + model.inbound.uses_member.dropped
148        + model.inbound.implements.dropped
149        + model.inbound.overrides.dropped;
150    if inbound_dropped != 0 {
151        out.push(format!("  +{inbound_dropped} more"));
152    }
153    if let Some(m) = &model.member_refs {
154        out.push(member_refs_line(m));
155    }
156    // Prints only when the symbol carries at least one bus-hop row -- the
157    // same present-only-when-non-empty rule `implements`/`overrides` follow,
158    // so a symbol untouched by a bus hop renders exactly as it did before
159    // this section existed.
160    ref_kind_block_if_any(&mut out, "bus-hop", &model.bus, bus_hop_line);
161
162    if let Some(ob) = &model.outbound {
163        out.push("outbound:".to_string());
164        ref_kind_block(
165            &mut out,
166            "inherits",
167            Some(&ob.inherits),
168            |r: &query::OutboundRow| {
169                format!(
170                    "{}:{}  inherits  -> {}{}{}",
171                    r.file,
172                    r.line,
173                    r.to_file,
174                    heuristic_suffix(r.heuristic, r.tier),
175                    source_suffix(&r.source)
176                )
177            },
178        );
179        ref_kind_block(
180            &mut out,
181            "uses-type",
182            Some(&ob.uses_type),
183            |r: &query::OutboundRow| {
184                format!(
185                    "{}:{}  uses-type  -> {}{}{}",
186                    r.file,
187                    r.line,
188                    r.to_file,
189                    heuristic_suffix(r.heuristic, r.tier),
190                    source_suffix(&r.source)
191                )
192            },
193        );
194        ref_kind_block(
195            &mut out,
196            "uses-member",
197            Some(&ob.uses_member),
198            |r: &query::OutboundRow| {
199                format!(
200                    "{}:{}  uses-member  -> {}{}{}",
201                    r.file,
202                    r.line,
203                    r.to_file,
204                    heuristic_suffix(r.heuristic, r.tier),
205                    source_suffix(&r.source)
206                )
207            },
208        );
209        ref_kind_block_if_any(
210            &mut out,
211            "implements",
212            &ob.implements,
213            |r: &query::OutboundRow| {
214                format!(
215                    "{}:{}  implements  -> {}{}",
216                    r.file,
217                    r.line,
218                    r.to_file,
219                    source_suffix(&r.source)
220                )
221            },
222        );
223        ref_kind_block_if_any(
224            &mut out,
225            "overrides",
226            &ob.overrides,
227            |r: &query::OutboundRow| {
228                format!(
229                    "{}:{}  overrides  -> {}{}",
230                    r.file,
231                    r.line,
232                    r.to_file,
233                    source_suffix(&r.source)
234                )
235            },
236        );
237        ref_kind_block(
238            &mut out,
239            "imports",
240            Some(&ob.imports),
241            |r: &query::ImportRow| {
242                format!(
243                    "{}:{}  imports  -> {}{}",
244                    r.file,
245                    r.line,
246                    r.target,
247                    source_suffix(&r.source)
248                )
249            },
250        );
251        // One trailer for the six outbound kinds, because they share one cap
252        // (mirrors the inbound trailer above). Printing only ever happens when
253        // `--all` would actually return more rows, so the hint is never dead
254        // advice -- true of the inbound trailer above too, which is why that one
255        // stays text-only rather than gaining the same hint.
256        let outbound_dropped = ob.inherits.dropped
257            + ob.uses_type.dropped
258            + ob.uses_member.dropped
259            + ob.implements.dropped
260            + ob.overrides.dropped
261            + ob.imports.dropped;
262        if outbound_dropped != 0 {
263            out.push(format!("  +{outbound_dropped} more, use --all"));
264        }
265    }
266
267    let amb_in = &model.ambiguous.inbound;
268    let amb_out = &model.ambiguous.outbound;
269    if amb_in.total != 0 || amb_out.total != 0 {
270        out.push("ambiguous:".to_string());
271        let amb_row = |r: &query::AmbiguousRow| {
272            format!(
273                "{}:{}  {}  raw=\"{}\"  candidates={}",
274                r.file, r.line, r.origin, r.raw, r.candidate_count
275            )
276        };
277        if amb_in.total != 0 {
278            ref_kind_block(&mut out, "inbound", Some(amb_in), amb_row);
279        }
280        if amb_out.total != 0 {
281            ref_kind_block(&mut out, "outbound", Some(amb_out), amb_row);
282        }
283    }
284
285    if model.manifest_gap != 0 {
286        out.push(format!(
287            "manifest gap: {} graph file(s) not in manifest",
288            model.manifest_gap
289        ));
290    }
291    out.join("\n")
292}
293
294/// `--compact` `refs` rendering.
295#[allow(
296    clippy::too_many_lines,
297    reason = "one ordered pass mirroring render_refs_text's section order in the compact shape"
298)]
299pub fn render_refs_compact(model: &query::RefsModel) -> String {
300    let mut out: Vec<String> = Vec::new();
301    out.push(format!("{}  ({})", model.id, model.kind));
302    out.push(format!(
303        "def: {}",
304        model
305            .sites
306            .iter()
307            .map(|s| format!("{}:{}", s.file, s.line))
308            .collect::<Vec<_>>()
309            .join("  ")
310    ));
311
312    // `file_of_*` are explicitly typed as `fn(&R) -> &str` (not left to
313    // inference) so the closure is assigned its higher-ranked signature
314    // (`for<'a> fn(&'a R) -> &'a str`) at the point of declaration --
315    // without the annotation, an unannotated `let` binds the closure to
316    // one concrete (non-reusable) lifetime instead, which then fails to
317    // coerce back down at `compact_block`'s `fn` parameter on the second
318    // and later call sites below.
319    // The one-character marker is `compact_marker`'s (`x` extension, `h`
320    // guess); neither collides with a line number, and the run-length collapse
321    // treats `5`, `5h` and `5x` as the three distinct entries they are -- a `5x`
322    // seen twice reads `5xx2`.
323    //
324    // Compact groups a file's hits into one `path:line,line` entry, which
325    // leaves no slot for a per-hit source line -- the snippet is a
326    // default-renderer and `--json` affordance only.
327    let file_of_ib: fn(&query::InboundRow) -> &str = |r| r.file.as_str();
328    let line_ib =
329        |r: &query::InboundRow| format!("{}{}", r.line, compact_marker(r.heuristic, r.tier));
330    compact_block(
331        &mut out,
332        "in:inherits",
333        Some(&model.inbound.inherits),
334        file_of_ib,
335        line_ib,
336    );
337    compact_block(
338        &mut out,
339        "in:uses-type",
340        Some(&model.inbound.uses_type),
341        file_of_ib,
342        line_ib,
343    );
344    compact_block(
345        &mut out,
346        "in:uses-member",
347        Some(&model.inbound.uses_member),
348        file_of_ib,
349        line_ib,
350    );
351    compact_block(
352        &mut out,
353        "in:implements",
354        Some(&model.inbound.implements),
355        file_of_ib,
356        line_ib,
357    );
358    compact_block(
359        &mut out,
360        "in:overrides",
361        Some(&model.inbound.overrides),
362        file_of_ib,
363        line_ib,
364    );
365    // The direction letter ('i'/'o') stands in for the full `in`/`out` word --
366    // compact strips message/evidence to the same terse shape every other
367    // block here already takes. The trailing '?' is the possible-route
368    // marker: every bus-hop row is unverified, and compact mode has no room
369    // for the full disclosure text, only a marker plus where to find it.
370    let file_of_bus: fn(&query::BusHopRow) -> &str = |r| r.file.as_str();
371    let line_bus = |r: &query::BusHopRow| format!("{}{}?", r.line, &r.direction.as_str()[..1]);
372    compact_block(
373        &mut out,
374        "bus-hop (? = possible route, rerun without --compact for the full row)",
375        Some(&model.bus),
376        file_of_bus,
377        line_bus,
378    );
379
380    if let Some(ob) = &model.outbound {
381        let file_of_ob: fn(&query::OutboundRow) -> &str = |r| r.file.as_str();
382        let line_ob =
383            |r: &query::OutboundRow| format!("{}{}", r.line, compact_marker(r.heuristic, r.tier));
384        compact_block(
385            &mut out,
386            "out:inherits",
387            Some(&ob.inherits),
388            file_of_ob,
389            line_ob,
390        );
391        compact_block(
392            &mut out,
393            "out:uses-type",
394            Some(&ob.uses_type),
395            file_of_ob,
396            line_ob,
397        );
398        compact_block(
399            &mut out,
400            "out:uses-member",
401            Some(&ob.uses_member),
402            file_of_ob,
403            line_ob,
404        );
405        compact_block(
406            &mut out,
407            "out:implements",
408            Some(&ob.implements),
409            file_of_ob,
410            line_ob,
411        );
412        compact_block(
413            &mut out,
414            "out:overrides",
415            Some(&ob.overrides),
416            file_of_ob,
417            line_ob,
418        );
419
420        let file_of_imp: fn(&query::ImportRow) -> &str = |r| r.file.as_str();
421        let line_imp = |r: &query::ImportRow| r.line.to_string();
422        compact_block(
423            &mut out,
424            "out:imports",
425            Some(&ob.imports),
426            file_of_imp,
427            line_imp,
428        );
429    }
430
431    let amb_in = &model.ambiguous.inbound;
432    let amb_out = &model.ambiguous.outbound;
433    let file_of_amb: fn(&query::AmbiguousRow) -> &str = |r| r.file.as_str();
434    let line_amb =
435        |r: &query::AmbiguousRow| format!("{}(candidates={})", r.line, r.candidate_count);
436    compact_block(&mut out, "amb:in", Some(amb_in), file_of_amb, line_amb);
437    compact_block(&mut out, "amb:out", Some(amb_out), file_of_amb, line_amb);
438
439    // The missing-table tolerance documented at the top of this file is what an
440    // absent `outbound` falls through here: the three outbound tables plus
441    // imports contribute nothing to any of the three sums when the caller did not
442    // ask for them.
443    let ob = model.outbound.as_ref();
444    let ob_sum = |f: fn(&query::OutboundTables) -> usize| ob.map_or(0, f);
445    let edges = model.inbound.inherits.total
446        + model.inbound.uses_type.total
447        + model.inbound.uses_member.total
448        + model.inbound.implements.total
449        + model.inbound.overrides.total
450        + model.bus.total
451        + ob_sum(|o| {
452            o.inherits.total
453                + o.uses_type.total
454                + o.uses_member.total
455                + o.implements.total
456                + o.overrides.total
457                + o.imports.total
458        });
459    let shown = model.inbound.inherits.rows.len()
460        + model.inbound.uses_type.rows.len()
461        + model.inbound.uses_member.rows.len()
462        + model.inbound.implements.rows.len()
463        + model.inbound.overrides.rows.len()
464        + model.bus.rows.len()
465        + ob_sum(|o| {
466            o.inherits.rows.len()
467                + o.uses_type.rows.len()
468                + o.uses_member.rows.len()
469                + o.implements.rows.len()
470                + o.overrides.rows.len()
471                + o.imports.rows.len()
472        });
473    let dropped = model.inbound.inherits.dropped
474        + model.inbound.uses_type.dropped
475        + model.inbound.uses_member.dropped
476        + model.inbound.implements.dropped
477        + model.inbound.overrides.dropped
478        + model.bus.dropped
479        + ob_sum(|o| {
480            o.inherits.dropped
481                + o.uses_type.dropped
482                + o.uses_member.dropped
483                + o.implements.dropped
484                + o.overrides.dropped
485                + o.imports.dropped
486        });
487    let ambiguous = amb_in.total + amb_out.total;
488    let gap = if model.manifest_gap != 0 {
489        format!(" gap={}", model.manifest_gap)
490    } else {
491        String::new()
492    };
493    // Same fact as the default renderer's line, spelled the way every other
494    // compact block is: `name=count`, no prose, and absent entirely when there is
495    // nothing to say.
496    if let Some(m) = &model.member_refs {
497        let named = m
498            .members
499            .iter()
500            .map(|e| format!("{}={}", e.name, e.count))
501            .collect::<Vec<_>>()
502            .join(",");
503        let more = if m.dropped != 0 {
504            format!(",+{}", m.dropped)
505        } else {
506            String::new()
507        };
508        out.push(format!("mem: {named}{more}"));
509    }
510    out.push(format!(
511        "summary: edges={edges} shown={shown} dropped={dropped} ambiguous={ambiguous}{gap}"
512    ));
513    out.join("\n")
514}