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