devscout_rs/query/coverage.rs
1use std::collections::HashMap;
2
3use crate::graph;
4
5use super::index::{def_files, symbol_refs, GraphIndex};
6use super::member::{self, MemberCandidate, MemberSeedResolution};
7use super::refs_tables::{edge_loc, row_tier};
8use super::symbol::{resolve_symbol, Resolution};
9
10// ============================================================================
11// build_tests_model -- test coverage.
12// ============================================================================
13
14/// Which vouch earned a row in a tests model: an attribute-carrying def
15/// declared in the file (`Attribute`), or, absent that, the project model
16/// placing the file's unit inside a project marked `test` (`Project`).
17///
18/// The two are checked in that order -- a file's own attributed def is always
19/// the more specific vouch, so `Attribute` wins whenever both would apply.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum TestVia {
22 /// A def in the file carries a test-runner attribute.
23 Attribute,
24 /// No such def; the file's unit in the project model is a test project.
25 Project,
26}
27
28/// One row of a tests model: a test `file`, the test-carrying `test_defs` in
29/// it (possibly empty for a `Project`-vouched row -- a harness file in a test
30/// project need not declare any attributed method itself), the `lines` at
31/// which it references the symbol, the `ref_count`, whether the reference was
32/// guessed (`heuristic`), and which vouch (`via`) put the file in this model
33/// at all.
34#[derive(Debug, Clone, PartialEq)]
35pub struct TestRow {
36 /// The file value.
37 pub file: String,
38 /// The test defs value.
39 pub test_defs: Vec<String>,
40 /// The lines value.
41 pub lines: Vec<usize>,
42 /// The ref count value.
43 pub ref_count: usize,
44 /// The heuristic value.
45 pub heuristic: bool,
46 /// Which heuristic tier stands behind this FILE's references. A row folds
47 /// every reference the file makes, so one extension edge among them names
48 /// the whole row -- same rule as [`InboundRow::tier`], applied to a set.
49 pub tier: Option<graph::HeuristicTier>,
50 /// Which vouch (attribute or project) put this file in the model.
51 pub via: TestVia,
52}
53
54/// The resolved `tests` result for one symbol.
55#[derive(Debug, Clone, PartialEq)]
56pub struct TestsModel {
57 /// The query value.
58 pub query: String,
59 /// The symbol value.
60 pub symbol: String,
61 /// The def files value.
62 pub def_files: Vec<String>,
63 /// Precise rows first, heuristic rows after -- the same discipline every
64 /// other consumer keeps, so a guess never sits inside the list of facts.
65 pub rows: Vec<TestRow>,
66 /// The test file count value.
67 pub test_file_count: usize,
68 /// The ref count value.
69 pub ref_count: usize,
70 /// The heuristic file count value.
71 pub heuristic_file_count: usize,
72 /// The heuristic ref count value.
73 pub heuristic_ref_count: usize,
74}
75
76/// The outcome of a `tests` query: resolved, ambiguous, or not found.
77#[derive(Debug, Clone, PartialEq)]
78pub enum TestsResult {
79 /// Represents `Resolved`.
80 Resolved(TestsModel),
81 /// Represents `Ambiguous`.
82 Ambiguous(Vec<String>),
83 /// The seed named a member declared by more than one type -- `tests`
84 /// answers as the member's unique declaring type, so this can only ever
85 /// arise from a member seed.
86 MemberAmbiguous(Vec<MemberCandidate>),
87 /// Represents `NotFound`.
88 NotFound,
89}
90
91// The inbound kinds of one adjacency, walked in `REF_KINDS` order (plus
92// `implements`/`overrides`, which never carry a heuristic tier).
93fn collect_test_rows(index: &GraphIndex, kinds: [&[usize]; 5], heuristic: bool) -> Vec<TestRow> {
94 let mut rows: Vec<TestRow> = Vec::new();
95 let mut by_file: HashMap<String, usize> = HashMap::new();
96 for idxs in kinds {
97 for &i in idxs {
98 let (from_file, from_line) = edge_loc(&index.graph.edges[i]);
99 let test_defs = index.test_defs_by_file.get(from_file);
100 // The attribute vouch wins when it is there. Absent it, the file
101 // still earns a row -- with an empty `test_defs` -- when
102 // `is_test_file` vouches for it through the project model alone;
103 // a file neither vouch reaches is not a test file and is skipped,
104 // same as before this vouch existed.
105 let via = if test_defs.is_some() {
106 TestVia::Attribute
107 } else if index.is_test_file(from_file) {
108 TestVia::Project
109 } else {
110 continue;
111 };
112 let slot = match by_file.get(from_file) {
113 Some(&slot) => slot,
114 None => {
115 let slot = rows.len();
116 rows.push(TestRow {
117 file: from_file.to_string(),
118 test_defs: test_defs
119 .map(|defs| {
120 defs.iter()
121 .map(|&d| index.graph.defs[d].id.clone())
122 .collect()
123 })
124 .unwrap_or_default(),
125 lines: Vec::new(),
126 ref_count: 0,
127 heuristic,
128 tier: row_tier(heuristic, false),
129 via,
130 });
131 by_file.insert(from_file.to_string(), slot);
132 slot
133 }
134 };
135 // A row folds every reference ONE file makes, so its tier can only
136 // ever be raised as the rest of that file's edges arrive: the first
137 // extension edge names the row, and no later name guess takes that
138 // back (see `row_tier`).
139 if index.graph.edges[i].tier() == Some(graph::HeuristicTier::Ext) {
140 rows[slot].tier = row_tier(heuristic, true);
141 }
142 rows[slot].lines.push(from_line);
143 rows[slot].ref_count += 1;
144 }
145 }
146 for row in &mut rows {
147 row.lines.sort_unstable();
148 }
149 rows
150}
151
152/// Which TEST files reference this symbol, at which lines, via which
153/// test-carrying defs. Nothing here is a new kind of edge: it is the same
154/// inbound set `refs` reports, filtered to the files `test_defs_by_file`
155/// vouches for, which is why a symbol nothing tests answers "none" rather than
156/// falling back to a name convention.
157///
158/// Row order is FIRST-SEEN edge order, kind by kind (not sorted), so the file a
159/// consumer reads first is the one the graph reached first; the edge array
160/// walked is itself built in a pinned order.
161pub fn build_tests_model(index: &GraphIndex, query: &str) -> TestsResult {
162 let id = match resolve_symbol(index, query) {
163 Resolution::Resolved(id) => id,
164 Resolution::Ambiguous(ids) => return TestsResult::Ambiguous(ids),
165 // The member path is a FALLBACK, reached only when nothing in the
166 // graph declares `query` as a type -- same order `refs`/`impact`
167 // apply. `tests` has no member-shaped answer of its own: a member
168 // seed answers as its unique declaring type, exactly like `impact`.
169 Resolution::NotFound => match member::resolve_member_seed(index, query) {
170 MemberSeedResolution::Resolved(id) => id,
171 MemberSeedResolution::Ambiguous(candidates) => {
172 return TestsResult::MemberAmbiguous(candidates)
173 }
174 MemberSeedResolution::NotFound => return TestsResult::NotFound,
175 },
176 };
177 let refs = symbol_refs(index, &id);
178
179 let precise = collect_test_rows(
180 index,
181 [
182 &refs.inbound_inherits,
183 &refs.inbound_uses_type,
184 &refs.inbound_uses_member,
185 &refs.inbound_implements,
186 &refs.inbound_overrides,
187 ],
188 false,
189 );
190 const EMPTY: &[usize] = &[];
191 let heuristic = collect_test_rows(
192 index,
193 [
194 &refs.heuristic_inbound_inherits,
195 &refs.heuristic_inbound_uses_type,
196 &refs.heuristic_inbound_uses_member,
197 EMPTY,
198 EMPTY,
199 ],
200 true,
201 );
202
203 let test_file_count = precise.len();
204 let ref_count: usize = precise.iter().map(|r| r.ref_count).sum();
205 let heuristic_file_count = heuristic.len();
206 let heuristic_ref_count: usize = heuristic.iter().map(|r| r.ref_count).sum();
207
208 let mut rows = precise;
209 rows.extend(heuristic);
210
211 TestsResult::Resolved(TestsModel {
212 query: query.to_string(),
213 symbol: id.clone(),
214 def_files: def_files(index, &id),
215 rows,
216 test_file_count,
217 ref_count,
218 heuristic_file_count,
219 heuristic_ref_count,
220 })
221}