Skip to main content

devscout_rs/query/
refs.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::graph;
5
6use super::bus::{self, BusHopRow};
7use super::dispatch::{
8    self, outbound_foreign, push_ranked, RankedOutbound, K_IMPLEMENTS, K_IMPORTS, K_INHERITS,
9    K_OVERRIDES, K_USES_MEMBER, K_USES_TYPE,
10};
11use super::index::{def_sites, symbol_refs, DefSite, GraphIndex, SymbolRefs};
12use super::member::{self, MemberCandidate};
13use super::occurrence::{tag_inbound_rows, tag_outbound_rows};
14use super::refs_tables::{
15    ambiguous_row, build_table, cap_rows, edge_loc, loc_cmp, row_tier, AmbiguousTables, ImportRow,
16    InboundRow, InboundTables, OutboundRow, OutboundTables, Table, SOURCE_MAX,
17};
18use super::symbol::{resolve_symbol, Resolution};
19
20/// One enum member that carries at least one inbound reference.
21#[derive(Debug, Clone, PartialEq)]
22pub struct MemberRefEntry {
23    /// The name value.
24    pub name: String,
25    /// The count value.
26    pub count: usize,
27}
28
29/// How many of an ENUM's inbound member edges land on one of its members,
30/// split by member.
31#[derive(Debug, Clone, PartialEq)]
32pub struct MemberRefs {
33    /// The total value.
34    pub total: usize,
35    /// The member count value.
36    pub member_count: usize,
37    /// The members value.
38    pub members: Vec<MemberRefEntry>,
39    /// The dropped value.
40    pub dropped: usize,
41}
42
43// Maximum number of per-member rows kept in a `MemberRefs`.
44const MEMBER_NAME_CAP: usize = 5;
45
46// The edges themselves are already there (a `Toggles.EnableX` access resolves
47// to the member def and `symbol_refs` unions every member's inbound into the
48// enum's), but a caller reading `refs Toggles` could not tell a use of the
49// TYPE from a use of a member, and the per-member split is the thing an enum
50// question is usually actually about.
51//
52// Member order is def-table order (declaration order), never by count: a
53// stable list is what makes two runs print the same line. Iterates
54// `graph.defs` directly for the same reason `symbol_refs`'s own enum union
55// does -- that array order is the order to preserve.
56fn enum_member_refs(index: &GraphIndex, def_id: &str) -> Option<MemberRefs> {
57    let prefix = format!("{def_id}.");
58    let mut members: Vec<MemberRefEntry> = Vec::new();
59    let mut total = 0usize;
60    for d in &index.graph.defs {
61        if d.kind != "enum-member" || !d.id.starts_with(&prefix) {
62            continue;
63        }
64        let count = index.inbound.get(&d.id).map_or(0, |e| e.uses_member.len())
65            + index
66                .heuristic_inbound
67                .get(&d.id)
68                .map_or(0, |e| e.uses_member.len());
69        if count == 0 {
70            continue;
71        }
72        members.push(MemberRefEntry {
73            name: d.name.clone(),
74            count,
75        });
76        total += count;
77    }
78    if total == 0 {
79        return None;
80    }
81    let member_count = members.len();
82    let dropped = member_count.saturating_sub(MEMBER_NAME_CAP);
83    members.truncate(MEMBER_NAME_CAP);
84    Some(MemberRefs {
85        total,
86        member_count,
87        members,
88        dropped,
89    })
90}
91
92/// The resolved `refs` result for one symbol.
93#[derive(Debug, Clone, PartialEq)]
94pub struct RefsModel {
95    /// The query value.
96    pub query: String,
97    /// The id value.
98    pub id: String,
99    /// The kind value.
100    pub kind: String,
101    /// The sites value.
102    pub sites: Vec<DefSite>,
103    /// The inbound value.
104    pub inbound: InboundTables,
105    /// Present only under `--out`; `None` otherwise, which is what keeps
106    /// `--json` and the two text renderers agreeing about whether the outbound
107    /// side exists at all.
108    pub outbound: Option<OutboundTables>,
109    /// The ambiguous value.
110    pub ambiguous: AmbiguousTables,
111    /// Every `bus-hop` row naming this symbol, combined -- see `bus::BusHopRow`.
112    pub bus: Table<BusHopRow>,
113    /// Number of files present in the graph but absent from the manifest.
114    pub manifest_gap: usize,
115    /// `Some` only for an enum with member-level references; `None` for every
116    /// other symbol.
117    pub member_refs: Option<MemberRefs>,
118}
119
120/// The outcome of a `refs` query: resolved, ambiguous, a bare-member answer,
121/// or not found.
122#[derive(Debug, Clone, PartialEq)]
123pub enum RefsResult {
124    /// Represents `Resolved`.
125    Resolved(RefsModel),
126    /// Represents `Ambiguous`.
127    Ambiguous(Vec<String>),
128    /// A bare-member answer: one resolved-shaped model per declaring type.
129    Members(Vec<RefsModel>),
130    /// A member seed (`Member`, `Type.Member`, `Namespace.Type.Member`) whose
131    /// name survives edge verification on more than one declaring type: one
132    /// row per candidate, never the bare type list [`RefsResult::Ambiguous`]
133    /// renders.
134    MemberAmbiguous(Vec<MemberCandidate>),
135    /// Represents `NotFound`.
136    NotFound,
137}
138
139// A file's project is the first segment of its repo-relative path -- the graph
140// carries no other grouping, and the paths it stores are always repo-relative
141// with `/` separators. A file at the repo root has the empty project, which
142// only ever matches another root file.
143pub(super) fn project_of(file: &str) -> &str {
144    match file.find('/') {
145        Some(i) => &file[..i],
146        None => "",
147    }
148}
149
150// Trims exactly this ASCII set and nothing else. `str::trim` would strip the
151// full Unicode White_Space property (U+00A0 included) but not U+FEFF; a fixed
152// explicit set keeps trimming stable and independent of the Unicode tables, so
153// a line whose first or last character is one of these is trimmed predictably.
154pub(super) fn trim_source(text: &str) -> &str {
155    text.trim_matches(|c| matches!(c, ' ' | '\t' | '\r' | '\n' | '\u{0b}' | '\u{0c}'))
156}
157
158/// Per-file cache of source lines, so a widely used type does not re-read one
159/// consumer file once per hit. The value is `None` for a file that could not
160/// be read.
161pub type LineCache = HashMap<String, Option<Vec<String>>>;
162
163fn cached_line(root: &Path, file: &str, line: usize, cache: &mut LineCache) -> String {
164    let lines = cache.entry(file.to_string()).or_insert_with(|| {
165        std::fs::read_to_string(root.join(file))
166            .ok()
167            .map(|body| body.split('\n').map(str::to_string).collect())
168    });
169    let Some(lines) = lines else {
170        return String::new();
171    };
172    if line < 1 || line > lines.len() {
173        return String::new();
174    }
175    let mut raw = lines[line - 1].as_str();
176    // A UTF-8 BOM survives `read_to_string` and is not in the trim set, so line
177    // 1 of a BOM'd file would otherwise print it.
178    if line == 1 {
179        raw = raw.strip_prefix('\u{feff}').unwrap_or(raw);
180    }
181    trim_source(raw).to_string()
182}
183
184// Interior tabs collapse to one space each and the result is cut to `SOURCE_MAX`.
185fn clip_source(text: &str) -> String {
186    let text = text.replace('\t', " ");
187    // Cutting at `SOURCE_MAX` UTF-16 code units can split a surrogate pair; the
188    // lone surrogate is then emitted as U+FFFD, which is what `from_utf16_lossy`
189    // produces here.
190    let units: Vec<u16> = text.encode_utf16().collect();
191    if units.len() > SOURCE_MAX {
192        String::from_utf16_lossy(&units[..SOURCE_MAX])
193    } else {
194        text
195    }
196}
197
198// The one trimmed, clipped line of source a refs hit sits on, so a caller can
199// judge the hit without opening the file.
200fn hit_source(root: &Path, file: &str, line: usize, cache: &mut LineCache) -> String {
201    clip_source(&cached_line(root, file, line, cache))
202}
203
204// A `uses-member` edge records the member's declaring TYPE, the file and the
205// line, and never the member's own name, so no bare-member answer can be read
206// off an edge. This whole-token test stands in for the field that is missing:
207// an occurrence with a word character on either side does not count, so `Foo`
208// never answers for a line whose only occurrence is `FooEx`. A word character
209// is an ASCII letter, an ASCII digit or `_`, and nothing else -- every other
210// code point, non-ASCII included, is a boundary. That rule is stated as a
211// literal test rather than a character class so it does not depend on any
212// regex engine's Unicode handling.
213//
214// The scan works over UTF-8 bytes: a valid UTF-8 needle can only match at a
215// character boundary (lead and continuation byte ranges are disjoint), and
216// every byte outside ASCII fails the word-character test on both sides.
217fn is_word_byte(b: u8) -> bool {
218    b.is_ascii_alphanumeric() || b == b'_'
219}
220
221pub(super) fn line_has_token(line: &str, token: &str) -> bool {
222    let hay = line.as_bytes();
223    let needle = token.as_bytes();
224    if needle.is_empty() || needle.len() > hay.len() {
225        return false;
226    }
227    for at in 0..=(hay.len() - needle.len()) {
228        if &hay[at..at + needle.len()] != needle {
229            continue;
230        }
231        let after_at = at + needle.len();
232        let before_ok = at == 0 || !is_word_byte(hay[at - 1]);
233        let after_ok = after_at >= hay.len() || !is_word_byte(hay[after_at]);
234        if before_ok && after_ok {
235            return true;
236        }
237    }
238    false
239}
240
241// The two non-empty outcomes of `build_member_refs_models`: a plain models
242// array, or an ambiguity -- more than one declaring type surviving edge-line
243// verification is reported, never turned into several models.
244enum MemberRefsOutcome {
245    Models(Vec<RefsModel>),
246    Ambiguous(Vec<MemberCandidate>),
247}
248
249// `refs <member seed>` (`Member`, `Type.Member`, `Namespace.Type.Member`): the
250// name index names the declaring type(s) the seed's own qualifier admits
251// (`member::qualified_member_owners`); each candidate type's inbound
252// `uses-member` edges survives only if the line it starts on carries the
253// member as a whole token. A type whose edges all fail verification is
254// dropped only while another type still answers, so a bare name still
255// disambiguates on evidence; when none answers, the declared owners stand and
256// a member nothing references resolves to an empty answer, not to nothing.
257//
258// More than one declaring type surviving verification answers
259// `Ambiguous(candidates)`, in name-index order, rather than several models --
260// the house rule of never guessing between candidates. Overloads of one name
261// on ONE type are one group (one entry in `owners`, several sites), never an
262// ambiguity.
263// One owner's inbound `uses-member` edges, kept only if the line they start
264// on carries `name` as a whole token, ranked the same way a type's own
265// inbound table is: precise before guessed, the declaring type's own project
266// before every other, then file, then line.
267fn verified_member_edges(
268    index: &GraphIndex,
269    edges: &[graph::Edge],
270    owner: &str,
271    name: &str,
272    cache: &mut LineCache,
273) -> Vec<(usize, bool)> {
274    let refs = symbol_refs(index, owner);
275    let owner_def = &index.graph.defs[index.by_id[owner]];
276    let owner_project = project_of(&owner_def.file).to_string();
277    let mut kept: Vec<(usize, bool)> = refs
278        .inbound_uses_member
279        .iter()
280        .map(|&e| (e, false))
281        .chain(
282            refs.heuristic_inbound_uses_member
283                .iter()
284                .map(|&e| (e, true)),
285        )
286        .collect();
287    kept.retain(|&(e, _)| {
288        let (file, line) = edge_loc(&edges[e]);
289        line_has_token(&cached_line(&index.root, file, line, cache), name)
290    });
291    let foreign = |e: usize| usize::from(project_of(edge_loc(&edges[e]).0) != owner_project);
292    kept.sort_by(|a, b| {
293        a.1.cmp(&b.1)
294            .then_with(|| foreign(a.0).cmp(&foreign(b.0)))
295            .then_with(|| loc_cmp(&edges[a.0], &edges[b.0]))
296    });
297    kept
298}
299
300fn empty_table<R>() -> Table<R> {
301    Table {
302        total: 0,
303        dropped: 0,
304        rows: Vec::new(),
305    }
306}
307
308// One declaring type's resolved-shaped model: `take` (bounded by the shared
309// `budget`, spent across every group in call order) of its verified edges
310// become inbound rows; the rest are counted in `dropped` but never shown.
311#[allow(clippy::too_many_arguments)]
312fn member_group_model(
313    index: &GraphIndex,
314    edges: &[graph::Edge],
315    seed: &str,
316    name: &str,
317    owner: String,
318    sites: Vec<DefSite>,
319    kept: Vec<(usize, bool)>,
320    dispatch: Vec<usize>,
321    budget: &mut usize,
322    cache: &mut LineCache,
323) -> RefsModel {
324    let take = (*budget).min(kept.len());
325    *budget -= take;
326    let mut rows = Vec::new();
327    for &(e, heuristic) in &kept[..take] {
328        let (file, line) = edge_loc(&edges[e]);
329        let source = clip_source(&cached_line(&index.root, file, line, cache));
330        rows.push(InboundRow {
331            file: file.to_string(),
332            line,
333            heuristic,
334            tier: row_tier(
335                heuristic,
336                edges[e].tier() == Some(graph::HeuristicTier::Ext),
337            ),
338            source,
339            occurrence_index: None,
340        });
341    }
342    tag_inbound_rows(&mut rows);
343    let total = kept.len();
344    let (mut implements_rows, mut overrides_rows) =
345        dispatch::split_dispatch_rows(edges, &dispatch, |e| {
346            let (file, line) = edge_loc(&edges[e]);
347            InboundRow {
348                file: file.to_string(),
349                line,
350                heuristic: false,
351                tier: None,
352                source: clip_source(&cached_line(&index.root, file, line, cache)),
353                occurrence_index: None,
354            }
355        });
356    tag_inbound_rows(&mut implements_rows);
357    tag_inbound_rows(&mut overrides_rows);
358    RefsModel {
359        query: seed.to_string(),
360        id: format!("{owner}.{name}"),
361        kind: "member".to_string(),
362        sites,
363        inbound: InboundTables {
364            inherits: empty_table(),
365            uses_type: empty_table(),
366            uses_member: Table {
367                total,
368                dropped: total - rows.len(),
369                rows,
370            },
371            implements: dispatch::dispatch_table(implements_rows),
372            overrides: dispatch::dispatch_table(overrides_rows),
373        },
374        outbound: None,
375        ambiguous: AmbiguousTables {
376            inbound: empty_table(),
377            outbound: empty_table(),
378        },
379        bus: bus::empty_bus_table(),
380        manifest_gap: index.flagged_files.len(),
381        member_refs: None,
382    }
383}
384
385fn build_member_refs_models(
386    index: &GraphIndex,
387    seed: &str,
388    inbound_cap: usize,
389) -> Option<MemberRefsOutcome> {
390    let (name, qualifier) = member::split_member_seed(seed);
391    let owners = member::qualified_member_owners(index, name, qualifier);
392    if owners.is_empty() {
393        return None;
394    }
395    let edges = &index.graph.edges;
396    let mut cache: LineCache = HashMap::new();
397    let mut groups: Vec<(String, Vec<DefSite>, Vec<(usize, bool)>, Vec<usize>)> = owners
398        .into_iter()
399        .map(|(owner, sites)| {
400            let kept = verified_member_edges(index, edges, &owner, name, &mut cache);
401            let hits = dispatch::member_dispatch_edges(&symbol_refs(index, &owner), edges, name);
402            (owner, sites, kept, hits)
403        })
404        .collect();
405    let answers =
406        |kept: &Vec<(usize, bool)>, hits: &Vec<usize>| !kept.is_empty() || !hits.is_empty();
407    if groups.iter().any(|(_, _, kept, hits)| answers(kept, hits)) {
408        groups.retain(|(_, _, kept, hits)| answers(kept, hits));
409    }
410    // Reported before the inbound cap is ever spent, so the answer does not
411    // depend on `inbound_cap`: an ambiguity is a refusal, not a budgeted,
412    // capped multi-block resolution.
413    if groups.len() > 1 {
414        return Some(MemberRefsOutcome::Ambiguous(
415            groups
416                .into_iter()
417                .map(|(owner, sites, ..)| MemberCandidate {
418                    owner,
419                    name: name.to_string(),
420                    file: sites[0].file.clone(),
421                    line: sites[0].line,
422                })
423                .collect(),
424        ));
425    }
426
427    let mut budget = inbound_cap;
428    let models = groups
429        .into_iter()
430        .map(|(owner, sites, kept, hits)| {
431            member_group_model(
432                index,
433                edges,
434                seed,
435                name,
436                owner,
437                sites,
438                kept,
439                hits,
440                &mut budget,
441                &mut cache,
442            )
443        })
444        .collect();
445    Some(MemberRefsOutcome::Models(models))
446}
447
448/// One inbound edge awaiting the global cap: which kind's table it belongs to,
449/// which edge it is, and whether it was guessed.
450struct RankedInbound {
451    kind: usize,
452    edge: usize,
453    heuristic: bool,
454}
455
456// The six outbound kinds share ONE cap and ONE ranking under `--out`,
457// mirroring `build_refs_model`'s own inbound block below: resolved before
458// heuristic, then the def's own project before every other (imports always
459// foreign, per `outbound_foreign` above), then file, then line. Each shown hit
460// carries the same trimmed source line an inbound hit does, read at the SAME
461// `from_line` an inbound row reads -- for an outbound edge that is a line in
462// the def's own file, the site actually making the reference, not the caller's.
463#[allow(
464    clippy::too_many_lines,
465    reason = "one ordered ranking pass over all six outbound kinds, sharing the same cap and tie-break rule across them"
466)]
467fn build_outbound_tables(
468    refs: &SymbolRefs,
469    def_project: &str,
470    edges: &[graph::Edge],
471    root: &Path,
472    outbound_cap: usize,
473) -> OutboundTables {
474    const EMPTY: &[usize] = &[];
475    let mut ranked: Vec<RankedOutbound> = Vec::new();
476    push_ranked(
477        &mut ranked,
478        K_INHERITS,
479        &refs.outbound_inherits,
480        &refs.heuristic_outbound_inherits,
481    );
482    push_ranked(
483        &mut ranked,
484        K_USES_TYPE,
485        &refs.outbound_uses_type,
486        &refs.heuristic_outbound_uses_type,
487    );
488    push_ranked(
489        &mut ranked,
490        K_USES_MEMBER,
491        &refs.outbound_uses_member,
492        &refs.heuristic_outbound_uses_member,
493    );
494    push_ranked(&mut ranked, K_IMPLEMENTS, &refs.outbound_implements, EMPTY);
495    push_ranked(&mut ranked, K_OVERRIDES, &refs.outbound_overrides, EMPTY);
496    push_ranked(&mut ranked, K_IMPORTS, &refs.outbound_imports, EMPTY);
497
498    let mut totals = [0usize; 6];
499    for r in &ranked {
500        totals[r.kind] += 1;
501    }
502    ranked.sort_by(|a, b| {
503        a.heuristic
504            .cmp(&b.heuristic)
505            .then_with(|| {
506                outbound_foreign(def_project, edges, a).cmp(&outbound_foreign(
507                    def_project,
508                    edges,
509                    b,
510                ))
511            })
512            .then_with(|| loc_cmp(&edges[a.edge], &edges[b.edge]))
513    });
514    let (shown, _) = cap_rows(ranked, outbound_cap);
515
516    let mut source_cache: LineCache = HashMap::new();
517    let mut rows: [Vec<OutboundRow>; 5] = Default::default();
518    let mut imports = Vec::new();
519    for r in shown {
520        let (file, line) = edge_loc(&edges[r.edge]);
521        let source = hit_source(root, file, line, &mut source_cache);
522        if r.kind == K_IMPORTS {
523            let graph::Edge::Imports { target, .. } = &edges[r.edge] else {
524                unreachable!("outbound kind K_IMPORTS only ever holds imports edge indices");
525            };
526            imports.push(ImportRow {
527                file: file.to_string(),
528                line,
529                target: target.clone(),
530                source,
531            });
532            continue;
533        }
534        let (to_file, to) = match &edges[r.edge] {
535            graph::Edge::Inherits { to_file, to, .. }
536            | graph::Edge::UsesType { to_file, to, .. }
537            | graph::Edge::UsesMember { to_file, to, .. }
538            | graph::Edge::Implements { to_file, to, .. }
539            | graph::Edge::Overrides { to_file, to, .. } => (to_file.clone(), to.clone()),
540            _ => unreachable!("outbound ranked kinds other than K_IMPORTS only ever hold inherits/uses-type/uses-member/implements/overrides edge indices"),
541        };
542        let row = OutboundRow {
543            file: file.to_string(),
544            line,
545            to_file,
546            to,
547            heuristic: r.heuristic,
548            tier: row_tier(
549                r.heuristic,
550                edges[r.edge].tier() == Some(graph::HeuristicTier::Ext),
551            ),
552            source,
553            occurrence_index: None,
554        };
555        rows[r.kind].push(row);
556    }
557    let [mut inherits, mut uses_type, mut uses_member, mut implements, mut overrides] = rows;
558    // Same per-kind, post-cap tagging rule `build_refs_model_inner`'s inbound
559    // side applies; `imports` is excluded -- an imports edge is never a
560    // guess and never shares a line with another imports edge the way a
561    // `uses-member` call site can, so `ImportRow` carries no such field.
562    tag_outbound_rows(&mut inherits);
563    tag_outbound_rows(&mut uses_type);
564    tag_outbound_rows(&mut uses_member);
565    tag_outbound_rows(&mut implements);
566    tag_outbound_rows(&mut overrides);
567
568    let table = |kind: usize, rows: Vec<OutboundRow>| Table {
569        total: totals[kind],
570        dropped: totals[kind] - rows.len(),
571        rows,
572    };
573    OutboundTables {
574        inherits: table(K_INHERITS, inherits),
575        uses_type: table(K_USES_TYPE, uses_type),
576        uses_member: table(K_USES_MEMBER, uses_member),
577        implements: table(K_IMPLEMENTS, implements),
578        overrides: table(K_OVERRIDES, overrides),
579        imports: Table {
580            total: totals[K_IMPORTS],
581            dropped: totals[K_IMPORTS] - imports.len(),
582            rows: imports,
583        },
584    }
585}
586
587/// Build the `refs` model for `query`. `out` includes the outbound tables;
588/// `cap`/`inbound_cap`/`outbound_cap` bound the tables; `all_out` (`--all`)
589/// lifts the inbound and outbound caps.
590pub fn build_refs_model(
591    index: &GraphIndex,
592    query: &str,
593    out: bool,
594    cap: usize,
595    inbound_cap: usize,
596    outbound_cap: usize,
597    all_out: bool,
598) -> RefsResult {
599    build_refs_model_inner(
600        index,
601        query,
602        out,
603        cap,
604        inbound_cap,
605        outbound_cap,
606        all_out,
607        false,
608    )
609}
610
611#[allow(clippy::too_many_arguments)]
612#[allow(
613    clippy::too_many_lines,
614    reason = "one ordered assembly of the refs model's inbound and outbound sections, sharing the same caps and options across both"
615)]
616pub(super) fn build_refs_model_inner(
617    index: &GraphIndex,
618    query: &str,
619    out: bool,
620    cap: usize,
621    inbound_cap: usize,
622    outbound_cap: usize,
623    all_out: bool,
624    exclude_self_inbound: bool,
625) -> RefsResult {
626    let id = match resolve_symbol(index, query) {
627        Resolution::Resolved(id) => id,
628        Resolution::Ambiguous(ids) => return RefsResult::Ambiguous(ids),
629        // The member path is a FALLBACK, reached only when nothing in the graph
630        // declares `query` as a type, so a name that is both a type and a
631        // member still answers as the type. `out` has no member reading (a
632        // type's outbound edges are the type's answer, not the member's) and is
633        // ignored here.
634        Resolution::NotFound => {
635            return match build_member_refs_models(index, query, inbound_cap) {
636                Some(MemberRefsOutcome::Models(models)) => RefsResult::Members(models),
637                Some(MemberRefsOutcome::Ambiguous(candidates)) => {
638                    RefsResult::MemberAmbiguous(candidates)
639                }
640                None => RefsResult::NotFound,
641            };
642        }
643    };
644    let def_idx = *index
645        .by_id
646        .get(&id)
647        .expect("resolved id must exist in the index it was resolved from");
648    let def = &index.graph.defs[def_idx];
649    let refs = symbol_refs(index, &id);
650    let edges = &index.graph.edges;
651
652    // The three inbound kinds share ONE cap and ONE ranking, so a widely used
653    // type spends its whole budget on the most specific edges it has rather
654    // than on whichever kind the walk reached first. Rank: precise before
655    // heuristic, then the def's own project before every other, then file,
656    // then line. Push order (kind by kind, precise then heuristic within a
657    // kind) is load-bearing: the sort is stable, so two edges of different
658    // kinds at the same file:line keep their push order.
659    let def_project = project_of(&def.file);
660    let mut ranked: Vec<RankedInbound> = Vec::new();
661    let mut totals = [0usize; 5];
662    let is_self_inbound = |edge: usize| {
663        let (file, line) = edge_loc(&edges[edge]);
664        exclude_self_inbound
665            && def.end_line >= def.line
666            && file == def.file
667            && (def.line..=def.end_line).contains(&line)
668    };
669    const EMPTY: &[usize] = &[];
670    for (kind, (precise, heuristic)) in [
671        (
672            &refs.inbound_inherits[..],
673            &refs.heuristic_inbound_inherits[..],
674        ),
675        (
676            &refs.inbound_uses_type[..],
677            &refs.heuristic_inbound_uses_type[..],
678        ),
679        (
680            &refs.inbound_uses_member[..],
681            &refs.heuristic_inbound_uses_member[..],
682        ),
683        (&refs.inbound_implements[..], EMPTY),
684        (&refs.inbound_overrides[..], EMPTY),
685    ]
686    .into_iter()
687    .enumerate()
688    {
689        for &e in precise {
690            if is_self_inbound(e) {
691                continue;
692            }
693            ranked.push(RankedInbound {
694                kind,
695                edge: e,
696                heuristic: false,
697            });
698            totals[kind] += 1;
699        }
700        for &e in heuristic {
701            if is_self_inbound(e) {
702                continue;
703            }
704            ranked.push(RankedInbound {
705                kind,
706                edge: e,
707                heuristic: true,
708            });
709            totals[kind] += 1;
710        }
711    }
712    let foreign = |e: usize| usize::from(project_of(edge_loc(&edges[e]).0) != def_project);
713    ranked.sort_by(|a, b| {
714        a.heuristic
715            .cmp(&b.heuristic)
716            .then_with(|| foreign(a.edge).cmp(&foreign(b.edge)))
717            .then_with(|| loc_cmp(&edges[a.edge], &edges[b.edge]))
718    });
719    // `all_out` (`--all`, the same flag the outbound cap-lift below reads) lifts
720    // the inbound cap too, not just the outbound one: a caller reaching for
721    // `--all` on a truncated inbound table is asking for exactly this.
722    let (shown_inbound, _) = cap_rows(ranked, if all_out { usize::MAX } else { inbound_cap });
723
724    let mut source_cache: LineCache = HashMap::new();
725    let mut rows: [Vec<InboundRow>; 5] = Default::default();
726    for r in shown_inbound {
727        let (file, line) = edge_loc(&edges[r.edge]);
728        let source = hit_source(&index.root, file, line, &mut source_cache);
729        rows[r.kind].push(InboundRow {
730            file: file.to_string(),
731            line,
732            heuristic: r.heuristic,
733            tier: row_tier(
734                r.heuristic,
735                edges[r.edge].tier() == Some(graph::HeuristicTier::Ext),
736            ),
737            source,
738            occurrence_index: None,
739        });
740    }
741    let [mut inherits_rows, mut uses_type_rows, mut uses_member_rows, mut implements_rows, mut overrides_rows] =
742        rows;
743    // Tagged per kind, AFTER capping and BEFORE the `Table` wrapper is built:
744    // an occurrence collision is scoped to one table of one answer, never
745    // across kinds (a `uses-type` row and a `uses-member` row at the same
746    // file:line are already distinct by kind and need no ordinal), and a row
747    // dropped by the cap above is never assigned one, per the same
748    // truncation-is-not-a-collapse rule `dropped` already states.
749    tag_inbound_rows(&mut inherits_rows);
750    tag_inbound_rows(&mut uses_type_rows);
751    tag_inbound_rows(&mut uses_member_rows);
752    tag_inbound_rows(&mut implements_rows);
753    tag_inbound_rows(&mut overrides_rows);
754    let inbound_table = |total: usize, rows: Vec<InboundRow>| Table {
755        total,
756        dropped: total - rows.len(),
757        rows,
758    };
759    let inbound = InboundTables {
760        inherits: inbound_table(totals[0], inherits_rows),
761        uses_type: inbound_table(totals[1], uses_type_rows),
762        uses_member: inbound_table(totals[2], uses_member_rows),
763        implements: inbound_table(totals[3], implements_rows),
764        overrides: inbound_table(totals[4], overrides_rows),
765    };
766
767    let outbound = out.then(|| {
768        build_outbound_tables(
769            &refs,
770            def_project,
771            edges,
772            &index.root,
773            if all_out { usize::MAX } else { outbound_cap },
774        )
775    });
776    let ambiguous = AmbiguousTables {
777        inbound: build_table(refs.ambiguous_inbound, edges, cap, ambiguous_row),
778        outbound: build_table(refs.ambiguous_outbound, edges, cap, ambiguous_row),
779    };
780    let bus = bus::symbol_bus_rows(index, &id, cap);
781
782    let member_refs = if def.kind == "enum" {
783        enum_member_refs(index, &id)
784    } else {
785        None
786    };
787
788    RefsResult::Resolved(RefsModel {
789        query: query.to_string(),
790        id: id.clone(),
791        kind: def.kind.clone(),
792        sites: def_sites(index, &id),
793        inbound,
794        outbound,
795        ambiguous,
796        bus,
797        manifest_gap: index.flagged_files.len(),
798        member_refs,
799    })
800}