/ src / languages / go.rs
go.rs
  1  use crate::languages::LanguageSupport;
  2  use crate::types::EnvSourceKind;
  3  use std::sync::OnceLock;
  4  use tracing::error;
  5  use tree_sitter::{Language, Node, Query};
  6  
  7  pub struct Go;
  8  
  9  static REFERENCE_QUERY: OnceLock<Query> = OnceLock::new();
 10  static BINDING_QUERY: OnceLock<Query> = OnceLock::new();
 11  static IMPORT_QUERY: OnceLock<Query> = OnceLock::new();
 12  static COMPLETION_QUERY: OnceLock<Query> = OnceLock::new();
 13  static REASSIGNMENT_QUERY: OnceLock<Query> = OnceLock::new();
 14  static IDENTIFIER_QUERY: OnceLock<Query> = OnceLock::new();
 15  static EXPORT_QUERY: OnceLock<Query> = OnceLock::new();
 16  
 17  static ASSIGNMENT_QUERY: OnceLock<Query> = OnceLock::new();
 18  static DESTRUCTURE_QUERY: OnceLock<Query> = OnceLock::new();
 19  static SCOPE_QUERY: OnceLock<Query> = OnceLock::new();
 20  
 21  /// Compiles a tree-sitter query and fails fast on errors to surface invalid language query definitions early.
 22  fn compile_query(grammar: &Language, source: &str, query_name: &str) -> Query {
 23      match Query::new(grammar, source) {
 24          Ok(query) => query,
 25          Err(e) => {
 26              error!(
 27                  language = "go",
 28                  query = query_name,
 29                  error = %e,
 30                  "Failed to compile query, failing fast"
 31              );
 32              panic!("Failed to compile query '{}': {}", query_name, e)
 33          }
 34      }
 35  }
 36  
 37  impl LanguageSupport for Go {
 38      fn id(&self) -> &'static str {
 39          "go"
 40      }
 41  
 42      fn is_standard_env_object(&self, name: &str) -> bool {
 43          name == "os"
 44      }
 45  
 46      fn extensions(&self) -> &'static [&'static str] {
 47          &["go"]
 48      }
 49  
 50      fn language_ids(&self) -> &'static [&'static str] {
 51          &["go"]
 52      }
 53  
 54      fn grammar(&self) -> Language {
 55          tree_sitter_go::LANGUAGE.into()
 56      }
 57  
 58      fn reference_query(&self) -> &Query {
 59          REFERENCE_QUERY.get_or_init(|| {
 60              compile_query(
 61                  &self.grammar(),
 62                  include_str!("../../queries/go/references.scm"),
 63                  "references",
 64              )
 65          })
 66      }
 67  
 68      fn binding_query(&self) -> Option<&Query> {
 69          Some(BINDING_QUERY.get_or_init(|| {
 70              compile_query(
 71                  &self.grammar(),
 72                  include_str!("../../queries/go/bindings.scm"),
 73                  "bindings",
 74              )
 75          }))
 76      }
 77  
 78      fn import_query(&self) -> Option<&Query> {
 79          Some(IMPORT_QUERY.get_or_init(|| {
 80              compile_query(
 81                  &self.grammar(),
 82                  include_str!("../../queries/go/imports.scm"),
 83                  "imports",
 84              )
 85          }))
 86      }
 87  
 88      fn completion_query(&self) -> Option<&Query> {
 89          Some(COMPLETION_QUERY.get_or_init(|| {
 90              compile_query(
 91                  &self.grammar(),
 92                  include_str!("../../queries/go/completion.scm"),
 93                  "completion",
 94              )
 95          }))
 96      }
 97  
 98      fn reassignment_query(&self) -> Option<&Query> {
 99          Some(REASSIGNMENT_QUERY.get_or_init(|| {
100              compile_query(
101                  &self.grammar(),
102                  include_str!("../../queries/go/reassignments.scm"),
103                  "reassignments",
104              )
105          }))
106      }
107  
108      fn identifier_query(&self) -> Option<&Query> {
109          Some(IDENTIFIER_QUERY.get_or_init(|| {
110              compile_query(
111                  &self.grammar(),
112                  include_str!("../../queries/go/identifiers.scm"),
113                  "identifiers",
114              )
115          }))
116      }
117  
118      fn export_query(&self) -> Option<&Query> {
119          Some(EXPORT_QUERY.get_or_init(|| {
120              compile_query(
121                  &self.grammar(),
122                  include_str!("../../queries/go/exports.scm"),
123                  "exports",
124              )
125          }))
126      }
127  
128      fn assignment_query(&self) -> Option<&Query> {
129          Some(ASSIGNMENT_QUERY.get_or_init(|| {
130              compile_query(
131                  &self.grammar(),
132                  include_str!("../../queries/go/assignments.scm"),
133                  "assignments",
134              )
135          }))
136      }
137  
138      fn destructure_query(&self) -> Option<&Query> {
139          Some(DESTRUCTURE_QUERY.get_or_init(|| {
140              compile_query(
141                  &self.grammar(),
142                  include_str!("../../queries/go/destructures.scm"),
143                  "destructures",
144              )
145          }))
146      }
147  
148      fn scope_query(&self) -> Option<&Query> {
149          Some(SCOPE_QUERY.get_or_init(|| {
150              compile_query(
151                  &self.grammar(),
152                  include_str!("../../queries/go/scopes.scm"),
153                  "scopes",
154              )
155          }))
156      }
157  
158      fn is_env_source_node(&self, node: Node, source: &[u8]) -> Option<EnvSourceKind> {
159          if node.kind() == "identifier" {
160              let text = node.utf8_text(source).ok()?;
161              if text == "os" {
162                  return Some(EnvSourceKind::Object {
163                      canonical_name: "os".into(),
164                  });
165              }
166          }
167  
168          None
169      }
170  
171      fn known_env_modules(&self) -> &'static [&'static str] {
172          &["os"]
173      }
174  
175      fn completion_trigger_characters(&self) -> &'static [&'static str] {
176          &["(\"", "('"]
177      }
178  
179      fn is_scope_node(&self, node: tree_sitter::Node) -> bool {
180          matches!(
181              node.kind(),
182              "function_declaration"
183                  | "method_declaration"
184                  | "func_literal"
185                  | "block"
186                  | "for_statement"
187                  | "if_statement"
188                  | "switch_statement"
189                  | "select_statement"
190          )
191      }
192  
193      fn extract_var_name(
194          &self,
195          node: tree_sitter::Node,
196          source: &[u8],
197      ) -> Option<compact_str::CompactString> {
198          use compact_str::CompactString;
199          node.utf8_text(source)
200              .ok()
201              .map(|s| CompactString::from(self.strip_quotes(s)))
202      }
203  
204      fn strip_quotes<'a>(&self, text: &'a str) -> &'a str {
205          text.trim_matches(|c| c == '"' || c == '\'' || c == '`')
206      }
207  
208      fn extract_property_access(
209          &self,
210          tree: &tree_sitter::Tree,
211          content: &str,
212          byte_offset: usize,
213      ) -> Option<(compact_str::CompactString, compact_str::CompactString)> {
214          let node = tree
215              .root_node()
216              .descendant_for_byte_range(byte_offset, byte_offset)?;
217  
218          let selector = if node.kind() == "selector_expression" {
219              node
220          } else if let Some(parent) = node.parent() {
221              if parent.kind() == "selector_expression" {
222                  parent
223              } else {
224                  return None;
225              }
226          } else {
227              return None;
228          };
229  
230          let operand_node = selector.child_by_field_name("operand")?;
231          let field_node = selector.child_by_field_name("field")?;
232  
233          if operand_node.kind() != "identifier" {
234              return None;
235          }
236  
237          let object_name = operand_node.utf8_text(content.as_bytes()).ok()?;
238          let property_name = field_node.utf8_text(content.as_bytes()).ok()?;
239  
240          Some((object_name.into(), property_name.into()))
241      }
242  }
243  
244  #[cfg(test)]
245  mod tests {
246      use super::*;
247  
248      fn get_go() -> Go {
249          Go
250      }
251  
252      #[test]
253      fn test_id() {
254          assert_eq!(get_go().id(), "go");
255      }
256  
257      #[test]
258      fn test_extensions() {
259          let exts = get_go().extensions();
260          assert!(exts.contains(&"go"));
261      }
262  
263      #[test]
264      fn test_language_ids() {
265          let ids = get_go().language_ids();
266          assert!(ids.contains(&"go"));
267      }
268  
269      #[test]
270      fn test_is_standard_env_object() {
271          let go = get_go();
272          assert!(go.is_standard_env_object("os"));
273          assert!(!go.is_standard_env_object("process"));
274          assert!(!go.is_standard_env_object("something.else"));
275      }
276  
277      #[test]
278      fn test_known_env_modules() {
279          let modules = get_go().known_env_modules();
280          assert!(modules.contains(&"os"));
281      }
282  
283      #[test]
284      fn test_grammar_compiles() {
285          let go = get_go();
286          let _grammar = go.grammar();
287      }
288  
289      #[test]
290      fn test_reference_query_compiles() {
291          let go = get_go();
292          let _query = go.reference_query();
293      }
294  
295      #[test]
296      fn test_binding_query_compiles() {
297          let go = get_go();
298          assert!(go.binding_query().is_some());
299      }
300  
301      #[test]
302      fn test_import_query_compiles() {
303          let go = get_go();
304          assert!(go.import_query().is_some());
305      }
306  
307      #[test]
308      fn test_completion_query_compiles() {
309          let go = get_go();
310          assert!(go.completion_query().is_some());
311      }
312  
313      #[test]
314      fn test_reassignment_query_compiles() {
315          let go = get_go();
316          assert!(go.reassignment_query().is_some());
317      }
318  
319      #[test]
320      fn test_identifier_query_compiles() {
321          let go = get_go();
322          assert!(go.identifier_query().is_some());
323      }
324  
325      #[test]
326      fn test_export_query_compiles() {
327          let go = get_go();
328          assert!(go.export_query().is_some());
329      }
330  
331      #[test]
332      fn test_assignment_query_compiles() {
333          let go = get_go();
334          assert!(go.assignment_query().is_some());
335      }
336  
337      #[test]
338      fn test_scope_query_compiles() {
339          let go = get_go();
340          assert!(go.scope_query().is_some());
341      }
342  
343      #[test]
344      fn test_destructure_query_compiles() {
345          let go = get_go();
346          assert!(go.destructure_query().is_some());
347      }
348  
349      #[test]
350      fn test_strip_quotes() {
351          let go = get_go();
352          assert_eq!(go.strip_quotes("\"hello\""), "hello");
353          assert_eq!(go.strip_quotes("'a'"), "a");
354          assert_eq!(go.strip_quotes("`raw`"), "raw");
355          assert_eq!(go.strip_quotes("noquotes"), "noquotes");
356      }
357  
358      #[test]
359      fn test_is_env_source_node_os() {
360          let go = get_go();
361          let mut parser = tree_sitter::Parser::new();
362          parser.set_language(&go.grammar()).unwrap();
363  
364          let code = "package main\nimport \"os\"\nfunc main() { os.Getenv(\"VAR\") }";
365          let tree = parser.parse(code, None).unwrap();
366          let root = tree.root_node();
367  
368          fn walk_tree(cursor: &mut tree_sitter::TreeCursor, go: &Go, code: &str) -> bool {
369              loop {
370                  let node = cursor.node();
371                  if node.kind() == "identifier" {
372                      if let Some(kind) = go.is_env_source_node(node, code.as_bytes()) {
373                          if let EnvSourceKind::Object { canonical_name } = kind {
374                              if canonical_name == "os" {
375                                  return true;
376                              }
377                          }
378                      }
379                  }
380  
381                  if cursor.goto_first_child() {
382                      if walk_tree(cursor, go, code) {
383                          return true;
384                      }
385                      cursor.goto_parent();
386                  }
387  
388                  if !cursor.goto_next_sibling() {
389                      break;
390                  }
391              }
392              false
393          }
394  
395          let mut cursor = root.walk();
396          let found = walk_tree(&mut cursor, &go, code);
397          assert!(found, "Should detect os as env source");
398      }
399  
400      #[test]
401      fn test_extract_property_access() {
402          let go = get_go();
403          let mut parser = tree_sitter::Parser::new();
404          parser.set_language(&go.grammar()).unwrap();
405  
406          let code = "package main\nfunc main() { env.DATABASE_URL }";
407          let tree = parser.parse(code, None).unwrap();
408  
409          let offset = code.find("DATABASE_URL").unwrap();
410          let result = go.extract_property_access(&tree, code, offset);
411          assert!(result.is_some());
412          let (obj, prop) = result.unwrap();
413          assert_eq!(obj.as_str(), "env");
414          assert_eq!(prop.as_str(), "DATABASE_URL");
415      }
416  
417      #[test]
418      fn test_is_scope_node() {
419          let go = get_go();
420          let mut parser = tree_sitter::Parser::new();
421          parser.set_language(&go.grammar()).unwrap();
422  
423          let code = "package main\nfunc test() {}";
424          let tree = parser.parse(code, None).unwrap();
425          let root = tree.root_node();
426  
427          fn find_node_of_kind<'a>(
428              node: tree_sitter::Node<'a>,
429              kind: &str,
430          ) -> Option<tree_sitter::Node<'a>> {
431              if node.kind() == kind {
432                  return Some(node);
433              }
434              for i in 0..node.child_count() {
435                  if let Some(child) = node.child(i) {
436                      if let Some(found) = find_node_of_kind(child, kind) {
437                          return Some(found);
438                      }
439                  }
440              }
441              None
442          }
443  
444          if let Some(func) = find_node_of_kind(root, "function_declaration") {
445              assert!(go.is_scope_node(func));
446          }
447      }
448  
449      #[test]
450      fn test_extract_var_name() {
451          let go = get_go();
452          let mut parser = tree_sitter::Parser::new();
453          parser.set_language(&go.grammar()).unwrap();
454  
455          let code = "package main\nconst VAR = \"value\"";
456          let tree = parser.parse(code, None).unwrap();
457          let root = tree.root_node();
458  
459          fn find_node_of_kind<'a>(
460              node: tree_sitter::Node<'a>,
461              kind: &str,
462          ) -> Option<tree_sitter::Node<'a>> {
463              if node.kind() == kind {
464                  return Some(node);
465              }
466              for i in 0..node.child_count() {
467                  if let Some(child) = node.child(i) {
468                      if let Some(found) = find_node_of_kind(child, kind) {
469                          return Some(found);
470                      }
471                  }
472              }
473              None
474          }
475  
476          if let Some(str_lit) = find_node_of_kind(root, "interpreted_string_literal") {
477              let name = go.extract_var_name(str_lit, code.as_bytes());
478              assert!(name.is_some());
479              assert_eq!(name.unwrap().as_str(), "value");
480          }
481      }
482  }