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}
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}