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 three inbound kinds of one adjacency, walked in `REF_KINDS` order.
92fn collect_test_rows(index: &GraphIndex, kinds: [&[usize]; 3], heuristic: bool) -> Vec<TestRow> {
93 let mut rows: Vec<TestRow> = Vec::new();
94 let mut by_file: HashMap<String, usize> = HashMap::new();
95 for idxs in kinds {
96 for &i in idxs {
97 let (from_file, from_line) = edge_loc(&index.graph.edges[i]);
98 let test_defs = index.test_defs_by_file.get(from_file);
99 // The attribute vouch wins when it is there. Absent it, the file
100 // still earns a row -- with an empty `test_defs` -- when
101 // `is_test_file` vouches for it through the project model alone;
102 // a file neither vouch reaches is not a test file and is skipped,
103 // same as before this vouch existed.
104 let via = if test_defs.is_some() {
105 TestVia::Attribute
106 } else if index.is_test_file(from_file) {
107 TestVia::Project
108 } else {
109 continue;
110 };
111 let slot = match by_file.get(from_file) {
112 Some(&slot) => slot,
113 None => {
114 let slot = rows.len();
115 rows.push(TestRow {
116 file: from_file.to_string(),
117 test_defs: test_defs
118 .map(|defs| {
119 defs.iter()
120 .map(|&d| index.graph.defs[d].id.clone())
121 .collect()
122 })
123 .unwrap_or_default(),
124 lines: Vec::new(),
125 ref_count: 0,
126 heuristic,
127 tier: row_tier(heuristic, false),
128 via,
129 });
130 by_file.insert(from_file.to_string(), slot);
131 slot
132 }
133 };
134 // A row folds every reference ONE file makes, so its tier can only
135 // ever be raised as the rest of that file's edges arrive: the first
136 // extension edge names the row, and no later name guess takes that
137 // back (see `row_tier`).
138 if index.graph.edges[i].tier() == Some(graph::HeuristicTier::Ext) {
139 rows[slot].tier = row_tier(heuristic, true);
140 }
141 rows[slot].lines.push(from_line);
142 rows[slot].ref_count += 1;
143 }
144 }
145 for row in &mut rows {
146 row.lines.sort_unstable();
147 }
148 rows
149}
150
151/// Which TEST files reference this symbol, at which lines, via which
152/// test-carrying defs. Nothing here is a new kind of edge: it is the same
153/// inbound set `refs` reports, filtered to the files `test_defs_by_file`
154/// vouches for, which is why a symbol nothing tests answers "none" rather than
155/// falling back to a name convention.
156///
157/// Row order is FIRST-SEEN edge order, kind by kind (not sorted), so the file a
158/// consumer reads first is the one the graph reached first; the edge array
159/// walked is itself built in a pinned order.
160pub fn build_tests_model(index: &GraphIndex, query: &str) -> TestsResult {
161 let id = match resolve_symbol(index, query) {
162 Resolution::Resolved(id) => id,
163 Resolution::Ambiguous(ids) => return TestsResult::Ambiguous(ids),
164 // The member path is a FALLBACK, reached only when nothing in the
165 // graph declares `query` as a type -- same order `refs`/`impact`
166 // apply. `tests` has no member-shaped answer of its own: a member
167 // seed answers as its unique declaring type, exactly like `impact`.
168 Resolution::NotFound => match member::resolve_member_seed(index, query) {
169 MemberSeedResolution::Resolved(id) => id,
170 MemberSeedResolution::Ambiguous(candidates) => {
171 return TestsResult::MemberAmbiguous(candidates)
172 }
173 MemberSeedResolution::NotFound => return TestsResult::NotFound,
174 },
175 };
176 let refs = symbol_refs(index, &id);
177
178 let precise = collect_test_rows(
179 index,
180 [
181 &refs.inbound_inherits,
182 &refs.inbound_uses_type,
183 &refs.inbound_uses_member,
184 ],
185 false,
186 );
187 let heuristic = collect_test_rows(
188 index,
189 [
190 &refs.heuristic_inbound_inherits,
191 &refs.heuristic_inbound_uses_type,
192 &refs.heuristic_inbound_uses_member,
193 ],
194 true,
195 );
196
197 let test_file_count = precise.len();
198 let ref_count: usize = precise.iter().map(|r| r.ref_count).sum();
199 let heuristic_file_count = heuristic.len();
200 let heuristic_ref_count: usize = heuristic.iter().map(|r| r.ref_count).sum();
201
202 let mut rows = precise;
203 rows.extend(heuristic);
204
205 TestsResult::Resolved(TestsModel {
206 query: query.to_string(),
207 symbol: id.clone(),
208 def_files: def_files(index, &id),
209 rows,
210 test_file_count,
211 ref_count,
212 heuristic_file_count,
213 heuristic_ref_count,
214 })
215}