1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::graph;
5
6use super::dispatch::{
7 self, outbound_foreign, push_ranked, RankedOutbound, K_IMPLEMENTS, K_IMPORTS, K_INHERITS,
8 K_OVERRIDES, K_USES_MEMBER, K_USES_TYPE,
9};
10use super::index::{def_sites, symbol_refs, DefSite, GraphIndex, SymbolRefs};
11use super::member::{self, MemberCandidate};
12use super::refs_tables::{
13 ambiguous_row, build_table, cap_rows, edge_loc, loc_cmp, row_tier, AmbiguousTables, ImportRow,
14 InboundRow, InboundTables, OutboundRow, OutboundTables, Table, SOURCE_MAX,
15};
16use super::symbol::{resolve_symbol, Resolution};
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct MemberRefEntry {
21 pub name: String,
23 pub count: usize,
25}
26
27#[derive(Debug, Clone, PartialEq)]
30pub struct MemberRefs {
31 pub total: usize,
33 pub member_count: usize,
35 pub members: Vec<MemberRefEntry>,
37 pub dropped: usize,
39}
40
41const MEMBER_NAME_CAP: usize = 5;
43
44fn enum_member_refs(index: &GraphIndex, def_id: &str) -> Option<MemberRefs> {
55 let prefix = format!("{def_id}.");
56 let mut members: Vec<MemberRefEntry> = Vec::new();
57 let mut total = 0usize;
58 for d in &index.graph.defs {
59 if d.kind != "enum-member" || !d.id.starts_with(&prefix) {
60 continue;
61 }
62 let count = index.inbound.get(&d.id).map_or(0, |e| e.uses_member.len())
63 + index
64 .heuristic_inbound
65 .get(&d.id)
66 .map_or(0, |e| e.uses_member.len());
67 if count == 0 {
68 continue;
69 }
70 members.push(MemberRefEntry {
71 name: d.name.clone(),
72 count,
73 });
74 total += count;
75 }
76 if total == 0 {
77 return None;
78 }
79 let member_count = members.len();
80 let dropped = member_count.saturating_sub(MEMBER_NAME_CAP);
81 members.truncate(MEMBER_NAME_CAP);
82 Some(MemberRefs {
83 total,
84 member_count,
85 members,
86 dropped,
87 })
88}
89
90#[derive(Debug, Clone, PartialEq)]
92pub struct RefsModel {
93 pub query: String,
95 pub id: String,
97 pub kind: String,
99 pub sites: Vec<DefSite>,
101 pub inbound: InboundTables,
103 pub outbound: Option<OutboundTables>,
107 pub ambiguous: AmbiguousTables,
109 pub manifest_gap: usize,
111 pub member_refs: Option<MemberRefs>,
114}
115
116#[derive(Debug, Clone, PartialEq)]
119pub enum RefsResult {
120 Resolved(RefsModel),
122 Ambiguous(Vec<String>),
124 Members(Vec<RefsModel>),
126 MemberAmbiguous(Vec<MemberCandidate>),
131 NotFound,
133}
134
135pub(super) fn project_of(file: &str) -> &str {
140 match file.find('/') {
141 Some(i) => &file[..i],
142 None => "",
143 }
144}
145
146pub(super) fn trim_source(text: &str) -> &str {
151 text.trim_matches(|c| matches!(c, ' ' | '\t' | '\r' | '\n' | '\u{0b}' | '\u{0c}'))
152}
153
154pub type LineCache = HashMap<String, Option<Vec<String>>>;
158
159fn cached_line(root: &Path, file: &str, line: usize, cache: &mut LineCache) -> String {
160 let lines = cache.entry(file.to_string()).or_insert_with(|| {
161 std::fs::read_to_string(root.join(file))
162 .ok()
163 .map(|body| body.split('\n').map(str::to_string).collect())
164 });
165 let Some(lines) = lines else {
166 return String::new();
167 };
168 if line < 1 || line > lines.len() {
169 return String::new();
170 }
171 let mut raw = lines[line - 1].as_str();
172 if line == 1 {
175 raw = raw.strip_prefix('\u{feff}').unwrap_or(raw);
176 }
177 trim_source(raw).to_string()
178}
179
180fn clip_source(text: &str) -> String {
182 let text = text.replace('\t', " ");
183 let units: Vec<u16> = text.encode_utf16().collect();
187 if units.len() > SOURCE_MAX {
188 String::from_utf16_lossy(&units[..SOURCE_MAX])
189 } else {
190 text
191 }
192}
193
194fn hit_source(root: &Path, file: &str, line: usize, cache: &mut LineCache) -> String {
197 clip_source(&cached_line(root, file, line, cache))
198}
199
200fn is_word_byte(b: u8) -> bool {
214 b.is_ascii_alphanumeric() || b == b'_'
215}
216
217pub(super) fn line_has_token(line: &str, token: &str) -> bool {
218 let hay = line.as_bytes();
219 let needle = token.as_bytes();
220 if needle.is_empty() || needle.len() > hay.len() {
221 return false;
222 }
223 for at in 0..=(hay.len() - needle.len()) {
224 if &hay[at..at + needle.len()] != needle {
225 continue;
226 }
227 let after_at = at + needle.len();
228 let before_ok = at == 0 || !is_word_byte(hay[at - 1]);
229 let after_ok = after_at >= hay.len() || !is_word_byte(hay[after_at]);
230 if before_ok && after_ok {
231 return true;
232 }
233 }
234 false
235}
236
237enum MemberRefsOutcome {
241 Models(Vec<RefsModel>),
242 Ambiguous(Vec<MemberCandidate>),
243}
244
245fn verified_member_edges(
264 index: &GraphIndex,
265 edges: &[graph::Edge],
266 owner: &str,
267 name: &str,
268 cache: &mut LineCache,
269) -> Vec<(usize, bool)> {
270 let refs = symbol_refs(index, owner);
271 let owner_def = &index.graph.defs[index.by_id[owner]];
272 let owner_project = project_of(&owner_def.file).to_string();
273 let mut kept: Vec<(usize, bool)> = refs
274 .inbound_uses_member
275 .iter()
276 .map(|&e| (e, false))
277 .chain(
278 refs.heuristic_inbound_uses_member
279 .iter()
280 .map(|&e| (e, true)),
281 )
282 .collect();
283 kept.retain(|&(e, _)| {
284 let (file, line) = edge_loc(&edges[e]);
285 line_has_token(&cached_line(&index.root, file, line, cache), name)
286 });
287 let foreign = |e: usize| usize::from(project_of(edge_loc(&edges[e]).0) != owner_project);
288 kept.sort_by(|a, b| {
289 a.1.cmp(&b.1)
290 .then_with(|| foreign(a.0).cmp(&foreign(b.0)))
291 .then_with(|| loc_cmp(&edges[a.0], &edges[b.0]))
292 });
293 kept
294}
295
296fn empty_table<R>() -> Table<R> {
297 Table {
298 total: 0,
299 dropped: 0,
300 rows: Vec::new(),
301 }
302}
303
304#[allow(clippy::too_many_arguments)]
308fn member_group_model(
309 index: &GraphIndex,
310 edges: &[graph::Edge],
311 seed: &str,
312 name: &str,
313 owner: String,
314 sites: Vec<DefSite>,
315 kept: Vec<(usize, bool)>,
316 dispatch: Vec<usize>,
317 budget: &mut usize,
318 cache: &mut LineCache,
319) -> RefsModel {
320 let take = (*budget).min(kept.len());
321 *budget -= take;
322 let mut rows = Vec::new();
323 for &(e, heuristic) in &kept[..take] {
324 let (file, line) = edge_loc(&edges[e]);
325 let source = clip_source(&cached_line(&index.root, file, line, cache));
326 rows.push(InboundRow {
327 file: file.to_string(),
328 line,
329 heuristic,
330 tier: row_tier(
331 heuristic,
332 edges[e].tier() == Some(graph::HeuristicTier::Ext),
333 ),
334 source,
335 });
336 }
337 let total = kept.len();
338 let (implements_rows, overrides_rows) = dispatch::split_dispatch_rows(edges, &dispatch, |e| {
339 let (file, line) = edge_loc(&edges[e]);
340 InboundRow {
341 file: file.to_string(),
342 line,
343 heuristic: false,
344 tier: None,
345 source: clip_source(&cached_line(&index.root, file, line, cache)),
346 }
347 });
348 RefsModel {
349 query: seed.to_string(),
350 id: format!("{owner}.{name}"),
351 kind: "member".to_string(),
352 sites,
353 inbound: InboundTables {
354 inherits: empty_table(),
355 uses_type: empty_table(),
356 uses_member: Table {
357 total,
358 dropped: total - rows.len(),
359 rows,
360 },
361 implements: dispatch::dispatch_table(implements_rows),
362 overrides: dispatch::dispatch_table(overrides_rows),
363 },
364 outbound: None,
365 ambiguous: AmbiguousTables {
366 inbound: empty_table(),
367 outbound: empty_table(),
368 },
369 manifest_gap: index.flagged_files.len(),
370 member_refs: None,
371 }
372}
373
374fn build_member_refs_models(
375 index: &GraphIndex,
376 seed: &str,
377 inbound_cap: usize,
378) -> Option<MemberRefsOutcome> {
379 let (name, qualifier) = member::split_member_seed(seed);
380 let owners = member::qualified_member_owners(index, name, qualifier);
381 if owners.is_empty() {
382 return None;
383 }
384 let edges = &index.graph.edges;
385 let mut cache: LineCache = HashMap::new();
386 let mut groups: Vec<(String, Vec<DefSite>, Vec<(usize, bool)>, Vec<usize>)> = owners
387 .into_iter()
388 .map(|(owner, sites)| {
389 let kept = verified_member_edges(index, edges, &owner, name, &mut cache);
390 let hits = dispatch::member_dispatch_edges(&symbol_refs(index, &owner), edges, name);
391 (owner, sites, kept, hits)
392 })
393 .collect();
394 let answers =
395 |kept: &Vec<(usize, bool)>, hits: &Vec<usize>| !kept.is_empty() || !hits.is_empty();
396 if groups.iter().any(|(_, _, kept, hits)| answers(kept, hits)) {
397 groups.retain(|(_, _, kept, hits)| answers(kept, hits));
398 }
399 if groups.len() > 1 {
403 return Some(MemberRefsOutcome::Ambiguous(
404 groups
405 .into_iter()
406 .map(|(owner, sites, ..)| MemberCandidate {
407 owner,
408 name: name.to_string(),
409 file: sites[0].file.clone(),
410 line: sites[0].line,
411 })
412 .collect(),
413 ));
414 }
415
416 let mut budget = inbound_cap;
417 let models = groups
418 .into_iter()
419 .map(|(owner, sites, kept, hits)| {
420 member_group_model(
421 index,
422 edges,
423 seed,
424 name,
425 owner,
426 sites,
427 kept,
428 hits,
429 &mut budget,
430 &mut cache,
431 )
432 })
433 .collect();
434 Some(MemberRefsOutcome::Models(models))
435}
436
437struct RankedInbound {
440 kind: usize,
441 edge: usize,
442 heuristic: bool,
443}
444
445#[allow(
453 clippy::too_many_lines,
454 reason = "one ordered ranking pass over all six outbound kinds, sharing the same cap and tie-break rule across them"
455)]
456fn build_outbound_tables(
457 refs: &SymbolRefs,
458 def_project: &str,
459 edges: &[graph::Edge],
460 root: &Path,
461 outbound_cap: usize,
462) -> OutboundTables {
463 const EMPTY: &[usize] = &[];
464 let mut ranked: Vec<RankedOutbound> = Vec::new();
465 push_ranked(
466 &mut ranked,
467 K_INHERITS,
468 &refs.outbound_inherits,
469 &refs.heuristic_outbound_inherits,
470 );
471 push_ranked(
472 &mut ranked,
473 K_USES_TYPE,
474 &refs.outbound_uses_type,
475 &refs.heuristic_outbound_uses_type,
476 );
477 push_ranked(
478 &mut ranked,
479 K_USES_MEMBER,
480 &refs.outbound_uses_member,
481 &refs.heuristic_outbound_uses_member,
482 );
483 push_ranked(&mut ranked, K_IMPLEMENTS, &refs.outbound_implements, EMPTY);
484 push_ranked(&mut ranked, K_OVERRIDES, &refs.outbound_overrides, EMPTY);
485 push_ranked(&mut ranked, K_IMPORTS, &refs.outbound_imports, EMPTY);
486
487 let mut totals = [0usize; 6];
488 for r in &ranked {
489 totals[r.kind] += 1;
490 }
491 ranked.sort_by(|a, b| {
492 a.heuristic
493 .cmp(&b.heuristic)
494 .then_with(|| {
495 outbound_foreign(def_project, edges, a).cmp(&outbound_foreign(
496 def_project,
497 edges,
498 b,
499 ))
500 })
501 .then_with(|| loc_cmp(&edges[a.edge], &edges[b.edge]))
502 });
503 let (shown, _) = cap_rows(ranked, outbound_cap);
504
505 let mut source_cache: LineCache = HashMap::new();
506 let mut rows: [Vec<OutboundRow>; 5] = Default::default();
507 let mut imports = Vec::new();
508 for r in shown {
509 let (file, line) = edge_loc(&edges[r.edge]);
510 let source = hit_source(root, file, line, &mut source_cache);
511 if r.kind == K_IMPORTS {
512 let graph::Edge::Imports { target, .. } = &edges[r.edge] else {
513 unreachable!("outbound kind K_IMPORTS only ever holds imports edge indices");
514 };
515 imports.push(ImportRow {
516 file: file.to_string(),
517 line,
518 target: target.clone(),
519 source,
520 });
521 continue;
522 }
523 let (to_file, to) = match &edges[r.edge] {
524 graph::Edge::Inherits { to_file, to, .. }
525 | graph::Edge::UsesType { to_file, to, .. }
526 | graph::Edge::UsesMember { to_file, to, .. }
527 | graph::Edge::Implements { to_file, to, .. }
528 | graph::Edge::Overrides { to_file, to, .. } => (to_file.clone(), to.clone()),
529 _ => unreachable!("outbound ranked kinds other than K_IMPORTS only ever hold inherits/uses-type/uses-member/implements/overrides edge indices"),
530 };
531 let row = OutboundRow {
532 file: file.to_string(),
533 line,
534 to_file,
535 to,
536 heuristic: r.heuristic,
537 tier: row_tier(
538 r.heuristic,
539 edges[r.edge].tier() == Some(graph::HeuristicTier::Ext),
540 ),
541 source,
542 };
543 rows[r.kind].push(row);
544 }
545 let [inherits, uses_type, uses_member, implements, overrides] = rows;
546
547 let table = |kind: usize, rows: Vec<OutboundRow>| Table {
548 total: totals[kind],
549 dropped: totals[kind] - rows.len(),
550 rows,
551 };
552 OutboundTables {
553 inherits: table(K_INHERITS, inherits),
554 uses_type: table(K_USES_TYPE, uses_type),
555 uses_member: table(K_USES_MEMBER, uses_member),
556 implements: table(K_IMPLEMENTS, implements),
557 overrides: table(K_OVERRIDES, overrides),
558 imports: Table {
559 total: totals[K_IMPORTS],
560 dropped: totals[K_IMPORTS] - imports.len(),
561 rows: imports,
562 },
563 }
564}
565
566pub fn build_refs_model(
570 index: &GraphIndex,
571 query: &str,
572 out: bool,
573 cap: usize,
574 inbound_cap: usize,
575 outbound_cap: usize,
576 all_out: bool,
577) -> RefsResult {
578 build_refs_model_inner(
579 index,
580 query,
581 out,
582 cap,
583 inbound_cap,
584 outbound_cap,
585 all_out,
586 false,
587 )
588}
589
590#[allow(clippy::too_many_arguments)]
591#[allow(
592 clippy::too_many_lines,
593 reason = "one ordered assembly of the refs model's inbound and outbound sections, sharing the same caps and options across both"
594)]
595pub(super) fn build_refs_model_inner(
596 index: &GraphIndex,
597 query: &str,
598 out: bool,
599 cap: usize,
600 inbound_cap: usize,
601 outbound_cap: usize,
602 all_out: bool,
603 exclude_self_inbound: bool,
604) -> RefsResult {
605 let id = match resolve_symbol(index, query) {
606 Resolution::Resolved(id) => id,
607 Resolution::Ambiguous(ids) => return RefsResult::Ambiguous(ids),
608 Resolution::NotFound => {
614 return match build_member_refs_models(index, query, inbound_cap) {
615 Some(MemberRefsOutcome::Models(models)) => RefsResult::Members(models),
616 Some(MemberRefsOutcome::Ambiguous(candidates)) => {
617 RefsResult::MemberAmbiguous(candidates)
618 }
619 None => RefsResult::NotFound,
620 };
621 }
622 };
623 let def_idx = *index
624 .by_id
625 .get(&id)
626 .expect("resolved id must exist in the index it was resolved from");
627 let def = &index.graph.defs[def_idx];
628 let refs = symbol_refs(index, &id);
629 let edges = &index.graph.edges;
630
631 let def_project = project_of(&def.file);
639 let mut ranked: Vec<RankedInbound> = Vec::new();
640 let mut totals = [0usize; 5];
641 let is_self_inbound = |edge: usize| {
642 let (file, line) = edge_loc(&edges[edge]);
643 exclude_self_inbound
644 && def.end_line >= def.line
645 && file == def.file
646 && (def.line..=def.end_line).contains(&line)
647 };
648 const EMPTY: &[usize] = &[];
649 for (kind, (precise, heuristic)) in [
650 (
651 &refs.inbound_inherits[..],
652 &refs.heuristic_inbound_inherits[..],
653 ),
654 (
655 &refs.inbound_uses_type[..],
656 &refs.heuristic_inbound_uses_type[..],
657 ),
658 (
659 &refs.inbound_uses_member[..],
660 &refs.heuristic_inbound_uses_member[..],
661 ),
662 (&refs.inbound_implements[..], EMPTY),
663 (&refs.inbound_overrides[..], EMPTY),
664 ]
665 .into_iter()
666 .enumerate()
667 {
668 for &e in precise {
669 if is_self_inbound(e) {
670 continue;
671 }
672 ranked.push(RankedInbound {
673 kind,
674 edge: e,
675 heuristic: false,
676 });
677 totals[kind] += 1;
678 }
679 for &e in heuristic {
680 if is_self_inbound(e) {
681 continue;
682 }
683 ranked.push(RankedInbound {
684 kind,
685 edge: e,
686 heuristic: true,
687 });
688 totals[kind] += 1;
689 }
690 }
691 let foreign = |e: usize| usize::from(project_of(edge_loc(&edges[e]).0) != def_project);
692 ranked.sort_by(|a, b| {
693 a.heuristic
694 .cmp(&b.heuristic)
695 .then_with(|| foreign(a.edge).cmp(&foreign(b.edge)))
696 .then_with(|| loc_cmp(&edges[a.edge], &edges[b.edge]))
697 });
698 let (shown_inbound, _) = cap_rows(ranked, if all_out { usize::MAX } else { inbound_cap });
702
703 let mut source_cache: LineCache = HashMap::new();
704 let mut rows: [Vec<InboundRow>; 5] = Default::default();
705 for r in shown_inbound {
706 let (file, line) = edge_loc(&edges[r.edge]);
707 let source = hit_source(&index.root, file, line, &mut source_cache);
708 rows[r.kind].push(InboundRow {
709 file: file.to_string(),
710 line,
711 heuristic: r.heuristic,
712 tier: row_tier(
713 r.heuristic,
714 edges[r.edge].tier() == Some(graph::HeuristicTier::Ext),
715 ),
716 source,
717 });
718 }
719 let [inherits_rows, uses_type_rows, uses_member_rows, implements_rows, overrides_rows] = rows;
720 let inbound_table = |total: usize, rows: Vec<InboundRow>| Table {
721 total,
722 dropped: total - rows.len(),
723 rows,
724 };
725 let inbound = InboundTables {
726 inherits: inbound_table(totals[0], inherits_rows),
727 uses_type: inbound_table(totals[1], uses_type_rows),
728 uses_member: inbound_table(totals[2], uses_member_rows),
729 implements: inbound_table(totals[3], implements_rows),
730 overrides: inbound_table(totals[4], overrides_rows),
731 };
732
733 let outbound = out.then(|| {
734 build_outbound_tables(
735 &refs,
736 def_project,
737 edges,
738 &index.root,
739 if all_out { usize::MAX } else { outbound_cap },
740 )
741 });
742 let ambiguous = AmbiguousTables {
743 inbound: build_table(refs.ambiguous_inbound, edges, cap, ambiguous_row),
744 outbound: build_table(refs.ambiguous_outbound, edges, cap, ambiguous_row),
745 };
746
747 let member_refs = if def.kind == "enum" {
748 enum_member_refs(index, &id)
749 } else {
750 None
751 };
752
753 RefsResult::Resolved(RefsModel {
754 query: query.to_string(),
755 id: id.clone(),
756 kind: def.kind.clone(),
757 sites: def_sites(index, &id),
758 inbound,
759 outbound,
760 ambiguous,
761 manifest_gap: index.flagged_files.len(),
762 member_refs,
763 })
764}