1use 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
11pub 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 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 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}