Skip to main content

normalize_languages/
jq.rs

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