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}
48
49impl Why {
50 /// Every value, in the order the doc comment above lists them.
51 pub const ALL: [Why; 10] = [
52 Why::Inherits,
53 Why::UsesType,
54 Why::UsesMemberPrecise,
55 Why::UsesMemberExt,
56 Why::UsesMemberGuess,
57 Why::CtorDi,
58 Why::Imports,
59 Why::Declaration,
60 Why::TestAttribute,
61 Why::TestProject,
62 ];
63
64 /// The exact `--json` word for this value.
65 pub fn as_str(self) -> &'static str {
66 match self {
67 Why::Inherits => "inherits",
68 Why::UsesType => "uses-type",
69 Why::UsesMemberPrecise => "uses-member-precise",
70 Why::UsesMemberExt => "uses-member-ext",
71 Why::UsesMemberGuess => "uses-member-guess",
72 Why::CtorDi => "ctor-di",
73 Why::Imports => "imports",
74 Why::Declaration => "declaration",
75 Why::TestAttribute => "test-attribute",
76 Why::TestProject => "test-project",
77 }
78 }
79}
80
81/// The `why` a `uses-member` row or edge reports, from its own tier: `None`
82/// (precise), `Some(Ext)`, or `Some(Guess)` -- the exact three states
83/// `row_tier` already folds a row's edges down to, so a caller holding either
84/// a row's `tier` field or an edge's own `.tier()` reads the same word from
85/// it. `heuristic` decides the precise/guessed split; an edge or row that
86/// says it was guessed but names no tier (a graph written before schema 2,
87/// or a hand-built row exercising that case) still reports the WEAKER word,
88/// never the precise one -- the same direction `row_tier` itself never
89/// rounds.
90pub(crate) fn why_for_uses_member(heuristic: bool, tier: Option<graph::HeuristicTier>) -> Why {
91 if !heuristic {
92 return Why::UsesMemberPrecise;
93 }
94 match tier {
95 Some(graph::HeuristicTier::Ext) => Why::UsesMemberExt,
96 _ => Why::UsesMemberGuess,
97 }
98}
99
100/// The `why` value for the edge that produced a row, derived from its kind
101/// and -- for `uses-member` -- its own `heuristic`/`tier`. Only ever called
102/// with an `inherits`/`uses-type`/`uses-member`/`imports`/`ambiguous` edge,
103/// the only kinds that ever populate a `refs`/`read`/`impact` row; every
104/// other kind is unreachable here by construction (see the module header).
105pub(crate) fn why_for_edge(edge: &graph::Edge) -> Why {
106 match edge {
107 graph::Edge::Inherits { .. } => Why::Inherits,
108 graph::Edge::UsesType { .. } => Why::UsesType,
109 graph::Edge::UsesMember { heuristic, tier, .. } => why_for_uses_member(*heuristic, *tier),
110 graph::Edge::Imports { .. } => Why::Imports,
111 // An ambiguous edge's `origin` is the same ref-kind string the ladder
112 // steps record for a resolved edge ("inherits"/"uses-type"/
113 // "uses-member"); an ambiguous edge is never heuristic (ambiguity and
114 // the guess tiers are mutually exclusive resolution outcomes), so the
115 // `uses-member` fallback here is always the precise word.
116 graph::Edge::Ambiguous { origin, .. } => match origin.as_str() {
117 "inherits" => Why::Inherits,
118 "uses-type" => Why::UsesType,
119 _ => Why::UsesMemberPrecise,
120 },
121 _ => unreachable!(
122 "why_for_edge is only called with inherits/uses-type/uses-member/imports/ambiguous edges, the only kinds that ever populate a refs/read/impact row"
123 ),
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn every_why_word_is_one_of_the_closed_vocabulary() {
133 const EXPECTED: &[&str] = &[
134 "inherits",
135 "uses-type",
136 "uses-member-precise",
137 "uses-member-ext",
138 "uses-member-guess",
139 "ctor-di",
140 "imports",
141 "declaration",
142 "test-attribute",
143 "test-project",
144 ];
145 let words: Vec<&str> = Why::ALL.iter().map(|w| w.as_str()).collect();
146 for word in &words {
147 assert!(
148 EXPECTED.contains(word),
149 "{word} is not in the closed why vocabulary"
150 );
151 }
152 assert_eq!(
153 words.len(),
154 EXPECTED.len(),
155 "ALL must name every vocabulary word exactly once"
156 );
157 }
158
159 #[test]
160 fn why_for_edge_reads_uses_member_tier_the_same_way_row_tier_folds_it() {
161 let precise = graph::Edge::uses_member(
162 "a.cs".to_string(),
163 1,
164 "T".to_string(),
165 "b.cs".to_string(),
166 None,
167 None,
168 );
169 assert_eq!(why_for_edge(&precise), Why::UsesMemberPrecise);
170
171 let ext = graph::Edge::uses_member(
172 "a.cs".to_string(),
173 1,
174 "T".to_string(),
175 "b.cs".to_string(),
176 None,
177 Some(graph::HeuristicTier::Ext),
178 );
179 assert_eq!(why_for_edge(&ext), Why::UsesMemberExt);
180
181 let guess = graph::Edge::uses_member(
182 "a.cs".to_string(),
183 1,
184 "T".to_string(),
185 "b.cs".to_string(),
186 None,
187 Some(graph::HeuristicTier::Guess),
188 );
189 assert_eq!(why_for_edge(&guess), Why::UsesMemberGuess);
190 }
191
192 #[test]
193 fn why_for_edge_reads_ambiguous_origin_as_its_ref_kind() {
194 let inherits = graph::Edge::Ambiguous {
195 origin: "inherits".to_string(),
196 from_file: "a.cs".to_string(),
197 from_line: 1,
198 raw: "Base".to_string(),
199 candidates: Vec::new(),
200 candidate_count: 2,
201 };
202 assert_eq!(why_for_edge(&inherits), Why::Inherits);
203
204 let uses_type = graph::Edge::Ambiguous {
205 origin: "uses-type".to_string(),
206 from_file: "a.cs".to_string(),
207 from_line: 1,
208 raw: "T".to_string(),
209 candidates: Vec::new(),
210 candidate_count: 2,
211 };
212 assert_eq!(why_for_edge(&uses_type), Why::UsesType);
213
214 let uses_member = graph::Edge::Ambiguous {
215 origin: "uses-member".to_string(),
216 from_file: "a.cs".to_string(),
217 from_line: 1,
218 raw: "M".to_string(),
219 candidates: Vec::new(),
220 candidate_count: 2,
221 };
222 assert_eq!(why_for_edge(&uses_member), Why::UsesMemberPrecise);
223 }
224}