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