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#[derive(Debug, Clone, PartialEq)]
21pub struct MemberRefEntry {
22 pub name: String,
24 pub count: usize,
26}
27
28#[derive(Debug, Clone, PartialEq)]
31pub struct MemberRefs {
32 pub total: usize,
34 pub member_count: usize,
36 pub members: Vec<MemberRefEntry>,
38 pub dropped: usize,
40}
41
42const MEMBER_NAME_CAP: usize = 5;
44
45fn 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#[derive(Debug, Clone, PartialEq)]
93pub struct RefsModel {
94 pub query: String,
96 pub id: String,
98 pub kind: String,
100 pub sites: Vec<DefSite>,
102 pub inbound: InboundTables,
104 pub outbound: Option<OutboundTables>,
108 pub ambiguous: AmbiguousTables,
110 pub manifest_gap: usize,
112 pub member_refs: Option<MemberRefs>,
115}
116
117#[derive(Debug, Clone, PartialEq)]
120pub enum RefsResult {
121 Resolved(RefsModel),
123 Ambiguous(Vec<String>),
125 Members(Vec<RefsModel>),
127 MemberAmbiguous(Vec<MemberCandidate>),
132 NotFound,
134}
135
136pub(super) fn project_of(file: &str) -> &str {
141 match file.find('/') {
142 Some(i) => &file[..i],
143 None => "",
144 }
145}
146
147pub(super) fn trim_source(text: &str) -> &str {
152 text.trim_matches(|c| matches!(c, ' ' | '\t' | '\r' | '\n' | '\u{0b}' | '\u{0c}'))
153}
154
155pub 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 if line == 1 {
176 raw = raw.strip_prefix('\u{feff}').unwrap_or(raw);
177 }
178 trim_source(raw).to_string()
179}
180
181fn clip_source(text: &str) -> String {
183 let text = text.replace('\t', " ");
184 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
195fn hit_source(root: &Path, file: &str, line: usize, cache: &mut LineCache) -> String {
198 clip_source(&cached_line(root, file, line, cache))
199}
200
201fn 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
238enum MemberRefsOutcome {
242 Models(Vec<RefsModel>),
243 Ambiguous(Vec<MemberCandidate>),
244}
245
246fn 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#[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 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
444struct RankedInbound {
447 kind: usize,
448 edge: usize,
449 heuristic: bool,
450}
451
452#[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 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
583pub 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 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 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 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 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}