Skip to main content

normalize_languages/
caddy.rs

1//! Caddyfile configuration 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/// Caddy language support.
9pub struct Caddy;
10
11impl Language for Caddy {
12    fn name(&self) -> &'static str {
13        "Caddy"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["caddyfile"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "caddy"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &["site_block", "directive_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        &["import"]
39    }
40
41    fn public_symbol_kinds(&self) -> &'static [&'static str] {
42        &["site_block"]
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() != "site_block" {
51            return Vec::new();
52        }
53        if let Some(name) = self.node_name(node, content) {
54            return vec![Export {
55                name: name.to_string(),
56                kind: SymbolKind::Module,
57                line: node.start_position().row + 1,
58            }];
59        }
60        Vec::new()
61    }
62
63    fn scope_creating_kinds(&self) -> &'static [&'static str] {
64        &["site_block", "directive_block"]
65    }
66
67    fn control_flow_kinds(&self) -> &'static [&'static str] {
68        &[]
69    }
70    fn complexity_nodes(&self) -> &'static [&'static str] {
71        &[]
72    }
73    fn nesting_nodes(&self) -> &'static [&'static str] {
74        &["site_block", "directive_block"]
75    }
76
77    fn signature_suffix(&self) -> &'static str {
78        ""
79    }
80
81    fn extract_function(
82        &self,
83        _node: &Node,
84        _content: &str,
85        _in_container: bool,
86    ) -> Option<Symbol> {
87        None
88    }
89
90    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
91        if node.kind() != "site_block" && node.kind() != "directive_block" {
92            return None;
93        }
94
95        let name = self.node_name(node, content)?;
96        let text = &content[node.byte_range()];
97        let first_line = text.lines().next().unwrap_or(text);
98
99        Some(Symbol {
100            name: name.to_string(),
101            kind: SymbolKind::Module,
102            signature: first_line.trim().to_string(),
103            docstring: None,
104            attributes: Vec::new(),
105            start_line: node.start_position().row + 1,
106            end_line: node.end_position().row + 1,
107            visibility: Visibility::Public,
108            children: Vec::new(),
109            is_interface_impl: false,
110            implements: Vec::new(),
111        })
112    }
113
114    fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
115        None
116    }
117    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
118        None
119    }
120
121    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
122        Vec::new()
123    }
124
125    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
126        if node.kind() != "import" {
127            return Vec::new();
128        }
129
130        let text = &content[node.byte_range()];
131        vec![Import {
132            module: text.trim().to_string(),
133            names: Vec::new(),
134            alias: None,
135            is_wildcard: false,
136            is_relative: false,
137            line: node.start_position().row + 1,
138        }]
139    }
140
141    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
142        // Caddy: import path
143        format!("import {}", import.module)
144    }
145
146    fn is_public(&self, _node: &Node, _content: &str) -> bool {
147        true
148    }
149    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
150        Visibility::Public
151    }
152
153    fn is_test_symbol(&self, _symbol: &crate::Symbol) -> bool {
154        false
155    }
156
157    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
158        None
159    }
160
161    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
162        node.child_by_field_name("body")
163    }
164
165    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
166        false
167    }
168
169    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
170        if let Some(name_node) = node.child_by_field_name("name") {
171            return Some(&content[name_node.byte_range()]);
172        }
173        let mut cursor = node.walk();
174        if let Some(first_child) = node.children(&mut cursor).next() {
175            return Some(&content[first_child.byte_range()]);
176        }
177        None
178    }
179
180    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
181        let name = path.file_name()?.to_str()?;
182        if name.to_lowercase().contains("caddy") {
183            return Some(name.to_string());
184        }
185        None
186    }
187
188    fn module_name_to_paths(&self, _module: &str) -> Vec<String> {
189        vec!["Caddyfile".to_string()]
190    }
191
192    fn lang_key(&self) -> &'static str {
193        "caddy"
194    }
195
196    fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
197        false
198    }
199    fn find_stdlib(&self, _: &Path) -> Option<PathBuf> {
200        None
201    }
202    fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
203        None
204    }
205    fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
206        None
207    }
208    fn get_version(&self, _: &Path) -> Option<String> {
209        None
210    }
211    fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
212        None
213    }
214    fn indexable_extensions(&self) -> &'static [&'static str] {
215        &[]
216    }
217    fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
218        Vec::new()
219    }
220
221    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
222        use crate::traits::skip_dotfiles;
223        if skip_dotfiles(name) {
224            return true;
225        }
226        !is_dir && !name.to_lowercase().contains("caddy")
227    }
228
229    fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
230        Vec::new()
231    }
232
233    fn package_module_name(&self, entry_name: &str) -> String {
234        entry_name.to_string()
235    }
236
237    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
238        if path.is_file() {
239            Some(path.to_path_buf())
240        } else {
241            None
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::validate_unused_kinds_audit;
250
251    #[test]
252    fn unused_node_kinds_audit() {
253        #[rustfmt::skip]
254        let documented_unused: &[&str] = &[
255            // Matcher-related
256            "matcher_name", "matcher_path", "matcher_path_regexp", "matcher_token",
257            "matcher_definition", "standard_matcher", "uri_path_with_placeholders",
258            // Directive-related
259            "directive_import", "directive_request_body", "request_body_option_max_size",
260            "fastcgi_option_try_files", "encode_format", "log_option_format",
261            // Blocks
262            "global_options_block",
263        ];
264        validate_unused_kinds_audit(&Caddy, documented_unused)
265            .expect("Caddy unused node kinds audit failed");
266    }
267}