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