Skip to main content

devscout_rs/query/
refs.rs

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