Skip to main content

normalize_languages/
awk.rs

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