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