devscout_rs/query/why.rs
1// The closed `why` vocabulary every hit row on `refs`/`read`/`impact`/`tests`
2// carries in `--json`: which rule or tier produced the row, spelled once
3// here -- the same discipline `outcome.rs` applies to the `outcome` field, so
4// no caller can typo a word this vocabulary does not name.
5//
6// Every value is derived from what the graph already persists on the edge
7// that produced the row: its kind, and for a `uses-member` edge its tier.
8// Nothing here reads a new fact the resolver did not already record, and
9// nothing is stamped on an ambiguous or candidate-count row -- those already
10// carry their own `origin`/`candidateCount` fields describing why they are
11// UNRESOLVED, a different question from which rule produced a settled hit.
12//
13// A TS/TSX-only edge kind (`import`/`call`/`jsx-use`/`dispatch`) names no
14// word here: `query::index` never admits those kinds into the
15// inbound/outbound adjacency any of the four verbs read (see its own
16// `Edge::Import | Edge::Call | Edge::JsxUse | Edge::Dispatch => {}` arm), so a
17// row built from one never exists for `why` to describe.
18
19use crate::graph;
20
21/// One of the words a hit row's `why` field names.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Why {
24 /// A base-list (`inherits`) edge.
25 Inherits,
26 /// A `uses-type` edge.
27 UsesType,
28 /// A precise (non-heuristic) `uses-member` edge.
29 UsesMemberPrecise,
30 /// A `uses-member` edge the extension-method tier guessed.
31 UsesMemberExt,
32 /// A `uses-member` edge the scored-guess tier guessed (or one that
33 /// carries no tier at all -- see `row_tier`'s own rule for that case).
34 UsesMemberGuess,
35 /// A constructor-injection (`ctor-di`) edge.
36 CtorDi,
37 /// A C# `using` directive (an `imports` edge).
38 Imports,
39 /// The def's own declaration span -- `read` only, never an edge at all.
40 Declaration,
41 /// A `tests` row earned because a def declared in the file carries a
42 /// test-runner attribute.
43 TestAttribute,
44 /// A `tests` row earned because the project model places the file's
45 /// unit in a project marked `test`, with no attributed def of its own.
46 TestProject,
47 /// An `implements` edge: a service registration's implementation type, or
48 /// a member satisfying an interface member. Appended after the words that
49 /// pre-date the edge kind, so no existing word moves.
50 Implements,
51 /// An `overrides` edge: a member overriding the nearest in-graph base
52 /// member of the same name and arity.
53 Overrides,
54 /// An `impact` row reached only through an imported cross-repo edge --
55 /// never folded against the five kinds above, since a foreign file is
56 /// never also reached by one of this graph's own edges. The weakest
57 /// evidence this vocabulary names, below `uses-member-guess`: a foreign,
58 /// producer-asserted fact never outranks anything this crate resolved
59 /// for itself.
60 ImportedEdge,
61}
62
63impl Why {
64 /// Every value, in the order the doc comment above lists them.
65 pub const ALL: [Why; 13] = [
66 Why::Inherits,
67 Why::UsesType,
68 Why::UsesMemberPrecise,
69 Why::UsesMemberExt,
70 Why::UsesMemberGuess,
71 Why::CtorDi,
72 Why::Imports,
73 Why::Declaration,
74 Why::TestAttribute,
75 Why::TestProject,
76 Why::Implements,
77 Why::Overrides,
78 Why::ImportedEdge,
79 ];
80
81 /// The exact `--json` word for this value.
82 pub fn as_str(self) -> &'static str {
83 match self {
84 Why::Inherits => "inherits",
85 Why::UsesType => "uses-type",
86 Why::UsesMemberPrecise => "uses-member-precise",
87 Why::UsesMemberExt => "uses-member-ext",
88 Why::UsesMemberGuess => "uses-member-guess",
89 Why::CtorDi => "ctor-di",
90 Why::Imports => "imports",
91 Why::Declaration => "declaration",
92 Why::TestAttribute => "test-attribute",
93 Why::TestProject => "test-project",
94 Why::Implements => "implements",
95 Why::Overrides => "overrides",
96 Why::ImportedEdge => "imported-edge",
97 }
98 }
99}
100
101/// The `why` a `uses-member` row or edge reports, from its own tier: `None`
102/// (precise), `Some(Ext)`, or `Some(Guess)` -- the exact three states
103/// `row_tier` already folds a row's edges down to, so a caller holding either
104/// a row's `tier` field or an edge's own `.tier()` reads the same word from
105/// it. `heuristic` decides the precise/guessed split; an edge or row that
106/// says it was guessed but names no tier (a graph written before schema 2,
107/// or a hand-built row exercising that case) still reports the WEAKER word,
108/// never the precise one -- the same direction `row_tier` itself never
109/// rounds.
110pub(crate) fn why_for_uses_member(heuristic: bool, tier: Option<graph::HeuristicTier>) -> Why {
111 if !heuristic {
112 return Why::UsesMemberPrecise;
113 }
114 match tier {
115 Some(graph::HeuristicTier::Ext) => Why::UsesMemberExt,
116 _ => Why::UsesMemberGuess,
117 }
118}
119
120/// The `why` value for the edge that produced a row, derived from its kind
121/// and -- for `uses-member` -- its own `heuristic`/`tier`. Only ever called
122/// with an `inherits`/`uses-type`/`uses-member`/`imports`/`ambiguous` edge,
123/// the only kinds that ever populate a `refs`/`read`/`impact` row; every
124/// other kind is unreachable here by construction (see the module header).
125pub(crate) fn why_for_edge(edge: &graph::Edge) -> Why {
126 match edge {
127 graph::Edge::Inherits { .. } => Why::Inherits,
128 graph::Edge::UsesType { .. } => Why::UsesType,
129 graph::Edge::UsesMember { heuristic, tier, .. } => why_for_uses_member(*heuristic, *tier),
130 graph::Edge::Imports { .. } => Why::Imports,
131 graph::Edge::Implements { .. } => Why::Implements,
132 graph::Edge::Overrides { .. } => Why::Overrides,
133 // An ambiguous edge's `origin` is the same ref-kind string the ladder
134 // steps record for a resolved edge ("inherits"/"uses-type"/
135 // "uses-member"); an ambiguous edge is never heuristic (ambiguity and
136 // the guess tiers are mutually exclusive resolution outcomes), so the
137 // `uses-member` fallback here is always the precise word.
138 graph::Edge::Ambiguous { origin, .. } => match origin.as_str() {
139 "inherits" => Why::Inherits,
140 "uses-type" => Why::UsesType,
141 _ => Why::UsesMemberPrecise,
142 },
143 _ => unreachable!(
144 "why_for_edge is only called with inherits/uses-type/uses-member/imports/implements/overrides/ambiguous edges, the only kinds that ever populate a refs/read/impact row"
145 ),
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn every_why_word_is_one_of_the_closed_vocabulary() {
155 const EXPECTED: &[&str] = &[
156 "inherits",
157 "uses-type",
158 "uses-member-precise",
159 "uses-member-ext",
160 "uses-member-guess",
161 "ctor-di",
162 "imports",
163 "declaration",
164 "test-attribute",
165 "test-project",
166 "implements",
167 "overrides",
168 "imported-edge",
169 ];
170 let words: Vec<&str> = Why::ALL.iter().map(|w| w.as_str()).collect();
171 for word in &words {
172 assert!(
173 EXPECTED.contains(word),
174 "{word} is not in the closed why vocabulary"
175 );
176 }
177 assert_eq!(
178 words.len(),
179 EXPECTED.len(),
180 "ALL must name every vocabulary word exactly once"
181 );
182 }
183
184 #[test]
185 fn why_for_edge_reads_uses_member_tier_the_same_way_row_tier_folds_it() {
186 let precise = graph::Edge::uses_member(
187 "a.cs".to_string(),
188 1,
189 "T".to_string(),
190 "b.cs".to_string(),
191 None,
192 None,
193 );
194 assert_eq!(why_for_edge(&precise), Why::UsesMemberPrecise);
195
196 let ext = graph::Edge::uses_member(
197 "a.cs".to_string(),
198 1,
199 "T".to_string(),
200 "b.cs".to_string(),
201 None,
202 Some(graph::HeuristicTier::Ext),
203 );
204 assert_eq!(why_for_edge(&ext), Why::UsesMemberExt);
205
206 let guess = graph::Edge::uses_member(
207 "a.cs".to_string(),
208 1,
209 "T".to_string(),
210 "b.cs".to_string(),
211 None,
212 Some(graph::HeuristicTier::Guess),
213 );
214 assert_eq!(why_for_edge(&guess), Why::UsesMemberGuess);
215 }
216
217 #[test]
218 fn why_for_edge_reads_ambiguous_origin_as_its_ref_kind() {
219 let inherits = graph::Edge::Ambiguous {
220 origin: "inherits".to_string(),
221 from_file: "a.cs".to_string(),
222 from_line: 1,
223 raw: "Base".to_string(),
224 candidates: Vec::new(),
225 candidate_count: 2,
226 };
227 assert_eq!(why_for_edge(&inherits), Why::Inherits);
228
229 let uses_type = graph::Edge::Ambiguous {
230 origin: "uses-type".to_string(),
231 from_file: "a.cs".to_string(),
232 from_line: 1,
233 raw: "T".to_string(),
234 candidates: Vec::new(),
235 candidate_count: 2,
236 };
237 assert_eq!(why_for_edge(&uses_type), Why::UsesType);
238
239 let uses_member = graph::Edge::Ambiguous {
240 origin: "uses-member".to_string(),
241 from_file: "a.cs".to_string(),
242 from_line: 1,
243 raw: "M".to_string(),
244 candidates: Vec::new(),
245 candidate_count: 2,
246 };
247 assert_eq!(why_for_edge(&uses_member), Why::UsesMemberPrecise);
248 }
249}