Skip to main content

devscout_rs/query/
refs.rs

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