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#[derive(Debug, Clone, PartialEq)]
22pub struct MemberRefEntry {
23 pub name: String,
25 pub count: usize,
27}
28
29#[derive(Debug, Clone, PartialEq)]
32pub struct MemberRefs {
33 pub total: usize,
35 pub member_count: usize,
37 pub members: Vec<MemberRefEntry>,
39 pub dropped: usize,
41}
42
43const MEMBER_NAME_CAP: usize = 5;
45
46fn 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#[derive(Debug, Clone, PartialEq)]
94pub struct RefsModel {
95 pub query: String,
97 pub id: String,
99 pub kind: String,
101 pub sites: Vec<DefSite>,
103 pub inbound: InboundTables,
105 pub outbound: Option<OutboundTables>,
109 pub ambiguous: AmbiguousTables,
111 pub bus: Table<BusHopRow>,
113 pub manifest_gap: usize,
115 pub member_refs: Option<MemberRefs>,
118}
119
120#[derive(Debug, Clone, PartialEq)]
123pub enum RefsResult {
124 Resolved(RefsModel),
126 Ambiguous(Vec<String>),
128 Members(Vec<RefsModel>),
130 MemberAmbiguous(Vec<MemberCandidate>),
135 NotFound,
137}
138
139pub(super) fn project_of(file: &str) -> &str {
144 match file.find('/') {
145 Some(i) => &file[..i],
146 None => "",
147 }
148}
149
150pub(super) fn trim_source(text: &str) -> &str {
155 text.trim_matches(|c| matches!(c, ' ' | '\t' | '\r' | '\n' | '\u{0b}' | '\u{0c}'))
156}
157
158pub 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 if line == 1 {
179 raw = raw.strip_prefix('\u{feff}').unwrap_or(raw);
180 }
181 trim_source(raw).to_string()
182}
183
184fn clip_source(text: &str) -> String {
186 let text = text.replace('\t', " ");
187 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
198fn hit_source(root: &Path, file: &str, line: usize, cache: &mut LineCache) -> String {
201 clip_source(&cached_line(root, file, line, cache))
202}
203
204fn 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
241enum MemberRefsOutcome {
245 Models(Vec<RefsModel>),
246 Ambiguous(Vec<MemberCandidate>),
247}
248
249fn 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#[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 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
448struct RankedInbound {
451 kind: usize,
452 edge: usize,
453 heuristic: bool,
454}
455
456#[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 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
587pub 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 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 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 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 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}