1use bonsai_common::{FileId, SymbolId};
3use bonsai_lang_api::{
4 decl_index_with_handler, extract_imports_via,
5 kit::{
6 call_arg_from_nodes_with_handler, collect_kinds, first_named_child_of_kind, language_from_pack,
7 node_text, normalize_call_name_whitespace, parse_with, span_of,
8 },
9 AdapterContext, AdapterError, CallTargetExtraction, CharacterSubstitutionDomain,
10 CharacterSubstitutionFact, ConditionEquality, ConditionExpressionFact, ConditionOperandFact, DeclIndex,
11 DynamicKeyFilterFact, FiniteLiteralSelectionFact, GrammarHandler, ImportIndex, ImportScope, ImportSpec,
12 LanguageAdapter, LanguageCapabilities, LanguageId, SameOriginPathConstraintFact, StaticScalarValue,
13 StaticStringMapEntry, StaticStringMapFact, StringCompositionFact, StringCompositionPart,
14 TypeAliasBinding, Visibility, EMPTY_HANDLER,
15};
16use bonsai_lang_api::{CallArg, CallKind, DeclKind, FlowEvent};
17use std::collections::{HashMap, HashSet};
18use tree_sitter::{Language, Node, Tree};
19
20fn javascript_call_target<'tree>(node: Node<'tree>, src: &[u8]) -> Option<CallTargetExtraction<'tree>> {
21 let target = match node.kind() {
22 "call_expression" => node.child_by_field_name("function")?,
23 "new_expression" => node.child_by_field_name("constructor")?,
24 _ => return None,
25 };
26 let full_text = node_text(&target, src).trim();
27 (!full_text.is_empty()).then_some(CallTargetExtraction {
28 node: target,
29 full_text: full_text.to_string(),
30 })
31}
32
33fn javascript_foreach_binding(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
34 (node.kind() == "for_in_statement")
35 .then(|| {
36 Some((
37 node.child_by_field_name("left")?,
38 node.child_by_field_name("right")?,
39 ))
40 })
41 .flatten()
42}
43
44pub const LANG_ID: LanguageId = LanguageId::new("javascript");
45pub const JS_TS_MODULE_RESOLUTION_EXTENSIONS: &[&str] = &["js", "jsx", "ts", "tsx", "mjs", "cjs"];
46const PACK_NAME: &str = "javascript";
47const HANDLER: GrammarHandler = GrammarHandler {
48 expression_value_kind_extractor: None,
49 literal_value_kinds: &["null", "number", "true", "false"],
50 string_literal_kinds: &["string", "template_string"],
51 comment_kinds: &["comment", "hash_bang_line"],
52 doc_comment_prefixes: &["/**"],
53 decorator_kinds: &["decorator"],
54 parameter_container_kinds: &["formal_parameters"],
55 parameter_kinds: &["identifier", "required_parameter", "optional_parameter"],
56 parameter_annotation_kinds: &["decorator"],
57 variadic_parameter_kinds: &["rest_pattern", "rest_parameter"],
58 destructured_parameter_kinds: &["object_pattern", "array_pattern"],
59 binding_identifier_kinds: &["identifier", "shorthand_property_identifier_pattern"],
60 binding_lhs_pattern_kinds: &["assignment_pattern"],
61 binding_pattern_field_names: &["left"],
62 non_binding_pattern_field_names: &["type", "key", "property"],
63 identifier_kinds: &["identifier", "shorthand_property_identifier", "this", "super"],
64 aggregate_pattern_kinds: &["array_pattern", "object_pattern"],
65 named_aggregate_kinds: &["object"],
66 positional_aggregate_kinds: &["array"],
67 aggregate_pair_kinds: &["pair", "pair_pattern"],
68 aggregate_key_field_names: &["key"],
69 aggregate_value_field_names: &["value"],
70 static_field_name_kinds: &["identifier", "property_identifier"],
71 shorthand_field_kinds: &["shorthand_property_identifier"],
72 spread_kinds: &["spread_element"],
73 spread_value_field_names: &["argument"],
74 lambda_value_container_kinds: &["object", "pair", "array"],
75 transparent_call_wrapper_kinds: &[
76 "member_expression",
77 "parenthesized_expression",
78 "await_expression",
79 ],
80 single_expression_group_kinds: &["expressions"],
81 assignment_target_wrapper_kinds: &["variable_declarator"],
82 binding_declaration_keyword_spellings: &["var", "let", "const"],
83 fn_kinds: &[
84 "function_declaration",
85 "function_expression",
86 "method_definition",
87 "generator_function_declaration",
88 "generator_function",
89 ],
90 class_kinds: &["class_declaration"],
91 class_decl_kinds: &[("class_declaration", DeclKind::Class)],
92 method_kinds: &["method_definition"],
93 method_context_kinds: &["class_declaration"],
94 if_kinds: &["if_statement", "switch_statement"],
95 branch_then_field_names: &["consequence", "body"],
96 branch_else_field_names: &["alternative"],
97 branch_condition_field_names: &["condition", "value"],
98 loop_body_field_names: &["body"],
99 loop_body_kinds: &["statement_block", "expression_statement"],
100 branch_arm_kinds: &[
101 "statement_block",
102 "expression_statement",
103 "switch_case",
104 "switch_default",
105 ],
106 for_kinds: &["for_statement"],
107 foreach_kinds: &["for_in_statement"],
108 foreach_binding_extractor: Some(javascript_foreach_binding),
109 while_kinds: &["while_statement"],
110 do_kinds: &["do_statement"],
111 call_kinds: &["call_expression", "new_expression"],
112 constructor_call_kinds: &["new_expression"],
113 call_callee_field_names: &["function", "constructor"],
114 constructor_type_field_names: &["constructor"],
115 call_target_extractor: Some(javascript_call_target),
116 call_argument_field_names: &["arguments"],
117 call_argument_container_kinds: &["arguments"],
118 argument_wrapper_kinds: &["pair"],
119 argument_name_field_names: &["key"],
120 argument_value_field_names: &["value"],
121 transparent_expression_wrapper_kinds: &["parenthesized_expression"],
122 lambda_body_field_names: &["body"],
123 pseudo_call_extractor: Some(extract_ecmascript_pseudo_call),
124 syntax_event_extractor: None,
125 argument_passing_mode_extractor: None,
126 call_ref_kinds: &["call_expression", "new_expression"],
127 member_expression_kinds: &["member_expression"],
128 subscript_expression_kinds: &["subscript_expression"],
129 member_base_field_names: &["object"],
130 member_name_field_names: &["property"],
131 subscript_base_field_names: &["object"],
132 subscript_index_field_names: &["index"],
133 static_subscript_key_extractor: Some(ecmascript_static_subscript_key),
134 constructor_names: &["constructor"],
135 runtime_type_guard_operators: &["instanceof"],
136 runtime_typeof_operators: &["typeof"],
137 runtime_type_equality_operators: &["==", "==="],
138 runtime_type_wrapper_kinds: &["parenthesized_expression"],
139 value_free_unary_operators: &["typeof"],
140 assignment_kinds: &[
141 "assignment_expression",
142 "augmented_assignment_expression",
143 "variable_declarator",
144 "variable_declaration",
145 ],
146 compound_assignment_operators: &[
147 "+=", "-=", "*=", "/=", "%=", "**=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", "&&=", "||=", "??=",
148 ],
149 return_kinds: &["return_statement"],
150 throw_kinds: &["throw_statement"],
151 lambda_kinds: &["arrow_function", "function_expression"],
152 try_kinds: &["try_statement"],
153 catch_kinds: &["catch_clause"],
154 finally_kinds: &["finally_clause"],
155 break_kinds: &["break_statement"],
156 continue_kinds: &["continue_statement"],
157 control_label_field_names: &["label"],
158 yield_kinds: &["yield_expression"],
159 yield_value_field_names: &["argument"],
160 await_kinds: &["await_expression"],
161 using_kinds: &["with_statement"],
162 using_body_field_names: &["body"],
163 try_body_field_names: &["body"],
164 implicit_receiver_names: &["this"],
165 ..EMPTY_HANDLER
166};
167
168pub fn extract_ecmascript_pseudo_call(
172 node: Node<'_>,
173 file: FileId,
174 src: &[u8],
175 handler: &GrammarHandler,
176) -> Option<FlowEvent> {
177 if !matches!(node.kind(), "jsx_self_closing_element" | "jsx_opening_element") {
178 return None;
179 }
180 let name_node = node.child_by_field_name("name").or_else(|| {
181 let mut cursor = node.walk();
182 let name = node.named_children(&mut cursor).find(|child| {
183 matches!(
184 child.kind(),
185 "identifier"
186 | "nested_identifier"
187 | "member_expression"
188 | "jsx_namespace_name"
189 | "jsx_member_expression"
190 )
191 });
192 name
193 })?;
194 let component = node_text(&name_node, src).trim().to_string();
195 if component.is_empty() {
196 return None;
197 }
198
199 let mut args = Vec::new();
200 let mut cursor = node.walk();
201 for attribute in node.named_children(&mut cursor) {
202 if attribute.kind() != "jsx_attribute" {
203 continue;
204 }
205 let attr_name = attribute
206 .named_child(0)
207 .map(|name| node_text(&name, src).trim().to_string())
208 .unwrap_or_default();
209 let name = (!attr_name.is_empty()).then_some(attr_name.clone());
210 if let Some(wrapper) = attribute.named_child(1) {
211 let value = wrapper.named_child(0).unwrap_or(wrapper);
212 if let Some(argument) =
213 call_arg_from_nodes_with_handler(attribute, value, file, src, name, handler)
214 {
215 args.push(argument);
216 }
217 } else if !attr_name.is_empty() {
218 args.push(CallArg {
221 passing_mode: Default::default(),
222 span: span_of(file, &attribute),
223 name,
224 place: None,
225 source_names: Vec::new(),
226 value_text: normalize_call_name_whitespace(&attr_name),
227 });
228 }
229 }
230
231 Some(FlowEvent::Call {
232 span: span_of(file, &node),
233 receiver: None,
234 receiver_types: Vec::new(),
235 name: component,
236 call_kind: CallKind::Function,
237 args,
238 })
239}
240
241#[derive(Debug, Default, Copy, Clone)]
242pub struct JavaScriptAdapter;
243
244impl JavaScriptAdapter {
245 #[must_use]
247 pub fn new() -> Self {
248 Self
249 }
250}
251
252impl LanguageAdapter for JavaScriptAdapter {
253 fn language_id(&self) -> LanguageId {
254 LANG_ID
255 }
256 fn display_name(&self) -> &'static str {
257 "JavaScript"
258 }
259 fn file_extensions(&self) -> &'static [&'static str] {
260 &["js", "mjs", "cjs", "jsx"]
261 }
262 fn tree_sitter_language(&self) -> Result<Language, AdapterError> {
263 language_from_pack(PACK_NAME)
264 }
265 fn capabilities(&self) -> LanguageCapabilities {
266 LanguageCapabilities {
267 module_export_aliases: &["exports", "module.exports"],
268 module_default_export_names: &["default"],
269 universal_type_names: &[],
270 module_path_syntax: bonsai_lang_api::ModulePathSyntax::none(),
271 constructor_method_names: &["constructor"],
272 super_receiver_tokens: &["super"],
273 implicit_receiver_tokens: &["this"],
274 receiver_type_syntax: bonsai_lang_api::ReceiverTypeSyntax {
275 wrapper_calls: &[],
276 class_object_suffixes: &[".constructor"],
277 },
278 module_resolution_extensions: JS_TS_MODULE_RESOLUTION_EXTENSIONS,
279 ..LanguageCapabilities::partial_baseline()
280 }
281 }
282 fn extract_declarations(&self, file: FileId, ctx: &AdapterContext<'_>) -> DeclIndex {
283 let mut decl_index = decl_index_with_handler(PACK_NAME, file, ctx, &HANDLER);
284 if let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) {
285 let src = snapshot.text.as_bytes();
286 populate_ecmascript_compiler_facts(&mut decl_index, &tree, file, src);
287 apply_ecmascript_assigned_member_callable_owners(&mut decl_index, &tree, file, src);
288 apply_js_ts_commonjs_named_export_aliases(&mut decl_index, &tree, src, file);
289 }
290 let module_segments = ctx
292 .workspace_relative_path(file)
293 .map(|p| js_ts_module_segments(&p))
294 .unwrap_or_default();
295 if !module_segments.is_empty() {
296 bonsai_lang_api::apply_module_path_semantic_identity(&mut decl_index, module_segments);
297 } else {
298 bonsai_lang_api::apply_file_stem_semantic_identity(&mut decl_index, ctx);
300 }
301 if let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) {
302 apply_js_ts_default_export_aliases(&mut decl_index, &tree, snapshot.text.as_bytes(), file);
303 }
304 for decl in &mut decl_index.defs {
306 if decl.name.starts_with('#') {
307 decl.visibility = Visibility::Private;
308 }
309 }
310 if let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) {
313 let src = snapshot.text.as_bytes();
314 let bases_by_span = collect_javascript_class_bases(&tree, file, src);
315 for decl in &mut decl_index.defs {
316 if let Some(bases) = bases_by_span
317 .iter()
318 .find_map(|(span, bases)| (*span == decl.span).then_some(bases))
319 {
320 decl.bases = bases.clone();
321 }
322 }
323 rewrite_javascript_super_constructor_invocations(&mut decl_index);
324 apply_javascript_getter_property_sources(&mut decl_index, &tree, src, file);
325 }
326 for decl in &mut decl_index.defs {
327 bonsai_lang_api::normalize_call_result_assignment_sources(&mut decl.flow_events);
328 }
329 if let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) {
330 rewrite_javascript_object_destructuring_sources(
331 &mut decl_index,
332 &tree,
333 snapshot.text.as_bytes(),
334 file,
335 );
336 inject_javascript_object_literal_field_assigns(
337 &mut decl_index,
338 &tree,
339 snapshot.text.as_bytes(),
340 file,
341 );
342 apply_javascript_array_literal_types(&mut decl_index, &tree, snapshot.text.as_bytes(), file);
343 }
344 bonsai_lang_api::apply_constructor_result_type_aliases(&mut decl_index);
354 bonsai_lang_api::apply_class_field_type_aliases(&mut decl_index);
355 bonsai_lang_api::apply_call_receiver_types(&mut decl_index);
356 decl_index
357 }
358 fn extract_imports(&self, file: FileId, ctx: &AdapterContext<'_>) -> ImportIndex {
359 extract_imports_via(PACK_NAME, file, ctx, parse_imports)
360 }
361}
362
363pub fn populate_ecmascript_compiler_facts(index: &mut DeclIndex, tree: &Tree, file: FileId, src: &[u8]) {
368 mark_ecmascript_const_bindings_immutable(index, tree, file, src);
369 for branch in collect_kinds(tree, &["if_statement"]) {
370 let branch_span = span_of(file, &branch);
371 let Some(condition) = branch.child_by_field_name("condition") else {
372 continue;
373 };
374 let Some(fact) = index
375 .branch_conditions
376 .iter_mut()
377 .find(|fact| fact.branch_span == branch_span)
378 else {
379 continue;
380 };
381 fact.expression = Some(lower_ecmascript_condition_expression(condition, file, src));
382 }
383
384 for node in collect_kinds(tree, &["string", "string_literal"]) {
385 let span = span_of(file, &node);
386 let Some(value) = ecmascript_static_string_literal(node, src) else {
387 continue;
388 };
389 if let Some(literal) = index.strings.iter_mut().find(|literal| literal.span == span) {
390 literal.static_value = Some(value);
391 }
392 }
393 index.static_string_maps = ecmascript_static_string_maps(index, tree, file, src);
394 index.finite_literal_selections = ecmascript_finite_literal_selections(index, tree, file, src);
395 index.character_substitutions = ecmascript_character_substitutions(&index.defs, tree, file, src);
396 index.character_constraints = bonsai_lang_api::character_constraints_from_substitutions(
397 &index.defs,
398 &index.character_substitutions,
399 );
400 index.string_compositions = ecmascript_string_compositions(tree, file, src);
401 index.same_origin_path_constraints = ecmascript_same_origin_path_constraints(index, tree, file, src);
402 index.dynamic_key_filters = ecmascript_dynamic_key_filters(index, tree, file, src);
403 bonsai_lang_api::kit::populate_call_argument_static_values(
404 index,
405 tree,
406 file,
407 src,
408 &HANDLER,
409 ecmascript_static_scalar,
410 );
411}
412
413pub fn apply_ecmascript_assigned_member_callable_owners(
430 index: &mut DeclIndex,
431 tree: &Tree,
432 file: FileId,
433 src: &[u8],
434) {
435 #[derive(Debug)]
436 struct MemberCallable {
437 assignment_start: u64,
438 decl_index: usize,
439 owner: String,
440 owner_name: String,
441 owner_span: bonsai_common::Span,
442 property: String,
443 }
444
445 let mut members = Vec::new();
446 for assignment in collect_kinds(tree, &["assignment_expression"]) {
447 let (Some(left), Some(right)) = (
448 assignment.child_by_field_name("left"),
449 assignment.child_by_field_name("right"),
450 ) else {
451 continue;
452 };
453 if !matches!(right.kind(), "function_expression" | "generator_function")
454 || left.kind() != "member_expression"
455 {
456 continue;
457 }
458 let left_text = node_text(&left, src);
459 if left_text.trim() == "module.exports" || commonjs_named_export_member(left_text).is_some() {
460 continue;
464 }
465 let (Some(object), Some(property_node)) = (
466 left.child_by_field_name("object"),
467 left.child_by_field_name("property"),
468 ) else {
469 continue;
470 };
471 let Some(owner) = ecmascript_static_member_owner(object, src) else {
472 continue;
473 };
474 if matches!(owner.as_str(), "this" | "super") {
475 continue;
476 }
477 let property = node_text(&property_node, src).trim();
478 if property.is_empty()
479 || !matches!(
480 property_node.kind(),
481 "identifier" | "property_identifier" | "private_property_identifier"
482 )
483 {
484 continue;
485 }
486 let callable_span = span_of(file, &right);
487 let Some(decl_index) = index.defs.iter().position(|decl| {
488 decl.span == callable_span && decl.parent.is_none() && matches!(decl.kind, DeclKind::Function)
489 }) else {
490 continue;
491 };
492 let declared_name = index.defs[decl_index].name.trim();
493 let assigned_name = format!("{owner}.{property}");
494 if declared_name != property && declared_name != assigned_name {
495 continue;
496 }
497 let owner_name = owner
498 .strip_suffix(".prototype")
499 .unwrap_or(owner.as_str())
500 .to_string();
501 members.push(MemberCallable {
502 assignment_start: u64::try_from(assignment.start_byte()).unwrap_or(u64::MAX),
503 decl_index,
504 owner,
505 owner_name,
506 owner_span: span_of(file, &object),
507 property: property.to_string(),
508 });
509 }
510 members.sort_by_key(|member| member.assignment_start);
511 members.dedup_by_key(|member| member.decl_index);
512 if members.is_empty() {
513 return;
514 }
515
516 let mut next_symbol = index
517 .defs
518 .iter()
519 .map(|decl| decl.symbol.raw())
520 .max()
521 .map_or(0, |symbol| symbol.saturating_add(1));
522 let mut owners = HashMap::<String, SymbolId>::new();
523 for member in &members {
524 if owners.contains_key(&member.owner) {
525 continue;
526 }
527 if let Some(symbol) = index
528 .defs
529 .iter()
530 .find(|decl| {
531 decl.name == member.owner_name
532 && matches!(
533 decl.kind,
534 DeclKind::Class
535 | DeclKind::Struct
536 | DeclKind::Trait
537 | DeclKind::Interface
538 | DeclKind::Enum
539 )
540 })
541 .map(|decl| decl.symbol)
542 {
543 owners.insert(member.owner.clone(), symbol);
544 continue;
545 }
546
547 let symbol = SymbolId::new(next_symbol);
548 next_symbol = next_symbol.saturating_add(1);
549 owners.insert(member.owner.clone(), symbol);
550 index.defs.push(bonsai_lang_api::Decl {
551 symbol,
552 kind: DeclKind::Struct,
553 name: member.owner_name.clone(),
554 qualified_name: None,
555 module_path: Default::default(),
556 span: member.owner_span,
557 name_span: member.owner_span,
558 visibility: Visibility::Public,
559 parent: None,
560 body_span: None,
561 flow_events: Vec::new(),
562 has_implicit_returns: false,
563 params: Vec::new(),
564 param_annotations: Vec::new(),
565 param_default_calls: Vec::new(),
566 type_aliases: Vec::new(),
567 bases: Vec::new(),
568 receiver_param_index: None,
569 receiver_field_writes: Vec::new(),
570 receiver_field_initializers: Vec::new(),
571 implicit_receiver_names: Vec::new(),
572 receiver_state_sources: Vec::new(),
573 return_type: None,
574 is_variadic: false,
575 });
576 }
577
578 for member in members {
579 let Some(owner) = owners.get(&member.owner).copied() else {
580 continue;
581 };
582 let decl = &mut index.defs[member.decl_index];
583 decl.kind = DeclKind::Method;
584 decl.name = member.property;
585 decl.qualified_name = None;
586 decl.parent = Some(owner);
587 if !decl.implicit_receiver_names.iter().any(|name| name == "this") {
588 decl.implicit_receiver_names.push("this".to_string());
589 }
590 }
591}
592
593fn ecmascript_static_member_owner(node: Node<'_>, src: &[u8]) -> Option<String> {
594 match node.kind() {
595 "identifier" | "property_identifier" => {
596 let name = node_text(&node, src).trim();
597 (!name.is_empty()).then(|| name.to_string())
598 }
599 "member_expression" => {
600 let object = node.child_by_field_name("object")?;
601 let property = node.child_by_field_name("property")?;
602 if !matches!(
603 property.kind(),
604 "identifier" | "property_identifier" | "private_property_identifier"
605 ) {
606 return None;
607 }
608 let owner = ecmascript_static_member_owner(object, src)?;
609 let property = node_text(&property, src).trim();
610 (!property.is_empty()).then(|| format!("{owner}.{property}"))
611 }
612 _ => None,
613 }
614}
615
616fn mark_ecmascript_const_bindings_immutable(index: &mut DeclIndex, tree: &Tree, file: FileId, src: &[u8]) {
623 for declaration in collect_kinds(tree, &["lexical_declaration"]) {
624 let is_const = (0..declaration.child_count())
625 .filter_map(|index| declaration.child(index as u32))
626 .any(|child| child.kind() == "const");
627 if !is_const {
628 continue;
629 }
630 let mut cursor = declaration.walk();
631 for declarator in declaration
632 .named_children(&mut cursor)
633 .filter(|node| node.kind() == "variable_declarator")
634 {
635 let (Some(name), Some(value)) = (
636 declarator.child_by_field_name("name"),
637 declarator.child_by_field_name("value"),
638 ) else {
639 continue;
640 };
641 if name.kind() != "identifier" {
642 continue;
643 }
644 let target = node_text(&name, src).trim();
645 let value_span = span_of(file, &value);
646 let declarator_span = span_of(file, &declarator);
647 if target.is_empty() {
648 continue;
649 }
650 for fact in index.assignment_values.iter_mut().filter(|fact| {
651 fact.target.as_deref() == Some(target)
652 && fact.value_span == value_span
653 && fact.assignment_span.start <= declarator_span.start
654 && declarator_span.end <= fact.assignment_span.end
655 }) {
656 fact.target_is_immutable = true;
657 }
658 }
659 }
660}
661
662fn ecmascript_string_compositions(tree: &Tree, file: FileId, src: &[u8]) -> Vec<StringCompositionFact> {
666 let mut facts = Vec::new();
667 for declarator in collect_kinds(tree, &["variable_declarator"]) {
668 let (Some(name), Some(value)) = (
669 declarator.child_by_field_name("name"),
670 declarator.child_by_field_name("value"),
671 ) else {
672 continue;
673 };
674 if name.kind() != "identifier" {
675 continue;
676 }
677 let mut parts = Vec::new();
678 if lower_ecmascript_string_composition(value, file, src, &mut parts) && parts.len() > 1 {
679 facts.push(StringCompositionFact {
680 container_span: span_of(file, &declarator),
681 value_span: span_of(file, &value),
682 target: Some(node_text(&name, src).trim().to_string()),
683 parts,
684 });
685 }
686 }
687 for return_node in collect_kinds(tree, &["return_statement"]) {
688 let Some(value) = return_node.named_child(0) else {
689 continue;
690 };
691 let mut parts = Vec::new();
692 if lower_ecmascript_string_composition(value, file, src, &mut parts) && parts.len() > 1 {
693 facts.push(StringCompositionFact {
694 container_span: span_of(file, &return_node),
695 value_span: span_of(file, &value),
696 target: None,
697 parts,
698 });
699 }
700 }
701 for value in collect_kinds(tree, &["binary_expression", "template_string"]) {
706 let mut parts = Vec::new();
707 if lower_ecmascript_string_composition(value, file, src, &mut parts) && parts.len() > 1 {
708 let value_span = span_of(file, &value);
709 facts.push(StringCompositionFact {
710 container_span: value_span,
711 value_span,
712 target: None,
713 parts,
714 });
715 }
716 }
717 facts.sort_by_key(|fact| {
718 (
719 fact.container_span.start,
720 fact.container_span.end,
721 fact.value_span.start,
722 fact.value_span.end,
723 )
724 });
725 facts.dedup();
726 facts
727}
728
729fn lower_ecmascript_string_composition(
730 mut node: Node<'_>,
731 file: FileId,
732 src: &[u8],
733 out: &mut Vec<StringCompositionPart>,
734) -> bool {
735 node = unwrap_ecmascript_expression(node);
736 if let Some(value) = ecmascript_static_string_literal(node, src) {
737 out.push(StringCompositionPart::Literal { value });
738 return true;
739 }
740 if let Some(place) = ecmascript_exact_place(node, src) {
741 out.push(StringCompositionPart::Place { place });
742 return true;
743 }
744 if node.kind() == "call_expression" {
745 let Some(function) = node.child_by_field_name("function") else {
746 return false;
747 };
748 out.push(StringCompositionPart::Call {
749 span: span_of(file, &function),
750 });
751 return true;
752 }
753 if node.kind() == "template_string" {
754 let mut cursor = node.walk();
755 for child in node.named_children(&mut cursor) {
756 match child.kind() {
757 "string_fragment" => {
758 let Some(value) = decode_ecmascript_string_contents(node_text(&child, src), '`') else {
759 return false;
760 };
761 out.push(StringCompositionPart::Literal { value });
762 }
763 "template_substitution" => {
764 let Some(expression) = child.named_child(0) else {
765 return false;
766 };
767 if !lower_ecmascript_string_composition(expression, file, src, out) {
768 return false;
769 }
770 }
771 "comment" => {}
772 _ => return false,
773 }
774 }
775 return !out.is_empty();
776 }
777 if node.kind() != "binary_expression" {
778 return false;
779 }
780 let (Some(left), Some(right)) = (
781 node.child_by_field_name("left"),
782 node.child_by_field_name("right"),
783 ) else {
784 return false;
785 };
786 let operator = src
787 .get(left.end_byte()..right.start_byte())
788 .and_then(|bytes| std::str::from_utf8(bytes).ok())
789 .map(str::trim);
790 operator == Some("+")
791 && lower_ecmascript_string_composition(left, file, src, out)
792 && lower_ecmascript_string_composition(right, file, src, out)
793}
794
795fn ecmascript_exact_place(node: Node<'_>, src: &[u8]) -> Option<String> {
796 let node = unwrap_ecmascript_expression(node);
797 match node.kind() {
798 "identifier" | "this" => {
799 let place = node_text(&node, src).trim();
800 (!place.is_empty()).then(|| place.to_string())
801 }
802 "member_expression" => {
803 let object = ecmascript_exact_place(node.child_by_field_name("object")?, src)?;
804 let property = node.child_by_field_name("property")?;
805 if property.kind() == "private_property_identifier" {
806 return None;
807 }
808 let property = node_text(&property, src).trim();
809 (!property.is_empty()).then(|| format!("{object}.{property}"))
810 }
811 _ => None,
812 }
813}
814
815fn ecmascript_same_origin_path_constraints(
816 index: &DeclIndex,
817 tree: &Tree,
818 file: FileId,
819 src: &[u8],
820) -> Vec<SameOriginPathConstraintFact> {
821 let mut facts = Vec::new();
822 let mut guarded_expressions = collect_kinds(tree, &["return_statement"])
823 .into_iter()
824 .filter_map(|return_node| return_node.named_child(0))
825 .collect::<Vec<_>>();
826 guarded_expressions.extend(
827 collect_kinds(tree, &["arrow_function"])
828 .into_iter()
829 .filter_map(|arrow| arrow.child_by_field_name("body"))
830 .filter(|body| body.kind() != "statement_block"),
831 );
832 for expression in guarded_expressions {
833 let return_span = span_of(file, &expression);
834 let Some(decl) = index
835 .defs
836 .iter()
837 .filter(|decl| {
838 matches!(decl.kind, DeclKind::Function | DeclKind::Method)
839 && decl.span.start <= return_span.start
840 && return_span.end <= decl.span.end
841 })
842 .min_by_key(|decl| decl.span.len())
843 else {
844 continue;
845 };
846 let expression = unwrap_ecmascript_expression(expression);
847 if expression.kind() != "ternary_expression" {
848 continue;
849 }
850 let (Some(condition), Some(consequence), Some(alternative)) = (
851 expression.child_by_field_name("condition"),
852 expression.child_by_field_name("consequence"),
853 expression.child_by_field_name("alternative"),
854 ) else {
855 continue;
856 };
857 for (input_param_index, parameter) in decl.params.iter().enumerate() {
858 if !ecmascript_expression_is_exact_place(consequence, parameter, src)
859 || ecmascript_static_string_literal(alternative, src).as_deref() != Some("/")
860 {
861 continue;
862 }
863 let mut terms = Vec::new();
864 ecmascript_collect_logical_terms(condition, "&&", src, &mut terms);
865 let requires_absolute_path = terms
866 .iter()
867 .any(|term| ecmascript_starts_with_literal(*term, parameter, "/", false, src));
868 let rejects_scheme_relative_path = terms
869 .iter()
870 .any(|term| ecmascript_starts_with_literal(*term, parameter, "//", true, src));
871 if requires_absolute_path && rejects_scheme_relative_path {
872 facts.push(SameOriginPathConstraintFact {
876 function_span: decl.span,
877 guard_span: span_of(file, &expression),
878 input_place: parameter.clone(),
879 input_param_index: Some(input_param_index),
880 provider_call: None,
881 rejects_scheme: true,
882 rejects_authority: true,
883 requires_absolute_path,
884 rejects_scheme_relative_path,
885 });
886 }
887 }
888 }
889 facts.sort_by_key(|fact| (fact.function_span.start, fact.guard_span.start));
890 facts.dedup();
891 facts
892}
893
894fn ecmascript_collect_logical_terms<'tree>(
895 expression: Node<'tree>,
896 operator: &str,
897 src: &[u8],
898 out: &mut Vec<Node<'tree>>,
899) {
900 let expression = unwrap_ecmascript_expression(expression);
901 let operands = (
902 expression.child_by_field_name("left"),
903 expression.child_by_field_name("right"),
904 );
905 if expression.kind() == "binary_expression"
906 && operands.0.zip(operands.1).is_some_and(|(left, right)| {
907 src.get(left.end_byte()..right.start_byte())
908 .and_then(|bytes| std::str::from_utf8(bytes).ok())
909 .is_some_and(|value| value.trim() == operator)
910 })
911 {
912 let (Some(left), Some(right)) = operands else {
913 return;
914 };
915 ecmascript_collect_logical_terms(left, operator, src, out);
916 ecmascript_collect_logical_terms(right, operator, src, out);
917 } else {
918 out.push(expression);
919 }
920}
921
922fn ecmascript_expression_is_exact_place(expression: Node<'_>, place: &str, src: &[u8]) -> bool {
923 let expression = unwrap_ecmascript_expression(expression);
924 expression.kind() == "identifier" && node_text(&expression, src).trim() == place
925}
926
927fn ecmascript_starts_with_literal(
928 expression: Node<'_>,
929 receiver: &str,
930 literal: &str,
931 negated: bool,
932 src: &[u8],
933) -> bool {
934 let expression = unwrap_ecmascript_expression(expression);
935 let call = if negated {
936 if expression.kind() != "unary_expression" {
937 return false;
938 }
939 let Some(argument) = expression.child_by_field_name("argument") else {
940 return false;
941 };
942 if src
943 .get(expression.start_byte()..argument.start_byte())
944 .and_then(|bytes| std::str::from_utf8(bytes).ok())
945 .is_none_or(|prefix| prefix.trim() != "!")
946 {
947 return false;
948 }
949 unwrap_ecmascript_expression(argument)
950 } else {
951 expression
952 };
953 if call.kind() != "call_expression" {
954 return false;
955 }
956 let Some(function) = call.child_by_field_name("function") else {
957 return false;
958 };
959 if function.kind() != "member_expression"
960 || function
961 .child_by_field_name("object")
962 .is_none_or(|object| !ecmascript_expression_is_exact_place(object, receiver, src))
963 || function
964 .child_by_field_name("property")
965 .is_none_or(|property| node_text(&property, src).trim() != "startsWith")
966 {
967 return false;
968 }
969 let arguments = ecmascript_call_arguments(call);
970 let [argument] = arguments.as_slice() else {
971 return false;
972 };
973 ecmascript_static_string_literal(*argument, src).as_deref() == Some(literal)
974}
975
976fn ecmascript_dynamic_key_filters(
977 index: &DeclIndex,
978 tree: &Tree,
979 file: FileId,
980 src: &[u8],
981) -> Vec<DynamicKeyFilterFact> {
982 let mut facts = Vec::new();
983 for function in collect_kinds(tree, &["function_declaration"]) {
984 let function_span = span_of(file, &function);
985 let Some(decl) = index.defs.iter().find(|decl| decl.span == function_span) else {
986 continue;
987 };
988 let Some(body) = function.child_by_field_name("body") else {
989 continue;
990 };
991 if let Some(fact) = ecmascript_property_path_filter(tree, function, body, decl, file, src) {
992 facts.push(fact);
993 }
994 for block in named_descendants_of_kind(body, "statement_block") {
995 let Some(fact) =
996 ecmascript_dynamic_key_filter_in_block(tree, function, body, block, decl, file, src)
997 else {
998 continue;
999 };
1000 facts.push(fact);
1001 }
1002 }
1003 facts.sort_by_key(|fact| (fact.function_span.start, fact.guard_span.start));
1004 facts.dedup_by_key(|fact| (fact.function_span, fact.guard_span));
1005 facts
1006}
1007
1008fn ecmascript_dynamic_key_filter_in_block(
1009 tree: &Tree,
1010 function: Node<'_>,
1011 function_body: Node<'_>,
1012 block: Node<'_>,
1013 decl: &bonsai_lang_api::Decl,
1014 file: FileId,
1015 src: &[u8],
1016) -> Option<DynamicKeyFilterFact> {
1017 let statements = named_children(block);
1018 let [output_decl, loop_node, return_node] = statements.as_slice() else {
1019 return None;
1020 };
1021 if output_decl.kind() != "lexical_declaration"
1022 || loop_node.kind() != "for_in_statement"
1023 || return_node.kind() != "return_statement"
1024 {
1025 return None;
1026 }
1027
1028 let (output_name, output_value) = single_variable_declaration(*output_decl, src)?;
1029 if output_value.kind() != "object" || !named_children(output_value).is_empty() {
1030 return None;
1031 }
1032 let returned = return_node.named_child(0)?;
1033 if returned.kind() != "identifier" || node_text(&returned, src).trim() != output_name {
1034 return None;
1035 }
1036
1037 let left = loop_node.child_by_field_name("left")?;
1038 let bindings = named_children(left);
1039 let [key_node, value_node] = bindings.as_slice() else {
1040 return None;
1041 };
1042 if key_node.kind() != "identifier" || value_node.kind() != "identifier" {
1043 return None;
1044 }
1045 let key = node_text(key_node, src).trim();
1046 let value = node_text(value_node, src).trim();
1047
1048 let iteration = loop_node.child_by_field_name("right")?;
1049 let iteration_call = unwrap_ecmascript_expression(iteration);
1050 let (iteration_receiver, iteration_method) = ecmascript_member_call(iteration_call, src)?;
1051 if iteration_receiver != "Object" || iteration_method != "entries" {
1052 return None;
1053 }
1054 let iteration_args = ecmascript_call_arguments(iteration_call);
1055 let [iteration_input] = iteration_args.as_slice() else {
1056 return None;
1057 };
1058 let input = unwrap_ecmascript_expression(*iteration_input);
1059 if input.kind() != "identifier" {
1060 return None;
1061 }
1062 let input_name = node_text(&input, src).trim();
1063 let input_param_index = decl.params.iter().position(|param| param == input_name)?;
1064
1065 let loop_body = loop_node.child_by_field_name("body")?;
1066 let loop_statements = named_children(loop_body);
1067 let [guard, write_statement] = loop_statements.as_slice() else {
1068 return None;
1069 };
1070 if guard.kind() != "if_statement" || write_statement.kind() != "expression_statement" {
1071 return None;
1072 }
1073 if guard.child_by_field_name("alternative").is_some()
1074 || guard.child_by_field_name("consequence")?.kind() != "continue_statement"
1075 {
1076 return None;
1077 }
1078 let condition = unwrap_ecmascript_expression(guard.child_by_field_name("condition")?);
1079 let (collection, membership_check) = ecmascript_member_call(condition, src)?;
1080 let membership_args = ecmascript_call_arguments(condition);
1081 let [membership_subject] = membership_args.as_slice() else {
1082 return None;
1083 };
1084 if membership_subject.kind() != "identifier" || node_text(membership_subject, src).trim() != key {
1085 return None;
1086 }
1087
1088 let write = write_statement.named_child(0)?;
1089 if write.kind() != "assignment_expression" {
1090 return None;
1091 }
1092 let target = write.child_by_field_name("left")?;
1093 if target.kind() != "subscript_expression"
1094 || target
1095 .child_by_field_name("object")
1096 .is_none_or(|node| node.kind() != "identifier" || node_text(&node, src).trim() != output_name)
1097 || target
1098 .child_by_field_name("index")
1099 .is_none_or(|node| node.kind() != "identifier" || node_text(&node, src).trim() != key)
1100 {
1101 return None;
1102 }
1103 let recursive_call = unwrap_ecmascript_expression(write.child_by_field_name("right")?);
1104 if recursive_call.kind() != "call_expression" {
1105 return None;
1106 }
1107 let recursive_function = recursive_call.child_by_field_name("function")?;
1108 let recursive_args = ecmascript_call_arguments(recursive_call);
1109 let [recursive_value] = recursive_args.as_slice() else {
1110 return None;
1111 };
1112 if recursive_function.kind() != "identifier"
1113 || node_text(&recursive_function, src).trim() != decl.name
1114 || recursive_value.kind() != "identifier"
1115 || node_text(recursive_value, src).trim() != value
1116 {
1117 return None;
1118 }
1119
1120 if function.parent().is_none_or(|parent| {
1124 parent.kind() != "program"
1125 && !(parent.kind() == "export_statement"
1126 && parent
1127 .parent()
1128 .is_some_and(|grandparent| grandparent.kind() == "program"))
1129 }) {
1130 return None;
1131 }
1132 let (collection_constructor, rejected_exact_values) =
1133 ecmascript_exact_top_level_collection(tree, function, function_body, collection, src)?;
1134
1135 Some(DynamicKeyFilterFact {
1136 function_span: decl.span,
1137 guard_span: span_of(file, guard),
1138 input_param_index,
1139 output_place: Some(output_name.to_string()),
1140 collection_constructor,
1141 membership_check: membership_check.to_string(),
1142 rejected_exact_values,
1143 recursive: true,
1144 })
1145}
1146
1147fn ecmascript_property_path_filter(
1148 tree: &Tree,
1149 function: Node<'_>,
1150 function_body: Node<'_>,
1151 decl: &bonsai_lang_api::Decl,
1152 file: FileId,
1153 src: &[u8],
1154) -> Option<DynamicKeyFilterFact> {
1155 if function.parent().is_none_or(|parent| {
1156 parent.kind() != "program"
1157 && !(parent.kind() == "export_statement"
1158 && parent
1159 .parent()
1160 .is_some_and(|grandparent| grandparent.kind() == "program"))
1161 }) {
1162 return None;
1163 }
1164 let bindings = ecmascript_bindings(tree, src);
1165 let statements = named_children(function_body);
1166 for (declaration_index, declaration) in statements.iter().enumerate() {
1167 if declaration.kind() != "lexical_declaration"
1168 || declaration
1169 .child(0)
1170 .is_none_or(|keyword| keyword.kind() != "const")
1171 {
1172 continue;
1173 }
1174 let Some((segments, initializer)) = single_variable_declaration(*declaration, src) else {
1175 continue;
1176 };
1177 let Some(input_param_index) =
1178 ecmascript_property_path_split_input(initializer, &decl.params, &bindings, src)
1179 else {
1180 continue;
1181 };
1182 if ecmascript_binding_is_mutated(function_body, segments, declaration.end_byte(), src) {
1183 continue;
1184 }
1185 for guard in statements.iter().skip(declaration_index + 1) {
1186 if guard.kind() != "if_statement" || guard.child_by_field_name("alternative").is_some() {
1187 continue;
1188 }
1189 let condition = unwrap_ecmascript_expression(guard.child_by_field_name("condition")?);
1190 let Some((collection, membership_check)) =
1191 ecmascript_every_segment_is_denylisted(condition, segments, src)
1192 else {
1193 continue;
1194 };
1195 let (collection_constructor, rejected_exact_values) =
1196 ecmascript_exact_top_level_collection(tree, function, function_body, collection, src)?;
1197 return Some(DynamicKeyFilterFact {
1198 function_span: decl.span,
1199 guard_span: span_of(file, guard),
1200 input_param_index,
1201 output_place: Some(segments.to_string()),
1202 collection_constructor,
1203 membership_check: membership_check.to_string(),
1204 rejected_exact_values,
1205 recursive: false,
1206 });
1207 }
1208 }
1209 None
1210}
1211
1212fn ecmascript_property_path_split_input(
1213 initializer: Node<'_>,
1214 params: &[String],
1215 bindings: &EcmascriptBindings<'_>,
1216 src: &[u8],
1217) -> Option<usize> {
1218 let mut split = unwrap_ecmascript_expression(initializer);
1219 if split.kind() == "call_expression" {
1220 let function = split.child_by_field_name("function")?;
1221 if function.kind() == "member_expression"
1222 && function
1223 .child_by_field_name("property")
1224 .is_some_and(|property| node_text(&property, src).trim() == "filter")
1225 {
1226 let callback = ecmascript_call_arguments(split).first().copied()?;
1227 if !ecmascript_nonempty_segment_filter(callback, src) {
1228 return None;
1229 }
1230 split = unwrap_ecmascript_expression(function.child_by_field_name("object")?);
1231 }
1232 }
1233 if split.kind() != "call_expression" {
1234 return None;
1235 }
1236 let function = split.child_by_field_name("function")?;
1237 if function.kind() != "member_expression"
1238 || function
1239 .child_by_field_name("property")
1240 .is_none_or(|property| node_text(&property, src).trim() != "split")
1241 {
1242 return None;
1243 }
1244 let delimiter = ecmascript_call_arguments(split).first().copied()?;
1245 let pattern = delimiter.child_by_field_name("pattern")?;
1246 let pattern = node_text(&pattern, src);
1247 let character_class = pattern.strip_suffix('+').unwrap_or(pattern);
1248 let characters = ecmascript_exact_regex_characters_from_pattern(character_class)?;
1249 if ![".", "[", "]"]
1250 .iter()
1251 .all(|required| characters.iter().any(|character| character == required))
1252 {
1253 return None;
1254 }
1255 let input = unwrap_ecmascript_expression(function.child_by_field_name("object")?);
1256 let input_name = if input.kind() == "identifier" {
1257 node_text(&input, src).trim()
1258 } else if input.kind() == "call_expression" {
1259 let callee = input.child_by_field_name("function")?;
1260 if callee.kind() != "identifier"
1261 || node_text(&callee, src).trim() != "String"
1262 || bindings.by_name.contains_key("String")
1263 {
1264 return None;
1265 }
1266 let arguments = ecmascript_call_arguments(input);
1267 let [argument] = arguments.as_slice() else {
1268 return None;
1269 };
1270 if argument.kind() != "identifier" {
1271 return None;
1272 }
1273 node_text(argument, src).trim()
1274 } else {
1275 return None;
1276 };
1277 params.iter().position(|param| param == input_name)
1278}
1279
1280fn ecmascript_exact_regex_characters_from_pattern(pattern: &str) -> Option<Vec<String>> {
1281 let inner = pattern.strip_prefix('[')?.strip_suffix(']')?;
1282 if inner.starts_with('^') || inner.is_empty() {
1283 return None;
1284 }
1285 let mut characters = Vec::new();
1286 let mut input = inner.chars();
1287 while let Some(character) = input.next() {
1288 if character == '-' {
1289 return None;
1290 }
1291 let character = if character == '\\' {
1292 match input.next()? {
1293 escaped if !escaped.is_ascii_alphanumeric() => escaped,
1294 _ => return None,
1295 }
1296 } else {
1297 character
1298 };
1299 characters.push(character.to_string());
1300 }
1301 characters.sort();
1302 characters.dedup();
1303 Some(characters)
1304}
1305
1306fn ecmascript_nonempty_segment_filter(callback: Node<'_>, src: &[u8]) -> bool {
1307 let Some((parameter, body)) = ecmascript_arrow_parts(callback, src) else {
1308 return false;
1309 };
1310 let body = unwrap_ecmascript_expression(body);
1311 let (Some(left), Some(right)) = (
1312 body.child_by_field_name("left"),
1313 body.child_by_field_name("right"),
1314 ) else {
1315 return false;
1316 };
1317 let operator = src
1318 .get(left.end_byte()..right.start_byte())
1319 .and_then(|bytes| std::str::from_utf8(bytes).ok())
1320 .map(str::trim);
1321 let Some(object) = left.child_by_field_name("object") else {
1322 return false;
1323 };
1324 let Some(property) = left.child_by_field_name("property") else {
1325 return false;
1326 };
1327 left.kind() == "member_expression"
1328 && node_text(&object, src).trim() == parameter
1329 && node_text(&property, src).trim() == "length"
1330 && operator == Some(">")
1331 && node_text(&right, src).trim() == "0"
1332}
1333
1334fn ecmascript_every_segment_is_denylisted<'a>(
1335 condition: Node<'a>,
1336 segments: &str,
1337 src: &'a [u8],
1338) -> Option<(&'a str, &'a str)> {
1339 let function = condition.child_by_field_name("function")?;
1340 if condition.kind() != "call_expression"
1341 || function.kind() != "member_expression"
1342 || function
1343 .child_by_field_name("object")
1344 .is_none_or(|object| node_text(&object, src).trim() != segments)
1345 || function
1346 .child_by_field_name("property")
1347 .is_none_or(|property| node_text(&property, src).trim() != "some")
1348 {
1349 return None;
1350 }
1351 let condition_arguments = ecmascript_call_arguments(condition);
1352 let callback = condition_arguments.first().copied()?;
1353 let (parameter, body) = ecmascript_arrow_parts(callback, src)?;
1354 let (collection, membership_check) = ecmascript_member_call(unwrap_ecmascript_expression(body), src)?;
1355 let membership_arguments = ecmascript_call_arguments(unwrap_ecmascript_expression(body));
1356 let [subject] = membership_arguments.as_slice() else {
1357 return None;
1358 };
1359 (subject.kind() == "identifier" && node_text(subject, src).trim() == parameter)
1360 .then_some((collection, membership_check))
1361}
1362
1363fn ecmascript_binding_is_mutated(body: Node<'_>, binding: &str, after: usize, src: &[u8]) -> bool {
1364 let mut stack = vec![body];
1365 while let Some(node) = stack.pop() {
1366 if node.start_byte() > after {
1367 if matches!(
1368 node.kind(),
1369 "assignment_expression" | "augmented_assignment_expression"
1370 ) && node.child_by_field_name("left").is_some_and(|left| {
1371 node_text(&left, src).trim() == binding
1372 || left
1373 .child_by_field_name("object")
1374 .is_some_and(|object| node_text(&object, src).trim() == binding)
1375 }) {
1376 return true;
1377 }
1378 if node.kind() == "call_expression" {
1379 if let Some(function) = node.child_by_field_name("function") {
1380 if function.kind() == "member_expression"
1381 && function
1382 .child_by_field_name("object")
1383 .is_some_and(|object| node_text(&object, src).trim() == binding)
1384 && function
1385 .child_by_field_name("property")
1386 .is_some_and(|property| node_text(&property, src).trim() != "some")
1387 {
1388 return true;
1389 }
1390 }
1391 }
1392 }
1393 let mut cursor = node.walk();
1394 stack.extend(node.named_children(&mut cursor));
1395 }
1396 false
1397}
1398
1399fn ecmascript_exact_top_level_collection(
1400 tree: &Tree,
1401 function: Node<'_>,
1402 function_body: Node<'_>,
1403 collection: &str,
1404 src: &[u8],
1405) -> Option<(String, Vec<String>)> {
1406 if named_descendants_of_kind(function_body, "variable_declarator")
1407 .iter()
1408 .any(|declarator| {
1409 declarator
1410 .child_by_field_name("name")
1411 .is_some_and(|name| name.kind() == "identifier" && node_text(&name, src).trim() == collection)
1412 })
1413 {
1414 return None;
1415 }
1416 let mut candidates = Vec::new();
1417 for declarator in collect_kinds(tree, &["variable_declarator"]) {
1418 if declarator.start_byte() >= function.start_byte()
1419 || declarator
1420 .parent()
1421 .and_then(|parent| parent.parent())
1422 .is_none_or(|parent| parent.kind() != "program")
1423 {
1424 continue;
1425 }
1426 let Some(name) = declarator.child_by_field_name("name") else {
1427 continue;
1428 };
1429 if name.kind() != "identifier" || node_text(&name, src).trim() != collection {
1430 continue;
1431 }
1432 let declaration = declarator.parent()?;
1433 if declaration.child(0).is_none_or(|token| token.kind() != "const") {
1434 continue;
1435 }
1436 let value = declarator.child_by_field_name("value")?;
1437 if value.kind() != "new_expression" {
1438 continue;
1439 }
1440 let constructor = value.child_by_field_name("constructor")?;
1441 if constructor.kind() != "identifier" {
1442 continue;
1443 }
1444 let arguments = ecmascript_call_arguments(value);
1445 let [array] = arguments.as_slice() else {
1446 continue;
1447 };
1448 if array.kind() != "array" {
1449 continue;
1450 }
1451 let items = named_children(*array);
1452 if items.is_empty() || items.iter().any(|item| item.kind() == "spread_element") {
1453 continue;
1454 }
1455 let values: Option<Vec<_>> = items
1456 .iter()
1457 .map(|item| ecmascript_static_string_literal(*item, src))
1458 .collect();
1459 let Some(values) = values else {
1460 continue;
1461 };
1462 candidates.push((node_text(&constructor, src).trim().to_string(), values));
1463 }
1464 let [candidate] = candidates.as_slice() else {
1465 return None;
1466 };
1467 if collect_kinds(
1468 tree,
1469 &["assignment_expression", "augmented_assignment_expression"],
1470 )
1471 .iter()
1472 .any(|assignment| {
1473 assignment
1474 .child_by_field_name("left")
1475 .is_some_and(|left| left.kind() == "identifier" && node_text(&left, src).trim() == collection)
1476 }) {
1477 return None;
1478 }
1479 Some(candidate.clone())
1480}
1481
1482fn named_descendants_of_kind<'tree>(root: Node<'tree>, kind: &str) -> Vec<Node<'tree>> {
1483 let mut found = Vec::new();
1484 let mut stack = vec![root];
1485 while let Some(node) = stack.pop() {
1486 let mut cursor = node.walk();
1487 for child in node.named_children(&mut cursor) {
1488 if child.kind() == kind {
1489 found.push(child);
1490 }
1491 stack.push(child);
1492 }
1493 }
1494 found
1495}
1496
1497fn named_children(node: Node<'_>) -> Vec<Node<'_>> {
1498 let mut cursor = node.walk();
1499 node.named_children(&mut cursor).collect()
1500}
1501
1502fn single_variable_declaration<'tree, 'src>(
1503 declaration: Node<'tree>,
1504 src: &'src [u8],
1505) -> Option<(&'src str, Node<'tree>)> {
1506 let declarators: Vec<_> = named_children(declaration)
1507 .into_iter()
1508 .filter(|node| node.kind() == "variable_declarator")
1509 .collect();
1510 let [declarator] = declarators.as_slice() else {
1511 return None;
1512 };
1513 let name = declarator.child_by_field_name("name")?;
1514 let value = declarator.child_by_field_name("value")?;
1515 (name.kind() == "identifier").then_some((node_text(&name, src).trim(), value))
1516}
1517
1518fn ecmascript_member_call<'a>(call: Node<'_>, src: &'a [u8]) -> Option<(&'a str, &'a str)> {
1519 if call.kind() != "call_expression" {
1520 return None;
1521 }
1522 let function = call.child_by_field_name("function")?;
1523 if function.kind() != "member_expression" {
1524 return None;
1525 }
1526 let object = function.child_by_field_name("object")?;
1527 let property = function.child_by_field_name("property")?;
1528 if object.kind() != "identifier" || property.kind() != "property_identifier" {
1529 return None;
1530 }
1531 Some((node_text(&object, src).trim(), node_text(&property, src).trim()))
1532}
1533
1534fn ecmascript_static_string_maps(
1535 index: &DeclIndex,
1536 tree: &Tree,
1537 file: FileId,
1538 src: &[u8],
1539) -> Vec<StaticStringMapFact> {
1540 let mut maps = Vec::new();
1541 for declarator in collect_kinds(tree, &["variable_declarator"]) {
1542 let Some(target_node) = declarator.child_by_field_name("name") else {
1543 continue;
1544 };
1545 let Some(value_node) = declarator.child_by_field_name("value") else {
1546 continue;
1547 };
1548 if target_node.kind() != "identifier" || value_node.kind() != "object" {
1549 continue;
1550 }
1551 let target = node_text(&target_node, src).trim();
1552 if target.is_empty() {
1553 continue;
1554 }
1555 let Some(entries) = ecmascript_exact_string_map_entries(value_node, src) else {
1556 continue;
1557 };
1558 let value_span = span_of(file, &value_node);
1559 let Some(assignment_span) = index.assignment_values.iter().find_map(|fact| {
1560 (fact.value_span == value_span && fact.target.as_deref() == Some(target))
1561 .then_some(fact.assignment_span)
1562 }) else {
1563 continue;
1564 };
1565 maps.push(StaticStringMapFact {
1566 assignment_span,
1567 target: target.to_string(),
1568 entries,
1569 });
1570 }
1571 maps.sort_by_key(|fact| (fact.assignment_span.start, fact.assignment_span.end));
1572 maps
1573}
1574
1575fn ecmascript_finite_literal_selections(
1576 index: &DeclIndex,
1577 tree: &Tree,
1578 file: FileId,
1579 src: &[u8],
1580) -> Vec<FiniteLiteralSelectionFact> {
1581 let bindings = ecmascript_bindings(tree, src);
1582 if !bindings
1583 .bindings
1584 .iter()
1585 .any(|binding| binding.finite_map.is_some())
1586 {
1587 return Vec::new();
1588 }
1589 let mut selections = Vec::new();
1590 for node in collect_kinds(tree, &["call_expression", "subscript_expression"]) {
1591 let (map_target, object) = if node.kind() == "call_expression" {
1592 let Some(function) = node.child_by_field_name("function") else {
1593 continue;
1594 };
1595 if function.kind() != "member_expression" {
1596 continue;
1597 }
1598 let Some(object) = function.child_by_field_name("object") else {
1599 continue;
1600 };
1601 let Some(property) = function.child_by_field_name("property") else {
1602 continue;
1603 };
1604 if object.kind() != "identifier" || node_text(&property, src).trim() != "get" {
1605 continue;
1606 }
1607 (node_text(&object, src).trim(), object)
1608 } else {
1609 let Some(object) = node.child_by_field_name("object") else {
1610 continue;
1611 };
1612 if object.kind() != "identifier" {
1613 continue;
1614 }
1615 (node_text(&object, src).trim(), object)
1616 };
1617 let Some(binding) = bindings.resolve(map_target, object.start_byte(), object.end_byte()) else {
1618 continue;
1619 };
1620 if binding.finite_map.is_none()
1621 || binding.initializer.end_byte() > node.start_byte()
1622 || bindings.unsafe_bindings.contains(&binding.declaration.id())
1623 {
1624 continue;
1625 }
1626 let selection_span = span_of(file, &node);
1627 let Some(fact) = bonsai_lang_api::kit::finite_literal_selection_fact_for_span(
1628 index,
1629 tree,
1630 selection_span,
1631 |value_node| ecmascript_expression_is_finite_selection(value_node, node, src, &bindings),
1632 ) else {
1633 continue;
1634 };
1635 selections.push(fact);
1636 }
1637 bonsai_lang_api::kit::sort_dedup_finite_literal_selections(&mut selections);
1638 selections
1639}
1640
1641#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1642enum EcmascriptFiniteMapKind {
1643 Object,
1644 Map,
1645}
1646
1647#[derive(Copy, Clone, Debug)]
1648struct EcmascriptBinding<'tree> {
1649 name: &'tree str,
1650 declaration: Node<'tree>,
1651 initializer: Node<'tree>,
1652 scope: Node<'tree>,
1653 finite_map: Option<EcmascriptFiniteMapKind>,
1654}
1655
1656struct EcmascriptBindings<'tree> {
1657 bindings: Vec<EcmascriptBinding<'tree>>,
1658 by_name: HashMap<String, Vec<usize>>,
1659 unsafe_bindings: HashSet<usize>,
1660 object_intrinsic_unshadowed: bool,
1661}
1662
1663impl<'tree> EcmascriptBindings<'tree> {
1664 fn resolve(&self, name: &str, use_start: usize, use_end: usize) -> Option<&EcmascriptBinding<'tree>> {
1665 let candidates = self.by_name.get(name)?;
1666 let smallest_scope = candidates
1667 .iter()
1668 .map(|index| &self.bindings[*index])
1669 .filter(|binding| binding.scope.start_byte() <= use_start && use_end <= binding.scope.end_byte())
1670 .map(|binding| binding.scope.end_byte() - binding.scope.start_byte())
1671 .min()?;
1672 let mut candidates = candidates
1673 .iter()
1674 .map(|index| &self.bindings[*index])
1675 .filter(|binding| {
1676 binding.scope.start_byte() <= use_start
1677 && use_end <= binding.scope.end_byte()
1678 && binding.scope.end_byte() - binding.scope.start_byte() == smallest_scope
1679 });
1680 let binding = candidates.next()?;
1681 candidates.next().is_none().then_some(binding)
1682 }
1683}
1684
1685fn ecmascript_bindings<'tree>(tree: &'tree Tree, src: &'tree [u8]) -> EcmascriptBindings<'tree> {
1686 let mut bindings = Vec::new();
1687 for declarator in collect_kinds(tree, &["variable_declarator"]) {
1688 let Some(target) = declarator.child_by_field_name("name") else {
1689 continue;
1690 };
1691 let Some(scope) = ecmascript_binding_scope(declarator) else {
1692 continue;
1693 };
1694 let is_const = declarator
1695 .parent()
1696 .filter(|parent| parent.kind() == "lexical_declaration")
1697 .and_then(|declaration| declaration.child(0))
1698 .is_some_and(|keyword| keyword.kind() == "const");
1699 let is_exported = declarator
1700 .parent()
1701 .and_then(|declaration| declaration.parent())
1702 .is_some_and(|parent| parent.kind() == "export_statement");
1703 let value = declarator.child_by_field_name("value");
1704 for bound in ecmascript_pattern_identifiers(target) {
1705 let name = node_text(&bound, src).trim();
1706 if name.is_empty() {
1707 continue;
1708 }
1709 let is_simple_target = target.kind() == "identifier" && target.id() == bound.id();
1710 bindings.push(EcmascriptBinding {
1711 name,
1712 declaration: if is_simple_target { declarator } else { bound },
1713 initializer: value.unwrap_or(target),
1714 scope,
1715 finite_map: (is_simple_target && is_const && !is_exported)
1716 .then(|| value.and_then(|value| ecmascript_finite_literal_map_kind(value, src)))
1717 .flatten(),
1718 });
1719 }
1720 }
1721
1722 for parameters in collect_kinds(tree, &["formal_parameters"]) {
1723 let Some(owner) = parameters.parent() else {
1724 continue;
1725 };
1726 let Some(scope) = owner.child_by_field_name("body") else {
1727 continue;
1728 };
1729 let mut cursor = parameters.walk();
1730 for parameter in parameters.named_children(&mut cursor) {
1731 let pattern = match parameter.kind() {
1732 "required_parameter" | "optional_parameter" => parameter
1733 .child_by_field_name("pattern")
1734 .or_else(|| parameter.child_by_field_name("name"))
1735 .unwrap_or(parameter),
1736 _ => parameter,
1737 };
1738 for bound in ecmascript_pattern_identifiers(pattern) {
1739 push_ecmascript_blocking_binding(&mut bindings, bound, scope, src);
1740 }
1741 }
1742 }
1743
1744 for arrow in collect_kinds(tree, &["arrow_function"]) {
1745 let Some(parameter) = arrow.child_by_field_name("parameter") else {
1746 continue;
1747 };
1748 let Some(scope) = arrow.child_by_field_name("body") else {
1749 continue;
1750 };
1751 for bound in ecmascript_pattern_identifiers(parameter) {
1752 push_ecmascript_blocking_binding(&mut bindings, bound, scope, src);
1753 }
1754 }
1755
1756 for catch_clause in collect_kinds(tree, &["catch_clause"]) {
1757 let Some(parameter) = catch_clause.child_by_field_name("parameter") else {
1758 continue;
1759 };
1760 let Some(scope) = catch_clause.child_by_field_name("body") else {
1761 continue;
1762 };
1763 for bound in ecmascript_pattern_identifiers(parameter) {
1764 push_ecmascript_blocking_binding(&mut bindings, bound, scope, src);
1765 }
1766 }
1767
1768 for declaration in collect_kinds(
1769 tree,
1770 &[
1771 "function_declaration",
1772 "generator_function_declaration",
1773 "class_declaration",
1774 ],
1775 ) {
1776 let Some(name) = declaration.child_by_field_name("name") else {
1777 continue;
1778 };
1779 let Some(scope) = ecmascript_binding_scope(declaration) else {
1780 continue;
1781 };
1782 push_ecmascript_blocking_binding(&mut bindings, name, scope, src);
1783 }
1784
1785 for expression in collect_kinds(tree, &["function_expression", "generator_function", "class"]) {
1786 let Some(name) = expression.child_by_field_name("name") else {
1787 continue;
1788 };
1789 let Some(scope) = expression.child_by_field_name("body") else {
1790 continue;
1791 };
1792 push_ecmascript_blocking_binding(&mut bindings, name, scope, src);
1793 }
1794
1795 let mut by_name: HashMap<String, Vec<usize>> = HashMap::new();
1796 for (index, binding) in bindings.iter().enumerate() {
1797 by_name.entry(binding.name.to_string()).or_default().push(index);
1798 }
1799 let map_intrinsic_unshadowed =
1800 !by_name.contains_key("Map") && !ecmascript_declares_static_name(tree, src, "Map");
1801 let object_intrinsic_unshadowed =
1802 !by_name.contains_key("Object") && !ecmascript_declares_static_name(tree, src, "Object");
1803 if !map_intrinsic_unshadowed {
1804 for binding in &mut bindings {
1805 if binding.finite_map == Some(EcmascriptFiniteMapKind::Map) {
1806 binding.finite_map = None;
1807 }
1808 }
1809 }
1810 let mut compiler_bindings = EcmascriptBindings {
1811 bindings,
1812 by_name,
1813 unsafe_bindings: HashSet::new(),
1814 object_intrinsic_unshadowed,
1815 };
1816 let declaration_identifiers: HashSet<_> = compiler_bindings
1817 .bindings
1818 .iter()
1819 .filter_map(|binding| {
1820 if binding.declaration.kind() == "variable_declarator" {
1821 binding
1822 .declaration
1823 .child_by_field_name("name")
1824 .map(|name| name.id())
1825 } else {
1826 Some(binding.declaration.id())
1827 }
1828 })
1829 .collect();
1830 compiler_bindings.unsafe_bindings = collect_kinds(tree, &["identifier"])
1831 .into_iter()
1832 .filter_map(|identifier| {
1833 if declaration_identifiers.contains(&identifier.id()) {
1834 return None;
1835 }
1836 let name = node_text(&identifier, src).trim();
1837 let binding = compiler_bindings.resolve(name, identifier.start_byte(), identifier.end_byte())?;
1838 let kind = binding.finite_map?;
1839 (!ecmascript_identifier_is_read_only_map_use(
1840 identifier,
1841 kind,
1842 src,
1843 compiler_bindings.object_intrinsic_unshadowed,
1844 ))
1845 .then_some(binding.declaration.id())
1846 })
1847 .collect();
1848 compiler_bindings
1849}
1850
1851fn push_ecmascript_blocking_binding<'tree>(
1852 bindings: &mut Vec<EcmascriptBinding<'tree>>,
1853 bound: Node<'tree>,
1854 scope: Node<'tree>,
1855 src: &'tree [u8],
1856) {
1857 let name = node_text(&bound, src).trim();
1858 if name.is_empty() {
1859 return;
1860 }
1861 bindings.push(EcmascriptBinding {
1862 name,
1863 declaration: bound,
1864 initializer: bound,
1865 scope,
1866 finite_map: None,
1867 });
1868}
1869
1870fn ecmascript_pattern_identifiers(pattern: Node<'_>) -> Vec<Node<'_>> {
1871 match pattern.kind() {
1872 "identifier" | "shorthand_property_identifier_pattern" => vec![pattern],
1873 "required_parameter" | "optional_parameter" => pattern
1874 .child_by_field_name("pattern")
1875 .or_else(|| pattern.child_by_field_name("name"))
1876 .map(ecmascript_pattern_identifiers)
1877 .unwrap_or_default(),
1878 "assignment_pattern" => pattern
1879 .child_by_field_name("left")
1880 .map(ecmascript_pattern_identifiers)
1881 .unwrap_or_default(),
1882 "pair_pattern" => pattern
1883 .child_by_field_name("value")
1884 .map(ecmascript_pattern_identifiers)
1885 .unwrap_or_default(),
1886 "rest_pattern" => pattern
1887 .named_child(0)
1888 .map(ecmascript_pattern_identifiers)
1889 .unwrap_or_default(),
1890 "object_assignment_pattern" => pattern
1891 .child_by_field_name("left")
1892 .or_else(|| pattern.child_by_field_name("shorthand"))
1893 .map(ecmascript_pattern_identifiers)
1894 .unwrap_or_default(),
1895 "array_pattern" | "object_pattern" => {
1896 let mut cursor = pattern.walk();
1897 pattern
1898 .named_children(&mut cursor)
1899 .flat_map(ecmascript_pattern_identifiers)
1900 .collect()
1901 }
1902 _ => Vec::new(),
1903 }
1904}
1905
1906fn ecmascript_declares_static_name(tree: &Tree, src: &[u8], wanted: &str) -> bool {
1907 collect_kinds(
1908 tree,
1909 &[
1910 "function_declaration",
1911 "class_declaration",
1912 "generator_function_declaration",
1913 ],
1914 )
1915 .into_iter()
1916 .any(|declaration| {
1917 declaration
1918 .child_by_field_name("name")
1919 .is_some_and(|name| node_text(&name, src).trim() == wanted)
1920 }) || collect_kinds(tree, &["import_statement"])
1921 .into_iter()
1922 .any(|import| {
1923 let mut stack = vec![import];
1924 while let Some(node) = stack.pop() {
1925 if matches!(node.kind(), "identifier" | "type_identifier")
1926 && node_text(&node, src).trim() == wanted
1927 {
1928 return true;
1929 }
1930 let mut cursor = node.walk();
1931 stack.extend(node.named_children(&mut cursor));
1932 }
1933 false
1934 })
1935}
1936
1937fn ecmascript_binding_scope(mut node: Node<'_>) -> Option<Node<'_>> {
1938 while let Some(parent) = node.parent() {
1939 if matches!(
1940 parent.kind(),
1941 "for_statement"
1942 | "for_in_statement"
1943 | "for_of_statement"
1944 | "statement_block"
1945 | "switch_body"
1946 | "program"
1947 ) {
1948 return Some(parent);
1949 }
1950 node = parent;
1951 }
1952 None
1953}
1954
1955fn ecmascript_finite_literal_map_kind(mut node: Node<'_>, src: &[u8]) -> Option<EcmascriptFiniteMapKind> {
1956 while matches!(
1957 node.kind(),
1958 "parenthesized_expression" | "as_expression" | "satisfies_expression" | "type_assertion"
1959 ) && node.named_child_count() >= 1
1960 {
1961 let inner = node
1962 .child_by_field_name("expression")
1963 .or_else(|| node.named_child(0))?;
1964 node = inner;
1965 }
1966 if node.kind() == "object" {
1967 let mut cursor = node.walk();
1968 return node
1969 .named_children(&mut cursor)
1970 .all(|child| {
1971 if child.kind() != "pair" {
1972 return false;
1973 }
1974 child
1975 .child_by_field_name("key")
1976 .and_then(|key| ecmascript_static_property_name(key, src))
1977 .is_some()
1978 && child
1979 .child_by_field_name("value")
1980 .is_some_and(|value| ecmascript_is_literal_value(value, src))
1981 })
1982 .then_some(EcmascriptFiniteMapKind::Object);
1983 }
1984 if node.kind() != "new_expression" {
1985 return None;
1986 }
1987 let constructor = node.child_by_field_name("constructor")?;
1988 if constructor.kind() != "identifier" || node_text(&constructor, src).trim() != "Map" {
1989 return None;
1990 }
1991 let arguments = node.child_by_field_name("arguments")?;
1992 let mut cursor = arguments.walk();
1993 let values: Vec<_> = arguments.named_children(&mut cursor).collect();
1994 (values.len() == 1 && ecmascript_is_literal_map_entries(values[0], src))
1995 .then_some(EcmascriptFiniteMapKind::Map)
1996}
1997
1998fn ecmascript_is_literal_map_entries(node: Node<'_>, src: &[u8]) -> bool {
1999 if node.kind() != "array" {
2000 return false;
2001 }
2002 let mut cursor = node.walk();
2003 let is_literal = node.named_children(&mut cursor).all(|entry| {
2004 if entry.kind() != "array" {
2005 return false;
2006 }
2007 let mut entry_cursor = entry.walk();
2008 let values: Vec<_> = entry.named_children(&mut entry_cursor).collect();
2009 values.len() == 2
2010 && ecmascript_is_literal_value(values[0], src)
2011 && ecmascript_is_literal_value(values[1], src)
2012 });
2013 is_literal
2014}
2015
2016fn ecmascript_identifier_is_read_only_map_use(
2017 identifier: Node<'_>,
2018 kind: EcmascriptFiniteMapKind,
2019 src: &[u8],
2020 object_intrinsic_unshadowed: bool,
2021) -> bool {
2022 let Some(access) = identifier.parent() else {
2023 return object_intrinsic_unshadowed
2024 && ecmascript_identifier_is_safe_has_own_argument(identifier, src);
2025 };
2026 if access.child_by_field_name("object").map(|object| object.id()) != Some(identifier.id()) {
2027 return object_intrinsic_unshadowed
2028 && ecmascript_identifier_is_safe_has_own_argument(identifier, src);
2029 }
2030 if ecmascript_access_is_write_target(access) {
2031 return false;
2032 }
2033 if access.kind() == "subscript_expression" {
2034 return true;
2035 }
2036 if access.kind() != "member_expression" {
2037 return false;
2038 }
2039 let Some(parent) = access.parent() else {
2040 return true;
2041 };
2042 if parent.kind() != "call_expression"
2043 || parent
2044 .child_by_field_name("function")
2045 .map(|function| function.id())
2046 != Some(access.id())
2047 {
2048 return true;
2049 }
2050 let Some(property) = access.child_by_field_name("property") else {
2051 return false;
2052 };
2053 let property = node_text(&property, src).trim();
2054 match kind {
2055 EcmascriptFiniteMapKind::Map => matches!(
2056 property,
2057 "get" | "has" | "entries" | "keys" | "values" | "forEach"
2058 ),
2059 EcmascriptFiniteMapKind::Object => false,
2060 }
2061}
2062
2063fn ecmascript_identifier_is_safe_has_own_argument(identifier: Node<'_>, src: &[u8]) -> bool {
2064 let Some(arguments) = identifier.parent().filter(|parent| parent.kind() == "arguments") else {
2065 return false;
2066 };
2067 let Some(first_argument) = arguments.named_child(0) else {
2068 return false;
2069 };
2070 if first_argument.id() != identifier.id() {
2071 return false;
2072 }
2073 let Some(call) = arguments
2074 .parent()
2075 .filter(|parent| parent.kind() == "call_expression")
2076 else {
2077 return false;
2078 };
2079 let Some(function) = call.child_by_field_name("function") else {
2080 return false;
2081 };
2082 matches!(
2083 ecmascript_static_member_path(function, src).as_slice(),
2084 ["Object", "hasOwn"] | ["Object", "prototype", "hasOwnProperty", "call"]
2085 )
2086}
2087
2088fn ecmascript_static_member_path<'a>(node: Node<'_>, src: &'a [u8]) -> Vec<&'a str> {
2089 if matches!(node.kind(), "identifier" | "property_identifier") {
2090 let name = node_text(&node, src).trim();
2091 return (!name.is_empty()).then_some(vec![name]).unwrap_or_default();
2092 }
2093 if node.kind() != "member_expression" {
2094 return Vec::new();
2095 }
2096 let Some(object) = node.child_by_field_name("object") else {
2097 return Vec::new();
2098 };
2099 let Some(property) = node.child_by_field_name("property") else {
2100 return Vec::new();
2101 };
2102 let mut path = ecmascript_static_member_path(object, src);
2103 let property = node_text(&property, src).trim();
2104 if path.is_empty() || property.is_empty() {
2105 return Vec::new();
2106 }
2107 path.push(property);
2108 path
2109}
2110
2111fn ecmascript_access_is_write_target(access: Node<'_>) -> bool {
2112 let Some(parent) = access.parent() else {
2113 return false;
2114 };
2115 match parent.kind() {
2116 "assignment_expression" | "augmented_assignment_expression" => parent
2117 .child_by_field_name("left")
2118 .is_some_and(|left| left.id() == access.id()),
2119 "update_expression" => true,
2120 "unary_expression" => parent
2121 .child(0)
2122 .is_some_and(|operator| operator.kind() == "delete"),
2123 _ => false,
2124 }
2125}
2126
2127fn ecmascript_expression_is_finite_selection(
2128 node: Node<'_>,
2129 selection: Node<'_>,
2130 src: &[u8],
2131 bindings: &EcmascriptBindings<'_>,
2132) -> bool {
2133 if node.id() == selection.id() {
2134 return true;
2135 }
2136 if selection.start_byte() < node.start_byte() || selection.end_byte() > node.end_byte() {
2137 return false;
2138 }
2139 match node.kind() {
2140 "parenthesized_expression" | "as_expression" | "satisfies_expression" | "type_assertion" => node
2141 .named_child(0)
2142 .is_some_and(|inner| ecmascript_expression_is_finite_selection(inner, selection, src, bindings)),
2143 "binary_expression" => {
2144 let Some(left) = node.child_by_field_name("left") else {
2145 return false;
2146 };
2147 let Some(right) = node.child_by_field_name("right") else {
2148 return false;
2149 };
2150 (selection.start_byte() >= left.start_byte()
2151 && selection.end_byte() <= left.end_byte()
2152 && ecmascript_expression_is_finite_selection(left, selection, src, bindings)
2153 && ecmascript_is_literal_value_at(right, src, bindings))
2154 || (selection.start_byte() >= right.start_byte()
2155 && selection.end_byte() <= right.end_byte()
2156 && ecmascript_expression_is_finite_selection(right, selection, src, bindings)
2157 && ecmascript_is_literal_value_at(left, src, bindings))
2158 }
2159 "ternary_expression" => {
2160 let Some(consequence) = node.child_by_field_name("consequence") else {
2161 return false;
2162 };
2163 let Some(alternative) = node.child_by_field_name("alternative") else {
2164 return false;
2165 };
2166 (selection.start_byte() >= consequence.start_byte()
2167 && selection.end_byte() <= consequence.end_byte()
2168 && ecmascript_expression_is_finite_selection(consequence, selection, src, bindings)
2169 && ecmascript_is_literal_value_at(alternative, src, bindings))
2170 || (selection.start_byte() >= alternative.start_byte()
2171 && selection.end_byte() <= alternative.end_byte()
2172 && ecmascript_expression_is_finite_selection(alternative, selection, src, bindings)
2173 && ecmascript_is_literal_value_at(consequence, src, bindings))
2174 }
2175 _ => false,
2176 }
2177}
2178
2179fn ecmascript_is_literal_value_at(node: Node<'_>, src: &[u8], bindings: &EcmascriptBindings<'_>) -> bool {
2180 if matches!(node.kind(), "identifier" | "undefined") && node_text(&node, src).trim() == "undefined" {
2181 return bindings
2182 .resolve("undefined", node.start_byte(), node.end_byte())
2183 .is_none();
2184 }
2185 ecmascript_is_literal_value(node, src)
2186}
2187
2188fn ecmascript_is_literal_value(mut node: Node<'_>, src: &[u8]) -> bool {
2189 while matches!(
2190 node.kind(),
2191 "parenthesized_expression" | "as_expression" | "satisfies_expression" | "type_assertion"
2192 ) && node.named_child_count() >= 1
2193 {
2194 let Some(inner) = node
2195 .child_by_field_name("expression")
2196 .or_else(|| node.named_child(0))
2197 else {
2198 return false;
2199 };
2200 node = inner;
2201 }
2202 match node.kind() {
2203 "string" | "string_literal" => ecmascript_static_string_literal(node, src).is_some(),
2204 "number" | "true" | "false" | "null" => true,
2205 "array" => {
2206 let mut cursor = node.walk();
2207 let is_literal = node
2208 .named_children(&mut cursor)
2209 .all(|child| ecmascript_is_literal_value(child, src));
2210 is_literal
2211 }
2212 "object" => {
2213 let mut cursor = node.walk();
2214 let is_literal = node.named_children(&mut cursor).all(|child| {
2215 child.kind() == "pair"
2216 && child
2217 .child_by_field_name("key")
2218 .and_then(|key| ecmascript_static_property_name(key, src))
2219 .is_some()
2220 && child
2221 .child_by_field_name("value")
2222 .is_some_and(|value| ecmascript_is_literal_value(value, src))
2223 });
2224 is_literal
2225 }
2226 _ => false,
2227 }
2228}
2229
2230fn ecmascript_exact_string_map_entries(object: Node<'_>, src: &[u8]) -> Option<Vec<StaticStringMapEntry>> {
2231 let mut entries = Vec::new();
2232 let mut cursor = object.walk();
2233 for child in object.named_children(&mut cursor) {
2234 if child.kind() != "pair" {
2235 return None;
2236 }
2237 let key = child.child_by_field_name("key")?;
2238 let value = child.child_by_field_name("value")?;
2239 entries.push(StaticStringMapEntry {
2240 key: ecmascript_static_property_name(key, src)?,
2241 value: ecmascript_static_string_literal(value, src)?,
2242 });
2243 }
2244 (!entries.is_empty()).then_some(entries)
2245}
2246
2247fn ecmascript_static_property_name(node: Node<'_>, src: &[u8]) -> Option<String> {
2248 ecmascript_static_string_literal(node, src).or_else(|| {
2249 matches!(node.kind(), "property_identifier" | "identifier")
2250 .then(|| node_text(&node, src).trim().to_string())
2251 .filter(|value| !value.is_empty())
2252 })
2253}
2254
2255fn ecmascript_character_substitutions(
2256 defs: &[bonsai_lang_api::Decl],
2257 tree: &Tree,
2258 file: FileId,
2259 src: &[u8],
2260) -> Vec<CharacterSubstitutionFact> {
2261 let bindings = ecmascript_bindings(tree, src);
2262 let mut facts = Vec::new();
2263 for return_node in collect_kinds(tree, &["return_statement"]) {
2264 let return_span = span_of(file, &return_node);
2265 let Some(decl) = defs
2266 .iter()
2267 .filter(|decl| {
2268 matches!(
2269 decl.kind,
2270 DeclKind::Function | DeclKind::Method | DeclKind::Constructor
2271 ) && decl.span.start <= return_span.start
2272 && return_span.end <= decl.span.end
2273 })
2274 .min_by_key(|decl| decl.span.len())
2275 else {
2276 continue;
2277 };
2278 let Some(expression) = return_node.named_child(0) else {
2279 continue;
2280 };
2281 let Some((input_param_index, table, exact_mappings, domain, transform_span)) =
2282 ecmascript_character_substitution(expression, &decl.params, &bindings, file, src)
2283 else {
2284 continue;
2285 };
2286 facts.push(CharacterSubstitutionFact {
2287 function_span: decl.span,
2288 transform_span,
2289 input_param_index,
2290 exact_mappings,
2291 table,
2292 domain,
2293 });
2294 }
2295 for arrow in collect_kinds(tree, &["arrow_function"]) {
2298 let Some(body) = arrow.child_by_field_name("body") else {
2299 continue;
2300 };
2301 if body.kind() == "statement_block" {
2302 continue;
2303 }
2304 let arrow_span = span_of(file, &arrow);
2305 let Some(decl) = defs
2306 .iter()
2307 .filter(|decl| {
2308 matches!(decl.kind, DeclKind::Function | DeclKind::Method)
2309 && decl.span.start <= arrow_span.start
2310 && arrow_span.end <= decl.span.end
2311 })
2312 .min_by_key(|decl| decl.span.len())
2313 else {
2314 continue;
2315 };
2316 let Some((input_param_index, table, exact_mappings, domain, transform_span)) =
2317 ecmascript_character_substitution(body, &decl.params, &bindings, file, src)
2318 else {
2319 continue;
2320 };
2321 facts.push(CharacterSubstitutionFact {
2322 function_span: decl.span,
2323 transform_span,
2324 input_param_index,
2325 exact_mappings,
2326 table,
2327 domain,
2328 });
2329 }
2330 facts.sort_by_key(|fact| (fact.function_span.start, fact.transform_span.start));
2331 facts.dedup();
2332 facts
2333}
2334
2335fn ecmascript_character_substitution(
2336 expression: Node<'_>,
2337 params: &[String],
2338 bindings: &EcmascriptBindings<'_>,
2339 file: FileId,
2340 src: &[u8],
2341) -> Option<(
2342 usize,
2343 String,
2344 Vec<StaticStringMapEntry>,
2345 CharacterSubstitutionDomain,
2346 bonsai_common::Span,
2347)> {
2348 if let Some((input_param_index, mappings, characters, transform_span)) =
2349 ecmascript_inline_replace_chain(expression, params, bindings, file, src)
2350 {
2351 return Some((
2352 input_param_index,
2353 String::new(),
2354 mappings,
2355 CharacterSubstitutionDomain::ExactCharacters { characters },
2356 transform_span,
2357 ));
2358 }
2359 let call = unwrap_ecmascript_expression(expression);
2360 if call.kind() != "call_expression" {
2361 return None;
2362 }
2363 let function = call.child_by_field_name("function")?;
2364 if function.kind() != "member_expression" {
2365 return None;
2366 }
2367 let receiver = function.child_by_field_name("object")?;
2368 let method = function.child_by_field_name("property")?;
2369 let method = node_text(&method, src).trim();
2370 let arguments = ecmascript_call_arguments(call);
2371
2372 if method == "replace" {
2373 let input_param_index = ecmascript_transform_input_param_index(receiver, params, bindings, src)?;
2374 let pattern = arguments.first().copied()?;
2375 let callback = arguments.get(1).copied()?;
2376 let characters = ecmascript_exact_regex_characters(pattern, src)?;
2377 if let Some((table, callback_parameter)) = ecmascript_map_lookup_callback(callback, src) {
2378 if callback_parameter.is_empty() {
2379 return None;
2380 }
2381 return Some((
2382 input_param_index,
2383 table,
2384 Vec::new(),
2385 CharacterSubstitutionDomain::ExactCharacters { characters },
2386 span_of(file, &call),
2387 ));
2388 }
2389 let exact_mappings = ecmascript_numeric_hex_escape_mappings(callback, &characters, src)?;
2390 return Some((
2391 input_param_index,
2392 String::new(),
2393 exact_mappings,
2394 CharacterSubstitutionDomain::ExactCharacters { characters },
2395 span_of(file, &call),
2396 ));
2397 }
2398
2399 if method != "join" || receiver.kind() != "call_expression" {
2400 return None;
2401 }
2402 let join_separator = arguments.first().copied()?;
2403 if ecmascript_static_string_literal(join_separator, src).as_deref() != Some("") {
2404 return None;
2405 }
2406 let map_function = receiver.child_by_field_name("function")?;
2407 if map_function.kind() != "member_expression"
2408 || map_function
2409 .child_by_field_name("property")
2410 .map(|property| node_text(&property, src).trim() == "map")
2411 != Some(true)
2412 {
2413 return None;
2414 }
2415 let iterated = map_function.child_by_field_name("object")?;
2416 let input = ecmascript_spread_only_array_input(iterated, src)?;
2417 let input_param_index = params.iter().position(|param| param == input)?;
2418 let callback = ecmascript_call_arguments(receiver).first().copied()?;
2419 let (table, callback_parameter) = ecmascript_identity_fallback_map_callback(callback, src)?;
2420 if callback_parameter.is_empty() {
2421 return None;
2422 }
2423 Some((
2424 input_param_index,
2425 table,
2426 Vec::new(),
2427 CharacterSubstitutionDomain::TableKeysWithIdentityFallback,
2428 span_of(file, &call),
2429 ))
2430}
2431
2432fn ecmascript_inline_replace_chain(
2433 expression: Node<'_>,
2434 params: &[String],
2435 bindings: &EcmascriptBindings<'_>,
2436 file: FileId,
2437 src: &[u8],
2438) -> Option<(usize, Vec<StaticStringMapEntry>, Vec<String>, bonsai_common::Span)> {
2439 let transform_span = span_of(file, &expression);
2440 let mut current = unwrap_ecmascript_expression(expression);
2441 let mut mappings = Vec::new();
2442 let mut characters = Vec::new();
2443 while current.kind() == "call_expression" {
2444 let function = current.child_by_field_name("function")?;
2445 if function.kind() != "member_expression" {
2446 break;
2447 }
2448 let receiver = function.child_by_field_name("object")?;
2449 let method = function.child_by_field_name("property")?;
2450 if node_text(&method, src).trim() != "replace" {
2451 break;
2452 }
2453 let args = ecmascript_call_arguments(current);
2454 let [pattern, replacement] = args.as_slice() else {
2455 return None;
2456 };
2457 let replaced = ecmascript_global_regex_characters(*pattern, bindings, src)?;
2458 let replacement = ecmascript_static_string_literal(*replacement, src)?;
2459 for input in replaced {
2460 let output = ecmascript_expand_static_replacement(&replacement, &input)?;
2461 if mappings
2462 .iter()
2463 .any(|entry: &StaticStringMapEntry| entry.key == input && entry.value != output)
2464 {
2465 return None;
2466 }
2467 if !mappings.iter().any(|entry| entry.key == input) {
2468 characters.push(input.clone());
2469 mappings.push(StaticStringMapEntry {
2470 key: input,
2471 value: output.clone(),
2472 });
2473 }
2474 }
2475 current = unwrap_ecmascript_expression(receiver);
2476 }
2477 if mappings.is_empty() {
2478 return None;
2479 }
2480 let input = if current.kind() == "identifier" {
2481 node_text(¤t, src).trim()
2482 } else if current.kind() == "call_expression" {
2483 let function = current.child_by_field_name("function")?;
2484 if function.kind() != "identifier"
2485 || node_text(&function, src).trim() != "String"
2486 || bindings
2487 .resolve("String", function.start_byte(), function.end_byte())
2488 .is_some()
2489 {
2490 return None;
2491 }
2492 let args = ecmascript_call_arguments(current);
2493 let [input] = args.as_slice() else {
2494 return None;
2495 };
2496 if input.kind() != "identifier" {
2497 return None;
2498 }
2499 node_text(input, src).trim()
2500 } else {
2501 return None;
2502 };
2503 let input_param_index = params.iter().position(|param| param == input)?;
2504 characters.sort();
2505 characters.dedup();
2506 mappings.sort_by(|left, right| left.key.cmp(&right.key));
2507 Some((input_param_index, mappings, characters, transform_span))
2508}
2509
2510fn ecmascript_transform_input_param_index(
2511 receiver: Node<'_>,
2512 params: &[String],
2513 bindings: &EcmascriptBindings<'_>,
2514 src: &[u8],
2515) -> Option<usize> {
2516 let receiver = unwrap_ecmascript_expression(receiver);
2517 if receiver.kind() == "identifier" {
2518 let input = node_text(&receiver, src).trim();
2519 return params.iter().position(|parameter| parameter == input);
2520 }
2521 if receiver.kind() != "call_expression" {
2522 return None;
2523 }
2524 let function = receiver.child_by_field_name("function")?;
2525 if function.kind() != "identifier"
2526 || node_text(&function, src).trim() != "String"
2527 || bindings
2528 .resolve("String", function.start_byte(), function.end_byte())
2529 .is_some()
2530 {
2531 return None;
2532 }
2533 let args = ecmascript_call_arguments(receiver);
2534 let [input] = args.as_slice() else {
2535 return None;
2536 };
2537 if input.kind() != "identifier" {
2538 return None;
2539 }
2540 let input = node_text(input, src).trim();
2541 params.iter().position(|parameter| parameter == input)
2542}
2543
2544fn ecmascript_expand_static_replacement(template: &str, matched: &str) -> Option<String> {
2549 let mut out = String::new();
2550 let mut input = template.chars().peekable();
2551 while let Some(character) = input.next() {
2552 if character != '$' {
2553 out.push(character);
2554 continue;
2555 }
2556 match input.next() {
2557 Some('$') => out.push('$'),
2558 Some('&') => out.push_str(matched),
2559 Some('`' | '\'' | '0'..='9' | '<') => return None,
2560 Some(other) => {
2561 out.push('$');
2563 out.push(other);
2564 }
2565 None => out.push('$'),
2566 }
2567 }
2568 Some(out)
2569}
2570
2571fn ecmascript_global_regex_characters<'tree>(
2572 mut regex: Node<'tree>,
2573 bindings: &EcmascriptBindings<'tree>,
2574 src: &[u8],
2575) -> Option<Vec<String>> {
2576 if regex.kind() == "identifier" {
2577 let name = node_text(®ex, src).trim();
2578 let binding = bindings.resolve(name, regex.start_byte(), regex.end_byte())?;
2579 if binding.declaration.kind() != "variable_declarator"
2580 || binding
2581 .declaration
2582 .child_by_field_name("name")
2583 .is_none_or(|target| target.kind() != "identifier")
2584 || binding
2585 .declaration
2586 .parent()
2587 .filter(|declaration| declaration.kind() == "lexical_declaration")
2588 .and_then(|declaration| declaration.child(0))
2589 .is_none_or(|keyword| keyword.kind() != "const")
2590 {
2591 return None;
2592 }
2593 regex = binding.initializer;
2594 }
2595 if regex.kind() != "regex" {
2596 return None;
2597 }
2598 let flags = regex
2599 .child_by_field_name("flags")
2600 .map(|node| node_text(&node, src).trim())
2601 .or_else(|| node_text(®ex, src).rsplit_once('/').map(|(_, flags)| flags))?;
2602 if !flags.contains('g') {
2603 return None;
2604 }
2605 let pattern = regex.child_by_field_name("pattern")?;
2606 let pattern_text = node_text(&pattern, src);
2607 if let Some(characters) = ecmascript_exact_regex_characters(regex, src) {
2608 return Some(characters);
2609 }
2610 let mut chars = pattern_text.chars();
2611 let character = match chars.next()? {
2612 '\\' => match chars.next()? {
2613 'r' => '\r',
2614 'n' => '\n',
2615 't' => '\t',
2616 escaped if !escaped.is_ascii_alphanumeric() => escaped,
2617 _ => return None,
2618 },
2619 character if !".^$*+?()[]{}|".contains(character) => character,
2620 _ => return None,
2621 };
2622 chars.next().is_none().then(|| vec![character.to_string()])
2623}
2624
2625fn unwrap_ecmascript_expression(mut node: Node<'_>) -> Node<'_> {
2626 loop {
2627 if matches!(node.kind(), "parenthesized_expression" | "expression") && node.named_child_count() == 1 {
2628 node = node.named_child(0).expect("single named child");
2629 continue;
2630 }
2631 if matches!(node.kind(), "as_expression" | "type_assertion") {
2632 let Some(value) = node.named_child(0) else {
2633 break;
2634 };
2635 node = value;
2636 continue;
2637 }
2638 break;
2639 }
2640 node
2641}
2642
2643fn ecmascript_call_arguments(call: Node<'_>) -> Vec<Node<'_>> {
2644 let Some(arguments) = call.child_by_field_name("arguments") else {
2645 return Vec::new();
2646 };
2647 let mut cursor = arguments.walk();
2648 arguments.named_children(&mut cursor).collect()
2649}
2650
2651fn ecmascript_spread_only_array_input<'a>(array: Node<'a>, src: &'a [u8]) -> Option<&'a str> {
2652 if array.kind() != "array" || array.named_child_count() != 1 {
2653 return None;
2654 }
2655 let spread = array.named_child(0)?;
2656 if spread.kind() != "spread_element" {
2657 return None;
2658 }
2659 let argument = spread.named_child(0)?;
2660 (argument.kind() == "identifier").then(|| node_text(&argument, src).trim())
2661}
2662
2663fn ecmascript_map_lookup_callback(callback: Node<'_>, src: &[u8]) -> Option<(String, String)> {
2664 let (parameter, body) = ecmascript_arrow_parts(callback, src)?;
2665 let (table, key) = ecmascript_subscript_parts(body, src)?;
2666 (key == parameter).then_some((table.to_string(), parameter.to_string()))
2667}
2668
2669fn ecmascript_numeric_hex_escape_mappings(
2673 callback: Node<'_>,
2674 characters: &[String],
2675 src: &[u8],
2676) -> Option<Vec<StaticStringMapEntry>> {
2677 let (parameter, body) = ecmascript_arrow_parts(callback, src)?;
2678 let body = unwrap_ecmascript_expression(body);
2679 if body.kind() != "binary_expression" {
2680 return None;
2681 }
2682 let left = body.child_by_field_name("left")?;
2683 let right = body.child_by_field_name("right")?;
2684 if src
2685 .get(left.end_byte()..right.start_byte())
2686 .and_then(|bytes| std::str::from_utf8(bytes).ok())
2687 .map(str::trim)
2688 != Some("+")
2689 {
2690 return None;
2691 }
2692 let prefix = ecmascript_static_string_literal(left, src)?;
2693 let (pad_receiver, pad_method, pad_args) = ecmascript_nested_member_call(right, src)?;
2694 if pad_method != "padStart" || pad_args.len() != 2 {
2695 return None;
2696 }
2697 let width = ecmascript_static_usize(pad_args[0], src)?;
2698 let fill = ecmascript_static_string_literal(pad_args[1], src)?;
2699 if width == 0 || fill.is_empty() {
2700 return None;
2701 }
2702 let (string_receiver, string_method, string_args) = ecmascript_nested_member_call(pad_receiver, src)?;
2703 if string_method != "toString"
2704 || string_args.len() != 1
2705 || ecmascript_static_usize(string_args[0], src)? != 16
2706 {
2707 return None;
2708 }
2709 let (code_receiver, code_method, code_args) = ecmascript_nested_member_call(string_receiver, src)?;
2710 if code_method != "charCodeAt"
2711 || code_args.len() != 1
2712 || ecmascript_static_usize(code_args[0], src)? != 0
2713 || code_receiver.kind() != "identifier"
2714 || node_text(&code_receiver, src).trim() != parameter
2715 {
2716 return None;
2717 }
2718
2719 characters
2720 .iter()
2721 .map(|input| {
2722 let mut utf16 = input.encode_utf16();
2723 let code = utf16.next()?;
2724 if utf16.next().is_some() {
2725 return None;
2726 }
2727 let digits = format!("{code:x}");
2728 let padding = width.saturating_sub(digits.chars().count());
2729 let mut encoded = String::with_capacity(prefix.len() + padding * fill.len() + digits.len());
2730 encoded.push_str(&prefix);
2731 for _ in 0..padding {
2732 encoded.push_str(&fill);
2733 }
2734 encoded.push_str(&digits);
2735 Some(StaticStringMapEntry {
2736 key: input.clone(),
2737 value: encoded,
2738 })
2739 })
2740 .collect()
2741}
2742
2743fn ecmascript_nested_member_call<'tree>(
2744 call: Node<'tree>,
2745 src: &[u8],
2746) -> Option<(Node<'tree>, String, Vec<Node<'tree>>)> {
2747 let call = unwrap_ecmascript_expression(call);
2748 if call.kind() != "call_expression" {
2749 return None;
2750 }
2751 let function = call.child_by_field_name("function")?;
2752 if function.kind() != "member_expression" {
2753 return None;
2754 }
2755 let object = function.child_by_field_name("object")?;
2756 let property = function.child_by_field_name("property")?;
2757 let method = node_text(&property, src).trim().to_string();
2758 (!method.is_empty()).then(|| (object, method, ecmascript_call_arguments(call)))
2759}
2760
2761fn ecmascript_static_usize(node: Node<'_>, src: &[u8]) -> Option<usize> {
2762 (node.kind() == "number")
2763 .then(|| node_text(&node, src).trim().parse().ok())
2764 .flatten()
2765}
2766
2767fn ecmascript_identity_fallback_map_callback(callback: Node<'_>, src: &[u8]) -> Option<(String, String)> {
2768 let (parameter, body) = ecmascript_arrow_parts(callback, src)?;
2769 if body.kind() != "binary_expression" {
2770 return None;
2771 }
2772 let left = body.child_by_field_name("left")?;
2773 let right = body.child_by_field_name("right")?;
2774 if src
2775 .get(left.end_byte()..right.start_byte())
2776 .and_then(|bytes| std::str::from_utf8(bytes).ok())
2777 .map(str::trim)
2778 != Some("??")
2779 || node_text(&right, src).trim() != parameter
2780 {
2781 return None;
2782 }
2783 let (table, key) = ecmascript_subscript_parts(left, src)?;
2784 (key == parameter).then_some((table.to_string(), parameter.to_string()))
2785}
2786
2787fn ecmascript_arrow_parts<'a>(arrow: Node<'a>, src: &'a [u8]) -> Option<(&'a str, Node<'a>)> {
2788 if arrow.kind() != "arrow_function" {
2789 return None;
2790 }
2791 let parameter_node = arrow
2792 .child_by_field_name("parameter")
2793 .or_else(|| arrow.child_by_field_name("parameters"))?;
2794 let parameter = if parameter_node.kind() == "identifier" {
2795 parameter_node
2796 } else {
2797 let mut stack = vec![parameter_node];
2798 let mut found = None;
2799 while let Some(node) = stack.pop() {
2800 if node.kind() == "identifier" {
2801 found = Some(node);
2802 break;
2803 }
2804 let mut cursor = node.walk();
2805 stack.extend(node.named_children(&mut cursor));
2806 }
2807 found?
2808 };
2809 let body = arrow.child_by_field_name("body")?;
2810 Some((node_text(¶meter, src).trim(), body))
2811}
2812
2813fn ecmascript_subscript_parts<'a>(subscript: Node<'a>, src: &'a [u8]) -> Option<(&'a str, &'a str)> {
2814 if subscript.kind() != "subscript_expression" {
2815 return None;
2816 }
2817 let object = subscript.child_by_field_name("object")?;
2818 let index = subscript.child_by_field_name("index")?;
2819 if object.kind() != "identifier" || index.kind() != "identifier" {
2820 return None;
2821 }
2822 Some((node_text(&object, src).trim(), node_text(&index, src).trim()))
2823}
2824
2825fn ecmascript_exact_regex_characters(regex: Node<'_>, src: &[u8]) -> Option<Vec<String>> {
2826 if regex.kind() != "regex" {
2827 return None;
2828 }
2829 let pattern = regex.child_by_field_name("pattern")?;
2830 let pattern = node_text(&pattern, src);
2831 let inner = pattern.strip_prefix('[')?.strip_suffix(']')?;
2832 if inner.starts_with('^') || inner.is_empty() {
2833 return None;
2834 }
2835 let mut characters = Vec::new();
2836 let mut chars = inner.chars();
2837 while let Some(character) = chars.next() {
2838 if character == '-' {
2839 return None;
2840 }
2841 let decoded = if character == '\\' {
2842 match chars.next()? {
2843 'r' => '\r',
2844 'n' => '\n',
2845 't' => '\t',
2846 '0' => '\0',
2847 'x' => {
2848 let digits = [chars.next()?, chars.next()?].into_iter().collect::<String>();
2849 char::from_u32(u32::from_str_radix(&digits, 16).ok()?)?
2850 }
2851 'u' => {
2852 let digits = [chars.next()?, chars.next()?, chars.next()?, chars.next()?]
2853 .into_iter()
2854 .collect::<String>();
2855 char::from_u32(u32::from_str_radix(&digits, 16).ok()?)?
2856 }
2857 escaped if !escaped.is_ascii_alphanumeric() => escaped,
2858 _ => return None,
2859 }
2860 } else {
2861 character
2862 };
2863 characters.push(decoded.to_string());
2864 }
2865 characters.sort();
2866 characters.dedup();
2867 Some(characters)
2868}
2869
2870fn lower_ecmascript_condition_expression(
2871 node: Node<'_>,
2872 file: FileId,
2873 src: &[u8],
2874) -> ConditionExpressionFact {
2875 if node.kind() == "parenthesized_expression" {
2876 if let Some(inner) = node.named_child(0) {
2877 return lower_ecmascript_condition_expression(inner, file, src);
2878 }
2879 }
2880
2881 let span = span_of(file, &node);
2882 if node.kind() == "unary_expression" {
2883 if let Some(operand) = node
2884 .child_by_field_name("argument")
2885 .or_else(|| node.named_child(0))
2886 {
2887 let prefix = src
2888 .get(node.start_byte()..operand.start_byte())
2889 .and_then(|bytes| std::str::from_utf8(bytes).ok())
2890 .map(str::trim);
2891 if prefix == Some("!") {
2892 return ConditionExpressionFact::Not {
2893 span,
2894 operand: Box::new(lower_ecmascript_condition_expression(operand, file, src)),
2895 };
2896 }
2897 }
2898 }
2899
2900 if node.kind() == "binary_expression" {
2901 if let (Some(left), Some(right)) = (
2902 node.child_by_field_name("left"),
2903 node.child_by_field_name("right"),
2904 ) {
2905 let operator = src
2906 .get(left.end_byte()..right.start_byte())
2907 .and_then(|bytes| std::str::from_utf8(bytes).ok())
2908 .map(str::trim);
2909 match operator {
2910 Some("||") => {
2911 return merge_ecmascript_condition_junction(
2912 span,
2913 lower_ecmascript_condition_expression(left, file, src),
2914 lower_ecmascript_condition_expression(right, file, src),
2915 false,
2916 );
2917 }
2918 Some("&&") => {
2919 return merge_ecmascript_condition_junction(
2920 span,
2921 lower_ecmascript_condition_expression(left, file, src),
2922 lower_ecmascript_condition_expression(right, file, src),
2923 true,
2924 );
2925 }
2926 Some("==" | "===" | "!=" | "!==") => {
2927 if let Some((subject, type_name)) = ecmascript_type_test_operands(left, right, file, src)
2928 .or_else(|| ecmascript_type_test_operands(right, left, file, src))
2929 {
2930 let type_test = ConditionExpressionFact::TypeTest {
2931 span,
2932 subject,
2933 type_name,
2934 };
2935 return if matches!(operator, Some("==" | "===")) {
2936 type_test
2937 } else {
2938 ConditionExpressionFact::Not {
2939 span,
2940 operand: Box::new(type_test),
2941 }
2942 };
2943 }
2944 let relation = if matches!(operator, Some("==" | "===")) {
2945 ConditionEquality::Equal
2946 } else {
2947 ConditionEquality::NotEqual
2948 };
2949 return ConditionExpressionFact::Equality {
2950 span,
2951 relation,
2952 left: ecmascript_condition_operand(left, file, src),
2953 right: ecmascript_condition_operand(right, file, src),
2954 };
2955 }
2956 _ => {}
2957 }
2958 }
2959 }
2960
2961 ConditionExpressionFact::Atom { span }
2962}
2963
2964fn ecmascript_type_test_operands(
2965 type_query: Node<'_>,
2966 type_literal: Node<'_>,
2967 file: FileId,
2968 src: &[u8],
2969) -> Option<(ConditionOperandFact, String)> {
2970 if type_query.kind() != "unary_expression" {
2971 return None;
2972 }
2973 let subject = type_query
2974 .child_by_field_name("argument")
2975 .or_else(|| type_query.named_child(0))?;
2976 let operator = src
2977 .get(type_query.start_byte()..subject.start_byte())
2978 .and_then(|bytes| std::str::from_utf8(bytes).ok())?
2979 .trim();
2980 if operator != "typeof" {
2981 return None;
2982 }
2983 let type_name = ecmascript_static_string_literal(type_literal, src)?;
2984 Some((ecmascript_condition_operand(subject, file, src), type_name))
2985}
2986
2987fn merge_ecmascript_condition_junction(
2988 span: bonsai_common::Span,
2989 left: ConditionExpressionFact,
2990 right: ConditionExpressionFact,
2991 all: bool,
2992) -> ConditionExpressionFact {
2993 let mut operands = Vec::new();
2994 let mut push = |operand: ConditionExpressionFact| match (all, operand) {
2995 (true, ConditionExpressionFact::All { operands: nested, .. })
2996 | (false, ConditionExpressionFact::Any { operands: nested, .. }) => operands.extend(nested),
2997 (_, operand) => operands.push(operand),
2998 };
2999 push(left);
3000 push(right);
3001 if all {
3002 ConditionExpressionFact::All { span, operands }
3003 } else {
3004 ConditionExpressionFact::Any { span, operands }
3005 }
3006}
3007
3008fn ecmascript_condition_operand(node: Node<'_>, file: FileId, src: &[u8]) -> ConditionOperandFact {
3009 ConditionOperandFact {
3010 span: span_of(file, &node),
3011 value_flow: bonsai_lang_api::kit::expression_flow_from_node_with_handler(node, file, src, &HANDLER),
3012 static_string: ecmascript_static_string_literal(node, src),
3013 static_value: ecmascript_static_scalar(node, src),
3014 }
3015}
3016
3017fn ecmascript_static_string_literal(node: Node<'_>, src: &[u8]) -> Option<String> {
3018 if !matches!(node.kind(), "string" | "string_literal") {
3019 return None;
3020 }
3021 let text = node_text(&node, src);
3022 let quote = text.as_bytes().first().copied()?;
3023 if !matches!(quote, b'\'' | b'"') || text.as_bytes().last().copied() != Some(quote) {
3024 return None;
3025 }
3026 let inner = text.get(1..text.len().checked_sub(1)?)?;
3027 decode_ecmascript_string_contents(inner, quote as char)
3028}
3029
3030fn ecmascript_static_scalar(node: Node<'_>, src: &[u8]) -> Option<StaticScalarValue> {
3031 match node.kind() {
3032 "true" => Some(StaticScalarValue::Boolean(true)),
3033 "false" => Some(StaticScalarValue::Boolean(false)),
3034 "null" => Some(StaticScalarValue::Null),
3035 "string" | "string_literal" => Some(StaticScalarValue::String(ecmascript_static_string_literal(
3036 node, src,
3037 )?)),
3038 _ => None,
3039 }
3040}
3041
3042pub fn ecmascript_static_subscript_key(node: Node<'_>, src: &[u8]) -> Option<String> {
3043 match ecmascript_static_scalar(node, src)? {
3044 StaticScalarValue::String(value) => Some(value),
3045 StaticScalarValue::Boolean(_) | StaticScalarValue::Null => None,
3046 }
3047}
3048
3049fn decode_ecmascript_string_contents(inner: &str, quote: char) -> Option<String> {
3050 let mut output = String::new();
3051 let mut chars = inner.chars().peekable();
3052 while let Some(character) = chars.next() {
3053 if character != '\\' {
3054 if matches!(character, '\r' | '\n') {
3055 return None;
3056 }
3057 output.push(character);
3058 continue;
3059 }
3060 let escaped = chars.next()?;
3061 match escaped {
3062 '\\' => output.push('\\'),
3063 '\'' if quote == '\'' => output.push('\''),
3064 '"' if quote == '"' => output.push('"'),
3065 '`' if quote == '`' => output.push('`'),
3066 '/' => output.push('/'),
3067 'b' => output.push('\u{0008}'),
3068 'f' => output.push('\u{000c}'),
3069 'n' => output.push('\n'),
3070 'r' => output.push('\r'),
3071 't' => output.push('\t'),
3072 'v' => output.push('\u{000b}'),
3073 '0' if !chars.peek().is_some_and(char::is_ascii_digit) => output.push('\0'),
3074 'x' => output.push(decode_ecmascript_hex(&mut chars, 2)?),
3075 'u' => {
3076 if chars.peek() == Some(&'{') {
3077 chars.next();
3078 let mut digits = String::new();
3079 for digit in chars.by_ref() {
3080 if digit == '}' {
3081 break;
3082 }
3083 if !digit.is_ascii_hexdigit() || digits.len() == 6 {
3084 return None;
3085 }
3086 digits.push(digit);
3087 }
3088 if digits.is_empty() {
3089 return None;
3090 }
3091 output.push(char::from_u32(u32::from_str_radix(&digits, 16).ok()?)?);
3092 } else {
3093 output.push(decode_ecmascript_hex(&mut chars, 4)?);
3094 }
3095 }
3096 '\n' => {}
3097 '\r' => {
3098 if chars.peek() == Some(&'\n') {
3099 chars.next();
3100 }
3101 }
3102 other if !other.is_ascii_digit() => output.push(other),
3106 _ => return None,
3107 }
3108 }
3109 Some(output)
3110}
3111
3112fn decode_ecmascript_hex(
3113 chars: &mut std::iter::Peekable<impl Iterator<Item = char>>,
3114 digits: usize,
3115) -> Option<char> {
3116 let mut value = 0_u32;
3117 for _ in 0..digits {
3118 value = value.checked_mul(16)?;
3119 value = value.checked_add(chars.next()?.to_digit(16)?)?;
3120 }
3121 char::from_u32(value)
3122}
3123
3124fn apply_javascript_array_literal_types(index: &mut DeclIndex, tree: &Tree, src: &[u8], file: FileId) {
3129 let mut bindings = Vec::new();
3130 for declarator in collect_kinds(tree, &["variable_declarator"]) {
3131 let Some(name_node) = declarator.child_by_field_name("name") else {
3132 continue;
3133 };
3134 let Some(value_node) = declarator.child_by_field_name("value") else {
3135 continue;
3136 };
3137 if value_node.kind() != "array" || name_node.kind() != "identifier" {
3138 continue;
3139 }
3140 let name = node_text(&name_node, src).trim();
3141 if !name.is_empty() {
3142 bindings.push((span_of(file, &declarator), name.to_string()));
3143 }
3144 }
3145 for (span, name) in bindings {
3146 let owner = index
3147 .defs
3148 .iter()
3149 .enumerate()
3150 .filter(|(_, decl)| {
3151 matches!(
3152 decl.kind,
3153 DeclKind::Function | DeclKind::Method | DeclKind::Constructor
3154 ) && decl.span.file == span.file
3155 && decl.span.start <= span.start
3156 && span.end <= decl.span.end
3157 })
3158 .min_by_key(|(_, decl)| decl.span.len())
3159 .map(|(idx, _)| idx);
3160 let Some(owner) = owner else { continue };
3161 let binding = TypeAliasBinding {
3162 name,
3163 type_name: "Array".to_string(),
3164 };
3165 if !index.defs[owner].type_aliases.contains(&binding) {
3166 index.defs[owner].type_aliases.push(binding);
3167 }
3168 }
3169}
3170
3171fn parse_imports(tree: &Tree, src: &[u8], file: FileId) -> Vec<ImportSpec> {
3173 let mut import_specs = js_ts_imports(file, tree, src);
3174 import_specs.extend(js_ts_require_calls(file, tree, src));
3175 import_specs
3176}
3177
3178pub fn js_ts_imports(file: FileId, tree: &tree_sitter::Tree, src: &[u8]) -> Vec<ImportSpec> {
3184 let mut imports = Vec::new();
3185 for import_node in collect_kinds(tree, &["import_statement"]) {
3186 let Some(source) = import_node.child_by_field_name("source") else {
3187 continue;
3188 };
3189 let module = first_named_child_of_kind(&source, "string_fragment")
3191 .map(|fragment| node_text(&fragment, src).to_string())
3192 .unwrap_or_else(|| {
3193 node_text(&source, src)
3195 .trim_matches(|c: char| matches!(c, '"' | '\''))
3196 .to_string()
3197 });
3198 let module = normalize_node_builtin_scheme(&module);
3199 if module.is_empty() {
3200 continue;
3201 }
3202 let import_clause = first_named_child_of_kind(&import_node, "import_clause");
3210 let mut module_alias: Option<String> = None;
3211 let mut default_alias: Option<String> = None;
3212 let mut is_wildcard = false;
3213 let mut renames: Vec<(String, String)> = Vec::new();
3214 let mut shorthands: Vec<String> = Vec::new();
3215 if let Some(import_clause) = import_clause {
3216 let mut clause_cursor = import_clause.walk();
3217 for clause_child in import_clause.named_children(&mut clause_cursor) {
3218 match clause_child.kind() {
3219 "identifier" => {
3220 default_alias = Some(node_text(&clause_child, src).to_string());
3222 }
3223 "namespace_import" => {
3224 module_alias = first_named_child_of_kind(&clause_child, "identifier")
3226 .map(|ident| node_text(&ident, src).to_string());
3227 is_wildcard = true;
3228 }
3229 "named_imports" => {
3230 let mut named_cursor = clause_child.walk();
3231 for specifier in clause_child.named_children(&mut named_cursor) {
3232 if specifier.kind() != "import_specifier" {
3233 continue;
3234 }
3235 let original_name = specifier
3236 .child_by_field_name("name")
3237 .map(|name_node| node_text(&name_node, src).to_string());
3238 let local_alias = specifier
3239 .child_by_field_name("alias")
3240 .map(|alias_node| node_text(&alias_node, src).to_string());
3241 match (original_name, local_alias) {
3242 (Some(orig), Some(local)) => renames.push((orig, local)),
3244 (Some(orig), None) => shorthands.push(orig),
3246 _ => {}
3247 }
3248 }
3249 }
3250 _ => {}
3251 }
3252 }
3253 }
3254 imports.push(ImportSpec {
3255 span: span_of(file, &import_node),
3256 module: module.clone(),
3257 alias: module_alias,
3258 is_wildcard,
3259 original_name: None,
3260 scope: ImportScope::Module,
3261 });
3262 if let Some(default_alias) = default_alias.filter(|alias| !alias.is_empty()) {
3263 imports.push(ImportSpec {
3264 span: span_of(file, &import_node),
3265 module: module.clone(),
3266 alias: Some(default_alias),
3267 is_wildcard: false,
3268 original_name: Some("default".to_string()),
3269 scope: ImportScope::Module,
3270 });
3271 }
3272 for (original_name, local_alias) in renames {
3273 imports.push(ImportSpec {
3274 span: span_of(file, &import_node),
3275 module: module.clone(),
3276 alias: Some(local_alias),
3277 is_wildcard: false,
3278 original_name: Some(original_name),
3279 scope: ImportScope::Module,
3280 });
3281 }
3282 for shorthand_name in shorthands {
3283 imports.push(ImportSpec {
3284 span: span_of(file, &import_node),
3285 module: module.clone(),
3286 alias: Some(shorthand_name.clone()),
3287 is_wildcard: false,
3288 original_name: Some(shorthand_name),
3289 scope: ImportScope::Local,
3290 });
3291 }
3292 }
3293 imports
3294}
3295
3296pub fn apply_js_ts_default_export_aliases(decl_index: &mut DeclIndex, tree: &Tree, src: &[u8], file: FileId) {
3303 let mut default_exports = Vec::new();
3304 for export_node in collect_kinds(tree, &["export_statement"]) {
3305 if !export_statement_has_default_modifier(export_node) {
3306 continue;
3307 }
3308 let target = export_node
3309 .child_by_field_name("declaration")
3310 .or_else(|| export_node.child_by_field_name("value"));
3311 let Some(target) = target else {
3312 continue;
3313 };
3314 if target.kind() == "identifier" {
3315 let name = node_text(&target, src).to_string();
3316 if !name.is_empty() {
3317 default_exports.push(DefaultExportTarget::Name(name));
3318 }
3319 } else {
3320 default_exports.push(DefaultExportTarget::Span(span_of(file, &target)));
3321 }
3322 }
3323 for assignment in collect_kinds(tree, &["assignment_expression"]) {
3324 let left = assignment.child_by_field_name("left");
3325 let right = assignment.child_by_field_name("right");
3326 let (Some(left), Some(right)) = (left, right) else {
3327 continue;
3328 };
3329 if node_text(&left, src).trim() != "module.exports" {
3330 continue;
3331 }
3332 if right.kind() == "identifier" {
3333 let name = node_text(&right, src).trim();
3334 if !name.is_empty() {
3335 default_exports.push(DefaultExportTarget::Name(name.to_string()));
3336 }
3337 continue;
3338 }
3339 default_exports.push(DefaultExportTarget::Span(span_of(file, &right)));
3340 if let Some(name_node) = right.child_by_field_name("name") {
3341 let name = node_text(&name_node, src).trim();
3342 if !name.is_empty() {
3343 default_exports.push(DefaultExportTarget::Name(name.to_string()));
3344 }
3345 }
3346 }
3347 if default_exports.is_empty() || decl_index.defs.iter().any(|decl| decl.name == "default") {
3348 return;
3349 }
3350
3351 let mut next_symbol = decl_index
3352 .defs
3353 .iter()
3354 .map(|decl| decl.symbol.raw())
3355 .max()
3356 .map_or(0, |raw| raw.saturating_add(1));
3357 let mut aliases = Vec::new();
3358 let mut seen_sources = Vec::new();
3359 for target in default_exports {
3360 let Some(source) = decl_index
3361 .defs
3362 .iter()
3363 .filter(|decl| {
3364 matches!(
3365 decl.kind,
3366 bonsai_lang_api::DeclKind::Function
3367 | bonsai_lang_api::DeclKind::Method
3368 | bonsai_lang_api::DeclKind::Constructor
3369 | bonsai_lang_api::DeclKind::Class
3370 )
3371 })
3372 .find(|decl| match &target {
3373 DefaultExportTarget::Span(span) => decl.span == *span,
3374 DefaultExportTarget::Name(name) => decl.name == *name,
3375 })
3376 else {
3377 continue;
3378 };
3379 if seen_sources.contains(&source.symbol) {
3380 continue;
3381 }
3382 seen_sources.push(source.symbol);
3383 let mut alias = source.clone();
3384 alias.symbol = SymbolId::new(next_symbol);
3385 next_symbol = next_symbol.saturating_add(1);
3386 alias.name = "default".to_string();
3387 alias.qualified_name = if alias.module_path.is_empty() {
3388 Some("default".to_string())
3389 } else {
3390 Some(format!("{}.default", alias.module_path.segments.join(".")))
3391 };
3392 aliases.push(alias);
3393 }
3394 decl_index.defs.extend(aliases);
3395}
3396
3397pub fn apply_js_ts_commonjs_named_export_aliases(
3411 decl_index: &mut DeclIndex,
3412 tree: &Tree,
3413 src: &[u8],
3414 file: FileId,
3415) {
3416 let mut aliases = Vec::new();
3417 let mut next_symbol = decl_index
3418 .defs
3419 .iter()
3420 .map(|decl| decl.symbol.raw())
3421 .max()
3422 .map_or(0, |raw| raw.saturating_add(1));
3423 for assignment in collect_kinds(tree, &["assignment_expression"]) {
3424 let left = assignment.child_by_field_name("left");
3425 let right = assignment.child_by_field_name("right");
3426 let (Some(left), Some(right)) = (left, right) else {
3427 continue;
3428 };
3429 let Some(export_name) = commonjs_named_export_member(node_text(&left, src)) else {
3430 if node_text(&left, src).trim() == "module.exports" {
3431 collect_commonjs_object_export_aliases(
3432 decl_index,
3433 &mut aliases,
3434 &mut next_symbol,
3435 right,
3436 src,
3437 );
3438 }
3439 continue;
3440 };
3441 if export_name == "default" || export_name.is_empty() {
3442 continue;
3443 }
3444 let right_span = span_of(file, &right);
3445 push_named_export_alias_for_span(
3446 decl_index,
3447 &mut aliases,
3448 &mut next_symbol,
3449 export_name,
3450 right_span,
3451 );
3452 }
3453 collect_es_named_export_aliases(decl_index, &mut aliases, &mut next_symbol, tree, src);
3454 decl_index.defs.extend(aliases);
3455}
3456
3457fn collect_commonjs_object_export_aliases(
3458 decl_index: &DeclIndex,
3459 aliases: &mut Vec<bonsai_lang_api::Decl>,
3460 next_symbol: &mut u32,
3461 right: Node<'_>,
3462 src: &[u8],
3463) {
3464 if right.kind() != "object" {
3465 return;
3466 }
3467 let mut cursor = right.walk();
3468 for child in right.named_children(&mut cursor) {
3469 if child.kind() != "pair" {
3470 continue;
3471 }
3472 let Some(key) = child.child_by_field_name("key") else {
3473 continue;
3474 };
3475 let Some(value) = child.child_by_field_name("value") else {
3476 continue;
3477 };
3478 let export_name = node_text(&key, src).trim().to_string();
3479 if export_name.is_empty() || export_name == "default" {
3480 continue;
3481 }
3482 if value.kind() == "identifier" {
3483 let local_name = node_text(&value, src).trim();
3484 push_named_export_alias_for_name(decl_index, aliases, next_symbol, export_name, local_name);
3485 } else {
3486 push_named_export_alias_for_span(
3487 decl_index,
3488 aliases,
3489 next_symbol,
3490 export_name,
3491 span_of(decl_index.file, &value),
3492 );
3493 }
3494 }
3495}
3496
3497fn export_statement_has_default_modifier(node: Node<'_>) -> bool {
3502 let mut cursor = node.walk();
3503 if !cursor.goto_first_child() {
3504 return false;
3505 }
3506 loop {
3507 let child = cursor.node();
3508 if !child.is_named() && child.kind() == "default" {
3509 return true;
3510 }
3511 if !cursor.goto_next_sibling() {
3512 return false;
3513 }
3514 }
3515}
3516
3517fn collect_es_named_export_aliases(
3518 decl_index: &DeclIndex,
3519 aliases: &mut Vec<bonsai_lang_api::Decl>,
3520 next_symbol: &mut u32,
3521 tree: &Tree,
3522 src: &[u8],
3523) {
3524 for export_node in collect_kinds(tree, &["export_statement"]) {
3525 let mut stack = vec![export_node];
3526 while let Some(current) = stack.pop() {
3527 if current.kind() == "export_specifier" {
3528 let local_name = current.child_by_field_name("name");
3529 let export_name = current.child_by_field_name("alias");
3530 if let (Some(local), Some(exported)) = (local_name, export_name) {
3531 let export_name = node_text(&exported, src).trim().to_string();
3532 let local_name = node_text(&local, src).trim();
3533 push_named_export_alias_for_name(
3534 decl_index,
3535 aliases,
3536 next_symbol,
3537 export_name,
3538 local_name,
3539 );
3540 }
3541 continue;
3542 }
3543 let mut cursor = current.walk();
3544 for child in current.named_children(&mut cursor) {
3545 stack.push(child);
3546 }
3547 }
3548 }
3549}
3550
3551fn push_named_export_alias_for_name(
3552 decl_index: &DeclIndex,
3553 aliases: &mut Vec<bonsai_lang_api::Decl>,
3554 next_symbol: &mut u32,
3555 export_name: String,
3556 local_name: &str,
3557) {
3558 if export_name.is_empty() || local_name.is_empty() || export_name == local_name {
3559 return;
3560 }
3561 let Some(source) = decl_index.defs.iter().find(|decl| decl.name == local_name) else {
3562 return;
3563 };
3564 push_named_export_alias(decl_index, aliases, next_symbol, export_name, source);
3565}
3566
3567fn push_named_export_alias_for_span(
3568 decl_index: &DeclIndex,
3569 aliases: &mut Vec<bonsai_lang_api::Decl>,
3570 next_symbol: &mut u32,
3571 export_name: String,
3572 source_span: bonsai_common::Span,
3573) {
3574 if export_name.is_empty() {
3575 return;
3576 }
3577 let Some(source) = decl_index.defs.iter().find(|decl| decl.span == source_span) else {
3578 return;
3579 };
3580 push_named_export_alias(decl_index, aliases, next_symbol, export_name, source);
3581}
3582
3583fn push_named_export_alias(
3584 decl_index: &DeclIndex,
3585 aliases: &mut Vec<bonsai_lang_api::Decl>,
3586 next_symbol: &mut u32,
3587 export_name: String,
3588 source: &bonsai_lang_api::Decl,
3589) {
3590 if source.name == export_name {
3591 return;
3592 }
3593 if !matches!(
3594 source.kind,
3595 bonsai_lang_api::DeclKind::Function
3596 | bonsai_lang_api::DeclKind::Method
3597 | bonsai_lang_api::DeclKind::Constructor
3598 | bonsai_lang_api::DeclKind::Class
3599 ) {
3600 return;
3601 }
3602 if decl_index.defs.iter().chain(aliases.iter()).any(|decl| {
3603 decl.name == export_name && decl.span == source.span && decl.body_span == source.body_span
3604 }) {
3605 return;
3606 }
3607 let mut alias = source.clone();
3608 alias.symbol = SymbolId::new(*next_symbol);
3609 *next_symbol = next_symbol.saturating_add(1);
3610 alias.name = export_name;
3611 alias.qualified_name = None;
3612 aliases.push(alias);
3613}
3614
3615fn commonjs_named_export_member(left: &str) -> Option<String> {
3616 let left = left.trim();
3617 let member = left
3618 .strip_prefix("exports.")
3619 .or_else(|| left.strip_prefix("module.exports."))?;
3620 (!member.is_empty()
3621 && member
3622 .chars()
3623 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$'))
3624 .then(|| member.to_string())
3625}
3626
3627enum DefaultExportTarget {
3628 Span(bonsai_common::Span),
3629 Name(String),
3630}
3631
3632#[derive(Clone, Debug)]
3633struct JsDestructureSource {
3634 assign_span: bonsai_common::Span,
3635 target: String,
3636 base: String,
3637 source: String,
3638}
3639
3640fn rewrite_javascript_object_destructuring_sources(
3641 decl_index: &mut DeclIndex,
3642 tree: &Tree,
3643 src: &[u8],
3644 file: FileId,
3645) {
3646 let rewrites = collect_javascript_object_destructuring_sources(tree, src, file);
3647 if rewrites.is_empty() {
3648 return;
3649 }
3650 for decl in &mut decl_index.defs {
3651 let owner_span = decl.body_span.unwrap_or(decl.span);
3652 let relevant = rewrites
3653 .iter()
3654 .filter(|item| span_contains_or_equal(owner_span, item.assign_span))
3655 .cloned()
3656 .collect::<Vec<_>>();
3657 if !relevant.is_empty() {
3658 rewrite_destructuring_sources_in_events(&mut decl.flow_events, &relevant);
3659 }
3660 }
3661}
3662
3663fn collect_javascript_object_destructuring_sources(
3664 tree: &Tree,
3665 src: &[u8],
3666 file: FileId,
3667) -> Vec<JsDestructureSource> {
3668 let mut out = Vec::new();
3669 for declarator in collect_kinds(tree, &["variable_declarator"]) {
3670 let Some(pattern) = declarator.child_by_field_name("name") else {
3671 continue;
3672 };
3673 let Some(value) = declarator.child_by_field_name("value") else {
3674 continue;
3675 };
3676 if pattern.kind() != "object_pattern" {
3677 continue;
3678 }
3679 let base = normalize_js_member_text(node_text(&value, src));
3680 if base.is_empty() {
3681 continue;
3682 }
3683 collect_js_object_pattern_sources(pattern, &base, span_of(file, &declarator), src, &mut out);
3684 }
3685 out
3686}
3687
3688fn collect_js_object_pattern_sources(
3689 pattern: Node<'_>,
3690 base: &str,
3691 assign_span: bonsai_common::Span,
3692 src: &[u8],
3693 out: &mut Vec<JsDestructureSource>,
3694) {
3695 let mut cursor = pattern.walk();
3696 for child in pattern.named_children(&mut cursor) {
3697 match child.kind() {
3698 "shorthand_property_identifier_pattern" => {
3699 let target = node_text(&child, src).trim().to_string();
3700 if !target.is_empty() {
3701 out.push(JsDestructureSource {
3702 assign_span,
3703 base: base.to_string(),
3704 source: format!("{base}.{target}"),
3705 target,
3706 });
3707 }
3708 }
3709 "pair_pattern" => {
3710 let Some(key_node) = child.child_by_field_name("key") else {
3711 continue;
3712 };
3713 let Some(value_node) = child.child_by_field_name("value") else {
3714 continue;
3715 };
3716 let Some(key) = js_object_field_key(key_node, src) else {
3717 continue;
3718 };
3719 if let Some(target) = js_destructure_target_name(value_node, src) {
3720 out.push(JsDestructureSource {
3721 assign_span,
3722 base: base.to_string(),
3723 source: format!("{base}.{key}"),
3724 target,
3725 });
3726 }
3727 }
3728 "object_assignment_pattern" => {
3729 let Some(left) = child.child_by_field_name("left") else {
3730 continue;
3731 };
3732 let target = node_text(&left, src).trim().to_string();
3733 if !target.is_empty() {
3734 out.push(JsDestructureSource {
3735 assign_span,
3736 base: base.to_string(),
3737 source: format!("{base}.{target}"),
3738 target,
3739 });
3740 }
3741 }
3742 "rest_pattern" => {}
3743 _ => {}
3744 }
3745 }
3746}
3747
3748fn js_destructure_target_name(node: Node<'_>, src: &[u8]) -> Option<String> {
3749 match node.kind() {
3750 "identifier" | "shorthand_property_identifier_pattern" => {
3751 let target = node_text(&node, src).trim().to_string();
3752 (!target.is_empty()).then_some(target)
3753 }
3754 "assignment_pattern" => node
3755 .child_by_field_name("left")
3756 .and_then(|left| js_destructure_target_name(left, src)),
3757 _ => None,
3758 }
3759}
3760
3761fn rewrite_destructuring_sources_in_events(events: &mut Vec<FlowEvent>, rewrites: &[JsDestructureSource]) {
3762 let original = std::mem::take(events);
3763 for mut event in original {
3764 match &mut event {
3765 FlowEvent::Branch {
3766 then_events,
3767 else_events,
3768 ..
3769 } => {
3770 rewrite_destructuring_sources_in_events(then_events, rewrites);
3771 rewrite_destructuring_sources_in_events(else_events, rewrites);
3772 }
3773 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
3774 rewrite_destructuring_sources_in_events(body, rewrites);
3775 }
3776 FlowEvent::Try {
3777 body,
3778 catch_events,
3779 finally_events,
3780 ..
3781 } => {
3782 rewrite_destructuring_sources_in_events(body, rewrites);
3783 rewrite_destructuring_sources_in_events(catch_events, rewrites);
3784 rewrite_destructuring_sources_in_events(finally_events, rewrites);
3785 }
3786 _ => {}
3787 }
3788
3789 let rewrite = match &event {
3790 FlowEvent::Assign { span, target, .. } => rewrites
3791 .iter()
3792 .find(|item| item.target == *target && spans_overlap_or_contain(*span, item.assign_span)),
3793 _ => None,
3794 };
3795 if let Some(rewrite) = rewrite {
3796 let mut aggregate_event = event.clone();
3802 set_destructuring_assignment_source(
3803 &mut aggregate_event,
3804 &rewrite.base,
3805 bonsai_lang_api::AssignValueKind::Destructure,
3806 );
3807 set_destructuring_assignment_source(
3808 &mut event,
3809 &rewrite.source,
3810 bonsai_lang_api::AssignValueKind::Compound,
3811 );
3812 events.push(aggregate_event);
3813 }
3814 events.push(event);
3815 }
3816}
3817
3818fn set_destructuring_assignment_source(
3819 event: &mut FlowEvent,
3820 source: &str,
3821 assignment_kind: bonsai_lang_api::AssignValueKind,
3822) {
3823 let FlowEvent::Assign {
3824 source_name,
3825 source_call,
3826 source_call_args,
3827 source_names,
3828 value_kind,
3829 ..
3830 } = event
3831 else {
3832 return;
3833 };
3834 *source_name = Some(source.to_string());
3835 *source_call = None;
3836 source_call_args.clear();
3837 source_names.clear();
3838 source_names.push(source.to_string());
3839 *value_kind = Some(assignment_kind);
3840}
3841
3842#[derive(Clone, Debug)]
3843struct JsObjectFieldAssigns {
3844 assign_span: bonsai_common::Span,
3845 target: String,
3846 fields: Vec<FlowEvent>,
3847}
3848
3849fn inject_javascript_object_literal_field_assigns(
3850 decl_index: &mut DeclIndex,
3851 tree: &Tree,
3852 src: &[u8],
3853 file: FileId,
3854) {
3855 let field_assigns = collect_javascript_object_literal_field_assigns(tree, src, file);
3856 if field_assigns.is_empty() {
3857 return;
3858 }
3859 for decl in &mut decl_index.defs {
3860 let owner_span = decl.body_span.unwrap_or(decl.span);
3861 let relevant = field_assigns
3862 .iter()
3863 .filter(|item| span_contains_or_equal(owner_span, item.assign_span))
3864 .cloned()
3865 .collect::<Vec<_>>();
3866 if !relevant.is_empty() {
3867 insert_object_field_assigns_in_events(&mut decl.flow_events, &relevant);
3868 }
3869 }
3870}
3871
3872fn collect_javascript_object_literal_field_assigns(
3873 tree: &Tree,
3874 src: &[u8],
3875 file: FileId,
3876) -> Vec<JsObjectFieldAssigns> {
3877 let mut out = Vec::new();
3878 for declarator in collect_kinds(tree, &["variable_declarator"]) {
3879 let Some(name) = declarator.child_by_field_name("name") else {
3880 continue;
3881 };
3882 let Some(value) = declarator.child_by_field_name("value") else {
3883 continue;
3884 };
3885 if value.kind() != "object" || name.kind() != "identifier" {
3886 continue;
3887 }
3888 let target = node_text(&name, src).trim().to_string();
3889 push_javascript_object_literal_field_assigns(
3890 &mut out,
3891 span_of(file, &declarator),
3892 &target,
3893 value,
3894 src,
3895 file,
3896 );
3897 }
3898 for assignment in collect_kinds(tree, &["assignment_expression"]) {
3899 let Some(left) = assignment.child_by_field_name("left") else {
3900 continue;
3901 };
3902 let Some(right) = assignment.child_by_field_name("right") else {
3903 continue;
3904 };
3905 if right.kind() != "object" {
3906 continue;
3907 }
3908 let target = normalize_js_member_text(node_text(&left, src));
3909 push_javascript_object_literal_field_assigns(
3910 &mut out,
3911 span_of(file, &assignment),
3912 &target,
3913 right,
3914 src,
3915 file,
3916 );
3917 }
3918 out
3919}
3920
3921fn push_javascript_object_literal_field_assigns(
3922 out: &mut Vec<JsObjectFieldAssigns>,
3923 assign_span: bonsai_common::Span,
3924 target: &str,
3925 object: Node<'_>,
3926 src: &[u8],
3927 file: FileId,
3928) {
3929 if target.trim().is_empty() {
3930 return;
3931 }
3932 let mut fields = Vec::new();
3933 let mut cursor = object.walk();
3934 for child in object.named_children(&mut cursor) {
3935 match child.kind() {
3936 "pair" => {
3937 let Some(key_node) = child.child_by_field_name("key") else {
3938 continue;
3939 };
3940 let Some(value_node) = child.child_by_field_name("value") else {
3941 continue;
3942 };
3943 let Some(key) = js_object_field_key(key_node, src) else {
3944 continue;
3945 };
3946 let sources = js_value_source_names(value_node, src);
3947 fields.push(FlowEvent::Assign {
3948 span: span_of(file, &value_node),
3949 target: format!("{target}.{key}"),
3950 source_name: (sources.len() == 1).then(|| sources[0].clone()),
3951 source_call: None,
3952 source_call_args: Vec::new(),
3953 source_names: sources,
3954 declares_new_binding: false,
3955 value_kind: Some(bonsai_lang_api::AssignValueKind::Compound),
3956 });
3957 }
3958 "shorthand_property_identifier" => {
3959 let key = node_text(&child, src).trim().to_string();
3960 if key.is_empty() {
3961 continue;
3962 }
3963 fields.push(FlowEvent::Assign {
3964 span: span_of(file, &child),
3965 target: format!("{target}.{key}"),
3966 source_name: Some(key.clone()),
3967 source_call: None,
3968 source_call_args: Vec::new(),
3969 source_names: vec![key],
3970 declares_new_binding: false,
3971 value_kind: Some(bonsai_lang_api::AssignValueKind::Compound),
3972 });
3973 }
3974 "spread_element" => {}
3975 _ => {}
3976 }
3977 }
3978 if !fields.is_empty() {
3979 out.push(JsObjectFieldAssigns {
3980 assign_span,
3981 target: target.to_string(),
3982 fields,
3983 });
3984 }
3985}
3986
3987fn insert_object_field_assigns_in_events(
3988 events: &mut Vec<FlowEvent>,
3989 field_assigns: &[JsObjectFieldAssigns],
3990) {
3991 let mut index = 0usize;
3992 while index < events.len() {
3993 match &mut events[index] {
3994 FlowEvent::Branch {
3995 then_events,
3996 else_events,
3997 ..
3998 } => {
3999 insert_object_field_assigns_in_events(then_events, field_assigns);
4000 insert_object_field_assigns_in_events(else_events, field_assigns);
4001 }
4002 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
4003 insert_object_field_assigns_in_events(body, field_assigns);
4004 }
4005 FlowEvent::Try {
4006 body,
4007 catch_events,
4008 finally_events,
4009 ..
4010 } => {
4011 insert_object_field_assigns_in_events(body, field_assigns);
4012 insert_object_field_assigns_in_events(catch_events, field_assigns);
4013 insert_object_field_assigns_in_events(finally_events, field_assigns);
4014 }
4015 _ => {}
4016 }
4017
4018 let inserts = match &events[index] {
4019 FlowEvent::Assign { span, target, .. } => field_assigns
4020 .iter()
4021 .filter(|item| item.target == *target && spans_overlap_or_contain(*span, item.assign_span))
4022 .flat_map(|item| item.fields.clone())
4023 .filter(|field_event| !event_list_contains_assign(events, field_event))
4024 .collect::<Vec<_>>(),
4025 _ => Vec::new(),
4026 };
4027 if inserts.is_empty() {
4028 index += 1;
4029 continue;
4030 }
4031 let inserted = inserts.len();
4032 events.splice((index + 1)..=index, inserts);
4033 index += inserted + 1;
4034 }
4035}
4036
4037fn event_list_contains_assign(events: &[FlowEvent], candidate: &FlowEvent) -> bool {
4038 let FlowEvent::Assign {
4039 span: wanted_span,
4040 target: wanted_target,
4041 ..
4042 } = candidate
4043 else {
4044 return false;
4045 };
4046 events.iter().any(|event| match event {
4047 FlowEvent::Assign { span, target, .. } => span == wanted_span && target == wanted_target,
4048 FlowEvent::Branch {
4049 then_events,
4050 else_events,
4051 ..
4052 } => {
4053 event_list_contains_assign(then_events, candidate)
4054 || event_list_contains_assign(else_events, candidate)
4055 }
4056 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
4057 event_list_contains_assign(body, candidate)
4058 }
4059 FlowEvent::Try {
4060 body,
4061 catch_events,
4062 finally_events,
4063 ..
4064 } => {
4065 event_list_contains_assign(body, candidate)
4066 || event_list_contains_assign(catch_events, candidate)
4067 || event_list_contains_assign(finally_events, candidate)
4068 }
4069 _ => false,
4070 })
4071}
4072
4073fn js_object_field_key(node: Node<'_>, src: &[u8]) -> Option<String> {
4074 let raw = node_text(&node, src).trim();
4075 let key = raw
4076 .strip_prefix('"')
4077 .and_then(|part| part.strip_suffix('"'))
4078 .or_else(|| raw.strip_prefix('\'').and_then(|part| part.strip_suffix('\'')))
4079 .or_else(|| raw.strip_prefix('`').and_then(|part| part.strip_suffix('`')))
4080 .unwrap_or(raw)
4081 .trim();
4082 if key.is_empty()
4083 || !key
4084 .chars()
4085 .all(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphanumeric())
4086 {
4087 return None;
4088 }
4089 Some(key.to_string())
4090}
4091
4092fn js_value_source_names(node: Node<'_>, src: &[u8]) -> Vec<String> {
4093 let mut out = Vec::new();
4094 collect_js_value_source_names(node, src, &mut out);
4095 out.sort();
4096 out.dedup();
4097 out
4098}
4099
4100fn collect_js_value_source_names(node: Node<'_>, src: &[u8], out: &mut Vec<String>) {
4101 match node.kind() {
4102 "identifier" | "shorthand_property_identifier" => {
4103 push_js_source_name(out, node_text(&node, src).trim());
4104 }
4105 "member_expression" => {
4106 push_js_source_name(out, &normalize_js_member_text(node_text(&node, src)));
4107 if let Some(object) = node.child_by_field_name("object") {
4108 collect_js_value_source_names(object, src, out);
4109 }
4110 return;
4111 }
4112 "string" | "number" | "true" | "false" | "null" | "undefined" | "property_identifier" => return,
4113 _ => {}
4114 }
4115 let mut cursor = node.walk();
4116 for child in node.named_children(&mut cursor) {
4117 collect_js_value_source_names(child, src, out);
4118 }
4119}
4120
4121fn push_js_source_name(out: &mut Vec<String>, source: &str) {
4122 let source = source.trim();
4123 if source.is_empty()
4124 || source.chars().next().is_some_and(|ch| ch.is_ascii_digit())
4125 || source.contains(['"', '\'', '`', '{', '}'])
4126 {
4127 return;
4128 }
4129 out.push(source.to_string());
4130}
4131
4132fn normalize_js_member_text(text: &str) -> String {
4133 text.trim()
4134 .replace("?.", ".")
4135 .replace("?.[", ".[")
4136 .replace([' ', '\t', '\n', '\r'], "")
4137 .trim_end_matches(';')
4138 .to_string()
4139}
4140
4141fn span_contains_or_equal(outer: bonsai_common::Span, inner: bonsai_common::Span) -> bool {
4142 outer.file == inner.file && outer.start <= inner.start && outer.end >= inner.end
4143}
4144
4145fn spans_overlap_or_contain(left: bonsai_common::Span, right: bonsai_common::Span) -> bool {
4146 left.file == right.file
4147 && (span_contains_or_equal(left, right)
4148 || span_contains_or_equal(right, left)
4149 || (left.start <= right.end && right.start <= left.end))
4150}
4151
4152fn normalize_node_builtin_scheme(module: &str) -> String {
4164 module.strip_prefix("node:").unwrap_or(module).to_string()
4165}
4166
4167pub fn js_ts_require_calls(file: FileId, tree: &tree_sitter::Tree, src: &[u8]) -> Vec<ImportSpec> {
4168 let mut imports = Vec::new();
4169 for call_node in collect_kinds(tree, &["call_expression"]) {
4170 let Some(callee) = call_node.child_by_field_name("function") else {
4171 continue;
4172 };
4173 if node_text(&callee, src) != "require" {
4176 continue;
4177 }
4178 let Some(arguments) = call_node.child_by_field_name("arguments") else {
4179 continue;
4180 };
4181 let module = first_named_child_of_kind(&arguments, "string")
4183 .and_then(|string_node| first_named_child_of_kind(&string_node, "string_fragment"))
4184 .map(|fragment| node_text(&fragment, src).to_string())
4185 .unwrap_or_default();
4186 let module = normalize_node_builtin_scheme(&module);
4187 if module.is_empty() {
4188 continue;
4189 }
4190 let declarator = call_node
4193 .parent()
4194 .filter(|parent| parent.kind() == "variable_declarator");
4195 let lhs_name_node = declarator.and_then(|vd| vd.child_by_field_name("name"));
4196 let simple_alias = lhs_name_node
4198 .filter(|name| name.kind() == "identifier")
4199 .map(|name| node_text(&name, src).to_string());
4200 let mut rename_entries: Vec<(String, String)> = Vec::new();
4205 let mut shorthand_entries: Vec<String> = Vec::new();
4206 if let Some(object_pattern) = lhs_name_node.filter(|name| name.kind() == "object_pattern") {
4207 let mut pattern_cursor = object_pattern.walk();
4208 for pattern_child in object_pattern.named_children(&mut pattern_cursor) {
4209 match pattern_child.kind() {
4210 "pair_pattern" => {
4211 let key_node = pattern_child.child_by_field_name("key");
4212 let value_node = pattern_child.child_by_field_name("value");
4213 if let (Some(key), Some(value)) = (key_node, value_node) {
4214 let original_name = node_text(&key, src).to_string();
4215 let local_name = node_text(&value, src).to_string();
4216 if !original_name.is_empty()
4218 && !local_name.is_empty()
4219 && original_name != local_name
4220 {
4221 rename_entries.push((original_name, local_name));
4222 }
4223 }
4224 }
4225 "shorthand_property_identifier_pattern" => {
4226 let name = node_text(&pattern_child, src).to_string();
4227 if !name.is_empty() {
4228 shorthand_entries.push(name);
4229 }
4230 }
4231 "object_assignment_pattern" => {
4232 if let Some(left) = pattern_child.child_by_field_name("left") {
4234 let name = node_text(&left, src).to_string();
4235 if !name.is_empty() {
4236 shorthand_entries.push(name);
4237 }
4238 }
4239 }
4240 _ => {}
4241 }
4242 }
4243 }
4244 imports.push(ImportSpec {
4246 span: span_of(file, &call_node),
4247 module: module.clone(),
4248 alias: simple_alias,
4249 is_wildcard: false,
4250 original_name: None,
4251 scope: ImportScope::Module,
4252 });
4253 for (original_name, local_name) in rename_entries {
4255 imports.push(ImportSpec {
4256 span: span_of(file, &call_node),
4257 module: module.clone(),
4258 alias: Some(local_name),
4259 is_wildcard: false,
4260 original_name: Some(original_name),
4261 scope: ImportScope::Module,
4262 });
4263 }
4264 for shorthand_name in shorthand_entries {
4266 imports.push(ImportSpec {
4267 span: span_of(file, &call_node),
4268 module: module.clone(),
4269 alias: Some(shorthand_name.clone()),
4270 is_wildcard: false,
4271 original_name: Some(shorthand_name),
4272 scope: ImportScope::Local,
4273 });
4274 }
4275 }
4276 imports
4277}
4278
4279fn collect_javascript_class_bases(
4284 tree: &Tree,
4285 file: FileId,
4286 src: &[u8],
4287) -> Vec<(bonsai_common::Span, Vec<String>)> {
4288 let mut bases_by_class = Vec::new();
4289 for class_node in collect_kinds(tree, &["class_declaration", "class"]) {
4290 let mut bases: Vec<String> = Vec::new();
4291 let mut class_cursor = class_node.walk();
4292 for class_child in class_node.named_children(&mut class_cursor) {
4293 if matches!(class_child.kind(), "class_heritage" | "extends_clause") {
4295 collect_js_extends_names(class_child, src, &mut bases);
4296 }
4297 }
4298 if !bases.is_empty() {
4299 bases_by_class.push((span_of(file, &class_node), bases));
4300 }
4301 }
4302 bases_by_class
4303}
4304
4305fn collect_js_extends_names(node: Node<'_>, src: &[u8], collected_bases: &mut Vec<String>) {
4309 let mut stack = vec![node];
4310 while let Some(current) = stack.pop() {
4311 match current.kind() {
4312 "class_heritage" | "extends_clause" => {
4313 let mut cursor = current.walk();
4315 for child in current.named_children(&mut cursor) {
4316 stack.push(child);
4317 }
4318 }
4319 "identifier" | "member_expression" => {
4320 let raw_text = node_text(¤t, src).to_string();
4321 let canonical = raw_text
4323 .rsplit('.')
4324 .next()
4325 .unwrap_or(raw_text.as_str())
4326 .to_string();
4327 if !canonical.is_empty() && !collected_bases.iter().any(|b| b == &canonical) {
4328 collected_bases.push(canonical);
4329 }
4330 }
4331 _ => {
4332 let mut cursor = current.walk();
4334 for child in current.named_children(&mut cursor) {
4335 stack.push(child);
4336 }
4337 }
4338 }
4339 }
4340}
4341
4342fn rewrite_javascript_super_constructor_invocations(index: &mut DeclIndex) {
4343 let class_info: HashMap<SymbolId, (String, Vec<String>)> = index
4344 .defs
4345 .iter()
4346 .filter(|decl| matches!(decl.kind, DeclKind::Class))
4347 .map(|decl| (decl.symbol, (decl.name.clone(), decl.bases.clone())))
4348 .collect();
4349
4350 for decl in &mut index.defs {
4351 if !matches!(decl.kind, DeclKind::Constructor) {
4352 continue;
4353 }
4354 let Some(parent) = decl.parent else {
4355 continue;
4356 };
4357 let Some((_, bases)) = class_info.get(&parent) else {
4358 continue;
4359 };
4360 rewrite_javascript_super_constructor_invocations_in_events(
4361 &mut decl.flow_events,
4362 bases.first().map(String::as_str),
4363 );
4364 }
4365}
4366
4367fn rewrite_javascript_super_constructor_invocations_in_events(
4368 events: &mut [FlowEvent],
4369 super_ctor: Option<&str>,
4370) {
4371 for event in events {
4372 match event {
4373 FlowEvent::Call {
4374 name,
4375 receiver,
4376 receiver_types,
4377 call_kind,
4378 ..
4379 } => {
4380 if let Some(super_ctor) = super_ctor.filter(|ctor| !ctor.is_empty()) {
4381 if name.trim() == "super" {
4382 name.clear();
4383 name.push_str(super_ctor);
4384 *receiver = Some("super".to_string());
4385 receiver_types.clear();
4386 *call_kind = bonsai_lang_api::CallKind::Constructor;
4387 }
4388 }
4389 }
4390 FlowEvent::Branch {
4391 then_events,
4392 else_events,
4393 ..
4394 } => {
4395 rewrite_javascript_super_constructor_invocations_in_events(then_events, super_ctor);
4396 rewrite_javascript_super_constructor_invocations_in_events(else_events, super_ctor);
4397 }
4398 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
4399 rewrite_javascript_super_constructor_invocations_in_events(body, super_ctor);
4400 }
4401 FlowEvent::Try {
4402 body,
4403 catch_events,
4404 finally_events,
4405 ..
4406 } => {
4407 rewrite_javascript_super_constructor_invocations_in_events(body, super_ctor);
4408 rewrite_javascript_super_constructor_invocations_in_events(catch_events, super_ctor);
4409 rewrite_javascript_super_constructor_invocations_in_events(finally_events, super_ctor);
4410 }
4411 FlowEvent::Assign { .. }
4412 | FlowEvent::AggregateAssign { .. }
4413 | FlowEvent::Return { .. }
4414 | FlowEvent::Throw { .. }
4415 | FlowEvent::Break { .. }
4416 | FlowEvent::Continue { .. }
4417 | FlowEvent::Yield { .. }
4418 | FlowEvent::Await { .. }
4419 | FlowEvent::Lifecycle { .. } => {}
4420 }
4421 }
4422}
4423
4424#[derive(Clone, Debug, PartialEq, Eq)]
4425struct JsGetterProjection {
4426 property: String,
4427 projected_source: String,
4428}
4429
4430pub fn apply_javascript_getter_property_sources(
4431 decl_index: &mut DeclIndex,
4432 tree: &Tree,
4433 src: &[u8],
4434 file: FileId,
4435) {
4436 let own_getters = collect_javascript_getter_projections(decl_index, tree, src, file);
4437 if own_getters.is_empty() {
4438 return;
4439 }
4440
4441 let mut class_symbols = Vec::new();
4442 let mut class_symbol_by_name: HashMap<String, SymbolId> = HashMap::new();
4443 let mut base_symbols_by_class: HashMap<SymbolId, Vec<SymbolId>> = HashMap::new();
4444 for decl in &decl_index.defs {
4445 if !matches!(decl.kind, DeclKind::Class) {
4446 continue;
4447 }
4448 class_symbols.push(decl.symbol);
4449 class_symbol_by_name.insert(canonical_js_class_name(&decl.name), decl.symbol);
4450 if let Some(qualified) = &decl.qualified_name {
4451 class_symbol_by_name.insert(canonical_js_class_name(qualified), decl.symbol);
4452 }
4453 }
4454 for decl in &decl_index.defs {
4455 if !matches!(decl.kind, DeclKind::Class) {
4456 continue;
4457 }
4458 let bases = decl
4459 .bases
4460 .iter()
4461 .filter_map(|base| class_symbol_by_name.get(&canonical_js_class_name(base)).copied())
4462 .collect::<Vec<_>>();
4463 if !bases.is_empty() {
4464 base_symbols_by_class.insert(decl.symbol, bases);
4465 }
4466 }
4467
4468 let mut getters_by_class: HashMap<SymbolId, Vec<JsGetterProjection>> = HashMap::new();
4469 for class_symbol in class_symbols {
4470 let mut projections = Vec::new();
4471 let mut seen_properties = HashSet::new();
4472 let mut visiting = HashSet::new();
4473 collect_getters_for_class(
4474 class_symbol,
4475 &own_getters,
4476 &base_symbols_by_class,
4477 &mut seen_properties,
4478 &mut visiting,
4479 &mut projections,
4480 );
4481 if !projections.is_empty() {
4482 getters_by_class.insert(class_symbol, projections);
4483 }
4484 }
4485
4486 for decl in &mut decl_index.defs {
4487 if !matches!(decl.kind, DeclKind::Method | DeclKind::Constructor) {
4488 continue;
4489 }
4490 let Some(parent) = decl.parent else {
4491 continue;
4492 };
4493 let Some(projections) = getters_by_class.get(&parent) else {
4494 continue;
4495 };
4496 enrich_getter_property_sources_in_events(&mut decl.flow_events, projections);
4497 }
4498}
4499
4500fn collect_javascript_getter_projections(
4501 decl_index: &DeclIndex,
4502 tree: &Tree,
4503 src: &[u8],
4504 file: FileId,
4505) -> HashMap<SymbolId, Vec<JsGetterProjection>> {
4506 let mut by_class: HashMap<SymbolId, Vec<JsGetterProjection>> = HashMap::new();
4507 for method in collect_kinds(tree, &["method_definition"]) {
4508 if !is_javascript_getter_method(method, src) {
4509 continue;
4510 }
4511 let method_span = span_of(file, &method);
4512 let Some(decl) = decl_index.defs.iter().find(|decl| {
4513 decl.span == method_span && matches!(decl.kind, DeclKind::Method) && decl.parent.is_some()
4514 }) else {
4515 continue;
4516 };
4517 let Some(parent) = decl.parent else {
4518 continue;
4519 };
4520 let Some(projected_source) = first_simple_js_getter_return_projection(&decl.flow_events) else {
4521 continue;
4522 };
4523 let projection = JsGetterProjection {
4524 property: decl.name.clone(),
4525 projected_source,
4526 };
4527 let entries = by_class.entry(parent).or_default();
4528 if !entries.iter().any(|existing| existing == &projection) {
4529 entries.push(projection);
4530 }
4531 }
4532 by_class
4533}
4534
4535fn is_javascript_getter_method(method: Node<'_>, src: &[u8]) -> bool {
4536 if method.kind() != "method_definition" {
4537 return false;
4538 }
4539 let Some(name) = method.child_by_field_name("name") else {
4540 return false;
4541 };
4542 let Some(prefix) = src.get(method.start_byte()..name.start_byte()) else {
4543 return false;
4544 };
4545 let Ok(prefix) = std::str::from_utf8(prefix) else {
4546 return false;
4547 };
4548 prefix
4549 .split(|ch: char| !ch.is_ascii_alphabetic())
4550 .any(|token| token == "get")
4551}
4552
4553fn first_simple_js_getter_return_projection(events: &[FlowEvent]) -> Option<String> {
4554 for event in events {
4555 match event {
4556 FlowEvent::Return { value_flow, .. } => {
4557 if let Some(projected) = value_flow.projection.as_ref().and_then(|projection| {
4558 matches!(projection.base.as_str(), "this" | "super").then(|| projection.canonical_place())
4559 }) {
4560 return Some(projected);
4561 }
4562 }
4563 FlowEvent::Branch {
4564 then_events,
4565 else_events,
4566 ..
4567 } => {
4568 if let Some(projected) = first_simple_js_getter_return_projection(then_events)
4569 .or_else(|| first_simple_js_getter_return_projection(else_events))
4570 {
4571 return Some(projected);
4572 }
4573 }
4574 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
4575 if let Some(projected) = first_simple_js_getter_return_projection(body) {
4576 return Some(projected);
4577 }
4578 }
4579 FlowEvent::Try {
4580 body,
4581 catch_events,
4582 finally_events,
4583 ..
4584 } => {
4585 if let Some(projected) = first_simple_js_getter_return_projection(body)
4586 .or_else(|| first_simple_js_getter_return_projection(catch_events))
4587 .or_else(|| first_simple_js_getter_return_projection(finally_events))
4588 {
4589 return Some(projected);
4590 }
4591 }
4592 _ => {}
4593 }
4594 }
4595 None
4596}
4597
4598fn collect_getters_for_class(
4599 class_symbol: SymbolId,
4600 own_getters: &HashMap<SymbolId, Vec<JsGetterProjection>>,
4601 base_symbols_by_class: &HashMap<SymbolId, Vec<SymbolId>>,
4602 seen_properties: &mut HashSet<String>,
4603 visiting: &mut HashSet<SymbolId>,
4604 out: &mut Vec<JsGetterProjection>,
4605) {
4606 if !visiting.insert(class_symbol) {
4607 return;
4608 }
4609 if let Some(getters) = own_getters.get(&class_symbol) {
4610 for getter in getters {
4611 if seen_properties.insert(getter.property.clone()) {
4612 out.push(getter.clone());
4613 }
4614 }
4615 }
4616 if let Some(bases) = base_symbols_by_class.get(&class_symbol) {
4617 for base in bases {
4618 collect_getters_for_class(
4619 *base,
4620 own_getters,
4621 base_symbols_by_class,
4622 seen_properties,
4623 visiting,
4624 out,
4625 );
4626 }
4627 }
4628 visiting.remove(&class_symbol);
4629}
4630
4631fn enrich_getter_property_sources_in_events(events: &mut [FlowEvent], projections: &[JsGetterProjection]) {
4632 for event in events {
4633 match event {
4634 FlowEvent::Assign {
4635 source_name,
4636 source_names,
4637 ..
4638 } => {
4639 if let Some(projected) = source_name
4640 .as_deref()
4641 .and_then(|source| projected_js_getter_source(source, projections))
4642 {
4643 push_unique_source(source_names, projected);
4644 }
4645 enrich_getter_source_names(source_names, projections);
4646 }
4647 FlowEvent::Call { args, .. } => {
4648 for arg in args {
4649 enrich_getter_sources_in_call_arg(arg, projections);
4650 }
4651 }
4652 FlowEvent::Branch {
4653 then_events,
4654 else_events,
4655 ..
4656 } => {
4657 enrich_getter_property_sources_in_events(then_events, projections);
4658 enrich_getter_property_sources_in_events(else_events, projections);
4659 }
4660 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
4661 enrich_getter_property_sources_in_events(body, projections);
4662 }
4663 FlowEvent::Try {
4664 body,
4665 catch_events,
4666 finally_events,
4667 ..
4668 } => {
4669 enrich_getter_property_sources_in_events(body, projections);
4670 enrich_getter_property_sources_in_events(catch_events, projections);
4671 enrich_getter_property_sources_in_events(finally_events, projections);
4672 }
4673 _ => {}
4674 }
4675 }
4676}
4677
4678fn enrich_getter_sources_in_call_arg(arg: &mut CallArg, projections: &[JsGetterProjection]) {
4679 let mut candidates = Vec::new();
4680 candidates.push(arg.value_text.clone());
4681 if let Some(place) = arg.place.as_deref() {
4682 candidates.push(place.to_string());
4683 }
4684 for source in &arg.source_names {
4685 candidates.push(source.clone());
4686 }
4687 for candidate in candidates {
4688 if let Some(projected) = projected_js_getter_source(&candidate, projections) {
4689 push_unique_source(&mut arg.source_names, projected);
4690 }
4691 }
4692 enrich_getter_source_names(&mut arg.source_names, projections);
4693}
4694
4695fn enrich_getter_source_names(source_names: &mut Vec<String>, projections: &[JsGetterProjection]) {
4696 let existing = source_names.clone();
4697 for source in existing {
4698 if let Some(projected) = projected_js_getter_source(&source, projections) {
4699 push_unique_source(source_names, projected);
4700 }
4701 }
4702}
4703
4704fn projected_js_getter_source(source: &str, projections: &[JsGetterProjection]) -> Option<String> {
4705 let source = source.trim();
4706 for projection in projections {
4707 for receiver in ["this", "super"] {
4708 let property_read = format!("{receiver}.{}", projection.property);
4709 if source != property_read {
4710 continue;
4711 }
4712 if receiver == "this" {
4713 return Some(projection.projected_source.clone());
4714 }
4715 if let Some(rest) = projection.projected_source.strip_prefix("this.") {
4716 return Some(format!("super.{rest}"));
4717 }
4718 return Some(projection.projected_source.clone());
4719 }
4720 }
4721 None
4722}
4723
4724fn push_unique_source(source_names: &mut Vec<String>, source: String) {
4725 if !source.trim().is_empty() && !source_names.iter().any(|existing| existing == &source) {
4726 source_names.push(source);
4727 }
4728}
4729
4730fn canonical_js_class_name(name: &str) -> String {
4731 name.rsplit('.').next().unwrap_or(name).trim().to_string()
4732}
4733
4734pub fn js_ts_module_segments(path: &std::path::Path) -> Vec<String> {
4738 let mut segments: Vec<String> = path
4739 .components()
4740 .filter_map(|component| match component {
4741 std::path::Component::Normal(s) => Some(s.to_string_lossy().into_owned()),
4742 _ => None,
4743 })
4744 .collect();
4745 if let Some(last_segment) = segments.last_mut() {
4746 for extension in [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"] {
4748 if last_segment.ends_with(extension) {
4749 *last_segment = last_segment.trim_end_matches(extension).to_string();
4750 break;
4751 }
4752 }
4753 }
4754 segments.retain(|segment| !segment.is_empty());
4755 segments
4756}
4757
4758#[cfg(test)]
4759mod syntax_tests {
4760 use super::{collect_kinds, export_statement_has_default_modifier, language_from_pack, PACK_NAME};
4761
4762 fn export_has_default_modifier(source: &str) -> bool {
4763 let language = language_from_pack(PACK_NAME).expect("javascript grammar");
4764 let mut parser = tree_sitter::Parser::new();
4765 parser.set_language(&language).expect("set javascript grammar");
4766 let tree = parser.parse(source, None).expect("parse javascript");
4767 let exports = collect_kinds(&tree, &["export_statement"]);
4768 assert_eq!(exports.len(), 1, "expected one parsed export statement");
4769 export_statement_has_default_modifier(exports[0])
4770 }
4771
4772 #[test]
4773 fn default_export_modifier_comes_from_the_syntax_tree() {
4774 assert!(export_has_default_modifier("export default app;"));
4775 assert!(!export_has_default_modifier("export { app as default };"));
4776 assert!(!export_has_default_modifier("export const app = 1;"));
4777 }
4778}