Skip to main content

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}