Skip to main content

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