/ src / languages / bash.rs
bash.rs
  1  use crate::languages::LanguageSupport;
  2  use std::sync::OnceLock;
  3  use tracing::error;
  4  use tree_sitter::{Language, Query};
  5  
  6  pub struct Bash;
  7  
  8  static REFERENCE_QUERY: OnceLock<Query> = OnceLock::new();
  9  static BINDING_QUERY: OnceLock<Query> = OnceLock::new();
 10  static IMPORT_QUERY: OnceLock<Query> = OnceLock::new();
 11  static COMPLETION_QUERY: OnceLock<Query> = OnceLock::new();
 12  static REASSIGNMENT_QUERY: OnceLock<Query> = OnceLock::new();
 13  static IDENTIFIER_QUERY: OnceLock<Query> = OnceLock::new();
 14  static EXPORT_QUERY: OnceLock<Query> = OnceLock::new();
 15  static ASSIGNMENT_QUERY: OnceLock<Query> = OnceLock::new();
 16  static DESTRUCTURE_QUERY: OnceLock<Query> = OnceLock::new();
 17  static SCOPE_QUERY: OnceLock<Query> = OnceLock::new();
 18  
 19  fn compile_query(grammar: &Language, source: &str, query_name: &str) -> Query {
 20      match Query::new(grammar, source) {
 21          Ok(query) => query,
 22          Err(e) => {
 23              error!(
 24                  language = "bash",
 25                  query = query_name,
 26                  error = %e,
 27                  "Failed to compile query, failing fast"
 28              );
 29              panic!("Failed to compile query '{}': {}", query_name, e)
 30          }
 31      }
 32  }
 33  
 34  impl LanguageSupport for Bash {
 35      fn id(&self) -> &'static str {
 36          "bash"
 37      }
 38  
 39      fn extensions(&self) -> &'static [&'static str] {
 40          &[
 41              "sh",
 42              "bash",
 43              "zsh",
 44              "zshrc",
 45              "bashrc",
 46              "bash_profile",
 47              "zprofile",
 48          ]
 49      }
 50  
 51      fn language_ids(&self) -> &'static [&'static str] {
 52          &["shellscript", "bash", "sh", "zsh"]
 53      }
 54  
 55      fn grammar(&self) -> Language {
 56          tree_sitter_bash::LANGUAGE.into()
 57      }
 58  
 59      fn reference_query(&self) -> &Query {
 60          REFERENCE_QUERY.get_or_init(|| {
 61              compile_query(
 62                  &self.grammar(),
 63                  include_str!("../../queries/bash/references.scm"),
 64                  "references",
 65              )
 66          })
 67      }
 68  
 69      fn binding_query(&self) -> Option<&Query> {
 70          Some(BINDING_QUERY.get_or_init(|| {
 71              compile_query(
 72                  &self.grammar(),
 73                  include_str!("../../queries/bash/bindings.scm"),
 74                  "bindings",
 75              )
 76          }))
 77      }
 78  
 79      fn import_query(&self) -> Option<&Query> {
 80          Some(IMPORT_QUERY.get_or_init(|| {
 81              compile_query(
 82                  &self.grammar(),
 83                  include_str!("../../queries/bash/imports.scm"),
 84                  "imports",
 85              )
 86          }))
 87      }
 88  
 89      fn completion_query(&self) -> Option<&Query> {
 90          Some(COMPLETION_QUERY.get_or_init(|| {
 91              compile_query(
 92                  &self.grammar(),
 93                  include_str!("../../queries/bash/completion.scm"),
 94                  "completion",
 95              )
 96          }))
 97      }
 98  
 99      fn reassignment_query(&self) -> Option<&Query> {
100          Some(REASSIGNMENT_QUERY.get_or_init(|| {
101              compile_query(
102                  &self.grammar(),
103                  include_str!("../../queries/bash/reassignments.scm"),
104                  "reassignments",
105              )
106          }))
107      }
108  
109      fn identifier_query(&self) -> Option<&Query> {
110          Some(IDENTIFIER_QUERY.get_or_init(|| {
111              compile_query(
112                  &self.grammar(),
113                  include_str!("../../queries/bash/identifiers.scm"),
114                  "identifiers",
115              )
116          }))
117      }
118  
119      fn export_query(&self) -> Option<&Query> {
120          Some(EXPORT_QUERY.get_or_init(|| {
121              compile_query(
122                  &self.grammar(),
123                  include_str!("../../queries/bash/exports.scm"),
124                  "exports",
125              )
126          }))
127      }
128  
129      fn assignment_query(&self) -> Option<&Query> {
130          Some(ASSIGNMENT_QUERY.get_or_init(|| {
131              compile_query(
132                  &self.grammar(),
133                  include_str!("../../queries/bash/assignments.scm"),
134                  "assignments",
135              )
136          }))
137      }
138  
139      fn destructure_query(&self) -> Option<&Query> {
140          Some(DESTRUCTURE_QUERY.get_or_init(|| {
141              compile_query(
142                  &self.grammar(),
143                  include_str!("../../queries/bash/destructures.scm"),
144                  "destructures",
145              )
146          }))
147      }
148  
149      fn scope_query(&self) -> Option<&Query> {
150          Some(SCOPE_QUERY.get_or_init(|| {
151              compile_query(
152                  &self.grammar(),
153                  include_str!("../../queries/bash/scopes.scm"),
154                  "scopes",
155              )
156          }))
157      }
158  
159      fn completion_trigger_characters(&self) -> &'static [&'static str] {
160          &["$", "${"]
161      }
162  
163      fn is_standard_env_object(&self, _name: &str) -> bool {
164          // In bash, all variable expansions ($VAR, ${VAR}) are env var access
165          true
166      }
167  
168      fn comment_node_kinds(&self) -> &'static [&'static str] {
169          &["comment"]
170      }
171  
172      fn is_scope_node(&self, node: tree_sitter::Node) -> bool {
173          matches!(
174              node.kind(),
175              "function_definition"
176                  | "compound_statement"
177                  | "subshell"
178                  | "for_statement"
179                  | "while_statement"
180                  | "if_statement"
181                  | "case_statement"
182          )
183      }
184  }
185  
186  #[cfg(test)]
187  mod tests {
188      use super::*;
189  
190      fn get_bash() -> Bash {
191          Bash
192      }
193  
194      #[test]
195      fn test_id() {
196          assert_eq!(get_bash().id(), "bash");
197      }
198  
199      #[test]
200      fn test_extensions() {
201          let exts = get_bash().extensions();
202          assert!(exts.contains(&"sh"));
203          assert!(exts.contains(&"bash"));
204          assert!(exts.contains(&"zsh"));
205      }
206  
207      #[test]
208      fn test_language_ids() {
209          let ids = get_bash().language_ids();
210          assert!(ids.contains(&"bash"));
211          assert!(ids.contains(&"shellscript"));
212      }
213  
214      #[test]
215      fn test_grammar_compiles() {
216          let bash = get_bash();
217          let _grammar = bash.grammar();
218      }
219  
220      #[test]
221      fn test_reference_query_compiles() {
222          let bash = get_bash();
223          let _query = bash.reference_query();
224      }
225  
226      #[test]
227      fn test_binding_query_compiles() {
228          let bash = get_bash();
229          assert!(bash.binding_query().is_some());
230      }
231  
232      #[test]
233      fn test_import_query_compiles() {
234          let bash = get_bash();
235          assert!(bash.import_query().is_some());
236      }
237  
238      #[test]
239      fn test_completion_query_compiles() {
240          let bash = get_bash();
241          assert!(bash.completion_query().is_some());
242      }
243  
244      #[test]
245      fn test_reassignment_query_compiles() {
246          let bash = get_bash();
247          assert!(bash.reassignment_query().is_some());
248      }
249  
250      #[test]
251      fn test_identifier_query_compiles() {
252          let bash = get_bash();
253          assert!(bash.identifier_query().is_some());
254      }
255  
256      #[test]
257      fn test_export_query_compiles() {
258          let bash = get_bash();
259          assert!(bash.export_query().is_some());
260      }
261  
262      #[test]
263      fn test_assignment_query_compiles() {
264          let bash = get_bash();
265          assert!(bash.assignment_query().is_some());
266      }
267  
268      #[test]
269      fn test_scope_query_compiles() {
270          let bash = get_bash();
271          assert!(bash.scope_query().is_some());
272      }
273  
274      #[test]
275      fn test_destructure_query_compiles() {
276          let bash = get_bash();
277          assert!(bash.destructure_query().is_some());
278      }
279  }