1use bonsai_common::{FileId, Span, SymbolId};
3use bonsai_lang_api::{
4 decl_index_with_handler, extract_imports_via,
5 kit::{
6 call_arg_from_nodes_with_handler, collect_kinds, language_from_pack, node_text,
7 normalize_call_name_whitespace, parse_with, span_of,
8 },
9 AdapterContext, AdapterError, AssignmentValueIndex, CallArg, CallKind, CallTargetExtraction,
10 CharacterClass, CharacterConstraintDomain, CharacterConstraintFact, CharacterConstraintOutput,
11 CharacterSubstitutionDomain, CharacterSubstitutionFact, Comment, CommentKind, ConditionEquality,
12 ConditionExpressionFact, ConditionOperandFact, DeclIndex, DeclKind, FiniteLiteralSelectionFact,
13 FlowEvent, GrammarHandler, ImportIndex, ImportScope, ImportSpec, LanguageAdapter, LanguageCapabilities,
14 LanguageId, PatternSourceProjection, ProjectedPatternBindingSite, SameOriginPathConstraintFact,
15 StaticScalarValue, StaticStringMapEntry, StringCompositionFact, StringCompositionPart, TypeAliasBinding,
16 Visibility, EMPTY_HANDLER,
17};
18use tree_sitter::{Language, Node, Tree};
19
20pub const LANG_ID: LanguageId = LanguageId::new("python");
21const PACK_NAME: &str = "python";
22
23fn python_call_target<'tree>(node: Node<'tree>, src: &[u8]) -> Option<CallTargetExtraction<'tree>> {
24 if node.kind() != "call" {
25 return None;
26 }
27 let target = node.child_by_field_name("function")?;
28 let full_text = node_text(&target, src).trim();
29 (!full_text.is_empty()).then_some(CallTargetExtraction {
30 node: target,
31 full_text: full_text.to_string(),
32 })
33}
34
35fn python_pattern_bindings<'tree>(node: Node<'tree>, src: &[u8]) -> Vec<ProjectedPatternBindingSite<'tree>> {
36 if node.kind() != "match_statement" {
37 return Vec::new();
38 }
39 let Some(source) = node.child_by_field_name("subject") else {
40 return Vec::new();
41 };
42 let mut sites = Vec::new();
43 let mut stack = vec![node];
44 while let Some(current) = stack.pop() {
45 if current.id() != node.id() && current.kind() == "match_statement" {
46 continue;
47 }
48 if current.kind() == "case_clause" {
49 let mut cursor = current.walk();
50 for pattern in current
51 .named_children(&mut cursor)
52 .filter(|child| child.kind() == "case_pattern")
53 {
54 collect_python_pattern_bindings(pattern, pattern, source, &[], src, &mut sites);
59 }
60 continue;
61 }
62 let mut cursor = current.walk();
63 stack.extend(current.named_children(&mut cursor));
64 }
65 sites
66}
67
68fn collect_python_pattern_bindings<'tree>(
69 pattern: Node<'tree>,
70 span_node: Node<'tree>,
71 source: Node<'tree>,
72 projection: &[PatternSourceProjection],
73 src: &[u8],
74 out: &mut Vec<ProjectedPatternBindingSite<'tree>>,
75) {
76 match pattern.kind() {
77 "case_pattern" => {
78 if let Some(child) = pattern.named_child(0) {
79 collect_python_pattern_bindings(child, span_node, source, projection, src, out);
80 }
81 }
82 "dotted_name" => {
83 let mut cursor = pattern.walk();
84 let identifiers = pattern
85 .named_children(&mut cursor)
86 .filter(|child| child.kind() == "identifier")
87 .collect::<Vec<_>>();
88 if identifiers.len() == 1 {
89 push_python_pattern_binding(identifiers[0], span_node, source, projection, src, out);
90 }
91 }
92 "identifier" => {
93 push_python_pattern_binding(pattern, span_node, source, projection, src, out);
94 }
95 "dict_pattern" => {
96 let mut pending_key = None;
97 let mut cursor = pattern.walk();
98 if cursor.goto_first_child() {
99 loop {
100 let child = cursor.node();
101 if child.is_named() {
102 match cursor.field_name() {
103 Some("key") => {
104 pending_key = Some(python_static_subscript_key(child, src).map_or(
105 PatternSourceProjection::Descendants,
106 PatternSourceProjection::Field,
107 ));
108 }
109 Some("value") => {
110 let mut child_projection = projection.to_vec();
111 child_projection
112 .push(pending_key.take().unwrap_or(PatternSourceProjection::Descendants));
113 collect_python_pattern_bindings(
114 child,
115 span_node,
116 source,
117 &child_projection,
118 src,
119 out,
120 );
121 }
122 _ if child.kind() == "splat_pattern" => {
123 let mut child_projection = projection.to_vec();
124 child_projection.push(PatternSourceProjection::Descendants);
125 collect_python_pattern_bindings(
126 child,
127 span_node,
128 source,
129 &child_projection,
130 src,
131 out,
132 );
133 }
134 _ => {}
135 }
136 }
137 if !cursor.goto_next_sibling() {
138 break;
139 }
140 }
141 }
142 }
143 "class_pattern" => {
144 let mut cursor = pattern.walk();
145 let children = pattern.named_children(&mut cursor).collect::<Vec<_>>();
146 for child in children.into_iter().skip(1) {
147 let core = child.named_child(0).unwrap_or(child);
148 let child_projection = if core.kind() == "keyword_pattern" {
149 projection.to_vec()
150 } else {
151 let mut projected = projection.to_vec();
152 projected.push(PatternSourceProjection::Descendants);
153 projected
154 };
155 collect_python_pattern_bindings(child, span_node, source, &child_projection, src, out);
156 }
157 }
158 "keyword_pattern" => {
159 let mut cursor = pattern.walk();
160 let children = pattern.named_children(&mut cursor).collect::<Vec<_>>();
161 if let (Some(label), Some(value)) = (children.first(), children.get(1)) {
162 let label = node_text(label, src).trim();
163 let mut child_projection = projection.to_vec();
164 if label.is_empty() {
165 child_projection.push(PatternSourceProjection::Descendants);
166 } else {
167 child_projection.push(PatternSourceProjection::Field(label.to_string()));
168 }
169 collect_python_pattern_bindings(*value, span_node, source, &child_projection, src, out);
170 }
171 }
172 "as_pattern" => {
173 let alias_wrapper = pattern.child_by_field_name("alias");
174 let mut cursor = pattern.walk();
175 for child in pattern.named_children(&mut cursor) {
176 if alias_wrapper.is_some_and(|alias| alias.id() == child.id()) {
177 if let Some(alias) = first_python_identifier(child) {
178 push_python_pattern_binding(alias, span_node, source, projection, src, out);
179 }
180 } else {
181 collect_python_pattern_bindings(child, span_node, source, projection, src, out);
182 }
183 }
184 }
185 "list_pattern" | "tuple_pattern" => {
186 let mut cursor = pattern.walk();
187 let children = pattern.named_children(&mut cursor).collect::<Vec<_>>();
188 let has_remainder = children.iter().any(|child| {
189 child.kind() == "splat_pattern"
190 || child
191 .named_child(0)
192 .is_some_and(|nested| nested.kind() == "splat_pattern")
193 });
194 for (index, child) in children.into_iter().enumerate() {
195 let mut child_projection = projection.to_vec();
196 if has_remainder {
197 child_projection.push(PatternSourceProjection::Descendants);
198 } else {
199 child_projection.push(PatternSourceProjection::Element(index));
200 }
201 collect_python_pattern_bindings(child, span_node, source, &child_projection, src, out);
202 }
203 }
204 "splat_pattern" => {
205 if let Some(target) = first_python_identifier(pattern) {
206 push_python_pattern_binding(target, span_node, source, projection, src, out);
207 }
208 }
209 "union_pattern" => {
210 let mut cursor = pattern.walk();
211 for child in pattern.named_children(&mut cursor) {
212 collect_python_pattern_bindings(child, span_node, source, projection, src, out);
213 }
214 }
215 _ => {}
216 }
217}
218
219fn first_python_identifier(node: Node<'_>) -> Option<Node<'_>> {
220 if node.kind() == "identifier" {
221 return Some(node);
222 }
223 let mut stack = vec![node];
224 while let Some(current) = stack.pop() {
225 let mut cursor = current.walk();
226 for child in current.named_children(&mut cursor) {
227 if child.kind() == "identifier" {
228 return Some(child);
229 }
230 stack.push(child);
231 }
232 }
233 None
234}
235
236fn push_python_pattern_binding<'tree>(
237 target: Node<'tree>,
238 span_node: Node<'tree>,
239 source: Node<'tree>,
240 projection: &[PatternSourceProjection],
241 src: &[u8],
242 out: &mut Vec<ProjectedPatternBindingSite<'tree>>,
243) {
244 let name = node_text(&target, src).trim();
245 if !python_match_capture_identifier(name) {
246 return;
247 }
248 let site = ProjectedPatternBindingSite {
249 span_node,
250 target,
251 source,
252 projection: projection.to_vec(),
253 };
254 if !out.iter().any(|existing| {
255 existing.target.id() == site.target.id()
256 && existing.source.id() == site.source.id()
257 && existing.projection == site.projection
258 }) {
259 out.push(site);
260 }
261}
262
263fn python_using_alias(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
264 if node.kind() != "as_pattern" {
265 return None;
266 }
267 let alias_wrapper = node.child_by_field_name("alias")?;
268 let alias = if alias_wrapper.kind() == "identifier" {
269 alias_wrapper
270 } else {
271 let mut cursor = alias_wrapper.walk();
272 let alias = alias_wrapper
273 .named_children(&mut cursor)
274 .find(|child| child.kind() == "identifier");
275 alias?
276 };
277 let mut cursor = node.walk();
278 let value = node
279 .named_children(&mut cursor)
280 .find(|child| child.id() != alias_wrapper.id());
281 Some((alias, value?))
282}
283
284fn python_comprehension_binding(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
285 if node.kind() != "for_in_clause" {
286 return None;
287 }
288 let binding = node.child_by_field_name("left")?;
293 let iterable = node.child_by_field_name("right")?;
294 Some((binding, iterable))
295}
296
297fn python_foreach_binding(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
298 (node.kind() == "for_statement")
299 .then(|| {
300 Some((
301 node.child_by_field_name("left")?,
302 node.child_by_field_name("right")?,
303 ))
304 })
305 .flatten()
306}
307
308const HANDLER: GrammarHandler = GrammarHandler {
309 literal_value_kinds: &["none", "integer", "float", "true", "false"],
310 literal_value_spellings: &[],
311 string_literal_kinds: &["string", "concatenated_string"],
312 comment_kinds: &["comment"],
313 doc_comment_kinds: &[],
314 doc_comment_prefixes: &[],
315 decorator_kinds: &["decorator"],
316 parameter_container_kinds: &["parameters"],
317 parameter_kinds: &[
318 "identifier",
319 "typed_parameter",
320 "default_parameter",
321 "typed_default_parameter",
322 "list_splat_pattern",
323 "dictionary_splat_pattern",
324 ],
325 parameter_modifier_kinds: &[],
326 parameter_annotation_kinds: &["decorator"],
327 parameter_annotation_name_extractor: None,
328 keyword_parameter_kinds: &[],
329 parameter_selector_kinds: &[],
330 implicit_parameter_kinds: &[],
331 self_parameter_kinds: &[],
332 last_identifier_parameter_kinds: &[],
333 binding_identifier_kinds: &["identifier"],
334 non_binding_pattern_kinds: &[],
335 binding_lhs_pattern_kinds: &[],
336 binding_pattern_field_names: &[],
337 pattern_head_value_kinds: &["class_pattern"],
338 multi_segment_value_pattern_kinds: &["dotted_name"],
339 non_binding_pattern_field_names: &["type", "key", "class", "guard"],
340 binding_name_extractor: None,
341 binding_name_filter: None,
342 pattern_binding_extractor: None,
343 projected_pattern_binding_extractor: Some(python_pattern_bindings),
344 anonymous_variadic_token: None,
345 variadic_parameter_kinds: &["list_splat_pattern"],
346 destructured_parameter_kinds: &["list_pattern", "tuple_pattern", "dictionary_pattern"],
347 identifier_kinds: &["identifier"],
348 aggregate_pattern_kinds: &["pattern_list", "list_pattern", "tuple_pattern"],
349 comprehension_kinds: &[
350 "list_comprehension",
351 "dictionary_comprehension",
352 "set_comprehension",
353 "generator_expression",
354 ],
355 comprehension_binding_clause_kinds: &["for_in_clause"],
356 comprehension_binding_extractor: Some(python_comprehension_binding),
357 named_aggregate_kinds: &["dictionary"],
358 positional_aggregate_kinds: &["tuple", "list", "set"],
359 aggregate_pair_kinds: &["pair", "dict_pattern"],
360 two_child_aggregate_pair_kinds: &[],
361 aggregate_pair_extractor: None,
362 aggregate_key_field_names: &["key"],
363 aggregate_value_field_names: &["value"],
364 static_field_name_kinds: &["identifier"],
365 shorthand_field_kinds: &[],
366 spread_kinds: &["list_splat", "dictionary_splat", "parenthesized_list_splat"],
367 spread_value_field_names: &["value"],
368 aggregate_syntax_only_kinds: &[],
369 multi_child_aggregate_pattern_kinds: &[],
370 lambda_value_container_kinds: &["dictionary", "list", "set"],
371 transparent_call_wrapper_kinds: &["attribute", "subscript", "parenthesized_expression", "await"],
372 single_expression_group_kinds: &["expression_list"],
373 assignment_target_wrapper_kinds: &[],
374 binding_declaration_keyword_spellings: &[],
375 nested_type_ownership: true,
376 fn_kinds: &["function_definition"],
377 class_kinds: &["class_definition"],
378 class_decl_kinds: &[("class_definition", DeclKind::Class)],
379 method_kinds: &[],
380 method_context_kinds: &["class_definition"],
381 method_owner_barrier_kinds: &[],
382 constructor_method_kinds: &[],
383 constructor_names: &["__init__"],
384 function_definition_extractor: None,
385 inline_closure_yield_extractor: None,
386 if_kinds: &[
387 "if_statement",
388 "conditional_expression",
389 "match_statement",
390 "elif_clause",
391 ],
392 branch_then_field_names: &["consequence", "body"],
393 branch_else_field_names: &["alternative"],
394 branch_condition_field_names: &["condition", "subject"],
395 branch_condition_kinds: &[],
396 branch_alias_extractor: None,
397 branch_arm_kinds: &["block", "elif_clause", "else_clause"],
398 additional_alternative_kinds: &["elif_clause", "else_clause"],
399 for_kinds: &[],
400 foreach_kinds: &["for_statement"],
401 foreach_binding_extractor: Some(python_foreach_binding),
402 while_kinds: &["while_statement"],
403 do_kinds: &[],
404 loop_kinds: &[],
405 loop_body_field_names: &["body"],
406 loop_body_kinds: &["block"],
407 call_kinds: &["call"],
408 constructor_call_kinds: &[],
409 nested_call_component_kinds: &[],
410 call_callee_field_names: &["function"],
411 call_target_extractor: Some(python_call_target),
412 call_receiver_extractor: None,
413 call_receiver_field_names: &[],
414 call_member_field_names: &[],
415 constructor_type_field_names: &[],
416 call_argument_field_names: &["arguments"],
417 call_argument_container_kinds: &["argument_list"],
418 call_argument_wrapper_kinds: &[],
419 call_callee_is_first_named_child: false,
420 argument_wrapper_kinds: &["keyword_argument"],
421 argument_name_field_names: &["name"],
422 argument_value_field_names: &["value"],
423 named_argument_extractor: None,
424 direct_call_info_extractor: None,
425 call_ref_node_filter: None,
426 expression_call_span_extractor: None,
427 writeback_operand_field_names: &[],
428 direct_call_argument_excluded_fields: &[],
429 transparent_expression_wrapper_kinds: &["parenthesized_expression"],
430 pseudo_call_extractor: None,
431 syntax_event_extractor: None,
432 syntax_events_extractor: None,
433 call_encoded_control_flow_extractor: None,
434 pseudo_call_receiver_extractor: None,
435 argument_passing_mode_extractor: None,
436 expression_value_kind_extractor: None,
437 assignment_kinds: &["assignment", "augmented_assignment", "named_expression"],
438 assignment_semantics_extractor: None,
439 assignment_place_extractor: None,
440 compound_assignment_kinds: &["augmented_assignment"],
441 compound_assignment_operators: &[],
442 type_only_declaration_kinds: &[],
443 positional_aggregate_assignment_kinds: &[],
444 positional_aggregate_value_kinds: &[],
445 return_kinds: &["return_statement"],
446 throw_kinds: &["raise_statement"],
447 lambda_kinds: &["lambda"],
448 inline_closure_kinds: &[],
449 implicit_lambda_parameter_name: None,
450 lambda_body_field_names: &["body"],
451 lambda_body_kinds: &[],
452 try_kinds: &["try_statement"],
453 catch_kinds: &["except_clause"],
454 finally_kinds: &["finally_clause"],
455 try_fallback_body_kinds: &["block"],
456 catch_body_follows_marker: false,
457 break_kinds: &["break_statement"],
458 continue_kinds: &["continue_statement"],
459 control_label_field_names: &[],
460 yield_kinds: &["yield"],
461 yield_value_field_names: &["value", "expression"],
462 await_kinds: &["await"],
463 defer_kinds: &[],
464 deferred_body_extractor: None,
465 using_kinds: &["with_statement"],
466 using_body_field_names: &["body"],
467 try_body_field_names: &["body"],
468 using_alias_extractor: Some(python_using_alias),
469 special_forms: &[],
470 runtime_type_guard_calls: &["isinstance"],
471 runtime_type_guard_operators: &[],
472 runtime_typeof_operators: &[],
473 runtime_type_equality_operators: &[],
474 runtime_type_wrapper_kinds: &["parenthesized_expression"],
475 value_free_expression_kinds: &[],
476 value_free_call_names: &[],
477 value_free_unary_operators: &[],
478 call_ref_kinds: &["call"],
479 member_expression_kinds: &["attribute"],
480 subscript_expression_kinds: &["subscript"],
481 member_base_field_names: &["object"],
482 member_name_field_names: &["attribute"],
483 subscript_base_field_names: &["value"],
484 subscript_index_field_names: &["subscript"],
485 static_subscript_key_extractor: Some(python_static_subscript_key),
486 computed_subscript_extractor: None,
487 sigil_variable_kinds: &[],
488 global_variable_kinds: &[],
489 reference_name_extractor: None,
490 expression_place_extractor: None,
491 indirect_place_operand_extractor: None,
492 subscript_base_call_refs: true,
493 non_call_ref_names: &[],
494 call_name_suffix_tokens: &[],
495 syntax_error_tolerant_call_names: &[],
496 callable_reference_kinds: &[],
497 callable_reference_extractor: None,
498 method_receiver_param_index: Some(0),
499 receiver_presence_extractor: Some(python_function_has_receiver),
500 implicit_receiver_names: &["self", "super"],
505 implicit_receiver_prefixes: EMPTY_HANDLER.implicit_receiver_prefixes,
506 tail_expression_returns: EMPTY_HANDLER.tail_expression_returns,
507 void_return_type_names: EMPTY_HANDLER.void_return_type_names,
508};
509
510fn python_function_has_receiver(node: Node<'_>, src: &[u8]) -> bool {
514 let Some(parent) = node
515 .parent()
516 .filter(|parent| parent.kind() == "decorated_definition")
517 else {
518 return true;
519 };
520 let mut cursor = parent.walk();
521 let is_static = parent
522 .named_children(&mut cursor)
523 .filter(|child| child.kind() == "decorator")
524 .any(|decorator| python_decorator_is_staticmethod(decorator, src));
525 !is_static
526}
527
528fn python_decorator_is_staticmethod(decorator: Node<'_>, src: &[u8]) -> bool {
529 let mut cursor = decorator.walk();
530 let is_static = decorator
531 .named_children(&mut cursor)
532 .next()
533 .is_some_and(|expression| python_expr_is_staticmethod(expression, src));
534 is_static
535}
536
537fn python_expr_is_staticmethod(node: Node<'_>, src: &[u8]) -> bool {
538 match node.kind() {
539 "identifier" => node_text(&node, src).trim() == "staticmethod",
540 "attribute" => {
541 node.child_by_field_name("attribute")
542 .is_some_and(|attribute| node_text(&attribute, src).trim() == "staticmethod")
543 && node
544 .child_by_field_name("object")
545 .is_some_and(|object| node_text(&object, src).trim() == "builtins")
546 }
547 "parenthesized_expression" => {
548 let mut cursor = node.walk();
549 let is_static = node
550 .named_children(&mut cursor)
551 .next()
552 .is_some_and(|inner| python_expr_is_staticmethod(inner, src));
553 is_static
554 }
555 "call" => node
556 .child_by_field_name("function")
557 .is_some_and(|callee| python_expr_is_staticmethod(callee, src)),
558 _ => false,
559 }
560}
561
562#[derive(Debug, Default, Copy, Clone)]
563pub struct PythonAdapter;
564
565impl PythonAdapter {
566 #[must_use]
567 pub fn new() -> Self {
568 Self
569 }
570}
571
572impl LanguageAdapter for PythonAdapter {
573 fn language_id(&self) -> LanguageId {
574 LANG_ID
575 }
576 fn display_name(&self) -> &'static str {
577 "Python"
578 }
579 fn file_extensions(&self) -> &'static [&'static str] {
580 &["py", "pyi"]
581 }
582 fn tree_sitter_language(&self) -> Result<Language, AdapterError> {
583 language_from_pack(PACK_NAME)
584 }
585 fn capabilities(&self) -> LanguageCapabilities {
586 LanguageCapabilities {
593 module_default_export_names: &[],
594 universal_type_names: &["Any", "object"],
595 module_path_syntax: bonsai_lang_api::ModulePathSyntax::none(),
596 reflection: bonsai_lang_api::CapabilityLevel::Partial,
597 receiver_types: bonsai_lang_api::CapabilityLevel::Partial,
598 field_places_complete: false,
603 constructor_method_names: &["__init__"],
604 bare_call_constructor_syntax: true,
605 super_receiver_tokens: &["super", "super()"],
606 implicit_receiver_tokens: &[],
609 receiver_type_syntax: bonsai_lang_api::ReceiverTypeSyntax {
610 wrapper_calls: &["type"],
611 class_object_suffixes: &[".__class__"],
612 },
613 ..LanguageCapabilities::partial_baseline()
614 }
615 }
616 fn extract_declarations(&self, file: FileId, ctx: &AdapterContext<'_>) -> DeclIndex {
617 let mut idx = decl_index_with_handler(PACK_NAME, file, ctx, &HANDLER);
618 let segments: Vec<String> = ctx
625 .workspace_relative_path(file)
626 .and_then(|path| {
627 let stem = path.file_stem()?.to_string_lossy().into_owned();
628 let mut segs: Vec<String> = path
629 .parent()?
630 .components()
631 .filter_map(|c| match c {
632 std::path::Component::Normal(s) => Some(s.to_string_lossy().into_owned()),
633 _ => None,
634 })
635 .collect();
636 segs.push(stem);
637 Some(segs)
638 })
639 .unwrap_or_default();
640 if segments.is_empty() {
641 bonsai_lang_api::apply_file_stem_semantic_identity(&mut idx, ctx);
642 } else {
643 bonsai_lang_api::apply_module_path_semantic_identity(&mut idx, segments);
644 }
645 for decl in &mut idx.defs {
650 if decl.name.starts_with("__") && !decl.name.ends_with("__") {
651 decl.visibility = Visibility::Private;
652 }
653 }
654 if let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) {
677 let src = snapshot.text.as_bytes();
678 idx.comments.extend(python_docstring_comments(&tree, file, src));
679 populate_python_condition_expressions(&mut idx, &tree, file, src);
680 idx.string_compositions = python_string_compositions(&tree, file, src);
681 idx.finite_literal_selections = python_finite_literal_selections(&idx, &tree, file, src);
682 idx.character_substitutions = python_character_substitutions(&idx, &tree, file, src);
683 idx.character_constraints = python_character_constraints(&idx, &tree, file, src);
684 idx.same_origin_path_constraints = python_same_origin_path_constraints(&idx, &tree, file, src);
685 bonsai_lang_api::populate_decl_return_types(&mut idx, &tree, src, &HANDLER);
689 let aliases_by_span = collect_python_method_type_aliases(&tree, file, src);
690 let param_default_calls_by_span = collect_python_param_default_calls(&tree, file, src);
691 for decl in &mut idx.defs {
692 if let Some(aliases) = aliases_by_span
693 .iter()
694 .find_map(|(span, aliases)| (*span == decl.span).then_some(aliases))
695 {
696 decl.type_aliases = aliases.clone();
697 }
698 if let Some(default_calls) = param_default_calls_by_span
699 .iter()
700 .find_map(|(span, calls)| (*span == decl.span).then_some(calls))
701 {
702 merge_python_param_default_calls(decl, default_calls);
703 }
704 }
705 let bases_by_span = collect_python_class_bases(&tree, file, src);
710 for decl in &mut idx.defs {
711 if !matches!(decl.kind, bonsai_lang_api::DeclKind::Class) {
712 continue;
713 }
714 if let Some(bases) = bases_by_span
715 .iter()
716 .find_map(|(span, bases)| (*span == decl.span).then_some(bases))
717 {
718 decl.bases = bases.clone();
719 }
720 }
721 let iterable_yield_bindings = collect_python_iterable_yield_bindings(&tree, file, src);
722 let property_fn_spans = collect_python_property_function_spans(&tree, file, src);
723 let property_aliases = collect_python_property_aliases(&idx, &property_fn_spans);
724 let property_aliases_by_decl = python_property_aliases_by_decl(&idx, &property_aliases);
725 let assignment_values = AssignmentValueIndex::new(&idx.assignment_values);
726 let assignment_projected_reads = collect_python_assignment_projected_reads(&tree, file, src);
727 let call_argument_places = collect_python_call_argument_places(&tree, file, src);
728 let return_places = collect_python_return_places(&tree, file, src);
729 let callable_spans: Vec<Span> = idx
730 .defs
731 .iter()
732 .filter(|decl| {
733 matches!(
734 decl.kind,
735 bonsai_lang_api::DeclKind::Function
736 | bonsai_lang_api::DeclKind::Method
737 | bonsai_lang_api::DeclKind::Constructor
738 )
739 })
740 .map(|decl| decl.span)
741 .collect();
742 for decl in &mut idx.defs {
743 let owned_yield_bindings = iterable_yield_bindings
744 .iter()
745 .filter(|event| {
746 python_span_owned_by_decl(python_flow_event_span(event), decl.span, &callable_spans)
747 })
748 .cloned()
749 .collect::<Vec<_>>();
750 let comprehension_iterable_calls =
751 collect_python_comprehension_iterable_call_events(&tree, file, src, decl.span);
752 augment_python_comprehension_flow_events(
753 &mut decl.flow_events,
754 snapshot.text.as_ref(),
755 &assignment_values,
756 );
757 insert_python_flow_events_by_span(
758 &mut decl.flow_events,
759 decl.span,
760 &comprehension_iterable_calls,
761 );
762 insert_python_iterable_yield_bindings(&mut decl.flow_events, &owned_yield_bindings);
763 augment_python_dict_flow_events(
764 &mut decl.flow_events,
765 snapshot.text.as_ref(),
766 &assignment_values,
767 &assignment_projected_reads,
768 );
769 if let Some(property_aliases_for_decl) = property_aliases_by_decl.get(&decl.symbol) {
770 augment_python_property_flow_events(&mut decl.flow_events, property_aliases_for_decl);
771 }
772 rewrite_python_constant_reflection(&mut decl.flow_events);
773 rewrite_python_generator_send(&mut decl.flow_events);
774 augment_python_asyncio_to_thread_calls(&mut decl.flow_events);
775 apply_python_call_argument_places(&mut decl.flow_events, &call_argument_places);
776 apply_python_return_places(&mut decl.flow_events, &return_places);
777 }
778 bonsai_lang_api::kit::populate_call_argument_static_values(
779 &mut idx,
780 &tree,
781 file,
782 src,
783 &HANDLER,
784 python_static_scalar,
785 );
786 }
787 for decl in &mut idx.defs {
788 bonsai_lang_api::normalize_call_result_assignment_sources(&mut decl.flow_events);
789 }
790 bonsai_lang_api::apply_class_field_type_aliases(&mut idx);
796 idx
797 }
798 fn extract_imports(&self, file: FileId, ctx: &AdapterContext<'_>) -> ImportIndex {
799 extract_imports_via(PACK_NAME, file, ctx, parse_imports)
800 }
801}
802
803fn python_docstring_comments(tree: &Tree, file: FileId, src: &[u8]) -> Vec<Comment> {
808 let mut out = Vec::new();
809 for scope in collect_kinds(
810 tree,
811 &[
812 "module",
813 "function_definition",
814 "class_definition",
815 "async_function_definition",
816 ],
817 ) {
818 let body = scope.child_by_field_name("body").unwrap_or(scope);
819 let Some(first_statement) = body.named_child(0) else {
820 continue;
821 };
822 let value = if first_statement.kind() == "expression_statement" {
823 first_statement.named_child(0).unwrap_or(first_statement)
824 } else {
825 first_statement
826 };
827 if !HANDLER.string_literal_kinds.contains(&value.kind()) {
828 continue;
829 }
830 let text = node_text(&value, src).trim().to_string();
831 if text.is_empty() {
832 continue;
833 }
834 out.push(Comment {
835 span: span_of(file, &value),
836 kind: CommentKind::classify(&text, true),
837 text,
838 });
839 }
840 out.sort_by_key(|comment| (comment.span.start, comment.span.end));
841 out.dedup_by_key(|comment| comment.span);
842 out
843}
844
845fn populate_python_condition_expressions(index: &mut DeclIndex, tree: &Tree, file: FileId, src: &[u8]) {
850 for branch in collect_kinds(tree, &["if_statement", "elif_clause"]) {
851 let branch_span = span_of(file, &branch);
852 let Some(condition) = branch.child_by_field_name("condition") else {
853 continue;
854 };
855 let Some(fact) = index
856 .branch_conditions
857 .iter_mut()
858 .find(|fact| fact.branch_span == branch_span)
859 else {
860 continue;
861 };
862 fact.expression = Some(lower_python_condition_expression(condition, file, src));
863 }
864}
865
866fn lower_python_condition_expression(node: Node<'_>, file: FileId, src: &[u8]) -> ConditionExpressionFact {
867 if matches!(
868 node.kind(),
869 "parenthesized_expression" | "parenthesized_expression_list"
870 ) {
871 if let Some(inner) = node.named_child(0) {
872 return lower_python_condition_expression(inner, file, src);
873 }
874 }
875
876 let span = span_of(file, &node);
877 if node.kind() == "not_operator" {
878 if let Some(operand) = node
879 .child_by_field_name("argument")
880 .or_else(|| node.child_by_field_name("operand"))
881 .or_else(|| node.named_child(0))
882 {
883 return ConditionExpressionFact::Not {
884 span,
885 operand: Box::new(lower_python_condition_expression(operand, file, src)),
886 };
887 }
888 }
889
890 if node.kind() == "boolean_operator" {
891 if let (Some(left), Some(right)) = (
892 node.child_by_field_name("left"),
893 node.child_by_field_name("right"),
894 ) {
895 let operator = src
896 .get(left.end_byte()..right.start_byte())
897 .and_then(|bytes| std::str::from_utf8(bytes).ok())
898 .map(str::trim);
899 match operator {
900 Some("or") => {
901 return merge_python_condition_junction(
902 span,
903 lower_python_condition_expression(left, file, src),
904 lower_python_condition_expression(right, file, src),
905 false,
906 );
907 }
908 Some("and") => {
909 return merge_python_condition_junction(
910 span,
911 lower_python_condition_expression(left, file, src),
912 lower_python_condition_expression(right, file, src),
913 true,
914 );
915 }
916 _ => {}
917 }
918 }
919 }
920
921 if node.kind() == "comparison_operator" && node.named_child_count() == 2 {
922 if let (Some(left), Some(right)) = (node.named_child(0), node.named_child(1)) {
923 let operator = src
924 .get(left.end_byte()..right.start_byte())
925 .and_then(|bytes| std::str::from_utf8(bytes).ok())
926 .map(str::trim);
927 match operator {
928 Some("==" | "!=") => {
929 return ConditionExpressionFact::Equality {
930 span,
931 relation: if operator == Some("==") {
932 ConditionEquality::Equal
933 } else {
934 ConditionEquality::NotEqual
935 },
936 left: python_condition_operand(left, file, src),
937 right: python_condition_operand(right, file, src),
938 };
939 }
940 Some("in" | "not in") => {
941 return ConditionExpressionFact::Membership {
942 span,
943 subject: python_condition_operand(left, file, src),
944 collection: python_condition_operand(right, file, src),
945 then_contains: operator == Some("in"),
946 };
947 }
948 _ => {}
949 }
950 }
951 }
952
953 if node.kind() == "call" {
954 let function = node.child_by_field_name("function");
955 let arguments = node.child_by_field_name("arguments");
956 if let (Some(function), Some(arguments)) = (function, arguments) {
957 let mut cursor = arguments.walk();
958 let values: Vec<_> = arguments.named_children(&mut cursor).collect();
959 if function.kind() == "identifier"
960 && node_text(&function, src).trim() == "isinstance"
961 && values.len() == 2
962 && matches!(
963 values[1].kind(),
964 "identifier" | "type" | "attribute" | "generic_type"
965 )
966 {
967 let type_name = node_text(&values[1], src).trim().to_string();
968 if !type_name.is_empty() {
969 return ConditionExpressionFact::TypeTest {
970 span,
971 subject: python_condition_operand(values[0], file, src),
972 type_name,
973 };
974 }
975 }
976 }
977 }
978
979 if matches!(node.kind(), "identifier" | "attribute" | "subscript") {
980 return ConditionExpressionFact::Truthy {
981 span,
982 operand: python_condition_operand(node, file, src),
983 };
984 }
985
986 ConditionExpressionFact::Atom { span }
987}
988
989fn merge_python_condition_junction(
990 span: Span,
991 left: ConditionExpressionFact,
992 right: ConditionExpressionFact,
993 all: bool,
994) -> ConditionExpressionFact {
995 let mut operands = Vec::new();
996 let mut push = |operand: ConditionExpressionFact| match (all, operand) {
997 (true, ConditionExpressionFact::All { operands: nested, .. })
998 | (false, ConditionExpressionFact::Any { operands: nested, .. }) => operands.extend(nested),
999 (_, operand) => operands.push(operand),
1000 };
1001 push(left);
1002 push(right);
1003 if all {
1004 ConditionExpressionFact::All { span, operands }
1005 } else {
1006 ConditionExpressionFact::Any { span, operands }
1007 }
1008}
1009
1010fn python_condition_operand(node: Node<'_>, file: FileId, src: &[u8]) -> ConditionOperandFact {
1011 let value_node = python_condition_dynamic_value_node(node, src);
1012 ConditionOperandFact {
1013 span: span_of(file, &node),
1014 value_flow: bonsai_lang_api::kit::expression_flow_from_node_with_handler(
1015 value_node, file, src, &HANDLER,
1016 ),
1017 static_string: python_static_string(node, src),
1018 static_value: python_static_scalar(node, src),
1019 }
1020}
1021
1022fn python_condition_dynamic_value_node<'tree>(mut node: Node<'tree>, src: &[u8]) -> Node<'tree> {
1028 loop {
1029 if matches!(
1030 node.kind(),
1031 "parenthesized_expression" | "parenthesized_expression_list"
1032 ) {
1033 if let Some(inner) = node.named_child(0) {
1034 node = inner;
1035 continue;
1036 }
1037 }
1038 if node.kind() == "boolean_operator" {
1039 if let (Some(left), Some(right)) = (
1040 node.child_by_field_name("left"),
1041 node.child_by_field_name("right"),
1042 ) {
1043 let operator = src
1044 .get(left.end_byte()..right.start_byte())
1045 .and_then(|bytes| std::str::from_utf8(bytes).ok())
1046 .map(str::trim);
1047 if operator == Some("or") && python_static_scalar(right, src).is_some() {
1048 node = left;
1049 continue;
1050 }
1051 }
1052 }
1053 return node;
1054 }
1055}
1056
1057fn python_static_scalar(node: Node<'_>, src: &[u8]) -> Option<StaticScalarValue> {
1058 match node.kind() {
1059 "true" => Some(StaticScalarValue::Boolean(true)),
1060 "false" => Some(StaticScalarValue::Boolean(false)),
1061 "none" => Some(StaticScalarValue::Null),
1062 "string" => Some(StaticScalarValue::String(python_static_string(node, src)?)),
1063 _ => None,
1064 }
1065}
1066
1067fn python_static_subscript_key(node: Node<'_>, src: &[u8]) -> Option<String> {
1068 match python_static_scalar(node, src)? {
1069 StaticScalarValue::String(value) => Some(value),
1070 StaticScalarValue::Boolean(_) | StaticScalarValue::Null => None,
1071 }
1072}
1073
1074fn python_static_string(node: Node<'_>, src: &[u8]) -> Option<String> {
1075 if node.kind() != "string" {
1076 return None;
1077 }
1078 let text = node_text(&node, src).trim();
1079 let quote_start = text.find(['\'', '"'])?;
1080 let prefix = text.get(..quote_start)?.to_ascii_lowercase();
1081 if prefix.contains('f') || prefix.contains('b') || prefix.chars().any(|ch| !matches!(ch, 'r' | 'u')) {
1082 return None;
1083 }
1084 let quoted = text.get(quote_start..)?;
1085 let delimiter = if quoted.starts_with("'''") {
1086 "'''"
1087 } else if quoted.starts_with("\"\"\"") {
1088 "\"\"\""
1089 } else if quoted.starts_with('\'') {
1090 "'"
1091 } else if quoted.starts_with('"') {
1092 "\""
1093 } else {
1094 return None;
1095 };
1096 let inner = quoted.strip_prefix(delimiter)?.strip_suffix(delimiter)?;
1097 if prefix.contains('r') {
1098 return Some(inner.to_string());
1099 }
1100 let mut decoded = String::new();
1101 let mut characters = inner.chars();
1102 while let Some(character) = characters.next() {
1103 if character != '\\' {
1104 decoded.push(character);
1105 continue;
1106 }
1107 decoded.push(match characters.next()? {
1108 'r' => '\r',
1109 'n' => '\n',
1110 't' => '\t',
1111 '\\' => '\\',
1112 '\'' => '\'',
1113 '"' => '"',
1114 'x' => {
1115 let digits = [characters.next()?, characters.next()?];
1116 let value = u8::from_str_radix(&digits.iter().collect::<String>(), 16).ok()?;
1117 char::from(value)
1118 }
1119 _ => return None,
1120 });
1121 }
1122 Some(decoded)
1123}
1124
1125fn python_character_constraints(
1126 index: &DeclIndex,
1127 tree: &Tree,
1128 file: FileId,
1129 src: &[u8],
1130) -> Vec<CharacterConstraintFact> {
1131 let mut facts = python_comprehension_character_constraints(index, tree, file, src);
1132 facts.extend(python_regex_substitution_constraints(index, tree, file, src));
1133 facts.extend(python_regex_validation_constraints(index, tree, file, src));
1134 facts.sort_by_key(|fact| (fact.transform_span.start, fact.transform_span.end));
1135 facts.dedup_by_key(|fact| fact.transform_span);
1136 facts
1137}
1138
1139fn python_finite_literal_selections(
1140 index: &DeclIndex,
1141 tree: &Tree,
1142 file: FileId,
1143 src: &[u8],
1144) -> Vec<FiniteLiteralSelectionFact> {
1145 #[derive(Clone)]
1146 struct FiniteMapBinding {
1147 name: String,
1148 declaration_end: usize,
1149 owner: Option<Span>,
1150 }
1151
1152 fn binding_visible(
1153 resolver: &PythonLexicalBindingResolver<'_, '_>,
1154 bindings: &[FiniteMapBinding],
1155 map_name: &str,
1156 use_start: usize,
1157 use_span: Span,
1158 ) -> bool {
1159 let owner = resolver.owner_for_use(use_span, map_name);
1160 bindings.iter().any(|binding| {
1161 binding.name == map_name
1162 && (binding.owner.is_none() || binding.declaration_end <= use_start)
1163 && binding.owner == owner
1164 })
1165 }
1166
1167 let assignments = collect_kinds(tree, &["assignment"]);
1168 let binding_resolver = PythonLexicalBindingResolver::new(index, tree, file, src, &assignments);
1169 let mut finite_maps = Vec::new();
1170 for assignment in &assignments {
1171 let (Some(target), Some(value)) = (
1172 assignment.child_by_field_name("left"),
1173 assignment.child_by_field_name("right"),
1174 ) else {
1175 continue;
1176 };
1177 if target.kind() != "identifier" || !python_finite_static_map(value, src) {
1178 continue;
1179 }
1180 let name = node_text(&target, src).trim().to_string();
1181 let owner = binding_resolver.owner_for_use(span_of(file, assignment), &name);
1182 let writes = assignments
1183 .iter()
1184 .filter(|candidate| {
1185 candidate
1186 .child_by_field_name("left")
1187 .is_some_and(|left| left.kind() == "identifier" && node_text(&left, src).trim() == name)
1188 && binding_resolver.owner_for_use(span_of(file, candidate), &name) == owner
1189 })
1190 .count();
1191 let projected_write = assignments.iter().any(|candidate| {
1192 candidate.child_by_field_name("left").is_some_and(|left| {
1193 left.kind() == "subscript"
1194 && left.child_by_field_name("value").is_some_and(|base| {
1195 base.kind() == "identifier" && node_text(&base, src).trim() == name
1196 })
1197 }) && binding_resolver.owner_for_use(span_of(file, candidate), &name) == owner
1198 });
1199 let aliases_or_mutations = collect_kinds(tree, &["assignment", "augmented_assignment", "call"])
1200 .into_iter()
1201 .any(|candidate| {
1202 binding_resolver.owner_for_use(span_of(file, &candidate), &name) == owner
1203 && python_map_binding_may_escape_or_mutate(candidate, &name, assignment.id(), src)
1204 })
1205 || python_map_binding_has_unsafe_use(&binding_resolver, &name, assignment.id(), owner);
1206 if writes == 1 && !projected_write && !aliases_or_mutations {
1207 finite_maps.push(FiniteMapBinding {
1208 name,
1209 declaration_end: assignment.end_byte(),
1210 owner,
1211 });
1212 }
1213 }
1214
1215 let mut facts = Vec::new();
1216 for assignment in &assignments {
1217 let (Some(target), Some(value)) = (
1218 assignment.child_by_field_name("left"),
1219 assignment.child_by_field_name("right"),
1220 ) else {
1221 continue;
1222 };
1223 if target.kind() != "identifier" || value.kind() != "conditional_expression" {
1224 continue;
1225 }
1226 let mut cursor = value.walk();
1227 let operands: Vec<_> = value.named_children(&mut cursor).collect();
1228 let [selected, condition, fallback] = operands.as_slice() else {
1229 continue;
1230 };
1231 let Some(collection) = python_positive_membership_collection(*condition, *selected, src) else {
1232 continue;
1233 };
1234 let collection_is_finite = python_finite_literal_collection(collection, src)
1235 || (collection.kind() == "identifier"
1236 && binding_visible(
1237 &binding_resolver,
1238 &finite_maps,
1239 node_text(&collection, src).trim(),
1240 condition.start_byte(),
1241 span_of(file, condition),
1242 ));
1243 if !collection_is_finite || !python_finite_membership_literal(*fallback, src) {
1244 continue;
1245 }
1246 facts.push(FiniteLiteralSelectionFact {
1247 selection_span: span_of(file, &value),
1248 assignment_span: Some(span_of(file, assignment)),
1249 target: Some(node_text(&target, src).trim().to_string()),
1250 call_span: None,
1251 argument_index: None,
1252 });
1253 }
1254 for call in collect_kinds(tree, &["call"]) {
1255 let Some((function, _)) = python_call_parts(call) else {
1256 continue;
1257 };
1258 let Some((receiver, method)) = python_attribute_parts(function, src) else {
1259 continue;
1260 };
1261 if method != "get" || receiver.kind() != "identifier" {
1262 continue;
1263 }
1264 let map_name = node_text(&receiver, src).trim();
1265 let selection_span = span_of(file, &call);
1266 let finite_match = binding_visible(
1267 &binding_resolver,
1268 &finite_maps,
1269 map_name,
1270 call.start_byte(),
1271 selection_span,
1272 );
1273 if !finite_match {
1274 continue;
1275 }
1276 let Some(assignment) = index
1277 .assignment_values
1278 .iter()
1279 .filter(|fact| {
1280 fact.target.is_some()
1281 && fact.value_span.start <= selection_span.start
1282 && selection_span.end <= fact.value_span.end
1283 })
1284 .min_by_key(|fact| fact.value_span.len())
1285 else {
1286 continue;
1287 };
1288 facts.push(FiniteLiteralSelectionFact {
1289 selection_span,
1290 assignment_span: Some(assignment.assignment_span),
1291 target: assignment.target.clone(),
1292 call_span: None,
1293 argument_index: None,
1294 });
1295 }
1296 facts.sort_by_key(|fact| {
1297 let span = fact.assignment_span.unwrap_or(fact.selection_span);
1298 (span.start, span.end, fact.selection_span.start)
1299 });
1300 facts.dedup();
1301 facts
1302}
1303
1304fn python_positive_membership_collection<'tree>(
1308 condition: Node<'tree>,
1309 selected: Node<'tree>,
1310 src: &[u8],
1311) -> Option<Node<'tree>> {
1312 if condition.kind() != "comparison_operator" || selected.kind() != "identifier" {
1313 return None;
1314 }
1315 let mut cursor = condition.walk();
1316 let operands: Vec<_> = condition.named_children(&mut cursor).collect();
1317 let [subject, collection] = operands.as_slice() else {
1318 return None;
1319 };
1320 if subject.kind() != "identifier" || node_text(subject, src).trim() != node_text(&selected, src).trim() {
1321 return None;
1322 }
1323 let operator = src
1324 .get(subject.end_byte()..collection.start_byte())
1325 .and_then(|bytes| std::str::from_utf8(bytes).ok())
1326 .map(str::trim);
1327 if operator != Some("in") {
1328 return None;
1329 }
1330 Some(*collection)
1331}
1332
1333fn python_finite_literal_collection(collection: Node<'_>, src: &[u8]) -> bool {
1334 if !matches!(collection.kind(), "set" | "list" | "tuple") {
1335 return false;
1336 }
1337 let mut collection_cursor = collection.walk();
1338 let values: Vec<_> = collection.named_children(&mut collection_cursor).collect();
1339 !values.is_empty()
1340 && values
1341 .into_iter()
1342 .all(|value| python_finite_membership_literal(value, src))
1343}
1344
1345fn python_finite_membership_literal(node: Node<'_>, src: &[u8]) -> bool {
1346 match node.kind() {
1347 "string" => python_static_string(node, src).is_some(),
1348 "integer" | "float" | "true" | "false" | "none" => true,
1349 _ => false,
1350 }
1351}
1352
1353fn python_character_substitutions(
1354 index: &DeclIndex,
1355 tree: &Tree,
1356 file: FileId,
1357 src: &[u8],
1358) -> Vec<CharacterSubstitutionFact> {
1359 let assignments = collect_kinds(tree, &["assignment"]);
1360 let binding_resolver = PythonLexicalBindingResolver::new(index, tree, file, src, &assignments);
1361 let mut tables = Vec::new();
1362 for assignment in &assignments {
1363 let (Some(target), Some(value)) = (
1364 assignment.child_by_field_name("left"),
1365 assignment.child_by_field_name("right"),
1366 ) else {
1367 continue;
1368 };
1369 if target.kind() != "identifier" {
1370 continue;
1371 }
1372 let Some(entries) = python_static_string_map(value, src) else {
1373 continue;
1374 };
1375 let name = node_text(&target, src).trim().to_string();
1376 let owner = binding_resolver.owner_for_use(span_of(file, assignment), &name);
1377 let writes = assignments
1378 .iter()
1379 .filter(|candidate| {
1380 candidate
1381 .child_by_field_name("left")
1382 .is_some_and(|left| left.kind() == "identifier" && node_text(&left, src).trim() == name)
1383 && binding_resolver.owner_for_use(span_of(file, candidate), &name) == owner
1384 })
1385 .count();
1386 let projected_write = assignments.iter().any(|candidate| {
1387 candidate.child_by_field_name("left").is_some_and(|left| {
1388 left.kind() == "subscript"
1389 && left.child_by_field_name("value").is_some_and(|base| {
1390 base.kind() == "identifier" && node_text(&base, src).trim() == name
1391 })
1392 }) && binding_resolver.owner_for_use(span_of(file, candidate), &name) == owner
1393 });
1394 let aliases_or_mutations = collect_kinds(tree, &["assignment", "augmented_assignment", "call"])
1395 .into_iter()
1396 .any(|candidate| {
1397 binding_resolver.owner_for_use(span_of(file, &candidate), &name) == owner
1398 && python_map_binding_may_escape_or_mutate(candidate, &name, assignment.id(), src)
1399 })
1400 || python_map_binding_has_unsafe_use(&binding_resolver, &name, assignment.id(), owner);
1401 if writes == 1 && !projected_write && !aliases_or_mutations {
1402 tables.push((name, assignment.end_byte(), owner, entries));
1403 }
1404 }
1405
1406 let mut facts = Vec::new();
1407 for return_node in collect_kinds(tree, &["return_statement"]) {
1408 let Some(returned) = return_node.named_child(0) else {
1409 continue;
1410 };
1411 let Some((join_function, join_arguments)) = python_call_parts(returned) else {
1412 continue;
1413 };
1414 let Some((join_receiver, join_method)) = python_attribute_parts(join_function, src) else {
1415 continue;
1416 };
1417 if join_method != "join" || python_static_string(join_receiver, src).as_deref() != Some("") {
1418 continue;
1419 }
1420 let generator = if join_arguments.kind() == "generator_expression" {
1421 join_arguments
1422 } else {
1423 let arguments = python_argument_nodes(join_arguments);
1424 let [generator] = arguments.as_slice() else {
1425 continue;
1426 };
1427 *generator
1428 };
1429 let Some((table, input_place)) = python_static_map_substitution_generator(generator, src) else {
1430 continue;
1431 };
1432 let transform_span = span_of(file, &return_node);
1433 let Some(decl) = python_enclosing_callable(index, transform_span) else {
1434 continue;
1435 };
1436 if !python_is_single_statement_return(return_node) {
1437 continue;
1438 }
1439 let Some(input_param_index) = decl.params.iter().position(|parameter| parameter == &input_place)
1440 else {
1441 continue;
1442 };
1443 let owner = binding_resolver.owner_for_use(transform_span, &table);
1444 let Some((_, _, _, exact_mappings)) =
1445 tables.iter().find(|(name, declaration_end, binding_owner, _)| {
1446 name == &table
1447 && (binding_owner.is_none() || *declaration_end <= return_node.start_byte())
1448 && *binding_owner == owner
1449 })
1450 else {
1451 continue;
1452 };
1453 facts.push(CharacterSubstitutionFact {
1454 function_span: decl.span,
1455 transform_span,
1456 input_param_index,
1457 exact_mappings: exact_mappings.clone(),
1458 table,
1459 domain: CharacterSubstitutionDomain::TableKeysWithIdentityFallback,
1460 });
1461 }
1462 facts.sort_by_key(|fact| (fact.transform_span.start, fact.transform_span.end));
1463 facts.dedup_by_key(|fact| fact.transform_span);
1464 facts
1465}
1466
1467fn python_static_string_map(node: Node<'_>, src: &[u8]) -> Option<Vec<StaticStringMapEntry>> {
1468 if node.kind() != "dictionary" || node.named_child_count() == 0 {
1469 return None;
1470 }
1471 let mut entries = Vec::new();
1472 let mut cursor = node.walk();
1473 for entry in node.named_children(&mut cursor) {
1474 if entry.kind() != "pair" {
1475 return None;
1476 }
1477 let key = entry
1478 .child_by_field_name("key")
1479 .and_then(|key| python_static_string(key, src))?;
1480 let value = entry
1481 .child_by_field_name("value")
1482 .and_then(|value| python_static_string(value, src))?;
1483 entries.push(StaticStringMapEntry { key, value });
1484 }
1485 entries.sort_by(|left, right| left.key.cmp(&right.key));
1486 entries.dedup_by(|left, right| left.key == right.key && left.value == right.value);
1487 Some(entries)
1488}
1489
1490fn python_static_map_substitution_generator(generator: Node<'_>, src: &[u8]) -> Option<(String, String)> {
1491 if generator.kind() != "generator_expression" {
1492 return None;
1493 }
1494 let body = generator.named_child(0)?;
1495 let (function, arguments) = python_call_parts(body)?;
1496 let (receiver, method) = python_attribute_parts(function, src)?;
1497 if receiver.kind() != "identifier" || method != "get" {
1498 return None;
1499 }
1500 let table = node_text(&receiver, src).trim().to_string();
1501 let lookup_arguments = python_argument_nodes(arguments);
1502 let [key, fallback] = lookup_arguments.as_slice() else {
1503 return None;
1504 };
1505 if key.kind() != "identifier" || fallback.kind() != "identifier" {
1506 return None;
1507 }
1508 let loop_variable = node_text(key, src).trim().to_string();
1509 if node_text(fallback, src).trim() != loop_variable {
1510 return None;
1511 }
1512 let mut cursor = generator.walk();
1513 let clauses = generator.named_children(&mut cursor).skip(1).collect::<Vec<_>>();
1514 let [for_clause] = clauses.as_slice() else {
1515 return None;
1516 };
1517 if for_clause.kind() != "for_in_clause" {
1518 return None;
1519 }
1520 let left = for_clause
1521 .child_by_field_name("left")
1522 .or_else(|| for_clause.named_child(0))?;
1523 let right = for_clause
1524 .child_by_field_name("right")
1525 .or_else(|| for_clause.named_child(1))?;
1526 if left.kind() != "identifier" || node_text(&left, src).trim() != loop_variable {
1527 return None;
1528 }
1529 let input_place = python_identity_fallback_input(right, src)?;
1530 Some((table, input_place))
1531}
1532
1533fn python_identity_fallback_input(mut node: Node<'_>, src: &[u8]) -> Option<String> {
1534 while node.kind() == "parenthesized_expression" {
1535 node = node.named_child(0)?;
1536 }
1537 if node.kind() == "identifier" {
1538 return Some(node_text(&node, src).trim().to_string());
1539 }
1540 if node.kind() != "boolean_operator" {
1541 return None;
1542 }
1543 let (Some(left), Some(right)) = (
1544 node.child_by_field_name("left"),
1545 node.child_by_field_name("right"),
1546 ) else {
1547 return None;
1548 };
1549 let operator = src
1550 .get(left.end_byte()..right.start_byte())
1551 .and_then(|bytes| std::str::from_utf8(bytes).ok())
1552 .map(str::trim);
1553 (operator == Some("or")
1554 && left.kind() == "identifier"
1555 && python_static_string(right, src).as_deref() == Some(""))
1556 .then(|| node_text(&left, src).trim().to_string())
1557}
1558
1559fn python_lexical_owner(index: &DeclIndex, span: bonsai_common::Span) -> Option<bonsai_common::Span> {
1560 index
1561 .defs
1562 .iter()
1563 .filter(|decl| {
1564 decl.name != bonsai_lang_api::MODULE_DECL_NAME
1565 && matches!(
1566 decl.kind,
1567 DeclKind::Function | DeclKind::Method | DeclKind::Constructor | DeclKind::Class
1568 )
1569 && decl.span.start <= span.start
1570 && span.end <= decl.span.end
1571 })
1572 .min_by_key(|decl| decl.span.len())
1573 .map(|decl| decl.span)
1574}
1575
1576struct PythonLexicalBindingResolver<'a, 'tree> {
1580 index: &'a DeclIndex,
1581 file: FileId,
1582 src: &'a [u8],
1583 assignments: &'a [Node<'tree>],
1584 directives: Vec<Node<'tree>>,
1585 identifiers: Vec<Node<'tree>>,
1586}
1587
1588impl<'a, 'tree> PythonLexicalBindingResolver<'a, 'tree> {
1589 fn new(
1590 index: &'a DeclIndex,
1591 tree: &'tree Tree,
1592 file: FileId,
1593 src: &'a [u8],
1594 assignments: &'a [Node<'tree>],
1595 ) -> Self {
1596 Self {
1597 index,
1598 file,
1599 src,
1600 assignments,
1601 directives: collect_kinds(tree, &["global_statement", "nonlocal_statement"]),
1602 identifiers: collect_kinds(tree, &["identifier"]),
1603 }
1604 }
1605
1606 fn owner_for_use(&self, use_span: Span, name: &str) -> Option<Span> {
1614 let mut scopes = self
1615 .index
1616 .defs
1617 .iter()
1618 .filter(|decl| {
1619 decl.name != bonsai_lang_api::MODULE_DECL_NAME
1620 && matches!(
1621 decl.kind,
1622 DeclKind::Function | DeclKind::Method | DeclKind::Constructor | DeclKind::Class
1623 )
1624 && decl.span.start <= use_span.start
1625 && use_span.end <= decl.span.end
1626 })
1627 .collect::<Vec<_>>();
1628 scopes.sort_by_key(|decl| decl.span.len());
1629
1630 let mut inside_callable = false;
1631 for scope in scopes {
1632 let callable = matches!(
1633 scope.kind,
1634 DeclKind::Function | DeclKind::Method | DeclKind::Constructor
1635 );
1636 if scope.kind == DeclKind::Class && inside_callable {
1637 continue;
1638 }
1639 if callable {
1640 inside_callable = true;
1641 if self.scope_has_name_directive(scope.span, "global_statement", name) {
1642 return None;
1643 }
1644 if self.scope_has_name_directive(scope.span, "nonlocal_statement", name) {
1645 continue;
1646 }
1647 }
1648 let parameter = callable && scope.params.iter().any(|parameter| parameter == name);
1649 let assigned = self.assignments.iter().any(|assignment| {
1650 python_lexical_owner(self.index, span_of(self.file, assignment)) == Some(scope.span)
1651 && assignment.child_by_field_name("left").is_some_and(|left| {
1652 left.kind() == "identifier" && node_text(&left, self.src).trim() == name
1653 })
1654 });
1655 if parameter || assigned {
1656 return Some(scope.span);
1657 }
1658 }
1659 None
1660 }
1661
1662 fn scope_has_name_directive(&self, scope: Span, kind: &str, name: &str) -> bool {
1663 self.directives.iter().any(|directive| {
1664 if directive.kind() != kind
1665 || python_lexical_owner(self.index, span_of(self.file, directive)) != Some(scope)
1666 {
1667 return false;
1668 }
1669 let mut cursor = directive.walk();
1670 let contains_name = directive
1671 .named_children(&mut cursor)
1672 .any(|child| child.kind() == "identifier" && node_text(&child, self.src).trim() == name);
1673 contains_name
1674 })
1675 }
1676}
1677
1678fn python_map_binding_may_escape_or_mutate(
1679 node: Node<'_>,
1680 map_name: &str,
1681 declaration_id: usize,
1682 src: &[u8],
1683) -> bool {
1684 if node.id() == declaration_id {
1685 return false;
1686 }
1687 match node.kind() {
1688 "assignment" | "augmented_assignment" => {
1689 let left = node.child_by_field_name("left");
1690 let right = node.child_by_field_name("right");
1691 left.is_some_and(|left| {
1692 (left.kind() == "identifier" && node_text(&left, src).trim() == map_name)
1693 || (left.kind() == "subscript"
1694 && left.child_by_field_name("value").is_some_and(|base| {
1695 base.kind() == "identifier" && node_text(&base, src).trim() == map_name
1696 }))
1697 }) || right.is_some_and(|right| {
1698 right.kind() == "identifier" && node_text(&right, src).trim() == map_name
1699 })
1700 }
1701 "call" => {
1702 let Some((function, _)) = python_call_parts(node) else {
1703 return false;
1704 };
1705 python_attribute_parts(function, src).is_some_and(|(receiver, method)| {
1706 receiver.kind() == "identifier"
1707 && node_text(&receiver, src).trim() == map_name
1708 && method != "get"
1709 })
1710 }
1711 _ => false,
1712 }
1713}
1714
1715fn python_map_binding_has_unsafe_use(
1716 resolver: &PythonLexicalBindingResolver<'_, '_>,
1717 map_name: &str,
1718 declaration_id: usize,
1719 owner: Option<Span>,
1720) -> bool {
1721 resolver.identifiers.iter().any(|identifier| {
1722 if node_text(identifier, resolver.src).trim() != map_name {
1723 return false;
1724 }
1725 if resolver.owner_for_use(span_of(resolver.file, identifier), map_name) != owner {
1726 return false;
1727 }
1728 if identifier
1729 .parent()
1730 .is_some_and(|parent| parent.kind() == "assignment" && parent.id() == declaration_id)
1731 {
1732 return false;
1733 }
1734 if identifier
1735 .parent()
1736 .is_some_and(|parent| matches!(parent.kind(), "global_statement" | "nonlocal_statement"))
1737 {
1738 return false;
1741 }
1742 let Some(attribute) = identifier.parent().filter(|parent| parent.kind() == "attribute") else {
1743 if identifier.parent().is_some_and(|parent| {
1748 parent.kind() == "subscript"
1749 && parent
1750 .child_by_field_name("value")
1751 .is_some_and(|value| value.id() == identifier.id())
1752 }) {
1753 return false;
1754 }
1755 if identifier.parent().is_some_and(|parent| {
1760 if parent.kind() != "comparison_operator" {
1761 return false;
1762 }
1763 let mut cursor = parent.walk();
1764 let operands = parent.named_children(&mut cursor).collect::<Vec<_>>();
1765 let [subject, collection] = operands.as_slice() else {
1766 return false;
1767 };
1768 collection.id() == identifier.id()
1769 && resolver
1770 .src
1771 .get(subject.end_byte()..collection.start_byte())
1772 .and_then(|bytes| std::str::from_utf8(bytes).ok())
1773 .is_some_and(|operator| operator.trim() == "in")
1774 }) {
1775 return false;
1776 }
1777 return true;
1778 };
1779 if attribute
1780 .child_by_field_name("object")
1781 .is_none_or(|object| object.id() != identifier.id())
1782 {
1783 return true;
1784 }
1785 let Some(call) = attribute.parent().filter(|parent| parent.kind() == "call") else {
1786 return true;
1787 };
1788 call.child_by_field_name("function")
1789 .is_none_or(|function| function.id() != attribute.id())
1790 || attribute
1791 .child_by_field_name("attribute")
1792 .is_none_or(|method| node_text(&method, resolver.src).trim() != "get")
1793 })
1794}
1795
1796fn python_finite_static_map(node: Node<'_>, src: &[u8]) -> bool {
1797 if node.kind() != "dictionary" || node.named_child_count() == 0 {
1798 return false;
1799 }
1800 let mut cursor = node.walk();
1801 let finite = node.named_children(&mut cursor).all(|entry| {
1802 entry.kind() == "pair"
1803 && entry
1804 .child_by_field_name("key")
1805 .is_some_and(|key| python_static_string(key, src).is_some())
1806 && entry
1807 .child_by_field_name("value")
1808 .is_some_and(|value| python_statically_constructed_value(value, src))
1809 });
1810 finite
1811}
1812
1813fn python_statically_constructed_value(node: Node<'_>, src: &[u8]) -> bool {
1814 match node.kind() {
1815 "string" | "concatenated_string" => python_static_string(node, src).is_some(),
1816 "integer" | "float" | "true" | "false" | "none" => true,
1817 "list" | "tuple" | "set" | "dictionary" => {
1818 let mut cursor = node.walk();
1819 let finite = node.named_children(&mut cursor).all(|child| {
1820 if child.kind() == "pair" {
1821 child
1822 .child_by_field_name("key")
1823 .is_some_and(|key| python_statically_constructed_value(key, src))
1824 && child
1825 .child_by_field_name("value")
1826 .is_some_and(|value| python_statically_constructed_value(value, src))
1827 } else {
1828 python_statically_constructed_value(child, src)
1829 }
1830 });
1831 finite
1832 }
1833 "call" => {
1834 let Some((_, arguments)) = python_call_parts(node) else {
1835 return false;
1836 };
1837 python_argument_nodes(arguments)
1838 .into_iter()
1839 .all(|argument| python_statically_constructed_value(argument, src))
1840 }
1841 _ => false,
1842 }
1843}
1844
1845fn python_comprehension_character_constraints(
1846 index: &DeclIndex,
1847 tree: &Tree,
1848 file: FileId,
1849 src: &[u8],
1850) -> Vec<CharacterConstraintFact> {
1851 let mut facts = Vec::new();
1852 for assignment in collect_kinds(tree, &["assignment"]) {
1853 let Some(target_node) = assignment.child_by_field_name("left") else {
1854 continue;
1855 };
1856 let Some(mut value_node) = assignment.child_by_field_name("right") else {
1857 continue;
1858 };
1859 if target_node.kind() != "identifier" {
1860 continue;
1861 }
1862 while matches!(value_node.kind(), "subscript" | "parenthesized_expression") {
1863 let Some(inner) = value_node
1864 .child_by_field_name("value")
1865 .or_else(|| value_node.named_child(0))
1866 else {
1867 break;
1868 };
1869 value_node = inner;
1870 }
1871 let Some((function, arguments)) = python_call_parts(value_node) else {
1872 continue;
1873 };
1874 let Some((receiver, method)) = python_attribute_parts(function, src) else {
1875 continue;
1876 };
1877 if method != "join" || python_static_string(receiver, src).as_deref() != Some("") {
1878 continue;
1879 }
1880 let generator = if arguments.kind() == "generator_expression" {
1881 arguments
1882 } else {
1883 let args = python_argument_nodes(arguments);
1884 let [generator] = args.as_slice() else {
1885 continue;
1886 };
1887 *generator
1888 };
1889 let Some((input_place, classes, exact_characters)) =
1890 python_filtered_character_generator(generator, src)
1891 else {
1892 continue;
1893 };
1894 let transform_span = span_of(file, &assignment);
1895 let Some(decl) = python_enclosing_callable(index, transform_span) else {
1896 continue;
1897 };
1898 let target = node_text(&target_node, src).trim().to_string();
1899 let input_param_index = decl.params.iter().position(|param| param == &input_place);
1900 facts.push(CharacterConstraintFact {
1901 function_span: decl.span,
1902 transform_span,
1903 input_place,
1904 input_param_index,
1905 output: CharacterConstraintOutput::Assignment { target },
1906 domain: CharacterConstraintDomain::AllowOnly {
1907 classes,
1908 exact_characters,
1909 },
1910 });
1911 }
1912 facts
1913}
1914
1915fn python_filtered_character_generator(
1916 generator: Node<'_>,
1917 src: &[u8],
1918) -> Option<(String, Vec<CharacterClass>, Vec<String>)> {
1919 if generator.kind() != "generator_expression" {
1920 return None;
1921 }
1922 let body = generator.named_child(0)?;
1923 if body.kind() != "identifier" {
1924 return None;
1925 }
1926 let loop_variable = node_text(&body, src).trim();
1927 let mut cursor = generator.walk();
1928 let clauses: Vec<_> = generator.named_children(&mut cursor).skip(1).collect();
1929 let [for_clause, if_clause] = clauses.as_slice() else {
1930 return None;
1931 };
1932 if for_clause.kind() != "for_in_clause" || if_clause.kind() != "if_clause" {
1933 return None;
1934 }
1935 let left = for_clause
1936 .child_by_field_name("left")
1937 .or_else(|| for_clause.named_child(0))?;
1938 let right = for_clause
1939 .child_by_field_name("right")
1940 .or_else(|| for_clause.named_child(1))?;
1941 if left.kind() != "identifier"
1942 || right.kind() != "identifier"
1943 || node_text(&left, src).trim() != loop_variable
1944 {
1945 return None;
1946 }
1947 let condition = if_clause
1948 .child_by_field_name("condition")
1949 .or_else(|| if_clause.named_child(0))?;
1950 let mut classes = Vec::new();
1951 let mut exact_characters = Vec::new();
1952 if !python_character_predicate(condition, loop_variable, src, &mut classes, &mut exact_characters) {
1953 return None;
1954 }
1955 classes.sort_by_key(|class| match class {
1956 CharacterClass::Alphabetic => 0,
1957 CharacterClass::Alphanumeric => 1,
1958 CharacterClass::Digit => 2,
1959 });
1960 classes.dedup();
1961 exact_characters.sort();
1962 exact_characters.dedup();
1963 Some((
1964 node_text(&right, src).trim().to_string(),
1965 classes,
1966 exact_characters,
1967 ))
1968}
1969
1970fn python_character_predicate(
1971 node: Node<'_>,
1972 variable: &str,
1973 src: &[u8],
1974 classes: &mut Vec<CharacterClass>,
1975 exact_characters: &mut Vec<String>,
1976) -> bool {
1977 if node.kind() == "boolean_operator" {
1978 let (Some(left), Some(right)) = (
1979 node.child_by_field_name("left"),
1980 node.child_by_field_name("right"),
1981 ) else {
1982 return false;
1983 };
1984 let operator = src
1985 .get(left.end_byte()..right.start_byte())
1986 .and_then(|bytes| std::str::from_utf8(bytes).ok())
1987 .map(str::trim);
1988 return operator == Some("or")
1989 && python_character_predicate(left, variable, src, classes, exact_characters)
1990 && python_character_predicate(right, variable, src, classes, exact_characters);
1991 }
1992 if let Some((function, arguments)) = python_call_parts(node) {
1993 let Some((receiver, method)) = python_attribute_parts(function, src) else {
1994 return false;
1995 };
1996 if receiver.kind() != "identifier"
1997 || node_text(&receiver, src).trim() != variable
1998 || !python_argument_nodes(arguments).is_empty()
1999 {
2000 return false;
2001 }
2002 let class = match method {
2003 "isalpha" => CharacterClass::Alphabetic,
2004 "isalnum" => CharacterClass::Alphanumeric,
2005 "isdigit" => CharacterClass::Digit,
2006 _ => return false,
2007 };
2008 classes.push(class);
2009 return true;
2010 }
2011 if node.kind() != "comparison_operator" || node.named_child_count() != 2 {
2012 return false;
2013 }
2014 let (Some(left), Some(right)) = (node.named_child(0), node.named_child(1)) else {
2015 return false;
2016 };
2017 let operator = src
2018 .get(left.end_byte()..right.start_byte())
2019 .and_then(|bytes| std::str::from_utf8(bytes).ok())
2020 .map(str::trim);
2021 if operator != Some("==") {
2022 return false;
2023 }
2024 let literal = if left.kind() == "identifier" && node_text(&left, src).trim() == variable {
2025 python_static_string(right, src)
2026 } else if right.kind() == "identifier" && node_text(&right, src).trim() == variable {
2027 python_static_string(left, src)
2028 } else {
2029 None
2030 };
2031 let Some(literal) = literal.filter(|value| value.chars().count() == 1) else {
2032 return false;
2033 };
2034 exact_characters.push(literal);
2035 true
2036}
2037
2038fn python_regex_substitution_constraints(
2039 index: &DeclIndex,
2040 tree: &Tree,
2041 file: FileId,
2042 src: &[u8],
2043) -> Vec<CharacterConstraintFact> {
2044 let assignments = collect_kinds(tree, &["assignment"]);
2045 let mut compiled = Vec::new();
2046 for assignment in &assignments {
2047 let (Some(target), Some(value)) = (
2048 assignment.child_by_field_name("left"),
2049 assignment.child_by_field_name("right"),
2050 ) else {
2051 continue;
2052 };
2053 if target.kind() != "identifier" {
2054 continue;
2055 }
2056 let Some((function, arguments)) = python_call_parts(value) else {
2057 continue;
2058 };
2059 let args = python_argument_nodes(arguments);
2060 let Some(pattern) = args.first().and_then(|node| python_static_string(*node, src)) else {
2061 continue;
2062 };
2063 let Some(characters) = python_exact_regex_character_class(&pattern) else {
2064 continue;
2065 };
2066 let name = node_text(&target, src).trim().to_string();
2067 let writes = assignments
2068 .iter()
2069 .filter(|candidate| {
2070 candidate
2071 .child_by_field_name("left")
2072 .is_some_and(|left| node_text(&left, src).trim() == name)
2073 })
2074 .count();
2075 if writes == 1 {
2076 compiled.push((
2077 name,
2078 span_of(file, assignment),
2079 characters,
2080 node_text(&function, src).trim().to_string(),
2081 ));
2082 }
2083 }
2084
2085 let mut facts = Vec::new();
2086 for return_node in collect_kinds(tree, &["return_statement"]) {
2087 let Some(call) = return_node.named_child(0) else {
2088 continue;
2089 };
2090 let Some((function, arguments)) = python_call_parts(call) else {
2091 continue;
2092 };
2093 let Some((receiver, _)) = python_attribute_parts(function, src) else {
2094 continue;
2095 };
2096 if receiver.kind() != "identifier" {
2097 continue;
2098 }
2099 let receiver_name = node_text(&receiver, src).trim();
2100 let Some((_, _, mut excluded, factory_call)) = compiled
2101 .iter()
2102 .find(|(name, assignment_span, _, _)| {
2103 name == receiver_name && assignment_span.start < return_node.start_byte() as u64
2104 })
2105 .cloned()
2106 else {
2107 continue;
2108 };
2109 let args = python_argument_nodes(arguments);
2110 let [replacement, input] = args.as_slice() else {
2111 continue;
2112 };
2113 let (Some(replacement), true) = (
2114 python_exact_replacement_string(*replacement, src),
2115 input.kind() == "identifier",
2116 ) else {
2117 continue;
2118 };
2119 excluded.retain(|character| !replacement.contains(character));
2120 if excluded.is_empty() {
2121 continue;
2122 }
2123 let return_span = span_of(file, &return_node);
2124 let Some(decl) = python_enclosing_callable(index, return_span) else {
2125 continue;
2126 };
2127 if !python_is_single_statement_return(return_node) {
2128 continue;
2129 }
2130 let input_place = node_text(input, src).trim().to_string();
2131 let Some(input_param_index) = decl.params.iter().position(|param| param == &input_place) else {
2132 continue;
2133 };
2134 facts.push(CharacterConstraintFact {
2135 function_span: decl.span,
2136 transform_span: return_span,
2137 input_place,
2138 input_param_index: Some(input_param_index),
2139 output: CharacterConstraintOutput::Return,
2140 domain: CharacterConstraintDomain::ProviderBound {
2141 factory_call,
2142 operation_call: node_text(&function, src).trim().to_string(),
2143 domain: Box::new(CharacterConstraintDomain::ExcludesExact { characters: excluded }),
2144 },
2145 });
2146 }
2147 facts
2148}
2149
2150fn python_regex_validation_constraints(
2156 index: &DeclIndex,
2157 tree: &Tree,
2158 file: FileId,
2159 src: &[u8],
2160) -> Vec<CharacterConstraintFact> {
2161 let assignments = collect_kinds(tree, &["assignment"]);
2162 let mut compiled = Vec::new();
2163 for assignment in &assignments {
2164 let (Some(target), Some(value)) = (
2165 assignment.child_by_field_name("left"),
2166 assignment.child_by_field_name("right"),
2167 ) else {
2168 continue;
2169 };
2170 if target.kind() != "identifier" {
2171 continue;
2172 }
2173 let Some((function, arguments)) = python_call_parts(value) else {
2174 continue;
2175 };
2176 let args = python_argument_nodes(arguments);
2177 let Some(pattern) = args.first().and_then(|node| python_static_string(*node, src)) else {
2178 continue;
2179 };
2180 let Some(domain) = python_anchored_regex_character_domain(&pattern) else {
2181 continue;
2182 };
2183 let name = node_text(&target, src).trim().to_string();
2184 if assignments
2185 .iter()
2186 .filter(|candidate| {
2187 candidate
2188 .child_by_field_name("left")
2189 .is_some_and(|left| node_text(&left, src).trim() == name)
2190 })
2191 .count()
2192 != 1
2193 {
2194 continue;
2195 }
2196 compiled.push((
2197 name,
2198 span_of(file, assignment),
2199 node_text(&function, src).trim().to_string(),
2200 domain,
2201 ));
2202 }
2203
2204 let mut facts = Vec::new();
2205 for branch in collect_kinds(tree, &["if_statement"]) {
2206 let (Some(condition), Some(consequence)) = (
2207 branch.child_by_field_name("condition"),
2208 branch.child_by_field_name("consequence"),
2209 ) else {
2210 continue;
2211 };
2212 if branch.child_by_field_name("alternative").is_some() || !python_block_abruptly_exits(consequence) {
2213 continue;
2214 }
2215 let Some(call) = python_negated_guard_call(condition) else {
2216 continue;
2217 };
2218 let Some((function, arguments)) = python_call_parts(call) else {
2219 continue;
2220 };
2221 let Some((receiver, _)) = python_attribute_parts(function, src) else {
2222 continue;
2223 };
2224 if receiver.kind() != "identifier" {
2225 continue;
2226 }
2227 let args = python_argument_nodes(arguments);
2228 let [input] = args.as_slice() else {
2229 continue;
2230 };
2231 let Some(input_place) = python_exact_guarded_identifier(*input, src) else {
2232 continue;
2233 };
2234 let receiver_name = node_text(&receiver, src).trim();
2235 let Some((_, _, factory_call, domain)) = compiled
2236 .iter()
2237 .filter(|(name, span, _, _)| name == receiver_name && span.start < branch.start_byte() as u64)
2238 .max_by_key(|(_, span, _, _)| (span.start, span.end))
2239 .cloned()
2240 else {
2241 continue;
2242 };
2243 let branch_span = span_of(file, &branch);
2244 let Some(decl) = python_enclosing_callable(index, branch_span) else {
2245 continue;
2246 };
2247 if assignments.iter().any(|assignment| {
2248 assignment.start_byte() > branch.end_byte()
2249 && assignment.end_byte() <= decl.span.end as usize
2250 && assignment
2251 .child_by_field_name("left")
2252 .is_some_and(|left| node_text(&left, src).trim() == input_place)
2253 }) {
2254 continue;
2255 }
2256 facts.push(CharacterConstraintFact {
2257 function_span: decl.span,
2258 transform_span: branch_span,
2259 input_param_index: decl.params.iter().position(|parameter| parameter == &input_place),
2260 input_place: input_place.clone(),
2261 output: CharacterConstraintOutput::Assignment { target: input_place },
2262 domain: CharacterConstraintDomain::ProviderBound {
2263 factory_call,
2264 operation_call: node_text(&function, src).trim().to_string(),
2265 domain: Box::new(domain),
2266 },
2267 });
2268 }
2269 facts
2270}
2271
2272fn python_negated_guard_call(mut condition: Node<'_>) -> Option<Node<'_>> {
2273 while matches!(
2274 condition.kind(),
2275 "parenthesized_expression" | "parenthesized_expression_list"
2276 ) {
2277 condition = condition.named_child(0)?;
2278 }
2279 if condition.kind() != "not_operator" {
2280 return None;
2281 }
2282 condition
2283 .child_by_field_name("argument")
2284 .or_else(|| condition.named_child(0))
2285}
2286
2287fn python_block_abruptly_exits(block: Node<'_>) -> bool {
2288 let mut cursor = block.walk();
2289 let last = block
2290 .named_children(&mut cursor)
2291 .filter(|node| node.kind() != "comment")
2292 .last();
2293 last.is_some_and(|node| matches!(node.kind(), "return_statement" | "raise_statement"))
2294}
2295
2296fn python_exact_guarded_identifier(mut node: Node<'_>, src: &[u8]) -> Option<String> {
2297 while node.kind() == "parenthesized_expression" {
2298 node = node.named_child(0)?;
2299 }
2300 (node.kind() == "identifier").then(|| node_text(&node, src).trim().to_string())
2301}
2302
2303fn python_anchored_regex_character_domain(pattern: &str) -> Option<CharacterConstraintDomain> {
2304 let body = pattern.strip_prefix('^')?.strip_suffix('$')?;
2305 if body.is_empty() || body.contains("[^") || body.contains("(?") {
2306 return None;
2307 }
2308 let mut in_class = false;
2309 let mut escaped = false;
2310 let mut class = String::new();
2311 for character in body.chars() {
2312 if escaped {
2313 if !matches!(character, '.' | '-' | '_' | 'd' | 'w') {
2314 return None;
2315 }
2316 escaped = false;
2317 if in_class {
2318 class.push(character);
2319 }
2320 continue;
2321 }
2322 match character {
2323 '\\' => escaped = true,
2324 '[' if !in_class => {
2325 in_class = true;
2326 class.clear();
2327 }
2328 ']' if in_class => {
2329 if !python_safe_regex_character_class(&class) {
2330 return None;
2331 }
2332 in_class = false;
2333 }
2334 '/' => return None,
2335 '.' if !in_class => return None,
2336 character if in_class => class.push(character),
2337 character if character.is_ascii_alphanumeric() || "_-()|{}?+*,".contains(character) => {}
2338 _ => return None,
2339 }
2340 }
2341 if escaped || in_class {
2342 return None;
2343 }
2344 Some(CharacterConstraintDomain::ExcludesExact {
2345 characters: vec!["/".to_string(), "\\".to_string()],
2346 })
2347}
2348
2349fn python_safe_regex_character_class(class: &str) -> bool {
2350 if class.is_empty() || class.contains('/') || class.contains('\\') {
2351 return false;
2352 }
2353 let without_ranges = class.replace("A-Z", "").replace("a-z", "").replace("0-9", "");
2354 without_ranges
2355 .chars()
2356 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.'))
2357}
2358
2359fn python_exact_replacement_string(node: Node<'_>, src: &[u8]) -> Option<String> {
2360 python_static_string(node, src)
2361}
2362
2363fn python_exact_regex_character_class(pattern: &str) -> Option<Vec<String>> {
2364 let inner = pattern.strip_prefix('[')?.strip_suffix(']')?;
2365 if inner.starts_with('^') {
2366 return None;
2367 }
2368 let mut characters = Vec::new();
2369 let mut chars = inner.chars().peekable();
2370 while let Some(character) = chars.next() {
2371 if character == '-' {
2372 return None;
2373 }
2374 let decoded = if character != '\\' {
2375 character
2376 } else {
2377 match chars.next()? {
2378 'r' => '\r',
2379 'n' => '\n',
2380 't' => '\t',
2381 '\\' => '\\',
2382 '"' => '"',
2383 '\'' => '\'',
2384 'x' => {
2385 let digits = [chars.next()?, chars.next()?];
2386 let value = u8::from_str_radix(&digits.iter().collect::<String>(), 16).ok()?;
2387 char::from(value)
2388 }
2389 _ => return None,
2390 }
2391 };
2392 characters.push(decoded.to_string());
2393 }
2394 characters.sort();
2395 characters.dedup();
2396 (!characters.is_empty()).then_some(characters)
2397}
2398
2399fn python_call_parts(call: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
2400 (call.kind() == "call").then_some((
2401 call.child_by_field_name("function")?,
2402 call.child_by_field_name("arguments")?,
2403 ))
2404}
2405
2406fn python_attribute_parts<'a>(attribute: Node<'a>, src: &'a [u8]) -> Option<(Node<'a>, &'a str)> {
2407 if attribute.kind() != "attribute" {
2408 return None;
2409 }
2410 let object = attribute.child_by_field_name("object")?;
2411 let name = attribute.child_by_field_name("attribute")?;
2412 Some((object, node_text(&name, src).trim()))
2413}
2414
2415fn python_argument_nodes(arguments: Node<'_>) -> Vec<Node<'_>> {
2416 let mut cursor = arguments.walk();
2417 arguments
2418 .named_children(&mut cursor)
2419 .filter(|node| node.kind() != "keyword_argument")
2420 .collect()
2421}
2422
2423fn python_enclosing_callable(index: &DeclIndex, span: Span) -> Option<&bonsai_lang_api::Decl> {
2424 index
2425 .defs
2426 .iter()
2427 .filter(|decl| {
2428 matches!(
2429 decl.kind,
2430 bonsai_lang_api::DeclKind::Function
2431 | bonsai_lang_api::DeclKind::Method
2432 | bonsai_lang_api::DeclKind::Constructor
2433 ) && decl.span.start <= span.start
2434 && span.end <= decl.span.end
2435 })
2436 .min_by_key(|decl| decl.span.len())
2437}
2438
2439fn python_is_single_statement_return(return_node: Node<'_>) -> bool {
2440 return_node.parent().is_some_and(|block| {
2441 block.kind() == "block"
2442 && block
2443 .named_children(&mut block.walk())
2444 .filter(|node| node.kind() != "comment")
2445 .count()
2446 == 1
2447 })
2448}
2449
2450fn python_same_origin_path_constraints(
2451 index: &DeclIndex,
2452 tree: &Tree,
2453 file: FileId,
2454 src: &[u8],
2455) -> Vec<SameOriginPathConstraintFact> {
2456 let imports = parse_imports(tree, src, file);
2457 let mut facts = Vec::new();
2458 for function in collect_kinds(tree, &["function_definition"]) {
2459 let function_span = span_of(file, &function);
2460 let Some(decl) = index.defs.iter().find(|decl| decl.span == function_span) else {
2461 continue;
2462 };
2463 let Some(body) = function.child_by_field_name("body") else {
2464 continue;
2465 };
2466 let mut cursor = body.walk();
2467 let statements: Vec<_> = body
2468 .named_children(&mut cursor)
2469 .filter(|node| node.kind() != "comment")
2470 .collect();
2471 let [assignment, guard, final_return] = statements.as_slice() else {
2472 continue;
2473 };
2474 if assignment.kind() != "expression_statement" && assignment.kind() != "assignment" {
2475 continue;
2476 }
2477 let assignment = if assignment.kind() == "assignment" {
2478 *assignment
2479 } else {
2480 assignment.named_child(0).unwrap_or(*assignment)
2481 };
2482 let (Some(parsed_node), Some(parser_call)) = (
2483 assignment.child_by_field_name("left"),
2484 assignment.child_by_field_name("right"),
2485 ) else {
2486 continue;
2487 };
2488 if parsed_node.kind() != "identifier" {
2489 continue;
2490 }
2491 let parsed = node_text(&parsed_node, src).trim();
2492 let Some((parser, parser_arguments)) = python_call_parts(parser_call) else {
2493 continue;
2494 };
2495 let Some(provider_call) = python_imported_call_identity(parser, &imports, src) else {
2496 continue;
2497 };
2498 let parser_args = python_argument_nodes(parser_arguments);
2499 let [input_node] = parser_args.as_slice() else {
2500 continue;
2501 };
2502 if input_node.kind() != "identifier" {
2503 continue;
2504 }
2505 let input = node_text(input_node, src).trim();
2506 let Some(input_param_index) = decl.params.iter().position(|parameter| parameter == input) else {
2507 continue;
2508 };
2509 if guard.kind() != "if_statement" || guard.child_by_field_name("alternative").is_some() {
2510 continue;
2511 }
2512 let (Some(condition), Some(consequence)) = (
2513 guard.child_by_field_name("condition"),
2514 guard.child_by_field_name("consequence"),
2515 ) else {
2516 continue;
2517 };
2518 if !python_block_returns_static_path(consequence, src, "/")
2519 || !python_return_is_exact_place(*final_return, input, src)
2520 {
2521 continue;
2522 }
2523 let mut terms = Vec::new();
2524 python_collect_or_terms(condition, src, &mut terms);
2525 if terms.len() != 4 {
2526 continue;
2527 }
2528 let rejects_scheme = terms
2529 .iter()
2530 .any(|term| python_attribute_is(*term, parsed, "scheme", src));
2531 let rejects_authority = terms
2532 .iter()
2533 .any(|term| python_attribute_is(*term, parsed, "netloc", src));
2534 let requires_absolute_path = terms.iter().any(|term| {
2535 term.kind() == "not_operator"
2536 && term
2537 .named_child(0)
2538 .is_some_and(|operand| python_startswith_literal(operand, input, "/", src))
2539 });
2540 let rejects_scheme_relative_path = terms
2541 .iter()
2542 .any(|term| python_startswith_literal(*term, input, "//", src));
2543 if rejects_scheme && rejects_authority && requires_absolute_path && rejects_scheme_relative_path {
2544 facts.push(SameOriginPathConstraintFact {
2545 function_span,
2546 guard_span: span_of(file, guard),
2547 input_place: input.to_string(),
2548 input_param_index: Some(input_param_index),
2549 provider_call: Some(provider_call),
2550 rejects_scheme,
2551 rejects_authority,
2552 requires_absolute_path,
2553 rejects_scheme_relative_path,
2554 });
2555 }
2556 }
2557 facts.sort_by_key(|fact| (fact.function_span.start, fact.guard_span.start));
2558 facts.dedup();
2559 facts
2560}
2561
2562fn python_imported_call_identity(callee: Node<'_>, imports: &[ImportSpec], src: &[u8]) -> Option<String> {
2563 let rendered = node_text(&callee, src).trim();
2564 if rendered.is_empty() {
2565 return None;
2566 }
2567 for import in imports {
2568 if let Some(original) = import.original_name.as_deref() {
2569 let local = import.alias.as_deref().unwrap_or(original);
2570 if rendered == local {
2571 return Some(format!("{}.{}", import.module, original));
2572 }
2573 if let Some(suffix) = rendered
2574 .strip_prefix(local)
2575 .and_then(|tail| tail.strip_prefix('.'))
2576 {
2577 return Some(format!("{}.{}.{}", import.module, original, suffix));
2578 }
2579 continue;
2580 }
2581 if rendered == import.module || rendered.starts_with(&format!("{}.", import.module)) {
2582 return Some(rendered.to_string());
2583 }
2584 let Some(local) = import.alias.as_deref() else {
2585 continue;
2586 };
2587 if rendered == local {
2588 return Some(import.module.clone());
2589 }
2590 if let Some(suffix) = rendered
2591 .strip_prefix(local)
2592 .and_then(|tail| tail.strip_prefix('.'))
2593 {
2594 return Some(format!("{}.{}", import.module, suffix));
2595 }
2596 }
2597 None
2598}
2599
2600fn python_collect_or_terms<'tree>(node: Node<'tree>, src: &[u8], out: &mut Vec<Node<'tree>>) {
2601 if node.kind() == "boolean_operator" {
2602 let (Some(left), Some(right)) = (
2603 node.child_by_field_name("left"),
2604 node.child_by_field_name("right"),
2605 ) else {
2606 out.push(node);
2607 return;
2608 };
2609 let operator = src
2610 .get(left.end_byte()..right.start_byte())
2611 .and_then(|bytes| std::str::from_utf8(bytes).ok())
2612 .map(str::trim);
2613 if operator == Some("or") {
2614 python_collect_or_terms(left, src, out);
2615 python_collect_or_terms(right, src, out);
2616 return;
2617 }
2618 }
2619 out.push(node);
2620}
2621
2622fn python_attribute_is(node: Node<'_>, object: &str, field: &str, src: &[u8]) -> bool {
2623 python_attribute_parts(node, src).is_some_and(|(receiver, name)| {
2624 receiver.kind() == "identifier" && node_text(&receiver, src).trim() == object && name == field
2625 })
2626}
2627
2628fn python_startswith_literal(node: Node<'_>, receiver: &str, literal: &str, src: &[u8]) -> bool {
2629 let Some((function, arguments)) = python_call_parts(node) else {
2630 return false;
2631 };
2632 let Some((object, method)) = python_attribute_parts(function, src) else {
2633 return false;
2634 };
2635 let args = python_argument_nodes(arguments);
2636 object.kind() == "identifier"
2637 && node_text(&object, src).trim() == receiver
2638 && method == "startswith"
2639 && args
2640 .as_slice()
2641 .first()
2642 .and_then(|node| python_static_string(*node, src))
2643 .as_deref()
2644 == Some(literal)
2645 && args.len() == 1
2646}
2647
2648fn python_block_returns_static_path(block: Node<'_>, src: &[u8], expected: &str) -> bool {
2649 let mut cursor = block.walk();
2650 let statements: Vec<_> = block
2651 .named_children(&mut cursor)
2652 .filter(|node| node.kind() != "comment")
2653 .collect();
2654 let [return_node] = statements.as_slice() else {
2655 return false;
2656 };
2657 return_node.kind() == "return_statement"
2658 && return_node
2659 .named_child(0)
2660 .and_then(|value| python_static_string(value, src))
2661 .as_deref()
2662 == Some(expected)
2663}
2664
2665fn python_return_is_exact_place(return_node: Node<'_>, expected: &str, src: &[u8]) -> bool {
2666 return_node.kind() == "return_statement"
2667 && return_node
2668 .named_child(0)
2669 .is_some_and(|value| value.kind() == "identifier" && node_text(&value, src).trim() == expected)
2670}
2671
2672fn python_string_compositions(tree: &Tree, file: FileId, src: &[u8]) -> Vec<StringCompositionFact> {
2676 let mut facts = Vec::new();
2677 for assignment in collect_kinds(tree, &["assignment"]) {
2678 let (Some(target), Some(value)) = (
2679 assignment.child_by_field_name("left"),
2680 assignment.child_by_field_name("right"),
2681 ) else {
2682 continue;
2683 };
2684 if target.kind() != "identifier" {
2685 continue;
2686 }
2687 let mut parts = Vec::new();
2688 if lower_python_string_composition(value, file, src, &mut parts) && parts.len() > 1 {
2689 facts.push(StringCompositionFact {
2690 container_span: span_of(file, &assignment),
2691 value_span: span_of(file, &value),
2692 target: Some(node_text(&target, src).trim().to_string()),
2693 parts,
2694 });
2695 }
2696 }
2697 for return_node in collect_kinds(tree, &["return_statement"]) {
2698 let Some(value) = return_node.named_child(0) else {
2699 continue;
2700 };
2701 let mut parts = Vec::new();
2702 if lower_python_string_composition(value, file, src, &mut parts) && parts.len() > 1 {
2703 facts.push(StringCompositionFact {
2704 container_span: span_of(file, &return_node),
2705 value_span: span_of(file, &value),
2706 target: None,
2707 parts,
2708 });
2709 }
2710 }
2711 facts.sort_by_key(|fact| {
2712 (
2713 fact.container_span.start,
2714 fact.container_span.end,
2715 fact.value_span.start,
2716 fact.value_span.end,
2717 )
2718 });
2719 facts.dedup();
2720 facts
2721}
2722
2723fn lower_python_string_composition(
2724 mut node: Node<'_>,
2725 file: FileId,
2726 src: &[u8],
2727 out: &mut Vec<StringCompositionPart>,
2728) -> bool {
2729 while matches!(
2730 node.kind(),
2731 "parenthesized_expression" | "parenthesized_expression_list"
2732 ) {
2733 let Some(inner) = node.named_child(0) else {
2734 return false;
2735 };
2736 node = inner;
2737 }
2738 if let Some(value) = python_static_string(node, src) {
2739 out.push(StringCompositionPart::Literal { value });
2740 return true;
2741 }
2742 if node.kind() == "string" && lower_python_formatted_string(node, src, out) {
2743 return true;
2744 }
2745 if let Some(place) = python_exact_place(node, src) {
2746 out.push(StringCompositionPart::Place { place });
2747 return true;
2748 }
2749 if let Some((function, _)) = python_call_parts(node) {
2750 out.push(StringCompositionPart::Call {
2751 span: span_of(file, &function),
2752 });
2753 return true;
2754 }
2755 if node.kind() == "binary_operator" {
2756 let (Some(left), Some(right)) = (
2757 node.child_by_field_name("left"),
2758 node.child_by_field_name("right"),
2759 ) else {
2760 return false;
2761 };
2762 let operator = src
2763 .get(left.end_byte()..right.start_byte())
2764 .and_then(|bytes| std::str::from_utf8(bytes).ok())
2765 .map(str::trim);
2766 return operator == Some("+")
2767 && lower_python_string_composition(left, file, src, out)
2768 && lower_python_string_composition(right, file, src, out);
2769 }
2770 if node.kind() == "boolean_operator" {
2771 let (Some(left), Some(right)) = (
2772 node.child_by_field_name("left"),
2773 node.child_by_field_name("right"),
2774 ) else {
2775 return false;
2776 };
2777 let operator = src
2778 .get(left.end_byte()..right.start_byte())
2779 .and_then(|bytes| std::str::from_utf8(bytes).ok())
2780 .map(str::trim);
2781 let (Some(place), Some(fallback)) = (python_exact_place(left, src), python_static_string(right, src))
2782 else {
2783 return false;
2784 };
2785 if operator == Some("or") {
2786 out.push(StringCompositionPart::PlaceOrLiteral { place, fallback });
2787 return true;
2788 }
2789 }
2790 false
2791}
2792
2793fn lower_python_formatted_string(node: Node<'_>, src: &[u8], out: &mut Vec<StringCompositionPart>) -> bool {
2794 let text = node_text(&node, src).trim();
2795 let Some(quote_start) = text.find(['\'', '"']) else {
2796 return false;
2797 };
2798 let prefix = text[..quote_start].to_ascii_lowercase();
2799 if !prefix.contains('f')
2800 || prefix.contains('b')
2801 || prefix
2802 .chars()
2803 .any(|character| !matches!(character, 'f' | 'r' | 'u'))
2804 {
2805 return false;
2806 }
2807 let mut saw_interpolation = false;
2808 let mut cursor = node.walk();
2809 for child in node.named_children(&mut cursor) {
2810 match child.kind() {
2811 "string_start" | "string_end" => {}
2812 "string_content" => {
2813 let value = node_text(&child, src);
2814 if value.contains('\\') {
2815 return false;
2816 }
2817 push_python_composition_literal(out, value);
2818 }
2819 "interpolation" => {
2820 let Some(expression) = child
2821 .child_by_field_name("expression")
2822 .or_else(|| child.named_child(0))
2823 else {
2824 return false;
2825 };
2826 let Some(place) = python_exact_place(expression, src) else {
2827 return false;
2828 };
2829 out.push(StringCompositionPart::Place { place });
2830 saw_interpolation = true;
2831 }
2832 _ => return false,
2833 }
2834 }
2835 saw_interpolation
2836}
2837
2838fn push_python_composition_literal(out: &mut Vec<StringCompositionPart>, value: &str) {
2839 if value.is_empty() {
2840 return;
2841 }
2842 if let Some(StringCompositionPart::Literal { value: previous }) = out.last_mut() {
2843 previous.push_str(value);
2844 } else {
2845 out.push(StringCompositionPart::Literal {
2846 value: value.to_string(),
2847 });
2848 }
2849}
2850
2851fn python_exact_place(node: Node<'_>, src: &[u8]) -> Option<String> {
2852 match node.kind() {
2853 "identifier" => {
2854 let name = node_text(&node, src).trim();
2855 (!name.is_empty()).then(|| name.to_string())
2856 }
2857 "attribute" => {
2858 let object = node.child_by_field_name("object")?;
2859 let attribute = node.child_by_field_name("attribute")?;
2860 let object = python_exact_place(object, src)?;
2861 let attribute = node_text(&attribute, src).trim();
2862 (!attribute.is_empty()).then(|| format!("{object}.{attribute}"))
2863 }
2864 "parenthesized_expression" | "parenthesized_expression_list" => {
2865 python_exact_place(node.named_child(0)?, src)
2866 }
2867 _ => None,
2868 }
2869}
2870
2871fn collect_python_method_type_aliases(
2876 tree: &Tree,
2877 file: FileId,
2878 src: &[u8],
2879) -> Vec<(bonsai_common::Span, Vec<TypeAliasBinding>)> {
2880 let mut out = Vec::new();
2881 for fn_node in collect_kinds(tree, &["function_definition", "lambda"]) {
2882 let mut aliases: Vec<TypeAliasBinding> = Vec::new();
2883 if let Some(params) = fn_node.child_by_field_name("parameters") {
2884 collect_python_parameter_aliases(params, src, &mut aliases);
2885 }
2886 if let Some(body) = fn_node.child_by_field_name("body") {
2887 collect_python_annotated_assignment_aliases(body, src, &mut aliases);
2888 }
2889 dedup_python_type_aliases(&mut aliases);
2890 if !aliases.is_empty() {
2891 out.push((span_of(file, &fn_node), aliases));
2892 }
2893 }
2894 out
2895}
2896
2897fn collect_python_annotated_assignment_aliases(node: Node<'_>, src: &[u8], out: &mut Vec<TypeAliasBinding>) {
2902 if node.kind() == "assignment" {
2903 if let (Some(left), Some(type_node)) =
2904 (node.child_by_field_name("left"), node.child_by_field_name("type"))
2905 {
2906 if let (Some(place), Some(type_name)) = (
2907 python_exact_place(left, src),
2908 canonical_python_type_from_node(type_node, src),
2909 ) {
2910 push_python_type_alias(out, &place, &type_name);
2911 }
2912 }
2913 }
2914 let mut cursor = node.walk();
2915 for child in node.named_children(&mut cursor) {
2916 if matches!(
2917 child.kind(),
2918 "function_definition" | "lambda" | "class_definition"
2919 ) {
2920 continue;
2921 }
2922 collect_python_annotated_assignment_aliases(child, src, out);
2923 }
2924}
2925
2926fn collect_python_param_default_calls(
2932 tree: &Tree,
2933 file: FileId,
2934 src: &[u8],
2935) -> Vec<(Span, Vec<(String, String)>)> {
2936 let mut out = Vec::new();
2937 for fn_node in collect_kinds(tree, &["function_definition", "lambda"]) {
2938 let mut calls = Vec::new();
2939 if let Some(params) = fn_node.child_by_field_name("parameters") {
2940 collect_python_parameter_default_calls(params, src, &mut calls);
2941 }
2942 dedup_python_param_default_calls(&mut calls);
2943 if !calls.is_empty() {
2944 out.push((span_of(file, &fn_node), calls));
2945 }
2946 }
2947 out
2948}
2949
2950fn collect_python_parameter_default_calls(node: Node<'_>, src: &[u8], out: &mut Vec<(String, String)>) {
2951 let mut cursor = node.walk();
2952 for child in node.named_children(&mut cursor) {
2953 match child.kind() {
2954 "typed_default_parameter" | "default_parameter" => {
2955 if let Some(binding) = python_param_default_call(child, src) {
2956 out.push(binding);
2957 }
2958 }
2959 _ => collect_python_parameter_default_calls(child, src, out),
2960 }
2961 }
2962}
2963
2964fn python_param_default_call(node: Node<'_>, src: &[u8]) -> Option<(String, String)> {
2965 let name_node = node
2966 .child_by_field_name("name")
2967 .or_else(|| first_named_child_of_kind(node, &["identifier"]))?;
2968 let name = node_text(&name_node, src).trim().to_string();
2969 if name.is_empty() {
2970 return None;
2971 }
2972 let value_node = node.child_by_field_name("value")?;
2973 if value_node.kind() != "call" {
2974 return None;
2975 }
2976 let function = value_node.child_by_field_name("function")?;
2977 let callee = python_exact_place(function, src)?;
2978 Some((name, callee))
2979}
2980
2981fn dedup_python_param_default_calls(calls: &mut Vec<(String, String)>) {
2982 let mut deduped = Vec::new();
2983 for (name, callee) in calls.drain(..) {
2984 if !deduped
2985 .iter()
2986 .any(|(existing_name, existing_callee)| existing_name == &name && existing_callee == &callee)
2987 {
2988 deduped.push((name, callee));
2989 }
2990 }
2991 *calls = deduped;
2992}
2993
2994fn merge_python_param_default_calls(decl: &mut bonsai_lang_api::Decl, calls: &[(String, String)]) {
2995 if decl.params.is_empty() || calls.is_empty() {
2996 return;
2997 }
2998 if decl.param_default_calls.len() < decl.params.len() {
2999 decl.param_default_calls.resize_with(decl.params.len(), Vec::new);
3000 }
3001 for (name, callee) in calls {
3002 let Some(idx) = decl.params.iter().position(|param| param == name) else {
3003 continue;
3004 };
3005 let defaults = &mut decl.param_default_calls[idx];
3006 if !defaults.iter().any(|existing| existing == callee) {
3007 defaults.push(callee.clone());
3008 defaults.sort();
3009 defaults.dedup();
3010 }
3011 }
3012}
3013
3014#[derive(Clone, Debug, Eq, PartialEq)]
3015struct PythonPropertyAlias {
3016 class_symbol: SymbolId,
3017 property_name: String,
3018 receiver_name: String,
3019 target_tail: String,
3020}
3021
3022fn collect_python_property_function_spans(tree: &Tree, file: FileId, src: &[u8]) -> Vec<Span> {
3023 let mut spans = Vec::new();
3024 for decorated in collect_kinds(tree, &["decorated_definition"]) {
3025 if !python_decorated_definition_has_property(&decorated, src) {
3026 continue;
3027 }
3028 let Some(function) = first_named_child_of_kind(decorated, &["function_definition"]) else {
3029 continue;
3030 };
3031 let span = span_of(file, &function);
3032 if !spans.contains(&span) {
3033 spans.push(span);
3034 }
3035 }
3036 spans
3037}
3038
3039fn python_decorated_definition_has_property(node: &Node<'_>, src: &[u8]) -> bool {
3040 let mut cursor = node.walk();
3041 let has_property = node
3042 .named_children(&mut cursor)
3043 .any(|child| child.kind() == "decorator" && python_decorator_is_property(&child, src));
3044 has_property
3045}
3046
3047fn python_decorator_is_property(node: &Node<'_>, src: &[u8]) -> bool {
3048 let text = node_text(node, src).trim();
3049 text.strip_prefix('@')
3050 .map(str::trim)
3051 .is_some_and(|decorator| decorator == "property")
3052}
3053
3054fn collect_python_property_aliases(idx: &DeclIndex, property_fn_spans: &[Span]) -> Vec<PythonPropertyAlias> {
3055 let mut aliases = Vec::new();
3056 for decl in &idx.defs {
3057 if !property_fn_spans.contains(&decl.span) {
3058 continue;
3059 }
3060 let Some(class_symbol) = decl.parent else {
3061 continue;
3062 };
3063 let Some(receiver_idx) = decl.receiver_param_index else {
3064 continue;
3065 };
3066 let Some(receiver_name) = decl.params.get(receiver_idx) else {
3067 continue;
3068 };
3069 let Some(target_tail) = python_property_return_tail(decl, receiver_name) else {
3070 continue;
3071 };
3072 aliases.push(PythonPropertyAlias {
3073 class_symbol,
3074 property_name: decl.name.clone(),
3075 receiver_name: receiver_name.clone(),
3076 target_tail,
3077 });
3078 }
3079 aliases
3080}
3081
3082fn python_property_return_tail(decl: &bonsai_lang_api::Decl, receiver_name: &str) -> Option<String> {
3083 for event in &decl.flow_events {
3084 if let Some(tail) = python_property_return_tail_from_event(event, receiver_name) {
3085 return Some(tail);
3086 }
3087 }
3088 None
3089}
3090
3091fn python_property_return_tail_from_event(
3092 event: &bonsai_lang_api::FlowEvent,
3093 receiver_name: &str,
3094) -> Option<String> {
3095 match event {
3096 bonsai_lang_api::FlowEvent::Return { value_flow, .. } => value_flow
3097 .projection
3098 .as_ref()
3099 .filter(|projection| projection.base == receiver_name)
3100 .map(|projection| projection.path.join("."))
3101 .filter(|tail| !tail.is_empty()),
3102 bonsai_lang_api::FlowEvent::Branch {
3103 then_events,
3104 else_events,
3105 ..
3106 } => then_events
3107 .iter()
3108 .chain(else_events.iter())
3109 .find_map(|event| python_property_return_tail_from_event(event, receiver_name)),
3110 bonsai_lang_api::FlowEvent::Loop { body, .. }
3111 | bonsai_lang_api::FlowEvent::Defer { body, .. }
3112 | bonsai_lang_api::FlowEvent::Using { body, .. } => body
3113 .iter()
3114 .find_map(|event| python_property_return_tail_from_event(event, receiver_name)),
3115 bonsai_lang_api::FlowEvent::Try {
3116 body,
3117 catch_events,
3118 finally_events,
3119 ..
3120 } => body
3121 .iter()
3122 .chain(catch_events.iter())
3123 .chain(finally_events.iter())
3124 .find_map(|event| python_property_return_tail_from_event(event, receiver_name)),
3125 _ => None,
3126 }
3127}
3128
3129fn python_property_aliases_for_decl(
3130 idx: &DeclIndex,
3131 decl: &bonsai_lang_api::Decl,
3132 property_aliases: &[PythonPropertyAlias],
3133) -> Vec<PythonPropertyAlias> {
3134 let Some(class_symbol) = decl.parent else {
3135 return Vec::new();
3136 };
3137 let mut out = Vec::new();
3138 let mut seen_classes = std::collections::HashSet::new();
3139 collect_python_property_aliases_for_class(
3140 idx,
3141 class_symbol,
3142 property_aliases,
3143 &mut seen_classes,
3144 &mut out,
3145 );
3146 if let Some(receiver_name) = python_decl_receiver_name(decl) {
3147 for alias in &mut out {
3148 alias.receiver_name.clone_from(&receiver_name);
3149 }
3150 }
3151 out
3152}
3153
3154fn python_decl_receiver_name(decl: &bonsai_lang_api::Decl) -> Option<String> {
3155 decl.receiver_param_index
3156 .and_then(|idx| decl.params.get(idx))
3157 .filter(|name| !name.trim().is_empty())
3158 .cloned()
3159}
3160
3161fn python_property_aliases_by_decl(
3162 idx: &DeclIndex,
3163 property_aliases: &[PythonPropertyAlias],
3164) -> std::collections::HashMap<SymbolId, Vec<PythonPropertyAlias>> {
3165 let mut by_decl = std::collections::HashMap::new();
3166 for decl in &idx.defs {
3167 let aliases = python_property_aliases_for_decl(idx, decl, property_aliases);
3168 if !aliases.is_empty() {
3169 by_decl.insert(decl.symbol, aliases);
3170 }
3171 }
3172 by_decl
3173}
3174
3175fn collect_python_property_aliases_for_class(
3176 idx: &DeclIndex,
3177 class_symbol: SymbolId,
3178 property_aliases: &[PythonPropertyAlias],
3179 seen_classes: &mut std::collections::HashSet<SymbolId>,
3180 out: &mut Vec<PythonPropertyAlias>,
3181) {
3182 if !seen_classes.insert(class_symbol) {
3183 return;
3184 }
3185 for alias in property_aliases
3186 .iter()
3187 .filter(|alias| alias.class_symbol == class_symbol)
3188 {
3189 if !out.iter().any(|existing| existing == alias) {
3190 out.push(alias.clone());
3191 }
3192 }
3193 let Some(class_decl) = idx.defs.iter().find(|decl| decl.symbol == class_symbol) else {
3194 return;
3195 };
3196 for base in &class_decl.bases {
3197 let Some(base_symbol) = idx
3198 .defs
3199 .iter()
3200 .find(|decl| {
3201 matches!(decl.kind, bonsai_lang_api::DeclKind::Class)
3202 && (decl.name == *base || decl.name == base.rsplit('.').next().unwrap_or(base))
3203 })
3204 .map(|decl| decl.symbol)
3205 else {
3206 continue;
3207 };
3208 collect_python_property_aliases_for_class(idx, base_symbol, property_aliases, seen_classes, out);
3209 }
3210}
3211
3212fn augment_python_property_flow_events(
3213 events: &mut [bonsai_lang_api::FlowEvent],
3214 property_aliases: &[PythonPropertyAlias],
3215) {
3216 if property_aliases.is_empty() {
3217 return;
3218 }
3219 for event in events {
3220 match event {
3221 bonsai_lang_api::FlowEvent::Assign { source_names, .. } => {
3222 augment_python_property_source_names(source_names, property_aliases);
3223 }
3224 bonsai_lang_api::FlowEvent::Call { args, .. } => {
3225 for arg in args {
3226 augment_python_property_source_names(&mut arg.source_names, property_aliases);
3227 }
3228 }
3229 bonsai_lang_api::FlowEvent::Branch {
3230 then_events,
3231 else_events,
3232 ..
3233 } => {
3234 augment_python_property_flow_events(then_events, property_aliases);
3235 augment_python_property_flow_events(else_events, property_aliases);
3236 }
3237 bonsai_lang_api::FlowEvent::Loop { body, .. }
3238 | bonsai_lang_api::FlowEvent::Defer { body, .. }
3239 | bonsai_lang_api::FlowEvent::Using { body, .. } => {
3240 augment_python_property_flow_events(body, property_aliases);
3241 }
3242 bonsai_lang_api::FlowEvent::Try {
3243 body,
3244 catch_events,
3245 finally_events,
3246 ..
3247 } => {
3248 augment_python_property_flow_events(body, property_aliases);
3249 augment_python_property_flow_events(catch_events, property_aliases);
3250 augment_python_property_flow_events(finally_events, property_aliases);
3251 }
3252 _ => {}
3253 }
3254 }
3255}
3256
3257fn augment_python_property_source_names(
3258 source_names: &mut Vec<String>,
3259 property_aliases: &[PythonPropertyAlias],
3260) {
3261 let existing = source_names.clone();
3262 for source in existing {
3263 for alias in property_aliases {
3264 if let Some(rewritten) = python_property_alias_source_name(&source, alias) {
3265 push_python_source_name(source_names, rewritten);
3266 }
3267 }
3268 }
3269}
3270
3271fn python_property_alias_source_name(source: &str, alias: &PythonPropertyAlias) -> Option<String> {
3272 let prefix = format!("{}.{}", alias.receiver_name, alias.property_name);
3273 let source = source.trim();
3274 if source == prefix {
3275 return Some(format!("{}.{}", alias.receiver_name, alias.target_tail));
3276 }
3277 let tail = source.strip_prefix(&prefix)?.strip_prefix('.')?;
3278 if tail.is_empty() {
3279 return None;
3280 }
3281 Some(format!("{}.{}.{}", alias.receiver_name, alias.target_tail, tail))
3282}
3283
3284fn augment_python_comprehension_flow_events(
3285 events: &mut [bonsai_lang_api::FlowEvent],
3286 source: &str,
3287 assignment_values: &AssignmentValueIndex,
3288) {
3289 for event in events {
3290 match event {
3291 bonsai_lang_api::FlowEvent::Assign {
3292 span, source_names, ..
3293 } => {
3294 if let Some(rhs) = assignment_values.rendering(*span, source) {
3295 for iterable in python_comprehension_iterables(rhs) {
3296 push_python_source_name(source_names, iterable);
3297 }
3298 }
3299 }
3300 bonsai_lang_api::FlowEvent::Branch {
3301 then_events,
3302 else_events,
3303 ..
3304 } => {
3305 augment_python_comprehension_flow_events(then_events, source, assignment_values);
3306 augment_python_comprehension_flow_events(else_events, source, assignment_values);
3307 }
3308 bonsai_lang_api::FlowEvent::Loop { body, .. }
3309 | bonsai_lang_api::FlowEvent::Defer { body, .. }
3310 | bonsai_lang_api::FlowEvent::Using { body, .. } => {
3311 augment_python_comprehension_flow_events(body, source, assignment_values);
3312 }
3313 bonsai_lang_api::FlowEvent::Try {
3314 body,
3315 catch_events,
3316 finally_events,
3317 ..
3318 } => {
3319 augment_python_comprehension_flow_events(body, source, assignment_values);
3320 augment_python_comprehension_flow_events(catch_events, source, assignment_values);
3321 augment_python_comprehension_flow_events(finally_events, source, assignment_values);
3322 }
3323 _ => {}
3324 }
3325 }
3326}
3327
3328fn collect_python_comprehension_iterable_call_events(
3329 tree: &Tree,
3330 file: FileId,
3331 src: &[u8],
3332 decl_span: Span,
3333) -> Vec<FlowEvent> {
3334 let mut out = Vec::new();
3335 for clause in collect_kinds(tree, &["for_in_clause"]) {
3336 let clause_span = span_of(file, &clause);
3337 if !python_span_contains(decl_span, clause_span) || !python_for_in_clause_is_comprehension(&clause) {
3338 continue;
3339 }
3340 let Some(iterable) = clause.child_by_field_name("right") else {
3341 continue;
3342 };
3343 collect_python_call_events_from_node(iterable, file, src, &mut out);
3344 }
3345 out.sort_by_key(|event| python_flow_event_span(event).start);
3346 out.dedup_by(|left, right| python_flow_event_same_call(left, right));
3347 out
3348}
3349
3350fn python_for_in_clause_is_comprehension(clause: &Node<'_>) -> bool {
3351 let mut parent = clause.parent();
3352 while let Some(node) = parent {
3353 if matches!(
3354 node.kind(),
3355 "list_comprehension"
3356 | "dict_comprehension"
3357 | "dictionary_comprehension"
3358 | "set_comprehension"
3359 | "generator_expression"
3360 ) {
3361 return true;
3362 }
3363 if matches!(
3364 node.kind(),
3365 "function_definition" | "lambda" | "for_statement" | "while_statement" | "if_statement" | "block"
3366 ) {
3367 return false;
3368 }
3369 parent = node.parent();
3370 }
3371 false
3372}
3373
3374fn collect_python_call_events_from_node(node: Node<'_>, file: FileId, src: &[u8], out: &mut Vec<FlowEvent>) {
3375 if node.kind() == "call" {
3376 if let Some(event) = build_python_call_event(node, file, src) {
3377 out.push(event);
3378 }
3379 }
3380 let mut cursor = node.walk();
3381 for child in node.named_children(&mut cursor) {
3382 collect_python_call_events_from_node(child, file, src, out);
3383 }
3384}
3385
3386fn build_python_call_event(node: Node<'_>, file: FileId, src: &[u8]) -> Option<FlowEvent> {
3387 if node.kind() != "call" {
3388 return None;
3389 }
3390 let callee_node = node.child_by_field_name("function")?;
3391 let name = normalize_call_name_whitespace(node_text(&callee_node, src));
3392 if name.is_empty() {
3393 return None;
3394 }
3395 let receiver = python_call_receiver_from_name(&name);
3396 let call_kind = if receiver.is_some() {
3397 CallKind::Method
3398 } else {
3399 CallKind::Function
3400 };
3401 let mut args = Vec::new();
3402 if let Some(arguments) = node.child_by_field_name("arguments") {
3403 let mut cursor = arguments.walk();
3404 for arg in arguments.named_children(&mut cursor) {
3405 let (name, value_node) = if arg.kind() == "keyword_argument" {
3406 let key = arg
3407 .child_by_field_name("name")
3408 .map(|node| node_text(&node, src).trim().to_string())
3409 .filter(|name| !name.is_empty());
3410 let value = arg.child_by_field_name("value").unwrap_or(arg);
3411 (key, value)
3412 } else {
3413 (None, arg)
3414 };
3415 if let Some(argument) =
3416 call_arg_from_nodes_with_handler(arg, value_node, file, src, name, &HANDLER)
3417 {
3418 let mut argument = argument;
3419 if let Some(place) = python_exact_expression_place(value_node, src) {
3420 argument.place = Some(place);
3421 }
3422 args.push(argument);
3423 }
3424 }
3425 }
3426 Some(FlowEvent::Call {
3427 span: span_of(file, &callee_node),
3428 name,
3429 receiver,
3430 receiver_types: Vec::new(),
3431 call_kind,
3432 args,
3433 })
3434}
3435
3436fn collect_python_call_argument_places(tree: &Tree, file: FileId, src: &[u8]) -> Vec<(Span, String)> {
3443 let mut out = Vec::new();
3444 for call in collect_kinds(tree, &["call"]) {
3445 let Some(arguments) = call.child_by_field_name("arguments") else {
3446 continue;
3447 };
3448 let mut cursor = arguments.walk();
3449 for argument in arguments.named_children(&mut cursor) {
3450 let value = if argument.kind() == "keyword_argument" {
3451 argument.child_by_field_name("value").unwrap_or(argument)
3452 } else {
3453 argument
3454 };
3455 let Some(place) = python_exact_expression_place(value, src) else {
3456 continue;
3457 };
3458 out.push((span_of(file, &argument), place));
3459 }
3460 }
3461 out.sort_by_key(|(span, _)| (span.start, span.end));
3462 out.dedup_by_key(|(span, _)| *span);
3463 out
3464}
3465
3466fn python_exact_expression_place(node: Node<'_>, src: &[u8]) -> Option<String> {
3467 match node.kind() {
3468 "identifier" => {
3469 let name = node_text(&node, src).trim();
3470 (!name.is_empty()).then(|| name.to_string())
3471 }
3472 "attribute" => {
3473 let object = node.child_by_field_name("object")?;
3474 let attribute = node.child_by_field_name("attribute")?;
3475 let base = python_exact_expression_place(object, src)?;
3476 let field = node_text(&attribute, src).trim();
3477 (!field.is_empty()).then(|| format!("{base}.{field}"))
3478 }
3479 "subscript" => {
3480 let value = node.child_by_field_name("value")?;
3481 let subscript = node.child_by_field_name("subscript")?;
3482 let base = python_exact_expression_place(value, src)?;
3483 let field = python_static_string(subscript, src)?;
3484 if field.is_empty()
3485 || !field
3486 .chars()
3487 .next()
3488 .is_some_and(|ch| ch == '_' || ch.is_ascii_alphabetic())
3489 || !field.chars().all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
3490 {
3491 return None;
3492 }
3493 Some(format!("{base}.{field}"))
3494 }
3495 "parenthesized_expression" => {
3496 let mut cursor = node.walk();
3497 let child = node.named_children(&mut cursor).next()?;
3498 python_exact_expression_place(child, src)
3499 }
3500 _ => None,
3501 }
3502}
3503
3504fn collect_python_return_places(tree: &Tree, file: FileId, src: &[u8]) -> Vec<(Span, String)> {
3508 let mut out = Vec::new();
3509 for statement in collect_kinds(tree, &["return_statement"]) {
3510 let Some(value) = statement.named_child(0) else {
3511 continue;
3512 };
3513 let Some(place) = python_exact_expression_place(value, src) else {
3514 continue;
3515 };
3516 out.push((span_of(file, &statement), place));
3517 }
3518 out.sort_by_key(|(span, _)| (span.start, span.end));
3519 out.dedup_by_key(|(span, _)| *span);
3520 out
3521}
3522
3523fn apply_python_return_places(events: &mut [FlowEvent], places: &[(Span, String)]) {
3524 for event in events {
3525 match event {
3526 FlowEvent::Return {
3527 span,
3528 value_name,
3529 value_flow,
3530 ..
3531 } => {
3532 if let Ok(index) = places.binary_search_by_key(&(span.start, span.end), |(candidate, _)| {
3533 (candidate.start, candidate.end)
3534 }) {
3535 let place = places[index].1.clone();
3536 *value_name = Some(place.clone());
3537 value_flow.place = Some(place.clone());
3538 value_flow.source_names.clear();
3539 value_flow.source_names.push(place);
3540 }
3541 }
3542 FlowEvent::Branch {
3543 then_events,
3544 else_events,
3545 ..
3546 } => {
3547 apply_python_return_places(then_events, places);
3548 apply_python_return_places(else_events, places);
3549 }
3550 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
3551 apply_python_return_places(body, places);
3552 }
3553 FlowEvent::Try {
3554 body,
3555 catch_events,
3556 finally_events,
3557 ..
3558 } => {
3559 apply_python_return_places(body, places);
3560 apply_python_return_places(catch_events, places);
3561 apply_python_return_places(finally_events, places);
3562 }
3563 _ => {}
3564 }
3565 }
3566}
3567
3568fn apply_python_call_argument_places(events: &mut [FlowEvent], places: &[(Span, String)]) {
3569 for event in events {
3570 match event {
3571 FlowEvent::Call { args, .. } => {
3572 for argument in args {
3573 if let Ok(index) = places
3574 .binary_search_by_key(&(argument.span.start, argument.span.end), |(span, _)| {
3575 (span.start, span.end)
3576 })
3577 {
3578 argument.place = Some(places[index].1.clone());
3579 }
3580 }
3581 }
3582 FlowEvent::Branch {
3583 then_events,
3584 else_events,
3585 ..
3586 } => {
3587 apply_python_call_argument_places(then_events, places);
3588 apply_python_call_argument_places(else_events, places);
3589 }
3590 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
3591 apply_python_call_argument_places(body, places);
3592 }
3593 FlowEvent::Try {
3594 body,
3595 catch_events,
3596 finally_events,
3597 ..
3598 } => {
3599 apply_python_call_argument_places(body, places);
3600 apply_python_call_argument_places(catch_events, places);
3601 apply_python_call_argument_places(finally_events, places);
3602 }
3603 _ => {}
3604 }
3605 }
3606}
3607
3608fn collect_python_iterable_yield_bindings(tree: &Tree, file: FileId, src: &[u8]) -> Vec<FlowEvent> {
3615 let mut out = Vec::new();
3616 for loop_node in collect_kinds(tree, &["for_statement"]) {
3617 let (Some(binding), Some(iterable)) = (
3618 loop_node.child_by_field_name("left"),
3619 loop_node.child_by_field_name("right"),
3620 ) else {
3621 continue;
3622 };
3623 let Some(FlowEvent::Call { name, args, .. }) = build_python_call_event(iterable, file, src) else {
3624 continue;
3625 };
3626 let source_call_args = args.into_iter().map(|arg| arg.value_text).collect::<Vec<_>>();
3627 for target in python_loop_binding_targets(binding, src) {
3628 out.push(FlowEvent::Assign {
3629 span: span_of(file, &loop_node),
3637 target,
3638 source_name: None,
3639 source_call: Some(name.clone()),
3640 source_call_args: source_call_args.clone(),
3641 source_names: Vec::new(),
3642 declares_new_binding: false,
3643 value_kind: Some(bonsai_lang_api::AssignValueKind::YieldResult),
3644 });
3645 }
3646 }
3647 out.sort_by_key(|event| {
3648 let span = python_flow_event_span(event);
3649 let target = match event {
3650 FlowEvent::Assign { target, .. } => target.as_str(),
3651 _ => "",
3652 };
3653 (span.start, span.end, target.to_string())
3654 });
3655 out.dedup_by(|left, right| match (left, right) {
3656 (
3657 FlowEvent::Assign {
3658 span: left_span,
3659 target: left_target,
3660 source_call: left_call,
3661 ..
3662 },
3663 FlowEvent::Assign {
3664 span: right_span,
3665 target: right_target,
3666 source_call: right_call,
3667 ..
3668 },
3669 ) => left_span == right_span && left_target == right_target && left_call == right_call,
3670 _ => false,
3671 });
3672 out
3673}
3674
3675fn insert_python_iterable_yield_bindings(events: &mut Vec<FlowEvent>, bindings: &[FlowEvent]) {
3683 for event in events.iter_mut() {
3684 match event {
3685 FlowEvent::Branch {
3686 then_events,
3687 else_events,
3688 ..
3689 } => {
3690 insert_python_iterable_yield_bindings(then_events, bindings);
3691 insert_python_iterable_yield_bindings(else_events, bindings);
3692 }
3693 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
3694 insert_python_iterable_yield_bindings(body, bindings);
3695 }
3696 FlowEvent::Try {
3697 body,
3698 catch_events,
3699 finally_events,
3700 ..
3701 } => {
3702 insert_python_iterable_yield_bindings(body, bindings);
3703 insert_python_iterable_yield_bindings(catch_events, bindings);
3704 insert_python_iterable_yield_bindings(finally_events, bindings);
3705 }
3706 _ => {}
3707 }
3708 }
3709
3710 let loop_spans = events
3711 .iter()
3712 .filter_map(|event| match event {
3713 FlowEvent::Loop { span, .. } => Some(*span),
3714 _ => None,
3715 })
3716 .collect::<Vec<_>>();
3717 for loop_span in loop_spans {
3718 let Some(mut insert_at) = events
3719 .iter()
3720 .position(|event| matches!(event, FlowEvent::Loop { span, .. } if *span == loop_span))
3721 else {
3722 continue;
3723 };
3724 for binding in bindings
3725 .iter()
3726 .filter(|binding| python_flow_event_span(binding) == loop_span)
3727 {
3728 let mut binding = binding.clone();
3729 if let FlowEvent::Assign {
3736 target,
3737 source_names,
3738 value_kind: Some(bonsai_lang_api::AssignValueKind::YieldResult),
3739 ..
3740 } = &mut binding
3741 {
3742 if let Some(tuple_sources) = events.iter().find_map(|existing| match existing {
3743 FlowEvent::Assign {
3744 span,
3745 target: existing_target,
3746 source_names: existing_sources,
3747 value_kind,
3748 ..
3749 } if *span == loop_span
3750 && existing_target == target
3751 && !matches!(value_kind, Some(bonsai_lang_api::AssignValueKind::YieldResult))
3752 && existing_sources.iter().any(|source| {
3753 source.starts_with(bonsai_lang_api::kit::SYNTHETIC_TUPLE_RESULT_PREFIX)
3754 }) =>
3755 {
3756 Some(existing_sources.clone())
3757 }
3758 _ => None,
3759 }) {
3760 *source_names = tuple_sources;
3761 }
3762 }
3763 if events.iter().any(|existing| existing == &binding) {
3764 continue;
3765 }
3766 events.insert(insert_at, binding);
3767 insert_at += 1;
3768 }
3769 }
3770}
3771
3772fn python_loop_binding_targets(node: Node<'_>, src: &[u8]) -> Vec<String> {
3773 fn collect(node: Node<'_>, src: &[u8], out: &mut Vec<String>) {
3774 if node.kind() == "identifier" {
3775 let name = node_text(&node, src).trim();
3776 if python_match_capture_identifier(name) {
3777 out.push(name.to_string());
3778 }
3779 return;
3780 }
3781 if !matches!(
3782 node.kind(),
3783 "pattern_list" | "tuple_pattern" | "list_pattern" | "star_pattern"
3784 ) {
3785 return;
3786 }
3787 let mut cursor = node.walk();
3788 for child in node.named_children(&mut cursor) {
3789 collect(child, src, out);
3790 }
3791 }
3792
3793 let mut out = Vec::new();
3794 collect(node, src, &mut out);
3795 out.sort();
3796 out.dedup();
3797 out
3798}
3799
3800fn python_call_receiver_from_name(name: &str) -> Option<String> {
3801 let (receiver, _) = name.rsplit_once('.')?;
3802 let receiver = receiver.trim();
3803 (!receiver.is_empty()).then(|| receiver.to_string())
3804}
3805
3806fn python_is_identifier_like(text: &str) -> bool {
3807 let mut chars = text.chars();
3808 let Some(first) = chars.next() else {
3809 return false;
3810 };
3811 (first == '_' || first.is_alphabetic()) && chars.all(|ch| ch == '_' || ch.is_alphanumeric())
3812}
3813
3814fn augment_python_asyncio_to_thread_calls(events: &mut Vec<FlowEvent>) {
3815 let mut i = 0;
3816 while i < events.len() {
3817 match &mut events[i] {
3818 FlowEvent::Branch {
3819 then_events,
3820 else_events,
3821 ..
3822 } => {
3823 augment_python_asyncio_to_thread_calls(then_events);
3824 augment_python_asyncio_to_thread_calls(else_events);
3825 }
3826 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
3827 augment_python_asyncio_to_thread_calls(body);
3828 }
3829 FlowEvent::Try {
3830 body,
3831 catch_events,
3832 finally_events,
3833 ..
3834 } => {
3835 augment_python_asyncio_to_thread_calls(body);
3836 augment_python_asyncio_to_thread_calls(catch_events);
3837 augment_python_asyncio_to_thread_calls(finally_events);
3838 }
3839 _ => {}
3840 }
3841
3842 let synthetic = match &events[i] {
3843 FlowEvent::Call { span, name, args, .. } if python_is_asyncio_to_thread_name(name) => {
3844 let Some((target, shifted_args)) = python_to_thread_target_and_args(args) else {
3845 i += 1;
3846 continue;
3847 };
3848 let receiver = python_call_receiver_from_name(&target);
3849 let call_kind = if receiver.is_some() {
3850 CallKind::Method
3851 } else {
3852 CallKind::Function
3853 };
3854 Some(FlowEvent::Call {
3855 span: *span,
3856 name: target,
3857 receiver,
3858 receiver_types: Vec::new(),
3859 call_kind,
3860 args: shifted_args,
3861 })
3862 }
3863 _ => None,
3864 };
3865 if let Some(call) = synthetic {
3866 events.insert(i + 1, call);
3867 i += 1;
3868 }
3869 i += 1;
3870 }
3871}
3872
3873fn python_is_asyncio_to_thread_name(name: &str) -> bool {
3874 matches!(name.trim(), "asyncio.to_thread" | "to_thread")
3875}
3876
3877fn python_to_thread_target_and_args(args: &[CallArg]) -> Option<(String, Vec<CallArg>)> {
3878 let target = args.first()?.value_text.trim();
3879 if !python_is_qualified_identifier_like(target) {
3880 return None;
3881 }
3882 Some((target.to_string(), args.iter().skip(1).cloned().collect()))
3883}
3884
3885fn python_is_qualified_identifier_like(text: &str) -> bool {
3886 let text = text.trim();
3887 !text.is_empty()
3888 && text
3889 .split('.')
3890 .all(|part| !part.is_empty() && python_is_identifier_like(part))
3891}
3892
3893fn insert_python_flow_events_by_span(events: &mut Vec<FlowEvent>, owner_span: Span, synthetic: &[FlowEvent]) {
3894 for event in events.iter_mut() {
3895 let event_span = python_flow_event_span(event);
3896 match event {
3897 FlowEvent::Branch {
3898 then_events,
3899 else_events,
3900 ..
3901 } => {
3902 insert_python_flow_events_by_span(then_events, event_span, synthetic);
3903 insert_python_flow_events_by_span(else_events, event_span, synthetic);
3904 }
3905 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
3906 insert_python_flow_events_by_span(body, event_span, synthetic);
3907 }
3908 FlowEvent::Try {
3909 body,
3910 catch_events,
3911 finally_events,
3912 ..
3913 } => {
3914 insert_python_flow_events_by_span(body, event_span, synthetic);
3915 insert_python_flow_events_by_span(catch_events, event_span, synthetic);
3916 insert_python_flow_events_by_span(finally_events, event_span, synthetic);
3917 }
3918 _ => {}
3919 }
3920 }
3921
3922 let mut pending: Vec<FlowEvent> = synthetic
3923 .iter()
3924 .filter(|event| {
3925 let span = python_flow_event_span(event);
3926 python_span_contains(owner_span, span)
3927 && !python_event_tree_contains_call(events, event)
3928 && !events.iter().any(|candidate| {
3929 python_flow_event_is_container(candidate)
3930 && python_span_contains(python_flow_event_span(candidate), span)
3931 })
3932 })
3933 .cloned()
3934 .collect();
3935 pending.sort_by_key(|event| python_flow_event_span(event).start);
3936 for event in pending {
3937 let span = python_flow_event_span(&event);
3938 let insert_at = events
3939 .iter()
3940 .position(|existing| python_flow_event_span(existing).start > span.start)
3941 .unwrap_or(events.len());
3942 events.insert(insert_at, event);
3943 }
3944}
3945
3946fn python_flow_event_is_container(event: &FlowEvent) -> bool {
3947 matches!(
3948 event,
3949 FlowEvent::Branch { .. }
3950 | FlowEvent::Loop { .. }
3951 | FlowEvent::Try { .. }
3952 | FlowEvent::Defer { .. }
3953 | FlowEvent::Using { .. }
3954 )
3955}
3956
3957fn python_event_tree_contains_call(events: &[FlowEvent], needle: &FlowEvent) -> bool {
3958 events.iter().any(|event| {
3959 python_flow_event_same_call(event, needle)
3960 || match event {
3961 FlowEvent::Branch {
3962 then_events,
3963 else_events,
3964 ..
3965 } => {
3966 python_event_tree_contains_call(then_events, needle)
3967 || python_event_tree_contains_call(else_events, needle)
3968 }
3969 FlowEvent::Loop { body, .. }
3970 | FlowEvent::Defer { body, .. }
3971 | FlowEvent::Using { body, .. } => python_event_tree_contains_call(body, needle),
3972 FlowEvent::Try {
3973 body,
3974 catch_events,
3975 finally_events,
3976 ..
3977 } => {
3978 python_event_tree_contains_call(body, needle)
3979 || python_event_tree_contains_call(catch_events, needle)
3980 || python_event_tree_contains_call(finally_events, needle)
3981 }
3982 _ => false,
3983 }
3984 })
3985}
3986
3987fn python_flow_event_same_call(left: &FlowEvent, right: &FlowEvent) -> bool {
3988 matches!(
3989 (left, right),
3990 (
3991 FlowEvent::Call {
3992 span: left_span,
3993 name: left_name,
3994 ..
3995 },
3996 FlowEvent::Call {
3997 span: right_span,
3998 name: right_name,
3999 ..
4000 }
4001 ) if left_span == right_span && left_name == right_name
4002 )
4003}
4004
4005fn python_comprehension_iterables(rhs: &str) -> Vec<String> {
4006 let mut out = Vec::new();
4007 let mut quote: Option<char> = None;
4008 let mut escaped = false;
4009 let mut depth = 0usize;
4010 let iter = rhs.char_indices().peekable();
4011 for (idx, ch) in iter {
4012 if let Some(q) = quote {
4013 if escaped {
4014 escaped = false;
4015 } else if ch == '\\' {
4016 escaped = true;
4017 } else if ch == q {
4018 quote = None;
4019 }
4020 continue;
4021 }
4022 if matches!(ch, '\'' | '"' | '`') {
4023 quote = Some(ch);
4024 continue;
4025 }
4026 match ch {
4027 '(' | '[' | '{' => depth = depth.saturating_add(1),
4028 ')' | ']' | '}' => depth = depth.saturating_sub(1),
4029 'i' if rhs[idx..].starts_with("in") && python_keyword_boundary(rhs, idx, idx + 2) => {
4030 let start = idx + 2;
4031 let end = python_comprehension_iterable_end(rhs, start, depth);
4032 for token in python_access_tokens(&rhs[start..end]) {
4033 push_python_source_name(&mut out, token);
4034 }
4035 }
4036 _ => {}
4037 }
4038 }
4039 out
4040}
4041
4042fn python_keyword_boundary(text: &str, start: usize, end: usize) -> bool {
4043 let before = text[..start].chars().next_back();
4044 let after = text[end..].chars().next();
4045 !before.is_some_and(|ch| ch == '_' || ch.is_ascii_alphanumeric())
4046 && !after.is_some_and(|ch| ch == '_' || ch.is_ascii_alphanumeric())
4047}
4048
4049fn python_comprehension_iterable_end(text: &str, start: usize, initial_depth: usize) -> usize {
4050 let mut quote: Option<char> = None;
4051 let mut escaped = false;
4052 let mut depth = initial_depth;
4053 for (idx, ch) in text.char_indices().skip_while(|(idx, _)| *idx < start) {
4054 if let Some(q) = quote {
4055 if escaped {
4056 escaped = false;
4057 } else if ch == '\\' {
4058 escaped = true;
4059 } else if ch == q {
4060 quote = None;
4061 }
4062 continue;
4063 }
4064 if matches!(ch, '\'' | '"' | '`') {
4065 quote = Some(ch);
4066 continue;
4067 }
4068 match ch {
4069 '(' | '[' | '{' => depth = depth.saturating_add(1),
4070 ')' | ']' | '}' => {
4071 if depth <= initial_depth {
4072 return idx;
4073 }
4074 depth = depth.saturating_sub(1);
4075 }
4076 ',' if depth == initial_depth => return idx,
4077 'i' if depth == initial_depth
4078 && text[idx..].starts_with("if")
4079 && python_keyword_boundary(text, idx, idx + 2) =>
4080 {
4081 return idx;
4082 }
4083 'f' if depth == initial_depth
4084 && text[idx..].starts_with("for")
4085 && python_keyword_boundary(text, idx, idx + 3) =>
4086 {
4087 return idx;
4088 }
4089 _ => {}
4090 }
4091 }
4092 text.len()
4093}
4094
4095fn python_access_tokens(text: &str) -> Vec<String> {
4096 let mut out = Vec::new();
4097 let mut token = String::new();
4098 let mut quote: Option<char> = None;
4099 let mut escaped = false;
4100 for ch in text.chars().chain(std::iter::once(' ')) {
4101 if let Some(q) = quote {
4102 if escaped {
4103 escaped = false;
4104 } else if ch == '\\' {
4105 escaped = true;
4106 } else if ch == q {
4107 quote = None;
4108 }
4109 continue;
4110 }
4111 if matches!(ch, '\'' | '"' | '`') {
4112 push_python_source_name(&mut out, token.trim_matches('.').to_string());
4113 token.clear();
4114 quote = Some(ch);
4115 continue;
4116 }
4117 if ch == '.' || ch == '_' || ch.is_ascii_alphanumeric() {
4118 token.push(ch);
4119 continue;
4120 }
4121 push_python_source_name(&mut out, token.trim_matches('.').to_string());
4122 token.clear();
4123 }
4124 out
4125}
4126
4127fn push_python_source_name(out: &mut Vec<String>, value: String) {
4128 if !value.is_empty() && !out.iter().any(|existing| existing == &value) {
4129 out.push(value);
4130 }
4131}
4132
4133fn python_match_capture_identifier(text: &str) -> bool {
4134 if matches!(
4135 text,
4136 "" | "_" | "True" | "False" | "None" | "case" | "if" | "in" | "and" | "or" | "not"
4137 ) {
4138 return false;
4139 }
4140 let mut chars = text.chars();
4141 let Some(first) = chars.next() else {
4142 return false;
4143 };
4144 if !(first == '_' || first.is_alphabetic()) {
4145 return false;
4146 }
4147 chars.all(|ch| ch == '_' || ch.is_alphanumeric())
4148}
4149
4150fn python_span_owned_by_decl(span: Span, decl_span: Span, callable_spans: &[Span]) -> bool {
4151 if !python_span_contains(decl_span, span) {
4152 return false;
4153 }
4154 let Some(owner) = callable_spans
4155 .iter()
4156 .copied()
4157 .filter(|candidate| python_span_contains(*candidate, span))
4158 .min_by_key(|span| span.end.saturating_sub(span.start))
4159 else {
4160 return false;
4161 };
4162 owner == decl_span
4163}
4164
4165fn python_span_contains(outer: Span, inner: Span) -> bool {
4166 outer.file == inner.file && outer.start <= inner.start && inner.end <= outer.end
4167}
4168
4169fn python_flow_event_span(event: &bonsai_lang_api::FlowEvent) -> Span {
4170 match event {
4171 bonsai_lang_api::FlowEvent::Assign { span, .. }
4172 | bonsai_lang_api::FlowEvent::AggregateAssign { span, .. }
4173 | bonsai_lang_api::FlowEvent::Call { span, .. }
4174 | bonsai_lang_api::FlowEvent::Return { span, .. }
4175 | bonsai_lang_api::FlowEvent::Throw { span, .. }
4176 | bonsai_lang_api::FlowEvent::Branch { span, .. }
4177 | bonsai_lang_api::FlowEvent::Loop { span, .. }
4178 | bonsai_lang_api::FlowEvent::Try { span, .. }
4179 | bonsai_lang_api::FlowEvent::Defer { span, .. }
4180 | bonsai_lang_api::FlowEvent::Using { span, .. }
4181 | bonsai_lang_api::FlowEvent::Yield { span, .. }
4182 | bonsai_lang_api::FlowEvent::Await { span, .. }
4183 | bonsai_lang_api::FlowEvent::Break { span, .. }
4184 | bonsai_lang_api::FlowEvent::Continue { span, .. }
4185 | bonsai_lang_api::FlowEvent::Lifecycle { span, .. } => *span,
4186 }
4187}
4188
4189fn augment_python_dict_flow_events(
4190 events: &mut Vec<bonsai_lang_api::FlowEvent>,
4191 source: &str,
4192 assignment_values: &AssignmentValueIndex,
4193 assignment_projected_reads: &[(Span, Vec<String>)],
4194) {
4195 for event in events.iter_mut() {
4196 match event {
4197 bonsai_lang_api::FlowEvent::Branch {
4198 then_events,
4199 else_events,
4200 ..
4201 } => {
4202 augment_python_dict_flow_events(
4203 then_events,
4204 source,
4205 assignment_values,
4206 assignment_projected_reads,
4207 );
4208 augment_python_dict_flow_events(
4209 else_events,
4210 source,
4211 assignment_values,
4212 assignment_projected_reads,
4213 );
4214 }
4215 bonsai_lang_api::FlowEvent::Loop { body, .. }
4216 | bonsai_lang_api::FlowEvent::Defer { body, .. }
4217 | bonsai_lang_api::FlowEvent::Using { body, .. } => {
4218 augment_python_dict_flow_events(body, source, assignment_values, assignment_projected_reads);
4219 }
4220 bonsai_lang_api::FlowEvent::Try {
4221 body,
4222 catch_events,
4223 finally_events,
4224 ..
4225 } => {
4226 augment_python_dict_flow_events(body, source, assignment_values, assignment_projected_reads);
4227 augment_python_dict_flow_events(
4228 catch_events,
4229 source,
4230 assignment_values,
4231 assignment_projected_reads,
4232 );
4233 augment_python_dict_flow_events(
4234 finally_events,
4235 source,
4236 assignment_values,
4237 assignment_projected_reads,
4238 );
4239 }
4240 _ => {}
4241 }
4242 }
4243
4244 let mut known_fields: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
4245 let mut rewritten = Vec::with_capacity(events.len());
4246 for event in events.drain(..) {
4247 let mut synthetic = Vec::new();
4248 if let bonsai_lang_api::FlowEvent::Assign { span, target, .. } = &event {
4249 if let Some(rhs) = assignment_values.rendering(*span, source) {
4250 for (field, value) in python_dict_field_initializers(rhs) {
4251 push_python_source_name(known_fields.entry(target.clone()).or_default(), field.clone());
4252 let source_names = python_value_source_names(&value);
4253 synthetic.push(bonsai_lang_api::FlowEvent::Assign {
4254 span: *span,
4255 target: format!("{target}.{field}"),
4256 source_name: None,
4257 source_call: None,
4258 source_call_args: Vec::new(),
4259 source_names,
4260 declares_new_binding: false,
4261 value_kind: None,
4262 });
4263 }
4264 for field_read in assignment_projected_reads
4265 .binary_search_by_key(&(span.start, span.end), |(candidate, _)| {
4266 (candidate.start, candidate.end)
4267 })
4268 .ok()
4269 .and_then(|index| assignment_projected_reads.get(index))
4270 .map_or(&[][..], |(_, reads)| reads.as_slice())
4271 {
4272 synthetic.push(bonsai_lang_api::FlowEvent::Assign {
4273 span: *span,
4274 target: target.clone(),
4275 source_name: Some(field_read.clone()),
4276 source_call: None,
4277 source_call_args: Vec::new(),
4278 source_names: vec![field_read.clone()],
4279 declares_new_binding: false,
4280 value_kind: None,
4281 });
4282 }
4283 for spread in python_dict_spreads(rhs) {
4284 if let Some(fields) = known_fields.get(&spread).cloned() {
4285 for field in fields {
4286 synthetic.push(bonsai_lang_api::FlowEvent::Assign {
4287 span: *span,
4288 target: format!("{target}.{field}"),
4289 source_name: Some(format!("{spread}.{field}")),
4290 source_call: None,
4291 source_call_args: Vec::new(),
4292 source_names: vec![format!("{spread}.{field}")],
4293 declares_new_binding: false,
4294 value_kind: None,
4295 });
4296 push_python_source_name(known_fields.entry(target.clone()).or_default(), field);
4297 }
4298 }
4299 }
4300 }
4301 }
4302 rewritten.push(event);
4303 rewritten.extend(synthetic);
4304 }
4305 *events = rewritten;
4306}
4307
4308fn collect_python_assignment_projected_reads(
4316 tree: &Tree,
4317 file: FileId,
4318 src: &[u8],
4319) -> Vec<(Span, Vec<String>)> {
4320 fn collect(node: Node<'_>, src: &[u8], out: &mut Vec<String>) {
4321 if node.kind() == "subscript" {
4322 if let Some(place) = python_exact_expression_place(node, src) {
4323 push_python_source_name(out, place);
4324 return;
4325 }
4326 }
4327 if node.kind() == "call" {
4328 let selected_field = (|| {
4329 let function = node.child_by_field_name("function")?;
4330 let (receiver, method) = python_attribute_parts(function, src)?;
4331 if method != "get" {
4332 return None;
4333 }
4334 let arguments = node.child_by_field_name("arguments")?;
4335 let first = python_argument_nodes(arguments).into_iter().next()?;
4336 let field = python_static_string(first, src)?;
4337 let base = python_exact_expression_place(receiver, src)?;
4338 Some(format!("{base}.{field}"))
4339 })();
4340 if let Some(place) = selected_field {
4341 push_python_source_name(out, place);
4342 return;
4343 }
4344 }
4345
4346 let mut cursor = node.walk();
4347 for child in node.named_children(&mut cursor) {
4348 collect(child, src, out);
4349 }
4350 }
4351
4352 let mut reads = Vec::new();
4353 for assignment in collect_kinds(tree, &["assignment", "named_expression"]) {
4354 let Some(value) = assignment.child_by_field_name("right") else {
4355 continue;
4356 };
4357 if matches!(value.kind(), "dictionary" | "list" | "set" | "tuple") {
4362 continue;
4363 }
4364 let mut projected = Vec::new();
4365 collect(value, src, &mut projected);
4366 if !projected.is_empty() {
4367 reads.push((span_of(file, &assignment), projected));
4368 }
4369 }
4370 reads.sort_by_key(|(span, _)| (span.start, span.end));
4371 reads
4372}
4373
4374fn python_value_source_names(text: &str) -> Vec<String> {
4375 let mut out = python_access_tokens(text);
4376 for field_read in python_static_subscript_field_reads(text) {
4377 push_python_source_name(&mut out, field_read);
4378 }
4379 for field_read in python_static_get_field_reads(text) {
4380 push_python_source_name(&mut out, field_read);
4381 }
4382 out
4383}
4384
4385fn python_static_subscript_field_reads(text: &str) -> Vec<String> {
4386 let mut out = Vec::new();
4387 let mut quote: Option<char> = None;
4388 let mut escaped = false;
4389 let mut depth = 0usize;
4390 for (idx, ch) in text.char_indices() {
4391 if let Some(q) = quote {
4392 if escaped {
4393 escaped = false;
4394 } else if ch == '\\' {
4395 escaped = true;
4396 } else if ch == q {
4397 quote = None;
4398 }
4399 continue;
4400 }
4401 if matches!(ch, '\'' | '"' | '`') {
4402 quote = Some(ch);
4403 continue;
4404 }
4405 match ch {
4406 '[' if depth == 0 => {
4407 let Some(receiver) = python_receiver_before_index(text, idx) else {
4408 depth = depth.saturating_add(1);
4409 continue;
4410 };
4411 let Some(end) = python_matching_bracket_end(text, idx + 1) else {
4412 depth = depth.saturating_add(1);
4413 continue;
4414 };
4415 if let Some(field) = python_static_dict_key(&text[idx + 1..end]) {
4416 push_python_source_name(&mut out, format!("{receiver}.{field}"));
4417 }
4418 depth = depth.saturating_add(1);
4419 }
4420 '(' | '[' | '{' => depth = depth.saturating_add(1),
4421 ')' | ']' | '}' => depth = depth.saturating_sub(1),
4422 _ => {}
4423 }
4424 }
4425 out
4426}
4427
4428fn python_static_get_field_reads(text: &str) -> Vec<String> {
4429 let mut out = Vec::new();
4430 let mut quote: Option<char> = None;
4431 let mut escaped = false;
4432 let mut depth = 0usize;
4433 for (idx, ch) in text.char_indices() {
4434 if let Some(q) = quote {
4435 if escaped {
4436 escaped = false;
4437 } else if ch == '\\' {
4438 escaped = true;
4439 } else if ch == q {
4440 quote = None;
4441 }
4442 continue;
4443 }
4444 if matches!(ch, '\'' | '"' | '`') {
4445 quote = Some(ch);
4446 continue;
4447 }
4448 match ch {
4449 '(' | '[' | '{' => depth = depth.saturating_add(1),
4450 ')' | ']' | '}' => depth = depth.saturating_sub(1),
4451 '.' if depth == 0 && text[idx..].starts_with(".get(") => {
4452 let Some(receiver) = python_receiver_before_dot_get(text, idx) else {
4453 continue;
4454 };
4455 let args_start = idx + ".get(".len();
4456 let Some(args_end) = python_matching_paren_end(text, args_start) else {
4457 continue;
4458 };
4459 let args = &text[args_start..args_end];
4460 let Some(first_arg) = python_split_top_level(args, ',').into_iter().next() else {
4461 continue;
4462 };
4463 if let Some(field) = python_static_dict_key(&first_arg) {
4464 push_python_source_name(&mut out, format!("{receiver}.{field}"));
4465 }
4466 }
4467 _ => {}
4468 }
4469 }
4470 out
4471}
4472
4473fn python_receiver_before_index(text: &str, index_idx: usize) -> Option<String> {
4474 let prefix = text.get(..index_idx)?;
4475 let mut start = index_idx;
4476 for (idx, ch) in prefix.char_indices().rev() {
4477 if ch == '.' || ch == '_' || ch.is_ascii_alphanumeric() {
4478 start = idx;
4479 continue;
4480 }
4481 break;
4482 }
4483 let receiver = text[start..index_idx].trim_matches('.');
4484 if receiver.is_empty() {
4485 None
4486 } else {
4487 Some(receiver.to_string())
4488 }
4489}
4490
4491fn python_receiver_before_dot_get(text: &str, dot_idx: usize) -> Option<String> {
4492 let prefix = text.get(..dot_idx)?;
4493 let mut start = dot_idx;
4494 for (idx, ch) in prefix.char_indices().rev() {
4495 if ch == '.' || ch == '_' || ch.is_ascii_alphanumeric() {
4496 start = idx;
4497 continue;
4498 }
4499 break;
4500 }
4501 let receiver = text[start..dot_idx].trim_matches('.');
4502 if receiver.is_empty() {
4503 None
4504 } else {
4505 Some(receiver.to_string())
4506 }
4507}
4508
4509fn python_matching_bracket_end(text: &str, args_start: usize) -> Option<usize> {
4510 python_matching_delimiter_end(text, args_start, '[', ']')
4511}
4512
4513fn python_matching_paren_end(text: &str, args_start: usize) -> Option<usize> {
4514 python_matching_delimiter_end(text, args_start, '(', ')')
4515}
4516
4517fn python_matching_delimiter_end(
4518 text: &str,
4519 args_start: usize,
4520 open_delimiter: char,
4521 close_delimiter: char,
4522) -> Option<usize> {
4523 let mut quote: Option<char> = None;
4524 let mut escaped = false;
4525 let mut depth = 1usize;
4526 for (idx, ch) in text.char_indices().skip_while(|(idx, _)| *idx < args_start) {
4527 if let Some(q) = quote {
4528 if escaped {
4529 escaped = false;
4530 } else if ch == '\\' {
4531 escaped = true;
4532 } else if ch == q {
4533 quote = None;
4534 }
4535 continue;
4536 }
4537 if matches!(ch, '\'' | '"' | '`') {
4538 quote = Some(ch);
4539 continue;
4540 }
4541 match ch {
4542 ch if ch == open_delimiter => depth = depth.saturating_add(1),
4543 ch if ch == close_delimiter => {
4544 depth = depth.saturating_sub(1);
4545 if depth == 0 {
4546 return Some(idx);
4547 }
4548 }
4549 _ => {}
4550 }
4551 }
4552 None
4553}
4554
4555fn python_dict_field_initializers(text: &str) -> Vec<(String, String)> {
4556 let mut out = Vec::new();
4557 for body in python_dict_bodies(text) {
4558 for part in python_split_top_level(&body, ',') {
4559 if part.trim_start().starts_with("**") {
4560 continue;
4561 }
4562 let Some((key, value)) = python_split_top_level_once(&part, ':') else {
4563 continue;
4564 };
4565 let Some(field) = python_static_dict_key(&key) else {
4566 continue;
4567 };
4568 out.push((field, value.trim().to_string()));
4569 }
4570 }
4571 out
4572}
4573
4574fn python_dict_spreads(text: &str) -> Vec<String> {
4575 let mut out = Vec::new();
4576 for body in python_dict_bodies(text) {
4577 for part in python_split_top_level(&body, ',') {
4578 let Some(rest) = part.trim_start().strip_prefix("**") else {
4579 continue;
4580 };
4581 for token in python_access_tokens(rest) {
4582 push_python_source_name(&mut out, token);
4583 }
4584 }
4585 }
4586 out
4587}
4588
4589fn python_dict_bodies(text: &str) -> Vec<String> {
4590 let mut out = Vec::new();
4591 let mut stack = Vec::new();
4592 let mut quote: Option<char> = None;
4593 let mut escaped = false;
4594 for (idx, ch) in text.char_indices() {
4595 if let Some(q) = quote {
4596 if escaped {
4597 escaped = false;
4598 } else if ch == '\\' {
4599 escaped = true;
4600 } else if ch == q {
4601 quote = None;
4602 }
4603 continue;
4604 }
4605 if matches!(ch, '\'' | '"' | '`') {
4606 quote = Some(ch);
4607 continue;
4608 }
4609 match ch {
4610 '{' => stack.push(idx),
4611 '}' => {
4612 if let Some(start) = stack.pop() {
4613 if start < idx {
4614 out.push(text[start + 1..idx].to_string());
4615 }
4616 }
4617 }
4618 _ => {}
4619 }
4620 }
4621 out
4622}
4623
4624fn python_split_top_level(text: &str, delimiter: char) -> Vec<String> {
4625 let mut out = Vec::new();
4626 let mut quote: Option<char> = None;
4627 let mut escaped = false;
4628 let mut depth = 0usize;
4629 let mut start = 0usize;
4630 for (idx, ch) in text.char_indices() {
4631 if let Some(q) = quote {
4632 if escaped {
4633 escaped = false;
4634 } else if ch == '\\' {
4635 escaped = true;
4636 } else if ch == q {
4637 quote = None;
4638 }
4639 continue;
4640 }
4641 if matches!(ch, '\'' | '"' | '`') {
4642 quote = Some(ch);
4643 continue;
4644 }
4645 match ch {
4646 '(' | '[' | '{' => depth = depth.saturating_add(1),
4647 ')' | ']' | '}' => depth = depth.saturating_sub(1),
4648 ch if ch == delimiter && depth == 0 => {
4649 let part = text[start..idx].trim();
4650 if !part.is_empty() {
4651 out.push(part.to_string());
4652 }
4653 start = idx + ch.len_utf8();
4654 }
4655 _ => {}
4656 }
4657 }
4658 let part = text[start..].trim();
4659 if !part.is_empty() {
4660 out.push(part.to_string());
4661 }
4662 out
4663}
4664
4665fn python_split_top_level_once(text: &str, delimiter: char) -> Option<(String, String)> {
4666 let mut quote: Option<char> = None;
4667 let mut escaped = false;
4668 let mut depth = 0usize;
4669 for (idx, ch) in text.char_indices() {
4670 if let Some(q) = quote {
4671 if escaped {
4672 escaped = false;
4673 } else if ch == '\\' {
4674 escaped = true;
4675 } else if ch == q {
4676 quote = None;
4677 }
4678 continue;
4679 }
4680 if matches!(ch, '\'' | '"' | '`') {
4681 quote = Some(ch);
4682 continue;
4683 }
4684 match ch {
4685 '(' | '[' | '{' => depth = depth.saturating_add(1),
4686 ')' | ']' | '}' => depth = depth.saturating_sub(1),
4687 ch if ch == delimiter && depth == 0 => {
4688 return Some((text[..idx].to_string(), text[idx + ch.len_utf8()..].to_string()));
4689 }
4690 _ => {}
4691 }
4692 }
4693 None
4694}
4695
4696fn python_static_dict_key(text: &str) -> Option<String> {
4697 let key = text
4698 .trim()
4699 .strip_prefix('"')
4700 .and_then(|part| part.strip_suffix('"'))
4701 .or_else(|| {
4702 text.trim()
4703 .strip_prefix('\'')
4704 .and_then(|part| part.strip_suffix('\''))
4705 })?
4706 .trim();
4707 if key.is_empty()
4708 || !key
4709 .chars()
4710 .next()
4711 .is_some_and(|ch| ch == '_' || ch.is_ascii_alphabetic())
4712 || !key.chars().all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
4713 {
4714 return None;
4715 }
4716 Some(key.to_string())
4717}
4718
4719fn collect_python_parameter_aliases(node: Node<'_>, src: &[u8], out: &mut Vec<TypeAliasBinding>) {
4720 let mut cursor = node.walk();
4721 for child in node.named_children(&mut cursor) {
4722 match child.kind() {
4723 "typed_parameter" | "typed_default_parameter" => {
4724 python_typed_parameter_alias(child, src, out);
4725 }
4726 _ => collect_python_parameter_aliases(child, src, out),
4729 }
4730 }
4731}
4732
4733fn python_typed_parameter_alias(node: Node<'_>, src: &[u8], out: &mut Vec<TypeAliasBinding>) {
4734 let Some(name_node) = first_named_child_of_kind(node, &["identifier"]) else {
4735 return;
4736 };
4737 let name = node_text(&name_node, src).trim().to_string();
4738 if name.is_empty() {
4739 return;
4740 }
4741 let type_node = node.child_by_field_name("type");
4744 if let Some(t) = type_node {
4745 if let Some(canonical) = canonical_python_type_from_node(t, src) {
4746 push_python_type_alias(out, &name, &canonical);
4747 }
4748 }
4749}
4750
4751fn first_named_child_of_kind<'a>(node: Node<'a>, kinds: &[&str]) -> Option<Node<'a>> {
4752 let count = node.named_child_count();
4753 for i in 0..count {
4754 let idx = u32::try_from(i).ok()?;
4755 let child = node.named_child(idx)?;
4756 if kinds.contains(&child.kind()) {
4757 return Some(child);
4758 }
4759 }
4760 None
4761}
4762
4763fn canonical_python_type_from_node(node: Node<'_>, src: &[u8]) -> Option<String> {
4768 match node.kind() {
4769 "type" | "parenthesized_expression" | "parenthesized_list_splat" => {
4770 let mut cursor = node.walk();
4771 let canonical = node
4772 .named_children(&mut cursor)
4773 .find_map(|child| canonical_python_type_from_node(child, src));
4774 canonical
4775 }
4776 "generic_type" => {
4777 let base = node.named_child(0)?;
4778 let base_name = canonical_python_type_name(node_text(&base, src))?;
4779 if matches!(
4780 base_name.as_str(),
4781 "Annotated" | "Optional" | "ClassVar" | "Final" | "Required" | "NotRequired"
4782 ) {
4783 let parameters = node.named_child(1)?;
4784 let mut cursor = parameters.walk();
4785 return parameters
4786 .named_children(&mut cursor)
4787 .find_map(|child| canonical_python_type_from_node(child, src));
4788 }
4789 Some(base_name)
4790 }
4791 "union_type" | "binary_operator" => {
4792 let mut cursor = node.walk();
4793 let canonical = node.named_children(&mut cursor).find_map(|child| {
4794 let candidate = canonical_python_type_from_node(child, src)?;
4795 (!matches!(candidate.as_str(), "None" | "NoneType")).then_some(candidate)
4796 });
4797 canonical
4798 }
4799 _ => canonical_python_type_name(node_text(&node, src)),
4800 }
4801}
4802
4803fn canonical_python_type_name(raw: &str) -> Option<String> {
4804 let trimmed = raw.trim().split('|').next().unwrap_or(raw).trim();
4805 let head = trimmed.split('[').next().unwrap_or(trimmed).trim();
4806 let bare = head.rsplit('.').next().unwrap_or(head).trim();
4807 if bare.is_empty() {
4808 return None;
4809 }
4810 Some(bare.to_string())
4811}
4812
4813fn push_python_type_alias(out: &mut Vec<TypeAliasBinding>, name: &str, type_name: &str) {
4814 if name.is_empty() || type_name.is_empty() || name == type_name {
4815 return;
4816 }
4817 out.push(TypeAliasBinding {
4818 name: name.to_string(),
4819 type_name: type_name.to_string(),
4820 });
4821}
4822
4823fn dedup_python_type_aliases(out: &mut Vec<TypeAliasBinding>) {
4824 let mut seen = std::collections::HashSet::new();
4825 out.retain(|alias| seen.insert((alias.name.clone(), alias.type_name.clone())));
4826}
4827
4828fn collect_python_class_bases(
4832 tree: &Tree,
4833 file: FileId,
4834 src: &[u8],
4835) -> Vec<(bonsai_common::Span, Vec<String>)> {
4836 let mut out = Vec::new();
4837 for class_node in collect_kinds(tree, &["class_definition"]) {
4838 let Some(superclasses) = class_node.child_by_field_name("superclasses") else {
4839 continue;
4840 };
4841 let mut bases: Vec<String> = Vec::new();
4842 let count = superclasses.named_child_count();
4843 for i in 0..count {
4844 let Some(idx) = u32::try_from(i).ok() else {
4845 continue;
4846 };
4847 let Some(child) = superclasses.named_child(idx) else {
4848 continue;
4849 };
4850 if child.kind() == "keyword_argument" {
4852 continue;
4853 }
4854 let raw = node_text(&child, src);
4855 if let Some(canonical) = canonical_python_type_name(raw) {
4856 if !bases.iter().any(|b| b == &canonical) {
4857 bases.push(canonical);
4858 }
4859 }
4860 }
4861 if !bases.is_empty() {
4862 out.push((span_of(file, &class_node), bases));
4863 }
4864 }
4865 out
4866}
4867
4868fn parse_imports(tree: &Tree, src: &[u8], file: FileId) -> Vec<ImportSpec> {
4869 let mut out = Vec::new();
4870 for import_node in collect_kinds(tree, &["import_statement"]) {
4874 let mut cursor = import_node.walk();
4875 for child in import_node.named_children(&mut cursor) {
4876 let (module_node, alias_text) = if child.kind() == "aliased_import" {
4878 let module_field = child.child_by_field_name("name");
4879 let alias_field = child
4880 .child_by_field_name("alias")
4881 .map(|alias_node| node_text(&alias_node, src).to_string());
4882 (module_field, alias_field)
4883 } else if child.kind() == "dotted_name" {
4884 (Some(child), None)
4885 } else {
4886 continue;
4887 };
4888 let Some(module_name_node) = module_node else {
4889 continue;
4890 };
4891 let module_name = node_text(&module_name_node, src).trim().to_string();
4892 if module_name.is_empty() {
4893 continue;
4894 }
4895 let alias = alias_text.or_else(|| {
4907 module_name
4908 .split('.')
4909 .next()
4910 .map(str::trim)
4911 .filter(|leaf| !leaf.is_empty())
4912 .map(str::to_string)
4913 });
4914 out.push(ImportSpec {
4915 span: span_of(file, &import_node),
4916 module: module_name,
4917 alias,
4918 is_wildcard: false,
4919 original_name: None,
4920 scope: ImportScope::Module,
4921 });
4922 }
4923 }
4924 for from_import_node in collect_kinds(tree, &["import_from_statement"]) {
4927 let Some(module_node) = from_import_node.child_by_field_name("module_name") else {
4928 continue;
4929 };
4930 let module_name = node_text(&module_node, src).trim().to_string();
4931 let mut cursor = from_import_node.walk();
4932 let mut imported_symbols: Vec<(Option<String>, Option<String>)> = Vec::new();
4934 let mut is_wildcard = false;
4935 for child in from_import_node.named_children(&mut cursor) {
4936 if child.id() == module_node.id() {
4938 continue;
4939 }
4940 if child.kind() == "wildcard_import" {
4941 is_wildcard = true;
4942 continue;
4943 }
4944 if child.kind() == "aliased_import" {
4945 let original_name = child
4946 .child_by_field_name("name")
4947 .map(|name_node| node_text(&name_node, src).to_string());
4948 let alias_text = child
4949 .child_by_field_name("alias")
4950 .map(|alias_node| node_text(&alias_node, src).to_string());
4951 imported_symbols.push((original_name, alias_text));
4952 } else if child.kind() == "dotted_name" {
4953 imported_symbols.push((Some(node_text(&child, src).to_string()), None));
4954 }
4955 }
4956 if imported_symbols.is_empty() {
4958 out.push(ImportSpec {
4959 span: span_of(file, &from_import_node),
4960 module: module_name,
4961 alias: None,
4962 is_wildcard,
4963 original_name: None,
4964 scope: ImportScope::Module,
4965 });
4966 } else {
4967 for (original_name, alias_text) in imported_symbols {
4970 out.push(ImportSpec {
4971 span: span_of(file, &from_import_node),
4972 module: module_name.clone(),
4973 alias: alias_text,
4974 is_wildcard,
4975 original_name,
4976 scope: ImportScope::Module,
4977 });
4978 }
4979 }
4980 }
4981 out
4982}
4983
4984fn rewrite_python_constant_reflection(events: &mut [bonsai_lang_api::FlowEvent]) {
5001 use bonsai_lang_api::FlowEvent;
5002 for event in events {
5003 match event {
5004 FlowEvent::Call {
5008 name, receiver, args, ..
5009 } if matches!(name.as_str(), "getattr" | "setattr" | "hasattr") && args.len() >= 2 => {
5010 let receiver_arg = &args[0];
5011 let attr_arg = &args[1];
5012 if !is_python_string_literal(&attr_arg.value_text) {
5016 continue;
5017 }
5018 let attr_name = strip_python_string_quotes(&attr_arg.value_text);
5019 if attr_name.is_empty() {
5020 continue;
5021 }
5022 let receiver_text = receiver_arg.value_text.trim();
5023 if receiver_text.is_empty() {
5024 continue;
5025 }
5026 *name = format!("{receiver_text}.{attr_name}");
5030 *receiver = Some(receiver_text.to_string());
5031 }
5032 FlowEvent::Branch {
5034 then_events,
5035 else_events,
5036 ..
5037 } => {
5038 rewrite_python_constant_reflection(then_events);
5039 rewrite_python_constant_reflection(else_events);
5040 }
5041 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
5042 rewrite_python_constant_reflection(body);
5043 }
5044 FlowEvent::Try {
5045 body,
5046 catch_events,
5047 finally_events,
5048 ..
5049 } => {
5050 rewrite_python_constant_reflection(body);
5051 rewrite_python_constant_reflection(catch_events);
5052 rewrite_python_constant_reflection(finally_events);
5053 }
5054 _ => {}
5055 }
5056 }
5057}
5058
5059fn is_python_string_literal(text: &str) -> bool {
5063 let trimmed = text.trim();
5064 let starts_with_quote = trimmed.starts_with('"') || trimmed.starts_with('\'');
5065 let ends_with_quote = trimmed.ends_with('"') || trimmed.ends_with('\'');
5066 starts_with_quote && ends_with_quote && trimmed.len() >= 2
5067}
5068
5069fn strip_python_string_quotes(text: &str) -> String {
5073 text.trim()
5074 .trim_start_matches(['"', '\''])
5075 .trim_end_matches(['"', '\''])
5076 .to_string()
5077}
5078
5079#[allow(clippy::case_sensitive_file_extension_comparisons)] fn rewrite_python_generator_send(events: &mut [bonsai_lang_api::FlowEvent]) {
5098 use bonsai_lang_api::{CallKind, FlowEvent};
5099 use std::collections::HashMap;
5100 let mut gen_factories: HashMap<String, String> = HashMap::new();
5101 for event in events.iter_mut() {
5102 match event {
5103 FlowEvent::Assign {
5104 target, source_call, ..
5105 } => {
5106 if let Some(call) = source_call {
5107 if !call.contains('.') && !call.is_empty() {
5112 gen_factories.insert(target.clone(), call.clone());
5113 }
5114 }
5115 }
5116 FlowEvent::Call {
5117 name,
5118 receiver,
5119 args,
5120 call_kind,
5121 ..
5122 } => {
5123 let Some(rcv) = receiver.clone() else {
5124 continue;
5125 };
5126 if !name.ends_with(".send") {
5127 continue;
5128 }
5129 let Some(factory) = gen_factories.get(&rcv) else {
5130 continue;
5131 };
5132 name.clone_from(factory);
5133 *receiver = None;
5134 *call_kind = CallKind::Function;
5135 let _ = args; }
5137 FlowEvent::Branch {
5138 then_events,
5139 else_events,
5140 ..
5141 } => {
5142 rewrite_python_generator_send(then_events);
5143 rewrite_python_generator_send(else_events);
5144 }
5145 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
5146 rewrite_python_generator_send(body);
5147 }
5148 FlowEvent::Try {
5149 body,
5150 catch_events,
5151 finally_events,
5152 ..
5153 } => {
5154 rewrite_python_generator_send(body);
5155 rewrite_python_generator_send(catch_events);
5156 rewrite_python_generator_send(finally_events);
5157 }
5158 _ => {}
5159 }
5160 }
5161}
5162
5163#[cfg(test)]
5164mod pattern_tests {
5165 use super::*;
5166
5167 #[test]
5168 fn match_bindings_follow_only_capture_positions_with_exact_projections() {
5169 let src = br#"match subject:
5170 case {"value": value, "nested": {"item": item}, **rest} if limit:
5171 pass
5172 case Point(x=px, y=py) as point:
5173 pass
5174"#;
5175 let mut parser = tree_sitter::Parser::new();
5176 let language = language_from_pack(PACK_NAME).expect("Python grammar");
5177 parser.set_language(&language).expect("Python grammar");
5178 let tree = parser.parse(src, None).expect("Python parse");
5179 let match_node = tree
5180 .root_node()
5181 .named_child(0)
5182 .expect("top-level match statement");
5183 let sites = python_pattern_bindings(match_node, src);
5184 let mut facts = sites
5185 .iter()
5186 .map(|site| {
5187 (
5188 node_text(&site.target, src).trim().to_string(),
5189 site.projection.clone(),
5190 )
5191 })
5192 .collect::<Vec<_>>();
5193 facts.sort_by(|left, right| left.0.cmp(&right.0));
5194
5195 let mut expected = vec![
5196 (
5197 "item".to_string(),
5198 vec![
5199 PatternSourceProjection::Field("nested".to_string()),
5200 PatternSourceProjection::Field("item".to_string()),
5201 ],
5202 ),
5203 ("point".to_string(), Vec::<PatternSourceProjection>::new()),
5204 (
5205 "px".to_string(),
5206 vec![PatternSourceProjection::Field("x".to_string())],
5207 ),
5208 (
5209 "py".to_string(),
5210 vec![PatternSourceProjection::Field("y".to_string())],
5211 ),
5212 ("rest".to_string(), vec![PatternSourceProjection::Descendants]),
5213 (
5214 "value".to_string(),
5215 vec![PatternSourceProjection::Field("value".to_string())],
5216 ),
5217 ];
5218 expected.sort_by(|left, right| left.0.cmp(&right.0));
5219 assert_eq!(facts, expected);
5220 }
5221}