Skip to main content

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}