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 }