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