devscout_rs/query/index.rs
1use std::collections::{HashMap, HashSet};
2use std::path::Path;
3
4use crate::graph;
5use crate::manifest;
6
7use super::seq::{push_ordered_unique, SeqSet};
8
9// ============================================================================
10// GraphIndex -- the built query index.
11// ============================================================================
12
13#[derive(Debug, Clone, Default)]
14/// Represents `InboundEntry`.
15pub struct InboundEntry {
16 /// The inherits value.
17 pub inherits: Vec<usize>,
18 /// The uses type value.
19 pub uses_type: Vec<usize>,
20 /// The uses member value.
21 pub uses_member: Vec<usize>,
22}
23
24/// The value shape of both `heuristic_inbound` and `heuristic_outbound_by_file`
25/// (`inherits`/`uses-type`/`uses-member` -- note no `imports`: an imports edge
26/// names a namespace string, never a def, so no tier can guess one).
27/// Structurally identical to [`InboundEntry`], and deliberately the same type.
28pub type HeuristicEntry = InboundEntry;
29
30#[derive(Debug, Clone, Default)]
31/// Represents `OutboundEntry`.
32pub struct OutboundEntry {
33 /// The inherits value.
34 pub inherits: Vec<usize>,
35 /// The uses type value.
36 pub uses_type: Vec<usize>,
37 /// The uses member value.
38 pub uses_member: Vec<usize>,
39 /// The imports value.
40 pub imports: Vec<usize>,
41}
42
43/// The built query index over a `Graph`. `by_id`/`by_file`/`by_simple_name`/
44/// `by_lower_name` store `graph.defs` indices rather than cloned `Def`s, so a
45/// def is never copied into every bucket it is reachable from.
46pub struct GraphIndex<'g> {
47 /// The graph value.
48 pub graph: &'g graph::Graph,
49 /// The repository root every `file` in the graph is relative to. Held so a
50 /// query can read the one source line a hit sits on; nothing else in this
51 /// module touches the filesystem.
52 pub root: std::path::PathBuf,
53 /// Def id -> index into `graph.defs`.
54 pub by_id: HashMap<String, usize>,
55 /// File -> the def indices declared in it. The `Vec` preserves insertion
56 /// order (defs-array order for a def's own file, then `also_in`
57 /// cross-references in defs-array order) -- `impact_walk`'s next-frontier
58 /// expansion iterates this in order, one of the order-sensitive paths
59 /// documented in the module header.
60 pub by_file: HashMap<String, Vec<usize>>,
61 /// Exact name -> def indices, insertion order = `graph.defs` array order.
62 pub by_simple_name: HashMap<String, Vec<usize>>,
63 /// Lowercased name -> def indices.
64 pub by_lower_name: HashMap<String, Vec<usize>>,
65 /// Def id -> its inbound edges by kind (inherits/uses-type/uses-member).
66 pub inbound: HashMap<String, InboundEntry>,
67 /// File -> its outbound edges by kind (inherits/uses-type/uses-member/imports).
68 pub outbound_by_file: HashMap<String, OutboundEntry>,
69 /// Candidate def id -> the ambiguous edges naming it as a candidate.
70 pub ambiguous_by_candidate: HashMap<String, Vec<usize>>,
71 /// Source file -> the ambiguous edges originating in it.
72 pub ambiguous_by_file: HashMap<String, Vec<usize>>,
73 /// Def id -> its inbound HEURISTIC edges by kind. Heuristic edges live in
74 /// their own adjacency, never mixed into the precise one, so every consumer
75 /// (the impact walk's frontier, the refs tables, PageRank's edge set) reads
76 /// only precise edges unless it asks for guesses by name. Mixing them in
77 /// and filtering later is the shape that eventually leaks a guess into a
78 /// fact -- a filter forgotten in one call site is silent.
79 pub heuristic_inbound: HashMap<String, HeuristicEntry>,
80 /// File -> its outbound HEURISTIC edges (same three-kind shape).
81 pub heuristic_outbound_by_file: HashMap<String, HeuristicEntry>,
82 /// File -> the defs declared in it that carry non-empty `test_methods`. A
83 /// file is a TEST file here because a type declared in it carries an
84 /// attribute a runner discovers, never because of what it is called. Every
85 /// declaring site counts (`def.file` plus each `also_in`), and the per-def
86 /// file dedup keeps two partial blocks in ONE file from listing that def
87 /// twice. Holds `graph.defs` indices, same convention as the buckets above.
88 pub test_defs_by_file: HashMap<String, Vec<usize>>,
89 /// Files present in the graph but absent from the manifest.
90 pub flagged_files: HashSet<String>,
91 /// Whether a manifest was found and parsed.
92 pub manifest_present: bool,
93 /// Implementor def id -> every `ctor-di` edge the resolver confirmed
94 /// resolves TO it. Only an edge carrying a `to` is indexed here --
95 /// ambiguous/infra/unresolved edges never do, so an infra leaf like
96 /// `ILogger<T>` can never enter this map by construction.
97 pub ctor_di_by_to: HashMap<String, Vec<usize>>,
98 /// Injected-type BARE NAME -> the number of DISTINCT CONSTRUCTOR SITES
99 /// (`from_file` + `from_line`) that inject it. A `ctor-param` ref carries
100 /// its constructor's line, so every parameter of one constructor shares one
101 /// site; two classes in one file still count twice. This is the graph's
102 /// proxy for "distinct consuming classes". Every ctor-di edge counts,
103 /// whatever its resolution: how widely a contract is injected is a fact
104 /// about the contract, not about whether an implementor was confirmed.
105 pub ctor_di_fanin: HashMap<String, usize>,
106 /// The repo's `.csproj` project model, rebuilt from `graph.units` --
107 /// `None` when the graph carries none, which is every repo that declares
108 /// no project and every graph written before `units` existed. Owned
109 /// rather than borrowed: `ProjectModel` holds the derived directory map
110 /// and reference closure, neither of which is persisted.
111 pub project: Option<crate::project::ProjectModel>,
112 /// File -> the number of DISTINCT OTHER FILES that reference it through a
113 /// `direct` (inherits/uses-type/uses-member) or heuristic edge. The
114 /// file-level mirror of `ctor_di_fanin`, over the two edge kinds
115 /// responsible for the reach a fan-in brake cannot see. Distinct REFERRING
116 /// FILES, not distinct edges: one neighbour naming a hub fifty times is
117 /// still one neighbour. A file's references to ITSELF are excluded -- a
118 /// file is never its own dependant.
119 pub hub_indegree: HashMap<String, usize>,
120}
121
122impl<'g> GraphIndex<'g> {
123 /// Look up a def by id, e.g. for rendering an ambiguous candidate's
124 /// `{file, line, kind}`. Spelled as a method so callers don't reach through
125 /// `by_id` plus index arithmetic themselves.
126 pub fn def(&self, id: &str) -> Option<&graph::Def> {
127 self.by_id.get(id).map(|&i| &self.graph.defs[i])
128 }
129
130 /// Whether `file` counts as a TEST file for `tests`/`impact`: either an
131 /// attribute-vouched def is declared in it (`test_defs_by_file`), or the
132 /// project model places it inside a unit marked `test`. The second vouch
133 /// fires even for a file that declares no attributed test method at all --
134 /// a harness or fixture file living in a test project is still part of
135 /// what a symbol's tests touch. Fails open exactly like the rest of the
136 /// project model: with no model at all, or a file no discovered project
137 /// owns, this half answers `false` and the attribute vouch alone decides.
138 pub fn is_test_file(&self, file: &str) -> bool {
139 if self.test_defs_by_file.contains_key(file) {
140 return true;
141 }
142 let Some(project) = &self.project else {
143 return false;
144 };
145 project
146 .unit_of_file(file)
147 .is_some_and(|u| project.units[u].test)
148 }
149}
150
151fn note_file(flagged: &mut HashSet<String>, manifest_paths: Option<&HashSet<String>>, file: &str) {
152 if let Some(paths) = manifest_paths {
153 if !file.is_empty() && !paths.contains(file) {
154 flagged.insert(file.to_string());
155 }
156 }
157}
158
159/// What the caller wants left OUT of the index it is asking for.
160///
161/// The one knob is `include_guesses`, and it is spent HERE rather than at each
162/// render site on purpose: a filter applied while the adjacency is built cannot
163/// be forgotten by the next consumer of that adjacency, which is the same
164/// reasoning that keeps heuristic edges in their own buckets in the first place
165/// (see [`GraphIndex::heuristic_inbound`]).
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub struct IndexOptions {
168 /// Whether a scored-tier guess joins the heuristic adjacency at all.
169 /// `false` (`--no-guess`) admits ONLY [`graph::HeuristicTier::Ext`] edges
170 /// there: extension-method lookup is one of C#'s own rules, run over a
171 /// recorded bucket and unverifiable only against an out-of-graph receiver,
172 /// whereas the scored tier picked by name. An edge carrying no tier at all
173 /// -- a graph written before schema 2 -- is treated as the weaker of the
174 /// two and drops with the guesses.
175 pub include_guesses: bool,
176}
177
178impl Default for IndexOptions {
179 /// Guesses are IN by default: every caller that does not ask asked for the
180 /// whole index, and a default that quietly narrowed the answer would change
181 /// what `refs` means without anyone typing a flag.
182 fn default() -> Self {
183 IndexOptions {
184 include_guesses: true,
185 }
186 }
187}
188
189/// Build the query index, phase two of the two-phase build (see module
190/// header). `graph` is what the caller got back from `graph::read_graph(root)`;
191/// `root` is used here only for the manifest join.
192pub fn load_graph_index<'g>(graph: &'g graph::Graph, root: &Path) -> GraphIndex<'g> {
193 load_graph_index_with(graph, root, IndexOptions::default())
194}
195
196/// [`load_graph_index`] with the caller's own [`IndexOptions`]. Everything is
197/// built the same way either way; `opts` only decides which heuristic edges earn
198/// a place in the heuristic adjacency.
199#[allow(
200 clippy::too_many_lines,
201 reason = "one ordered pass building every index table over the same manifest and graph, so the tables stay mutually consistent"
202)]
203pub fn load_graph_index_with<'g>(
204 graph: &'g graph::Graph,
205 root: &Path,
206 opts: IndexOptions,
207) -> GraphIndex<'g> {
208 let manifest_value = match manifest::read_manifest(root) {
209 Ok(v) => v,
210 Err(_) => None, // corrupt manifest.json: fail open, see module header
211 };
212 let manifest_present = manifest_value.is_some();
213 let manifest_paths: Option<HashSet<String>> = manifest_value.as_ref().and_then(|m| {
214 m.get("entries")
215 .and_then(|e| e.as_object())
216 .map(|entries| entries.iter().map(|(k, _)| k.clone()).collect())
217 });
218 let manifest_paths_ref = manifest_paths.as_ref();
219
220 let mut flagged_files: HashSet<String> = HashSet::new();
221 let mut by_id: HashMap<String, usize> = HashMap::with_capacity(graph.defs.len());
222 let mut by_file: HashMap<String, Vec<usize>> = HashMap::new();
223 let mut by_simple_name: HashMap<String, Vec<usize>> = HashMap::new();
224 let mut by_lower_name: HashMap<String, Vec<usize>> = HashMap::new();
225 let mut test_defs_by_file: HashMap<String, Vec<usize>> = HashMap::new();
226
227 for (i, d) in graph.defs.iter().enumerate() {
228 by_id.insert(d.id.clone(), i);
229 note_file(&mut flagged_files, manifest_paths_ref, &d.file);
230 push_ordered_unique(&mut by_file, &d.file, i);
231 for also in &d.also_in {
232 note_file(&mut flagged_files, manifest_paths_ref, &also.file);
233 push_ordered_unique(&mut by_file, &also.file, i);
234 }
235 if !d.test_methods.is_empty() {
236 let mut files: Vec<&str> = vec![d.file.as_str()];
237 for also in &d.also_in {
238 if !files.contains(&also.file.as_str()) {
239 files.push(also.file.as_str());
240 }
241 }
242 for file in files {
243 test_defs_by_file
244 .entry(file.to_string())
245 .or_default()
246 .push(i);
247 }
248 }
249 by_simple_name.entry(d.name.clone()).or_default().push(i);
250 by_lower_name
251 .entry(d.name.to_lowercase())
252 .or_default()
253 .push(i);
254 }
255
256 let mut inbound: HashMap<String, InboundEntry> = HashMap::new();
257 let mut outbound_by_file: HashMap<String, OutboundEntry> = HashMap::new();
258 let mut ambiguous_by_candidate: HashMap<String, Vec<usize>> = HashMap::new();
259 let mut ambiguous_by_file: HashMap<String, Vec<usize>> = HashMap::new();
260 let mut heuristic_inbound: HashMap<String, HeuristicEntry> = HashMap::new();
261 let mut heuristic_outbound_by_file: HashMap<String, HeuristicEntry> = HashMap::new();
262 let mut ctor_di_by_to: HashMap<String, Vec<usize>> = HashMap::new();
263 let mut ctor_di_sites_by_iface: HashMap<String, HashSet<(String, usize)>> = HashMap::new();
264 // File -> the distinct other files that reference it (see `hub_indegree`).
265 let mut hub_referrers_by_file: HashMap<String, HashSet<String>> = HashMap::new();
266
267 // Whether a heuristic edge earns a place in the heuristic adjacency at all.
268 // Under `--no-guess` only the extension tier does; every other guess is
269 // dropped from the index, so no consumer downstream has to remember to ask.
270 // It is asked ONLY of an edge already known to be heuristic -- a precise
271 // edge carries no tier and this would wrongly refuse it.
272 let admits =
273 |e: &graph::Edge| opts.include_guesses || e.tier() == Some(graph::HeuristicTier::Ext);
274
275 for (i, e) in graph.edges.iter().enumerate() {
276 match e {
277 graph::Edge::Imports { from_file, .. } => {
278 note_file(&mut flagged_files, manifest_paths_ref, from_file);
279 outbound_by_file
280 .entry(from_file.clone())
281 .or_default()
282 .imports
283 .push(i);
284 }
285 graph::Edge::Ambiguous {
286 from_file,
287 candidates,
288 ..
289 } => {
290 note_file(&mut flagged_files, manifest_paths_ref, from_file);
291 ambiguous_by_file
292 .entry(from_file.clone())
293 .or_default()
294 .push(i);
295 for c in candidates {
296 ambiguous_by_candidate
297 .entry(c.id.clone())
298 .or_default()
299 .push(i);
300 }
301 }
302 graph::Edge::Inherits {
303 from_file,
304 to,
305 to_file,
306 heuristic,
307 ..
308 } => {
309 note_file(&mut flagged_files, manifest_paths_ref, from_file);
310 note_file(&mut flagged_files, manifest_paths_ref, to_file);
311 // Counted BEFORE the heuristic split below: the hub brake spans
312 // both kinds on purpose, because a hub file is reached through
313 // whichever of them the extractor happened to resolve.
314 if !to_file.is_empty() && !from_file.is_empty() && to_file != from_file {
315 hub_referrers_by_file
316 .entry(to_file.clone())
317 .or_default()
318 .insert(from_file.clone());
319 }
320 if *heuristic {
321 // A guess `--no-guess` refuses leaves the index with no
322 // bucket at all: it was already counted for the hub brake
323 // above, and it is not a precise edge, so there is nowhere
324 // else for it to go.
325 if admits(e) {
326 heuristic_outbound_by_file
327 .entry(from_file.clone())
328 .or_default()
329 .inherits
330 .push(i);
331 heuristic_inbound
332 .entry(to.clone())
333 .or_default()
334 .inherits
335 .push(i);
336 }
337 } else {
338 outbound_by_file
339 .entry(from_file.clone())
340 .or_default()
341 .inherits
342 .push(i);
343 inbound.entry(to.clone()).or_default().inherits.push(i);
344 }
345 }
346 graph::Edge::UsesType {
347 from_file,
348 to,
349 to_file,
350 heuristic,
351 ..
352 } => {
353 note_file(&mut flagged_files, manifest_paths_ref, from_file);
354 note_file(&mut flagged_files, manifest_paths_ref, to_file);
355 // Counted BEFORE the heuristic split below: the hub brake spans
356 // both kinds on purpose, because a hub file is reached through
357 // whichever of them the extractor happened to resolve.
358 if !to_file.is_empty() && !from_file.is_empty() && to_file != from_file {
359 hub_referrers_by_file
360 .entry(to_file.clone())
361 .or_default()
362 .insert(from_file.clone());
363 }
364 if *heuristic {
365 // Same rule as the `inherits` arm above.
366 if admits(e) {
367 heuristic_outbound_by_file
368 .entry(from_file.clone())
369 .or_default()
370 .uses_type
371 .push(i);
372 heuristic_inbound
373 .entry(to.clone())
374 .or_default()
375 .uses_type
376 .push(i);
377 }
378 } else {
379 outbound_by_file
380 .entry(from_file.clone())
381 .or_default()
382 .uses_type
383 .push(i);
384 inbound.entry(to.clone()).or_default().uses_type.push(i);
385 }
386 }
387 graph::Edge::UsesMember {
388 from_file,
389 to,
390 to_file,
391 heuristic,
392 ..
393 } => {
394 note_file(&mut flagged_files, manifest_paths_ref, from_file);
395 note_file(&mut flagged_files, manifest_paths_ref, to_file);
396 // Counted BEFORE the heuristic split below: the hub brake spans
397 // both kinds on purpose, because a hub file is reached through
398 // whichever of them the extractor happened to resolve.
399 if !to_file.is_empty() && !from_file.is_empty() && to_file != from_file {
400 hub_referrers_by_file
401 .entry(to_file.clone())
402 .or_default()
403 .insert(from_file.clone());
404 }
405 if *heuristic {
406 // The one kind that actually carries a tier, and so the one
407 // kind `--no-guess` can keep anything of: an extension-tier
408 // edge survives here, a scored one does not.
409 if admits(e) {
410 heuristic_outbound_by_file
411 .entry(from_file.clone())
412 .or_default()
413 .uses_member
414 .push(i);
415 heuristic_inbound
416 .entry(to.clone())
417 .or_default()
418 .uses_member
419 .push(i);
420 }
421 } else {
422 outbound_by_file
423 .entry(from_file.clone())
424 .or_default()
425 .uses_member
426 .push(i);
427 inbound.entry(to.clone()).or_default().uses_member.push(i);
428 }
429 }
430 // 'ctor-di' is deliberately NOT one of the kinds `refs`/`impact`
431 // render -- that kind list is fixed to
432 // inherits/uses-type/uses-member -- so it earns no `inbound`/
433 // `outbound_by_file` entry. It DOES earn its own reverse index,
434 // keyed by the resolved implementor, when it carries one.
435 graph::Edge::CtorDi {
436 from_file,
437 from_line,
438 iface,
439 to,
440 ..
441 } => {
442 // Fan-in counts EVERY ctor-di edge, resolved or not; only the
443 // reverse index below is restricted to the ones carrying a
444 // confirmed implementor.
445 ctor_di_sites_by_iface
446 .entry(iface.clone())
447 .or_default()
448 .insert((from_file.clone(), *from_line));
449 if let Some(to) = to {
450 ctor_di_by_to.entry(to.clone()).or_default().push(i);
451 }
452 }
453 // The four TS/TSX edge kinds earn no entry in this C#-shaped index,
454 // the same stance the `ctor-di` arm above states for its own kind:
455 // `refs`/`impact` render a fixed kind list. Listed rather than
456 // caught by a wildcard so a future edge kind still fails the
457 // exhaustiveness check here.
458 graph::Edge::Import { .. }
459 | graph::Edge::Call { .. }
460 | graph::Edge::JsxUse { .. }
461 | graph::Edge::Dispatch { .. } => {}
462 }
463 }
464
465 GraphIndex {
466 graph,
467 root: root.to_path_buf(),
468 by_id,
469 by_file,
470 by_simple_name,
471 by_lower_name,
472 inbound,
473 outbound_by_file,
474 ambiguous_by_candidate,
475 ambiguous_by_file,
476 heuristic_inbound,
477 heuristic_outbound_by_file,
478 test_defs_by_file,
479 flagged_files,
480 manifest_present,
481 ctor_di_by_to,
482 project: if graph.units.is_empty() {
483 None
484 } else {
485 Some(crate::project::ProjectModel::from_units(
486 crate::project::units_from_graph(&graph.units),
487 ))
488 },
489 ctor_di_fanin: ctor_di_sites_by_iface
490 .into_iter()
491 .map(|(name, sites)| (name, sites.len()))
492 .collect(),
493 hub_indegree: hub_referrers_by_file
494 .into_iter()
495 .map(|(file, refs)| (file, refs.len()))
496 .collect(),
497 }
498}
499
500// The interface def id(s) a class def's OWN file(s) declare an `inherits` edge
501// to, restricted to a def of kind `"interface"` -- a plain base class is not
502// part of the interface hop. Reuses `outbound_by_file`'s file-level union
503// rather than attributing an edge to one specific def in a multi-type file (an
504// accepted imprecision). Insertion order is `def_files(def_id)` order, then
505// that file's own inherits-edge array order -- the first-seen order of the
506// `via` labels a widened hit carries.
507pub(super) fn implemented_interfaces(index: &GraphIndex, def_id: &str) -> Vec<String> {
508 let mut seen: SeqSet<String> = SeqSet::new();
509 for file in def_files(index, def_id) {
510 let Some(o) = index.outbound_by_file.get(&file) else {
511 continue;
512 };
513 for &ei in &o.inherits {
514 let graph::Edge::Inherits { to, .. } = &index.graph.edges[ei] else {
515 continue;
516 };
517 if seen.contains(to) {
518 continue;
519 }
520 if index.def(to).map(|d| d.kind.as_str()) != Some("interface") {
521 continue;
522 }
523 seen.insert(to.clone());
524 }
525 }
526 seen.into_vec()
527}
528// ============================================================================
529// def_files / def_sites.
530// ============================================================================
531
532#[derive(Debug, Clone, PartialEq)]
533/// Represents `DefSite`.
534pub struct DefSite {
535 /// The file value.
536 pub file: String,
537 /// The line value.
538 pub line: usize,
539}
540
541/// The distinct files a def is declared in, deduped: a partial type's
542/// `also_in` site can share a file with its primary declaration.
543pub fn def_files(index: &GraphIndex, def_id: &str) -> Vec<String> {
544 let mut out = Vec::new();
545 let mut seen = HashSet::new();
546 if let Some(&i) = index.by_id.get(def_id) {
547 let d = &index.graph.defs[i];
548 if seen.insert(d.file.clone()) {
549 out.push(d.file.clone());
550 }
551 for also in &d.also_in {
552 if seen.insert(also.file.clone()) {
553 out.push(also.file.clone());
554 }
555 }
556 }
557 out
558}
559
560/// Every declaring site of a def, NOT deduped (unlike [`def_files`]) -- each
561/// site is its own row.
562pub fn def_sites(index: &GraphIndex, def_id: &str) -> Vec<DefSite> {
563 let mut out = Vec::new();
564 if let Some(&i) = index.by_id.get(def_id) {
565 let d = &index.graph.defs[i];
566 out.push(DefSite {
567 file: d.file.clone(),
568 line: d.line,
569 });
570 for also in &d.also_in {
571 out.push(DefSite {
572 file: also.file.clone(),
573 line: also.line,
574 });
575 }
576 }
577 out
578}
579
580// ============================================================================
581// symbol_refs.
582// ============================================================================
583
584/// The immediate inbound/outbound edges of one resolved symbol, holding
585/// `graph.edges` indices rather than cloned edges (the same convention
586/// [`GraphIndex`] uses).
587#[derive(Debug, Clone, Default)]
588pub struct SymbolRefs {
589 /// The inbound inherits value.
590 pub inbound_inherits: Vec<usize>,
591 /// The inbound uses type value.
592 pub inbound_uses_type: Vec<usize>,
593 /// The inbound uses member value.
594 pub inbound_uses_member: Vec<usize>,
595 /// The outbound inherits value.
596 pub outbound_inherits: Vec<usize>,
597 /// The outbound uses type value.
598 pub outbound_uses_type: Vec<usize>,
599 /// The outbound uses member value.
600 pub outbound_uses_member: Vec<usize>,
601 /// The outbound imports value.
602 pub outbound_imports: Vec<usize>,
603 /// The same two tables again over the HEURISTIC adjacency, built by the
604 /// identical rules (enum members union into the enum's inbound, partial
605 /// classes union outbound over every declaring file) so a heuristic row is
606 /// never present or absent for a reason a precise row would not have been.
607 pub heuristic_inbound_inherits: Vec<usize>,
608 /// The heuristic inbound uses type value.
609 pub heuristic_inbound_uses_type: Vec<usize>,
610 /// The heuristic inbound uses member value.
611 pub heuristic_inbound_uses_member: Vec<usize>,
612 /// The heuristic outbound inherits value.
613 pub heuristic_outbound_inherits: Vec<usize>,
614 /// The heuristic outbound uses type value.
615 pub heuristic_outbound_uses_type: Vec<usize>,
616 /// The heuristic outbound uses member value.
617 pub heuristic_outbound_uses_member: Vec<usize>,
618 /// The ambiguous inbound value.
619 pub ambiguous_inbound: Vec<usize>,
620 /// The ambiguous outbound value.
621 pub ambiguous_outbound: Vec<usize>,
622}
623
624/// Immediate (1-hop) inbound/outbound for one resolved symbol. Partial-class
625/// defs union outbound over every declaring file. An enum query unions every
626/// member's inbound `uses-member` edges in, iterating `index.graph.defs`
627/// directly (its array order) rather than the unordered `by_id` map.
628pub fn symbol_refs(index: &GraphIndex, def_id: &str) -> SymbolRefs {
629 let empty = InboundEntry::default();
630 let base = index.inbound.get(def_id).unwrap_or(&empty);
631 let inbound_inherits = base.inherits.clone();
632 let inbound_uses_type = base.uses_type.clone();
633 let mut inbound_uses_member = base.uses_member.clone();
634
635 if let Some(&i) = index.by_id.get(def_id) {
636 if index.graph.defs[i].kind == "enum" {
637 let prefix = format!("{def_id}.");
638 for d in &index.graph.defs {
639 if d.kind != "enum-member" || !d.id.starts_with(&prefix) {
640 continue;
641 }
642 if let Some(m) = index.inbound.get(&d.id) {
643 inbound_uses_member.extend(m.uses_member.iter().copied());
644 }
645 }
646 }
647 }
648
649 let mut outbound_inherits = Vec::new();
650 let mut outbound_uses_type = Vec::new();
651 let mut outbound_uses_member = Vec::new();
652 let mut outbound_imports = Vec::new();
653 for file in def_files(index, def_id) {
654 if let Some(o) = index.outbound_by_file.get(&file) {
655 outbound_inherits.extend(o.inherits.iter().copied());
656 outbound_uses_type.extend(o.uses_type.iter().copied());
657 outbound_uses_member.extend(o.uses_member.iter().copied());
658 outbound_imports.extend(o.imports.iter().copied());
659 }
660 }
661
662 // The heuristic halves, same rules, separate adjacency.
663 let heuristic_base = index.heuristic_inbound.get(def_id).unwrap_or(&empty);
664 let heuristic_inbound_inherits = heuristic_base.inherits.clone();
665 let heuristic_inbound_uses_type = heuristic_base.uses_type.clone();
666 let mut heuristic_inbound_uses_member = heuristic_base.uses_member.clone();
667
668 if let Some(&i) = index.by_id.get(def_id) {
669 if index.graph.defs[i].kind == "enum" {
670 let prefix = format!("{def_id}.");
671 for d in &index.graph.defs {
672 if d.kind != "enum-member" || !d.id.starts_with(&prefix) {
673 continue;
674 }
675 if let Some(m) = index.heuristic_inbound.get(&d.id) {
676 heuristic_inbound_uses_member.extend(m.uses_member.iter().copied());
677 }
678 }
679 }
680 }
681
682 let mut heuristic_outbound_inherits = Vec::new();
683 let mut heuristic_outbound_uses_type = Vec::new();
684 let mut heuristic_outbound_uses_member = Vec::new();
685 for file in def_files(index, def_id) {
686 if let Some(o) = index.heuristic_outbound_by_file.get(&file) {
687 heuristic_outbound_inherits.extend(o.inherits.iter().copied());
688 heuristic_outbound_uses_type.extend(o.uses_type.iter().copied());
689 heuristic_outbound_uses_member.extend(o.uses_member.iter().copied());
690 }
691 }
692
693 let ambiguous_inbound = index
694 .ambiguous_by_candidate
695 .get(def_id)
696 .cloned()
697 .unwrap_or_default();
698 let ambiguous_outbound: Vec<usize> = def_files(index, def_id)
699 .iter()
700 .flat_map(|f| index.ambiguous_by_file.get(f).cloned().unwrap_or_default())
701 .collect();
702
703 SymbolRefs {
704 inbound_inherits,
705 inbound_uses_type,
706 inbound_uses_member,
707 outbound_inherits,
708 outbound_uses_type,
709 outbound_uses_member,
710 outbound_imports,
711 heuristic_inbound_inherits,
712 heuristic_inbound_uses_type,
713 heuristic_inbound_uses_member,
714 heuristic_outbound_inherits,
715 heuristic_outbound_uses_type,
716 heuristic_outbound_uses_member,
717 ambiguous_inbound,
718 ambiguous_outbound,
719 }
720}