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}