devscout_rs/query/refs_tables.rs
1use crate::graph;
2
3// The three reference kinds and their fixed order. Not iterated generically
4// here -- each kind gets its own struct field (`inherits`/`uses_type`/
5// `uses_member`) throughout this module rather than a keyed collection. This
6// constant exists only as an anchor for the kind list and its order.
7#[allow(dead_code)]
8const REF_KINDS: [&str; 3] = ["inherits", "uses-type", "uses-member"];
9
10/// Default per-table row cap for the outbound and ambiguous tables. The three
11/// inbound tables share [`INBOUND_CAP`] instead.
12pub const DEFAULT_CAP: usize = 50;
13/// Default cap shared across all three inbound kinds -- one cap total, not one
14/// per table.
15pub const INBOUND_CAP: usize = 30;
16/// Default cap for the `--out` view: one cap shared across all four outbound
17/// kinds (inherits/uses-type/uses-member/imports), the mirror of
18/// [`INBOUND_CAP`]. `--all` lifts this cap.
19pub const OUTBOUND_CAP: usize = 30;
20/// Maximum length of a displayed source line, in UTF-16 code units.
21/// Truncation counts code units (see `clip_source`) so the cut is stable
22/// regardless of any astral characters on the line.
23pub const SOURCE_MAX: usize = 120;
24// ============================================================================
25// build_refs_model.
26// ============================================================================
27
28pub(super) fn cap_rows<T>(mut rows: Vec<T>, cap: usize) -> (Vec<T>, usize) {
29 if rows.len() <= cap {
30 (rows, 0)
31 } else {
32 let dropped = rows.len() - cap;
33 rows.truncate(cap);
34 (rows, dropped)
35 }
36}
37
38// The (from_file, from_line) an edge originates at, the key location sorts use.
39// See the module header re: `str::cmp` ordering.
40pub(super) fn edge_loc(e: &graph::Edge) -> (&str, usize) {
41 match e {
42 graph::Edge::Inherits { from_file, from_line, .. }
43 | graph::Edge::UsesType { from_file, from_line, .. }
44 | graph::Edge::UsesMember { from_file, from_line, .. }
45 | graph::Edge::Implements { from_file, from_line, .. }
46 | graph::Edge::Overrides { from_file, from_line, .. }
47 | graph::Edge::Imports { from_file, from_line, .. }
48 | graph::Edge::Ambiguous { from_file, from_line, .. }
49 // Never actually reached: 'ctor-di' edges are never pushed into any
50 // structure this helper sorts (see the query-index builder's own
51 // CtorDi arm). Included only for exhaustiveness, as are the four
52 // TS/TSX kinds below, for the same reason.
53 | graph::Edge::CtorDi { from_file, from_line, .. }
54 | graph::Edge::Import { from_file, from_line, .. }
55 | graph::Edge::Call { from_file, from_line, .. }
56 | graph::Edge::JsxUse { from_file, from_line, .. }
57 | graph::Edge::Dispatch { from_file, from_line, .. } => (from_file.as_str(), *from_line),
58 }
59}
60
61pub(super) fn loc_cmp(a: &graph::Edge, b: &graph::Edge) -> std::cmp::Ordering {
62 let (af, al) = edge_loc(a);
63 let (bf, bl) = edge_loc(b);
64 if af == bf {
65 al.cmp(&bl)
66 } else {
67 af.cmp(bf)
68 }
69}
70
71/// Which tier a ROW reports, folded from the tiers of the edges behind it.
72///
73/// One extension edge is enough to call the whole row an extension: that tier
74/// ran C#'s own lookup rule and only failed to check the receiver, so it is the
75/// stronger of the two and a name guess sitting beside it does not weaken it.
76/// Everything else heuristic is a guess, an edge carrying NO tier included -- a
77/// graph written before schema 2 cannot prove it was anything better, and
78/// reporting an unproven edge as the stronger tier is the one direction this
79/// surface must never round.
80pub(super) fn row_tier(heuristic: bool, ext_seen: bool) -> Option<graph::HeuristicTier> {
81 if !heuristic {
82 return None;
83 }
84 Some(if ext_seen {
85 graph::HeuristicTier::Ext
86 } else {
87 graph::HeuristicTier::Guess
88 })
89}
90
91/// One inbound-table row: `file` and `line` of the referencing site, then
92/// `heuristic` (whether the edge was guessed), `tier` (which guess tier said
93/// so) and `source` (the trimmed referencing line). An empty `source` is
94/// omitted from `--json`. `occurrence_index` distinguishes two rows that would
95/// otherwise be equal on every other field (see the `occurrence` module);
96/// it is `None` on every row outside such a collision.
97#[derive(Debug, Clone, PartialEq)]
98pub struct InboundRow {
99 /// The file value.
100 pub file: String,
101 /// The line value.
102 pub line: usize,
103 /// The heuristic value.
104 pub heuristic: bool,
105 /// Which heuristic tier stands behind this row; `None` exactly when
106 /// `heuristic` is false (see [`row_tier`]).
107 pub tier: Option<graph::HeuristicTier>,
108 /// The source value.
109 pub source: String,
110 /// A 0-based position among rows of this same table that are otherwise
111 /// identical, assigned in the table's existing stable emission order;
112 /// `None` for a row with no such collision. Scoped to one table of one
113 /// answer against one graph snapshot -- never a cross-query or
114 /// cross-snapshot identity. See the `occurrence` module.
115 pub occurrence_index: Option<usize>,
116}
117
118/// One outbound-table row: `file`/`line` of the referencing site, `to_file`/`to`
119/// of the target, then `heuristic` and `source` (same omit-when-empty rule as
120/// [`InboundRow`]). `source` is read at the site actually making the reference
121/// -- for an outbound edge that is a line in the def's own file, not the
122/// target's.
123#[derive(Debug, Clone, PartialEq)]
124pub struct OutboundRow {
125 /// The file value.
126 pub file: String,
127 /// The line value.
128 pub line: usize,
129 /// The to file value.
130 pub to_file: String,
131 /// The to value.
132 pub to: String,
133 /// The heuristic value.
134 pub heuristic: bool,
135 /// Which heuristic tier stands behind this row, same rule as
136 /// [`InboundRow::tier`].
137 pub tier: Option<graph::HeuristicTier>,
138 /// The source value.
139 pub source: String,
140 /// Same rule as [`InboundRow::occurrence_index`], scoped to this table.
141 pub occurrence_index: Option<usize>,
142}
143
144/// One imports-table row: `file`/`line`, the imported `target` namespace, and
145/// `source`. An imports edge is never a guess, so unlike [`OutboundRow`] this
146/// carries no `heuristic` field.
147#[derive(Debug, Clone, PartialEq)]
148pub struct ImportRow {
149 /// The file value.
150 pub file: String,
151 /// The line value.
152 pub line: usize,
153 /// The target value.
154 pub target: String,
155 /// The source value.
156 pub source: String,
157}
158
159/// One ambiguous-table row: the referencing `file`/`line`, the `origin` and
160/// `raw` text of the reference, and how many candidates it matched.
161#[derive(Debug, Clone, PartialEq)]
162pub struct AmbiguousRow {
163 /// The file value.
164 pub file: String,
165 /// The line value.
166 pub line: usize,
167 /// The origin value.
168 pub origin: String,
169 /// The raw value.
170 pub raw: String,
171 /// The candidate count value.
172 pub candidate_count: usize,
173}
174
175/// A capped table: `total` rows before capping, how many were `dropped`, and
176/// the surviving `rows`.
177#[derive(Debug, Clone, PartialEq)]
178pub struct Table<R> {
179 /// The total value.
180 pub total: usize,
181 /// The dropped value.
182 pub dropped: usize,
183 /// The rows value.
184 pub rows: Vec<R>,
185}
186
187pub(super) fn build_table<R>(
188 mut idxs: Vec<usize>,
189 edges: &[graph::Edge],
190 cap: usize,
191 map_row: fn(&graph::Edge) -> R,
192) -> Table<R> {
193 idxs.sort_by(|&a, &b| loc_cmp(&edges[a], &edges[b]));
194 let total = idxs.len();
195 let (shown, dropped) = cap_rows(idxs, cap);
196 let rows = shown.into_iter().map(|i| map_row(&edges[i])).collect();
197 Table {
198 total,
199 dropped,
200 rows,
201 }
202}
203
204// The per-table, per-kind outbound builder was removed here: the outbound
205// tables now go through `build_outbound_tables`'s shared cap and rank instead
206// (below `RankedOutbound`), the same way the inbound tables stopped going
207// through a per-kind builder. `build_table` above still backs the two
208// ambiguous tables.
209
210pub(super) fn ambiguous_row(e: &graph::Edge) -> AmbiguousRow {
211 match e {
212 graph::Edge::Ambiguous {
213 origin,
214 from_file,
215 from_line,
216 raw,
217 candidate_count,
218 ..
219 } => AmbiguousRow {
220 file: from_file.clone(),
221 line: *from_line,
222 origin: origin.clone(),
223 raw: raw.clone(),
224 candidate_count: *candidate_count,
225 },
226 _ => unreachable!("ambiguous table only ever holds ambiguous edge indices"),
227 }
228}
229
230/// The five inbound tables, one per kind.
231///
232/// `implements`/`overrides` are never heuristic, and a renderer shows
233/// either only when its own total is non-zero -- unlike the first three,
234/// which always show their header -- so a symbol untouched by dispatch
235/// edges renders byte-identical to before this pair existed.
236#[derive(Debug, Clone, PartialEq)]
237pub struct InboundTables {
238 /// The inherits value.
239 pub inherits: Table<InboundRow>,
240 /// The uses type value.
241 pub uses_type: Table<InboundRow>,
242 /// The uses member value.
243 pub uses_member: Table<InboundRow>,
244 /// The implements value.
245 pub implements: Table<InboundRow>,
246 /// The overrides value.
247 pub overrides: Table<InboundRow>,
248}
249
250/// The six outbound tables: three by kind, `implements`/`overrides`, plus
251/// imports.
252#[derive(Debug, Clone, PartialEq)]
253pub struct OutboundTables {
254 /// The inherits value.
255 pub inherits: Table<OutboundRow>,
256 /// The uses type value.
257 pub uses_type: Table<OutboundRow>,
258 /// The uses member value.
259 pub uses_member: Table<OutboundRow>,
260 /// The implements value.
261 pub implements: Table<OutboundRow>,
262 /// The overrides value.
263 pub overrides: Table<OutboundRow>,
264 /// The imports value.
265 pub imports: Table<ImportRow>,
266}
267
268/// The inbound and outbound ambiguous-reference tables.
269#[derive(Debug, Clone, PartialEq)]
270pub struct AmbiguousTables {
271 /// The inbound value.
272 pub inbound: Table<AmbiguousRow>,
273 /// The outbound value.
274 pub outbound: Table<AmbiguousRow>,
275}