/ src / languages / typescript.rs
typescript.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 TypeScript;
  9  pub struct TypeScriptReact;
 10  
 11  /// Compiles a tree-sitter query and fails fast on errors to surface invalid language query definitions early.
 12  fn compile_query(grammar: &Language, source: &str, lang_id: &str, query_name: &str) -> Query {
 13      match Query::new(grammar, source) {
 14          Ok(query) => query,
 15          Err(e) => {
 16              error!(
 17                  language = lang_id,
 18                  query = query_name,
 19                  error = %e,
 20                  "Failed to compile query, failing fast"
 21              );
 22              panic!("Failed to compile query '{}': {}", query_name, e)
 23          }
 24      }
 25  }
 26  
 27  static TS_REFERENCE_QUERY: OnceLock<Query> = OnceLock::new();
 28  static TS_BINDING_QUERY: OnceLock<Query> = OnceLock::new();
 29  static TS_COMPLETION_QUERY: OnceLock<Query> = OnceLock::new();
 30  static TSX_REFERENCE_QUERY: OnceLock<Query> = OnceLock::new();
 31  static TSX_BINDING_QUERY: OnceLock<Query> = OnceLock::new();
 32  static TSX_COMPLETION_QUERY: OnceLock<Query> = OnceLock::new();
 33  static TS_IMPORT_QUERY: OnceLock<Query> = OnceLock::new();
 34  static TS_REASSIGNMENT_QUERY: OnceLock<Query> = OnceLock::new();
 35  static TSX_IMPORT_QUERY: OnceLock<Query> = OnceLock::new();
 36  static TSX_REASSIGNMENT_QUERY: OnceLock<Query> = OnceLock::new();
 37  static TS_IDENTIFIER_QUERY: OnceLock<Query> = OnceLock::new();
 38  static TSX_IDENTIFIER_QUERY: OnceLock<Query> = OnceLock::new();
 39  static TS_EXPORT_QUERY: OnceLock<Query> = OnceLock::new();
 40  static TSX_EXPORT_QUERY: OnceLock<Query> = OnceLock::new();
 41  
 42  static TS_ASSIGNMENT_QUERY: OnceLock<Query> = OnceLock::new();
 43  static TS_DESTRUCTURE_QUERY: OnceLock<Query> = OnceLock::new();
 44  static TS_SCOPE_QUERY: OnceLock<Query> = OnceLock::new();
 45  static TSX_ASSIGNMENT_QUERY: OnceLock<Query> = OnceLock::new();
 46  static TSX_DESTRUCTURE_QUERY: OnceLock<Query> = OnceLock::new();
 47  static TSX_SCOPE_QUERY: OnceLock<Query> = OnceLock::new();
 48  
 49  /// Implements LanguageSupport for TypeScript-family languages.
 50  /// Both TypeScript and TypeScriptReact share nearly identical implementations,
 51  /// differing only in id, extensions, language_ids, grammar, and scope patterns.
 52  macro_rules! impl_typescript_language {
 53      (
 54          $struct_name:ty,
 55          id: $id:literal,
 56          language_ids: $lang_ids:expr,
 57          extensions: $extensions:expr,
 58          grammar: $grammar:expr,
 59          extra_scope_patterns: [$($extra_scope:pat),*],
 60          queries: {
 61              reference: $ref_query:ident,
 62              binding: $binding_query:ident,
 63              completion: $completion_query:ident,
 64              import: $import_query:ident,
 65              reassignment: $reassign_query:ident,
 66              identifier: $ident_query:ident,
 67              export: $export_query:ident,
 68              assignment: $assign_query:ident,
 69              destructure: $destruct_query:ident,
 70              scope: $scope_query:ident
 71          }
 72      ) => {
 73          impl LanguageSupport for $struct_name {
 74              fn id(&self) -> &'static str {
 75                  $id
 76              }
 77  
 78              fn is_standard_env_object(&self, name: &str) -> bool {
 79                  name == "process.env" || name == "import.meta.env"
 80              }
 81  
 82              fn default_env_object_name(&self) -> Option<&'static str> {
 83                  Some("process.env")
 84              }
 85  
 86              fn known_env_modules(&self) -> &'static [&'static str] {
 87                  &["process"]
 88              }
 89  
 90              fn completion_trigger_characters(&self) -> &'static [&'static str] {
 91                  &[".", "\"", "'"]
 92              }
 93  
 94              fn is_scope_node(&self, node: Node) -> bool {
 95                  match node.kind() {
 96                      "program"
 97                      | "function_declaration"
 98                      | "arrow_function"
 99                      | "function"
100                      | "method_definition"
101                      | "class_body"
102                      | "statement_block"
103                      | "for_statement"
104                      | "if_statement"
105                      | "else_clause"
106                      | "try_statement"
107                      | "catch_clause"
108                      | "interface_declaration"
109                      | "module"
110                      $(| $extra_scope)* => true,
111                      _ => false,
112                  }
113              }
114  
115              fn extensions(&self) -> &'static [&'static str] {
116                  $extensions
117              }
118  
119              fn language_ids(&self) -> &'static [&'static str] {
120                  $lang_ids
121              }
122  
123              fn grammar(&self) -> Language {
124                  $grammar.into()
125              }
126  
127              fn reference_query(&self) -> &Query {
128                  $ref_query.get_or_init(|| {
129                      compile_query(
130                          &self.grammar(),
131                          include_str!("../../queries/typescript/references.scm"),
132                          $id,
133                          "references",
134                      )
135                  })
136              }
137  
138              fn binding_query(&self) -> Option<&Query> {
139                  Some($binding_query.get_or_init(|| {
140                      compile_query(
141                          &self.grammar(),
142                          include_str!("../../queries/typescript/bindings.scm"),
143                          $id,
144                          "bindings",
145                      )
146                  }))
147              }
148  
149              fn completion_query(&self) -> Option<&Query> {
150                  Some($completion_query.get_or_init(|| {
151                      compile_query(
152                          &self.grammar(),
153                          include_str!("../../queries/typescript/completion.scm"),
154                          $id,
155                          "completion",
156                      )
157                  }))
158              }
159  
160              fn import_query(&self) -> Option<&Query> {
161                  Some($import_query.get_or_init(|| {
162                      compile_query(
163                          &self.grammar(),
164                          include_str!("../../queries/typescript/imports.scm"),
165                          $id,
166                          "imports",
167                      )
168                  }))
169              }
170  
171              fn reassignment_query(&self) -> Option<&Query> {
172                  Some($reassign_query.get_or_init(|| {
173                      compile_query(
174                          &self.grammar(),
175                          include_str!("../../queries/typescript/reassignments.scm"),
176                          $id,
177                          "reassignments",
178                      )
179                  }))
180              }
181  
182              fn identifier_query(&self) -> Option<&Query> {
183                  Some($ident_query.get_or_init(|| {
184                      compile_query(
185                          &self.grammar(),
186                          include_str!("../../queries/typescript/identifiers.scm"),
187                          $id,
188                          "identifiers",
189                      )
190                  }))
191              }
192  
193              fn export_query(&self) -> Option<&Query> {
194                  Some($export_query.get_or_init(|| {
195                      compile_query(
196                          &self.grammar(),
197                          include_str!("../../queries/typescript/exports.scm"),
198                          $id,
199                          "exports",
200                      )
201                  }))
202              }
203  
204              fn assignment_query(&self) -> Option<&Query> {
205                  Some($assign_query.get_or_init(|| {
206                      compile_query(
207                          &self.grammar(),
208                          include_str!("../../queries/typescript/assignments.scm"),
209                          $id,
210                          "assignments",
211                      )
212                  }))
213              }
214  
215              fn destructure_query(&self) -> Option<&Query> {
216                  Some($destruct_query.get_or_init(|| {
217                      compile_query(
218                          &self.grammar(),
219                          include_str!("../../queries/typescript/destructures.scm"),
220                          $id,
221                          "destructures",
222                      )
223                  }))
224              }
225  
226              fn scope_query(&self) -> Option<&Query> {
227                  Some($scope_query.get_or_init(|| {
228                      compile_query(
229                          &self.grammar(),
230                          include_str!("../../queries/typescript/scopes.scm"),
231                          $id,
232                          "scopes",
233                      )
234                  }))
235              }
236  
237              fn is_env_source_node(&self, node: Node, source: &[u8]) -> Option<EnvSourceKind> {
238                  typescript_is_env_source_node(node, source)
239              }
240  
241              fn extract_destructure_key(&self, node: Node, source: &[u8]) -> Option<CompactString> {
242                  typescript_extract_destructure_key(node, source)
243              }
244  
245              fn strip_quotes<'a>(&self, text: &'a str) -> &'a str {
246                  text.trim_matches(|c| c == '"' || c == '\'' || c == '`')
247              }
248  
249              fn extract_property_access(
250                  &self,
251                  tree: &tree_sitter::Tree,
252                  content: &str,
253                  byte_offset: usize,
254              ) -> Option<(CompactString, CompactString)> {
255                  typescript_extract_property_access(tree, content, byte_offset)
256              }
257          }
258      };
259  }
260  
261  /// Shared implementation for detecting env source nodes in TypeScript-family languages.
262  fn typescript_is_env_source_node(node: Node, source: &[u8]) -> Option<EnvSourceKind> {
263      if node.kind() == "member_expression" {
264          let object = node.child_by_field_name("object")?;
265          let property = node.child_by_field_name("property")?;
266  
267          let object_text = object.utf8_text(source).ok()?;
268          let property_text = property.utf8_text(source).ok()?;
269  
270          if object_text == "process" && property_text == "env" {
271              return Some(EnvSourceKind::Object {
272                  canonical_name: "process.env".into(),
273              });
274          }
275  
276          if object.kind() == "member_expression" {
277              let inner_object = object.child_by_field_name("object")?;
278              let inner_property = object.child_by_field_name("property")?;
279              let inner_object_text = inner_object.utf8_text(source).ok()?;
280              let inner_property_text = inner_property.utf8_text(source).ok()?;
281  
282              if inner_object_text == "import"
283                  && inner_property_text == "meta"
284                  && property_text == "env"
285              {
286                  return Some(EnvSourceKind::Object {
287                      canonical_name: "import.meta.env".into(),
288                  });
289              }
290          }
291      }
292  
293      None
294  }
295  
296  /// Shared implementation for extracting destructure keys in TypeScript-family languages.
297  fn typescript_extract_destructure_key(node: Node, source: &[u8]) -> Option<CompactString> {
298      if node.kind() == "pair_pattern" {
299          if let Some(key_node) = node.child_by_field_name("key") {
300              return key_node.utf8_text(source).ok().map(|s| s.into());
301          }
302      }
303  
304      node.utf8_text(source).ok().map(|s| s.into())
305  }
306  
307  /// Shared implementation for extracting property access in TypeScript-family languages.
308  fn typescript_extract_property_access(
309      tree: &tree_sitter::Tree,
310      content: &str,
311      byte_offset: usize,
312  ) -> Option<(CompactString, CompactString)> {
313      let node = tree
314          .root_node()
315          .descendant_for_byte_range(byte_offset, byte_offset)?;
316  
317      if node.kind() != "property_identifier" {
318          return None;
319      }
320  
321      let parent = node.parent()?;
322      if parent.kind() != "member_expression" {
323          return None;
324      }
325  
326      let object_node = parent.child_by_field_name("object")?;
327      if object_node.kind() != "identifier" {
328          return None;
329      }
330  
331      let object_name = object_node.utf8_text(content.as_bytes()).ok()?;
332      let property_name = node.utf8_text(content.as_bytes()).ok()?;
333  
334      Some((object_name.into(), property_name.into()))
335  }
336  
337  impl_typescript_language!(
338      TypeScript,
339      id: "typescript",
340      language_ids: &["typescript"],
341      extensions: &["ts", "mts", "cts"],
342      grammar: tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
343      extra_scope_patterns: [],
344      queries: {
345          reference: TS_REFERENCE_QUERY,
346          binding: TS_BINDING_QUERY,
347          completion: TS_COMPLETION_QUERY,
348          import: TS_IMPORT_QUERY,
349          reassignment: TS_REASSIGNMENT_QUERY,
350          identifier: TS_IDENTIFIER_QUERY,
351          export: TS_EXPORT_QUERY,
352          assignment: TS_ASSIGNMENT_QUERY,
353          destructure: TS_DESTRUCTURE_QUERY,
354          scope: TS_SCOPE_QUERY
355      }
356  );
357  
358  impl_typescript_language!(
359      TypeScriptReact,
360      id: "typescriptreact",
361      language_ids: &["typescriptreact"],
362      extensions: &["tsx"],
363      grammar: tree_sitter_typescript::LANGUAGE_TSX,
364      extra_scope_patterns: ["jsx_element"],
365      queries: {
366          reference: TSX_REFERENCE_QUERY,
367          binding: TSX_BINDING_QUERY,
368          completion: TSX_COMPLETION_QUERY,
369          import: TSX_IMPORT_QUERY,
370          reassignment: TSX_REASSIGNMENT_QUERY,
371          identifier: TSX_IDENTIFIER_QUERY,
372          export: TSX_EXPORT_QUERY,
373          assignment: TSX_ASSIGNMENT_QUERY,
374          destructure: TSX_DESTRUCTURE_QUERY,
375          scope: TSX_SCOPE_QUERY
376      }
377  );
378  
379  #[cfg(test)]
380  mod tests {
381      use super::*;
382  
383      fn get_ts() -> TypeScript {
384          TypeScript
385      }
386  
387      #[test]
388      fn test_ts_id() {
389          assert_eq!(get_ts().id(), "typescript");
390      }
391  
392      #[test]
393      fn test_ts_extensions() {
394          let exts = get_ts().extensions();
395          assert!(exts.contains(&"ts"));
396          assert!(exts.contains(&"mts"));
397          assert!(exts.contains(&"cts"));
398      }
399  
400      #[test]
401      fn test_ts_language_ids() {
402          let ids = get_ts().language_ids();
403          assert!(ids.contains(&"typescript"));
404      }
405  
406      #[test]
407      fn test_ts_is_standard_env_object() {
408          let ts = get_ts();
409          assert!(ts.is_standard_env_object("process.env"));
410          assert!(ts.is_standard_env_object("import.meta.env"));
411          assert!(!ts.is_standard_env_object("process"));
412          assert!(!ts.is_standard_env_object("import.meta"));
413          assert!(!ts.is_standard_env_object("something.else"));
414      }
415  
416      #[test]
417      fn test_ts_default_env_object_name() {
418          assert_eq!(get_ts().default_env_object_name(), Some("process.env"));
419      }
420  
421      #[test]
422      fn test_ts_known_env_modules() {
423          let modules = get_ts().known_env_modules();
424          assert!(modules.contains(&"process"));
425      }
426  
427      #[test]
428      fn test_ts_grammar_compiles() {
429          let ts = get_ts();
430          let _grammar = ts.grammar();
431      }
432  
433      #[test]
434      fn test_ts_reference_query_compiles() {
435          let ts = get_ts();
436          let _query = ts.reference_query();
437      }
438  
439      #[test]
440      fn test_ts_binding_query_compiles() {
441          let ts = get_ts();
442          assert!(ts.binding_query().is_some());
443      }
444  
445      #[test]
446      fn test_ts_completion_query_compiles() {
447          let ts = get_ts();
448          assert!(ts.completion_query().is_some());
449      }
450  
451      #[test]
452      fn test_ts_import_query_compiles() {
453          let ts = get_ts();
454          assert!(ts.import_query().is_some());
455      }
456  
457      #[test]
458      fn test_ts_reassignment_query_compiles() {
459          let ts = get_ts();
460          assert!(ts.reassignment_query().is_some());
461      }
462  
463      #[test]
464      fn test_ts_identifier_query_compiles() {
465          let ts = get_ts();
466          assert!(ts.identifier_query().is_some());
467      }
468  
469      #[test]
470      fn test_ts_export_query_compiles() {
471          let ts = get_ts();
472          assert!(ts.export_query().is_some());
473      }
474  
475      #[test]
476      fn test_ts_assignment_query_compiles() {
477          let ts = get_ts();
478          assert!(ts.assignment_query().is_some());
479      }
480  
481      #[test]
482      fn test_ts_destructure_query_compiles() {
483          let ts = get_ts();
484          assert!(ts.destructure_query().is_some());
485      }
486  
487      #[test]
488      fn test_ts_scope_query_compiles() {
489          let ts = get_ts();
490          assert!(ts.scope_query().is_some());
491      }
492  
493      #[test]
494      fn test_ts_strip_quotes() {
495          let ts = get_ts();
496          assert_eq!(ts.strip_quotes("\"hello\""), "hello");
497          assert_eq!(ts.strip_quotes("'world'"), "world");
498          assert_eq!(ts.strip_quotes("`template`"), "template");
499          assert_eq!(ts.strip_quotes("noquotes"), "noquotes");
500      }
501  
502      #[test]
503      fn test_ts_is_env_source_node_process_env() {
504          let ts = get_ts();
505          let mut parser = tree_sitter::Parser::new();
506          parser.set_language(&ts.grammar()).unwrap();
507  
508          let code = "const x = process.env;";
509          let tree = parser.parse(code, None).unwrap();
510          let root = tree.root_node();
511  
512          fn walk_tree(cursor: &mut tree_sitter::TreeCursor, ts: &TypeScript, code: &str) -> bool {
513              loop {
514                  let node = cursor.node();
515                  if node.kind() == "member_expression" {
516                      if let Some(kind) = ts.is_env_source_node(node, code.as_bytes()) {
517                          if let EnvSourceKind::Object { canonical_name } = kind {
518                              if canonical_name == "process.env" {
519                                  return true;
520                              }
521                          }
522                      }
523                  }
524  
525                  if cursor.goto_first_child() {
526                      if walk_tree(cursor, ts, code) {
527                          return true;
528                      }
529                      cursor.goto_parent();
530                  }
531  
532                  if !cursor.goto_next_sibling() {
533                      break;
534                  }
535              }
536              false
537          }
538  
539          let mut cursor = root.walk();
540          let found = walk_tree(&mut cursor, &ts, code);
541          assert!(found, "Should detect process.env as env source");
542      }
543  
544      #[test]
545      fn test_ts_extract_property_access() {
546          let ts = get_ts();
547          let mut parser = tree_sitter::Parser::new();
548          parser.set_language(&ts.grammar()).unwrap();
549  
550          let code = "const x = env.DATABASE_URL;";
551          let tree = parser.parse(code, None).unwrap();
552  
553          let offset = code.find("DATABASE_URL").unwrap();
554          let result = ts.extract_property_access(&tree, code, offset);
555          assert!(result.is_some());
556          let (obj, prop) = result.unwrap();
557          assert_eq!(obj.as_str(), "env");
558          assert_eq!(prop.as_str(), "DATABASE_URL");
559      }
560  
561      #[test]
562      fn test_ts_extract_destructure_key_shorthand() {
563          let ts = get_ts();
564          let mut parser = tree_sitter::Parser::new();
565          parser.set_language(&ts.grammar()).unwrap();
566  
567          let code = "const { VAR } = process.env;";
568          let tree = parser.parse(code, None).unwrap();
569          let root = tree.root_node();
570  
571          fn find_node<'a>(node: tree_sitter::Node<'a>, kind: &str) -> Option<tree_sitter::Node<'a>> {
572              if node.kind() == kind {
573                  return Some(node);
574              }
575              for i in 0..node.child_count() {
576                  if let Some(child) = node.child(i) {
577                      if let Some(found) = find_node(child, kind) {
578                          return Some(found);
579                      }
580                  }
581              }
582              None
583          }
584  
585          let shorthand = find_node(root, "shorthand_property_identifier_pattern");
586          assert!(shorthand.is_some());
587  
588          let key = ts.extract_destructure_key(shorthand.unwrap(), code.as_bytes());
589          assert!(key.is_some());
590          assert_eq!(key.unwrap().as_str(), "VAR");
591      }
592  
593      #[test]
594      fn test_ts_is_scope_node() {
595          let ts = get_ts();
596          let mut parser = tree_sitter::Parser::new();
597          parser.set_language(&ts.grammar()).unwrap();
598  
599          let code = "function test() {}";
600          let tree = parser.parse(code, None).unwrap();
601          let root = tree.root_node();
602  
603          fn find_node_of_kind<'a>(
604              node: tree_sitter::Node<'a>,
605              kind: &str,
606          ) -> Option<tree_sitter::Node<'a>> {
607              if node.kind() == kind {
608                  return Some(node);
609              }
610              for i in 0..node.child_count() {
611                  if let Some(child) = node.child(i) {
612                      if let Some(found) = find_node_of_kind(child, kind) {
613                          return Some(found);
614                      }
615                  }
616              }
617              None
618          }
619  
620          if let Some(func) = find_node_of_kind(root, "function_declaration") {
621              assert!(ts.is_scope_node(func));
622          }
623      }
624  
625      fn get_tsx() -> TypeScriptReact {
626          TypeScriptReact
627      }
628  
629      #[test]
630      fn test_tsx_id() {
631          assert_eq!(get_tsx().id(), "typescriptreact");
632      }
633  
634      #[test]
635      fn test_tsx_extensions() {
636          let exts = get_tsx().extensions();
637          assert!(exts.contains(&"tsx"));
638      }
639  
640      #[test]
641      fn test_tsx_language_ids() {
642          let ids = get_tsx().language_ids();
643          assert!(ids.contains(&"typescriptreact"));
644      }
645  
646      #[test]
647      fn test_tsx_is_standard_env_object() {
648          let tsx = get_tsx();
649          assert!(tsx.is_standard_env_object("process.env"));
650          assert!(tsx.is_standard_env_object("import.meta.env"));
651          assert!(!tsx.is_standard_env_object("process"));
652          assert!(!tsx.is_standard_env_object("import.meta"));
653          assert!(!tsx.is_standard_env_object("something.else"));
654      }
655  
656      #[test]
657      fn test_tsx_default_env_object_name() {
658          assert_eq!(get_tsx().default_env_object_name(), Some("process.env"));
659      }
660  
661      #[test]
662      fn test_tsx_known_env_modules() {
663          let modules = get_tsx().known_env_modules();
664          assert!(modules.contains(&"process"));
665      }
666  
667      #[test]
668      fn test_tsx_grammar_compiles() {
669          let tsx = get_tsx();
670          let _grammar = tsx.grammar();
671      }
672  
673      #[test]
674      fn test_tsx_reference_query_compiles() {
675          let tsx = get_tsx();
676          let _query = tsx.reference_query();
677      }
678  
679      #[test]
680      fn test_tsx_binding_query_compiles() {
681          let tsx = get_tsx();
682          assert!(tsx.binding_query().is_some());
683      }
684  
685      #[test]
686      fn test_tsx_completion_query_compiles() {
687          let tsx = get_tsx();
688          assert!(tsx.completion_query().is_some());
689      }
690  
691      #[test]
692      fn test_tsx_import_query_compiles() {
693          let tsx = get_tsx();
694          assert!(tsx.import_query().is_some());
695      }
696  
697      #[test]
698      fn test_tsx_reassignment_query_compiles() {
699          let tsx = get_tsx();
700          assert!(tsx.reassignment_query().is_some());
701      }
702  
703      #[test]
704      fn test_tsx_identifier_query_compiles() {
705          let tsx = get_tsx();
706          assert!(tsx.identifier_query().is_some());
707      }
708  
709      #[test]
710      fn test_tsx_export_query_compiles() {
711          let tsx = get_tsx();
712          assert!(tsx.export_query().is_some());
713      }
714  
715      #[test]
716      fn test_tsx_assignment_query_compiles() {
717          let tsx = get_tsx();
718          assert!(tsx.assignment_query().is_some());
719      }
720  
721      #[test]
722      fn test_tsx_destructure_query_compiles() {
723          let tsx = get_tsx();
724          assert!(tsx.destructure_query().is_some());
725      }
726  
727      #[test]
728      fn test_tsx_scope_query_compiles() {
729          let tsx = get_tsx();
730          assert!(tsx.scope_query().is_some());
731      }
732  
733      #[test]
734      fn test_tsx_strip_quotes() {
735          let tsx = get_tsx();
736          assert_eq!(tsx.strip_quotes("\"hello\""), "hello");
737          assert_eq!(tsx.strip_quotes("'world'"), "world");
738          assert_eq!(tsx.strip_quotes("`template`"), "template");
739          assert_eq!(tsx.strip_quotes("noquotes"), "noquotes");
740      }
741  
742      #[test]
743      fn test_tsx_is_env_source_node_process_env() {
744          let tsx = get_tsx();
745          let mut parser = tree_sitter::Parser::new();
746          parser.set_language(&tsx.grammar()).unwrap();
747  
748          let code = "const x = process.env;";
749          let tree = parser.parse(code, None).unwrap();
750          let root = tree.root_node();
751  
752          fn walk_tree(
753              cursor: &mut tree_sitter::TreeCursor,
754              tsx: &TypeScriptReact,
755              code: &str,
756          ) -> bool {
757              loop {
758                  let node = cursor.node();
759                  if node.kind() == "member_expression" {
760                      if let Some(kind) = tsx.is_env_source_node(node, code.as_bytes()) {
761                          if let EnvSourceKind::Object { canonical_name } = kind {
762                              if canonical_name == "process.env" {
763                                  return true;
764                              }
765                          }
766                      }
767                  }
768  
769                  if cursor.goto_first_child() {
770                      if walk_tree(cursor, tsx, code) {
771                          return true;
772                      }
773                      cursor.goto_parent();
774                  }
775  
776                  if !cursor.goto_next_sibling() {
777                      break;
778                  }
779              }
780              false
781          }
782  
783          let mut cursor = root.walk();
784          let found = walk_tree(&mut cursor, &tsx, code);
785          assert!(found, "Should detect process.env as env source");
786      }
787  
788      #[test]
789      fn test_tsx_extract_property_access() {
790          let tsx = get_tsx();
791          let mut parser = tree_sitter::Parser::new();
792          parser.set_language(&tsx.grammar()).unwrap();
793  
794          let code = "const x = env.API_KEY;";
795          let tree = parser.parse(code, None).unwrap();
796  
797          let offset = code.find("API_KEY").unwrap();
798          let result = tsx.extract_property_access(&tree, code, offset);
799          assert!(result.is_some());
800          let (obj, prop) = result.unwrap();
801          assert_eq!(obj.as_str(), "env");
802          assert_eq!(prop.as_str(), "API_KEY");
803      }
804  
805      #[test]
806      fn test_tsx_is_scope_node_jsx_element() {
807          let tsx = get_tsx();
808          let mut parser = tree_sitter::Parser::new();
809          parser.set_language(&tsx.grammar()).unwrap();
810  
811          let code = "const App = () => <div>Hello</div>;";
812          let tree = parser.parse(code, None).unwrap();
813          let root = tree.root_node();
814  
815          fn find_node_of_kind<'a>(
816              node: tree_sitter::Node<'a>,
817              kind: &str,
818          ) -> Option<tree_sitter::Node<'a>> {
819              if node.kind() == kind {
820                  return Some(node);
821              }
822              for i in 0..node.child_count() {
823                  if let Some(child) = node.child(i) {
824                      if let Some(found) = find_node_of_kind(child, kind) {
825                          return Some(found);
826                      }
827                  }
828              }
829              None
830          }
831  
832          if let Some(jsx) = find_node_of_kind(root, "jsx_element") {
833              assert!(tsx.is_scope_node(jsx));
834          }
835      }
836  }