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