Skip to main content

normalize_languages/
nginx.rs

1//! Nginx configuration file support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// Nginx language support.
9pub struct Nginx;
10
11impl Language for Nginx {
12    fn name(&self) -> &'static str {
13        "Nginx"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["nginx", "conf"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "nginx"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &["block"]
28    }
29
30    fn function_kinds(&self) -> &'static [&'static str] {
31        &[]
32    }
33    fn type_kinds(&self) -> &'static [&'static str] {
34        &[]
35    }
36
37    fn import_kinds(&self) -> &'static [&'static str] {
38        &["directive"] // include directive
39    }
40
41    fn public_symbol_kinds(&self) -> &'static [&'static str] {
42        &["block", "directive"]
43    }
44
45    fn visibility_mechanism(&self) -> VisibilityMechanism {
46        VisibilityMechanism::AllPublic
47    }
48
49    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
50        if node.kind() != "block" {
51            return Vec::new();
52        }
53
54        if let Some(name) = self.node_name(node, content) {
55            return vec![Export {
56                name: name.to_string(),
57                kind: SymbolKind::Module,
58                line: node.start_position().row + 1,
59            }];
60        }
61
62        Vec::new()
63    }
64
65    fn scope_creating_kinds(&self) -> &'static [&'static str] {
66        &["block"]
67    }
68
69    fn control_flow_kinds(&self) -> &'static [&'static str] {
70        &["block_directive"] // if blocks are block_directives
71    }
72
73    fn complexity_nodes(&self) -> &'static [&'static str] {
74        &["block_directive"]
75    }
76
77    fn nesting_nodes(&self) -> &'static [&'static str] {
78        &["block", "block_directive"]
79    }
80
81    fn signature_suffix(&self) -> &'static str {
82        ""
83    }
84
85    fn extract_function(
86        &self,
87        _node: &Node,
88        _content: &str,
89        _in_container: bool,
90    ) -> Option<Symbol> {
91        None
92    }
93
94    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
95        if node.kind() != "block" {
96            return None;
97        }
98
99        let name = self.node_name(node, content)?;
100        let text = &content[node.byte_range()];
101        let first_line = text.lines().next().unwrap_or(text);
102
103        Some(Symbol {
104            name: name.to_string(),
105            kind: SymbolKind::Module,
106            signature: first_line.trim().to_string(),
107            docstring: None,
108            attributes: Vec::new(),
109            start_line: node.start_position().row + 1,
110            end_line: node.end_position().row + 1,
111            visibility: Visibility::Public,
112            children: Vec::new(),
113            is_interface_impl: false,
114            implements: Vec::new(),
115        })
116    }
117
118    fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
119        None
120    }
121    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
122        None
123    }
124
125    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
126        Vec::new()
127    }
128
129    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
130        if node.kind() != "directive" {
131            return Vec::new();
132        }
133
134        let text = &content[node.byte_range()];
135        if text.starts_with("include ") {
136            return vec![Import {
137                module: text[8..].trim_end_matches(';').trim().to_string(),
138                names: Vec::new(),
139                alias: None,
140                is_wildcard: text.contains('*'),
141                is_relative: false,
142                line: node.start_position().row + 1,
143            }];
144        }
145
146        Vec::new()
147    }
148
149    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
150        // Nginx: include path
151        format!("include {}", import.module)
152    }
153
154    fn is_public(&self, _node: &Node, _content: &str) -> bool {
155        true
156    }
157    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
158        Visibility::Public
159    }
160
161    fn is_test_symbol(&self, _symbol: &crate::Symbol) -> bool {
162        false
163    }
164
165    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
166        None
167    }
168
169    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
170        node.child_by_field_name("body")
171    }
172
173    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
174        false
175    }
176
177    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
178        // For nginx blocks, the name is the directive name (server, location, etc.)
179        if let Some(dir_node) = node.child_by_field_name("directive") {
180            return Some(&content[dir_node.byte_range()]);
181        }
182        let mut cursor = node.walk();
183        if let Some(first_child) = node.children(&mut cursor).next() {
184            return Some(&content[first_child.byte_range()]);
185        }
186        None
187    }
188
189    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
190        let ext = path.extension()?.to_str()?;
191        if !["nginx", "conf"].contains(&ext) {
192            return None;
193        }
194        let stem = path.file_stem()?.to_str()?;
195        Some(stem.to_string())
196    }
197
198    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
199        vec![format!("{}.conf", module), format!("{}.nginx", module)]
200    }
201
202    fn lang_key(&self) -> &'static str {
203        "nginx"
204    }
205
206    fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
207        false
208    }
209    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
210        None
211    }
212    fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
213        None
214    }
215    fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
216        None
217    }
218    fn get_version(&self, _: &Path) -> Option<String> {
219        None
220    }
221    fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
222        None
223    }
224    fn indexable_extensions(&self) -> &'static [&'static str] {
225        &["nginx", "conf"]
226    }
227    fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
228        Vec::new()
229    }
230
231    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
232        use crate::traits::{has_extension, skip_dotfiles};
233        if skip_dotfiles(name) {
234            return true;
235        }
236        !is_dir && !has_extension(name, self.indexable_extensions())
237    }
238
239    fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
240        Vec::new()
241    }
242
243    fn package_module_name(&self, entry_name: &str) -> String {
244        entry_name
245            .strip_suffix(".conf")
246            .or_else(|| entry_name.strip_suffix(".nginx"))
247            .unwrap_or(entry_name)
248            .to_string()
249    }
250
251    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
252        if path.is_file() {
253            Some(path.to_path_buf())
254        } else {
255            None
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::validate_unused_kinds_audit;
264
265    #[test]
266    fn unused_node_kinds_audit() {
267        #[rustfmt::skip]
268        let documented_unused: &[&str] = &[
269            "lua_block", "lua_block_directive", "modifier",
270        ];
271        validate_unused_kinds_audit(&Nginx, documented_unused)
272            .expect("Nginx unused node kinds audit failed");
273    }
274}