Skip to main content

whitaker_common/
context.rs

1//! Context tracking utilities for analysing traversal stacks.
2
3use crate::attributes::{
4    Attribute, AttributePath, has_test_like_attribute, has_test_like_attribute_with,
5};
6
7/// Categorises a frame within the traversal stack.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum ContextKind {
10    /// A free function (including methods lowered to free functions).
11    Function,
12    /// An implementation block.
13    Impl,
14    /// A module or namespace boundary.
15    Module,
16    /// A lexical block (e.g. closure, loop, or bare block).
17    Block,
18}
19
20/// Records contextual information for traversal decisions.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct ContextEntry {
23    name: String,
24    kind: ContextKind,
25    attributes: Vec<Attribute>,
26}
27
28impl ContextEntry {
29    /// Builds a new context entry with the provided attributes.
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath};
35    /// use whitaker_common::context::{ContextEntry, ContextKind};
36    ///
37    /// let entry = ContextEntry::new(
38    ///     "demo",
39    ///     ContextKind::Function,
40    ///     vec![Attribute::new(AttributePath::from("test"), AttributeKind::Outer)],
41    /// );
42    /// assert_eq!(entry.name(), "demo");
43    /// ```
44    #[must_use]
45    pub fn new(name: impl Into<String>, kind: ContextKind, attributes: Vec<Attribute>) -> Self {
46        Self {
47            name: name.into(),
48            kind,
49            attributes,
50        }
51    }
52
53    /// Convenience constructor for function contexts.
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath};
59    /// use whitaker_common::context::ContextEntry;
60    ///
61    /// let entry = ContextEntry::function(
62    ///     "demo",
63    ///     vec![Attribute::new(AttributePath::from("test"), AttributeKind::Outer)],
64    /// );
65    /// assert!(entry.kind().matches_function());
66    /// ```
67    #[must_use]
68    pub fn function(name: impl Into<String>, attributes: Vec<Attribute>) -> Self {
69        Self::new(name, ContextKind::Function, attributes)
70    }
71
72    /// Returns the entry name.
73    #[must_use]
74    pub fn name(&self) -> &str {
75        &self.name
76    }
77
78    /// Returns the entry kind.
79    #[must_use]
80    pub const fn kind(&self) -> &ContextKind {
81        &self.kind
82    }
83
84    /// Returns a snapshot of the entry attributes.
85    #[must_use]
86    pub fn attributes(&self) -> &[Attribute] {
87        &self.attributes
88    }
89
90    /// Returns a mutable reference to the attributes for in-place updates.
91    #[must_use]
92    pub fn attributes_mut(&mut self) -> &mut Vec<Attribute> {
93        &mut self.attributes
94    }
95
96    /// Adds an attribute to the entry.
97    pub fn push_attribute(&mut self, attribute: Attribute) {
98        self.attributes.push(attribute);
99    }
100}
101
102impl ContextKind {
103    /// Returns `true` when the kind is [`ContextKind::Function`].
104    #[must_use]
105    pub const fn matches_function(&self) -> bool {
106        matches!(self, Self::Function)
107    }
108}
109
110/// Tests whether a slice of attributes marks an item as a test function.
111///
112/// # Examples
113///
114/// ```
115/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath};
116/// use whitaker_common::context::is_test_fn;
117///
118/// let attrs = vec![Attribute::new(AttributePath::from("rstest"), AttributeKind::Outer)];
119/// assert!(is_test_fn(&attrs));
120/// ```
121#[must_use]
122pub fn is_test_fn(attrs: &[Attribute]) -> bool {
123    has_test_like_attribute(attrs)
124}
125
126/// Tests whether a slice of attributes marks an item as a test function while
127/// honouring custom attribute paths.
128///
129/// # Examples
130///
131/// ```
132/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath};
133/// use whitaker_common::context::is_test_fn_with;
134///
135/// let attrs = vec![Attribute::new(AttributePath::from("custom::test"), AttributeKind::Outer)];
136/// let additional = vec![AttributePath::from("custom::test")];
137/// assert!(is_test_fn_with(&attrs, &additional));
138/// ```
139#[must_use]
140pub fn is_test_fn_with(attrs: &[Attribute], additional: &[AttributePath]) -> bool {
141    has_test_like_attribute_with(attrs, additional)
142}
143
144/// Returns `true` when any entry in the stack participates in a test-like context.
145///
146/// # Examples
147///
148/// ```
149/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath};
150/// use whitaker_common::context::{in_test_like_context, ContextEntry};
151///
152/// let mut entry = ContextEntry::function("demo", Vec::new());
153/// entry.push_attribute(Attribute::new(AttributePath::from("test"), AttributeKind::Outer));
154/// assert!(in_test_like_context(&[entry]));
155/// ```
156#[must_use]
157pub fn in_test_like_context(stack: &[ContextEntry]) -> bool {
158    in_test_like_context_with(stack, &[])
159}
160
161/// Returns `true` when any entry in the stack participates in a test-like
162/// context, including those provided via the `additional` attribute paths.
163///
164/// # Examples
165///
166/// ```
167/// use whitaker_common::attributes::{Attribute, AttributeKind, AttributePath};
168/// use whitaker_common::context::{in_test_like_context_with, ContextEntry};
169///
170/// let mut entry = ContextEntry::function("demo", Vec::new());
171/// entry.push_attribute(Attribute::new(AttributePath::from("custom::test"), AttributeKind::Outer));
172/// let additional = vec![AttributePath::from("custom::test")];
173/// assert!(in_test_like_context_with(&[entry], &additional));
174/// ```
175#[must_use]
176pub fn in_test_like_context_with(stack: &[ContextEntry], additional: &[AttributePath]) -> bool {
177    stack
178        .iter()
179        .any(|entry| has_test_like_attribute_with(entry.attributes(), additional))
180}
181
182/// Detects whether the current traversal stack is inside a `main` function.
183///
184/// This treats any function named `main` as an entry point. Module-qualified
185/// helper functions with the same name therefore satisfy the predicate.
186///
187/// # Examples
188///
189/// ```
190/// use whitaker_common::context::{is_in_main_fn, ContextEntry};
191///
192/// let stack = vec![ContextEntry::function("main", Vec::new())];
193/// assert!(is_in_main_fn(&stack));
194/// ```
195#[must_use]
196pub fn is_in_main_fn(stack: &[ContextEntry]) -> bool {
197    stack
198        .iter()
199        .rev()
200        .any(|entry| entry.kind.matches_function() && entry.name() == "main")
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::attributes::{Attribute, AttributeKind, AttributePath};
207    use rstest::rstest;
208
209    fn test_attribute() -> Attribute {
210        Attribute::new(AttributePath::from("test"), AttributeKind::Outer)
211    }
212
213    #[rstest]
214    #[case::plain(Vec::new(), false)]
215    #[case::rstest(vec![test_attribute()], true)]
216    fn detects_test_functions(#[case] attrs: Vec<Attribute>, #[case] expected: bool) {
217        assert_eq!(is_test_fn(&attrs), expected);
218    }
219
220    #[rstest]
221    fn context_detection() {
222        let mut entry = ContextEntry::function("demo", Vec::new());
223        entry.push_attribute(test_attribute());
224        assert!(in_test_like_context(&[entry]));
225    }
226
227    #[rstest]
228    fn identifies_main() {
229        let stack = vec![ContextEntry::function("main", Vec::new())];
230        assert!(is_in_main_fn(&stack));
231    }
232
233    #[rstest]
234    fn rejects_non_main() {
235        let stack = vec![ContextEntry::function("helper", Vec::new())];
236        assert!(!is_in_main_fn(&stack));
237    }
238
239    #[rstest]
240    fn honours_additional_attributes() {
241        let additional = vec![AttributePath::from("custom::test")];
242        let attrs = vec![Attribute::new(
243            AttributePath::from("custom::test"),
244            AttributeKind::Outer,
245        )];
246
247        assert!(is_test_fn_with(&attrs, additional.as_slice()));
248
249        let entry = ContextEntry::function("demo", attrs);
250        assert!(in_test_like_context_with(&[entry], additional.as_slice()));
251    }
252}