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 }