nginx_lint_parser/
context.rs

1//! Context-aware directive traversal.
2//!
3//! This module provides [`DirectiveWithContext`] and [`AllDirectivesWithContextIter`],
4//! which perform depth-first traversal of the config AST while tracking the parent
5//! block hierarchy (e.g., `["http", "server"]`).
6//!
7//! Obtained via [`Config::all_directives_with_context()`](crate::ast::Config::all_directives_with_context).
8
9use crate::ast::{ConfigItem, Directive};
10
11/// A directive paired with its parent block context.
12///
13/// Yielded by [`Config::all_directives_with_context()`](crate::ast::Config::all_directives_with_context).
14/// Provides methods to query the parent block hierarchy without manually tracking nesting.
15#[derive(Debug, Clone)]
16pub struct DirectiveWithContext<'a> {
17    /// The directive itself.
18    pub directive: &'a Directive,
19    /// Stack of parent directive names (e.g., `["http", "server"]`).
20    pub parent_stack: Vec<String>,
21    /// Nesting depth (0 = root level).
22    pub depth: usize,
23}
24
25impl<'a> DirectiveWithContext<'a> {
26    /// Get the immediate parent directive name, if any
27    pub fn parent(&self) -> Option<&str> {
28        self.parent_stack.last().map(|s| s.as_str())
29    }
30
31    /// Check if this directive is inside a specific parent context
32    pub fn is_inside(&self, parent_name: &str) -> bool {
33        self.parent_stack.iter().any(|p| p == parent_name)
34    }
35
36    /// Check if the immediate parent is a specific directive
37    pub fn parent_is(&self, parent_name: &str) -> bool {
38        self.parent() == Some(parent_name)
39    }
40
41    /// Check if this directive is at root level
42    pub fn is_at_root(&self) -> bool {
43        self.parent_stack.is_empty()
44    }
45}
46
47/// Iterator over all directives with their parent context.
48///
49/// Obtained via [`Config::all_directives_with_context`](crate::ast::Config::all_directives_with_context).
50/// Performs depth-first traversal, tracking parent block names as it descends.
51pub struct AllDirectivesWithContextIter<'a> {
52    stack: Vec<(std::slice::Iter<'a, ConfigItem>, Option<String>)>,
53    current_parents: Vec<String>,
54}
55
56impl<'a> AllDirectivesWithContextIter<'a> {
57    pub(crate) fn new(items: &'a [ConfigItem], initial_context: Vec<String>) -> Self {
58        Self {
59            stack: vec![(items.iter(), None)],
60            current_parents: initial_context,
61        }
62    }
63}
64
65impl<'a> Iterator for AllDirectivesWithContextIter<'a> {
66    type Item = DirectiveWithContext<'a>;
67
68    fn next(&mut self) -> Option<Self::Item> {
69        while let Some((iter, _)) = self.stack.last_mut() {
70            if let Some(item) = iter.next() {
71                if let ConfigItem::Directive(directive) = item {
72                    let context = DirectiveWithContext {
73                        directive: directive.as_ref(),
74                        parent_stack: self.current_parents.clone(),
75                        depth: self.current_parents.len(),
76                    };
77
78                    if let Some(block) = &directive.block {
79                        self.current_parents.push(directive.name.clone());
80                        self.stack
81                            .push((block.items.iter(), Some(directive.name.clone())));
82                    }
83
84                    return Some(context);
85                }
86            } else {
87                let (_, parent_name) = self.stack.pop().unwrap();
88                if parent_name.is_some() {
89                    self.current_parents.pop();
90                }
91            }
92        }
93        None
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use crate::ast::Config;
100
101    #[test]
102    fn test_all_directives_with_context() {
103        let config =
104            crate::parse_string("http {\n    server {\n        listen 80;\n    }\n}").unwrap();
105
106        let contexts: Vec<_> = config.all_directives_with_context().collect();
107        assert_eq!(contexts.len(), 3);
108
109        // http at root
110        assert_eq!(contexts[0].directive.name, "http");
111        assert!(contexts[0].is_at_root());
112        assert_eq!(contexts[0].depth, 0);
113
114        // server inside http
115        assert_eq!(contexts[1].directive.name, "server");
116        assert!(contexts[1].is_inside("http"));
117        assert!(contexts[1].parent_is("http"));
118        assert_eq!(contexts[1].depth, 1);
119
120        // listen inside http > server
121        assert_eq!(contexts[2].directive.name, "listen");
122        assert!(contexts[2].is_inside("http"));
123        assert!(contexts[2].is_inside("server"));
124        assert!(contexts[2].parent_is("server"));
125        assert_eq!(contexts[2].depth, 2);
126    }
127
128    #[test]
129    fn test_all_directives_with_context_include_context() {
130        let mut config = crate::parse_string("server {\n    listen 80;\n}").unwrap();
131        config.include_context = vec!["http".to_string()];
132
133        let contexts: Vec<_> = config.all_directives_with_context().collect();
134        assert_eq!(contexts.len(), 2);
135
136        // server is inside http (from include_context)
137        assert_eq!(contexts[0].directive.name, "server");
138        assert!(contexts[0].is_inside("http"));
139        assert_eq!(contexts[0].depth, 1);
140
141        // listen is inside http > server
142        assert_eq!(contexts[1].directive.name, "listen");
143        assert!(contexts[1].is_inside("http"));
144        assert!(contexts[1].is_inside("server"));
145        assert_eq!(contexts[1].depth, 2);
146    }
147
148    #[test]
149    fn test_include_context_helpers() {
150        let mut config = Config::new();
151        assert!(!config.is_included_from_http());
152        assert!(!config.is_included_from_stream());
153
154        config.include_context = vec!["http".to_string()];
155        assert!(config.is_included_from_http());
156        assert!(config.is_included_from("http"));
157        assert!(!config.is_included_from_stream());
158        assert_eq!(config.immediate_parent_context(), Some("http"));
159
160        config.include_context = vec!["http".to_string(), "server".to_string()];
161        assert!(config.is_included_from_http());
162        assert!(config.is_included_from_http_server());
163        assert!(!config.is_included_from_http_location());
164
165        config.include_context = vec!["http".to_string(), "location".to_string()];
166        assert!(config.is_included_from_http_location());
167    }
168}