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