1use brokk_bifrost_core::analyzer::common::node_source_text_trimmed as node_text;
2use brokk_bifrost_core::analyzer::tree_walk::{named_children_iter, push_bounded_nodes};
3use tree_sitter::Node;
4
5use brokk_bifrost_core::hash::{HashMap, HashSet};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum PythonLexicalNameResolution {
9 Unbound,
10 Local,
11 Nonlocal,
12 Global,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum PythonDirectScopeBindingKind {
17 ClassDeclaration,
18 FunctionDeclaration,
19 Import,
20 Other,
21}
22
23pub fn python_comprehension_binds_name_at(name: &str, reference: Node<'_>, source: &str) -> bool {
31 python_comprehension_binds_name_at_bounded(name, reference, source, || true)
32 .expect("unbounded comprehension binding lookup completes")
33}
34
35pub fn python_comprehension_binds_name_at_bounded(
38 name: &str,
39 reference: Node<'_>,
40 source: &str,
41 mut scope_step: impl FnMut() -> bool,
42) -> Option<bool> {
43 if !scope_step() {
44 return None;
45 }
46 let mut current = reference;
47 while let Some(parent) = current.parent() {
48 if !scope_step() {
49 return None;
50 }
51 if is_comprehension(parent.kind()) {
52 let mut cursor = parent.walk();
53 let mut clauses = Vec::new();
54 for child in parent.named_children(&mut cursor) {
55 if !scope_step() {
56 return None;
57 }
58 if child.kind() == "for_in_clause" {
59 clauses.push(child);
60 }
61 }
62 let mut in_outer_iterable = false;
63 if let Some(clause) = clauses.first() {
64 let mut cursor = clause.walk();
65 for right in clause.children_by_field_name("right", &mut cursor) {
66 if !scope_step() {
67 return None;
68 }
69 if right.start_byte() <= reference.start_byte()
70 && reference.end_byte() <= right.end_byte()
71 {
72 in_outer_iterable = true;
73 break;
74 }
75 }
76 }
77 if in_outer_iterable {
78 current = parent;
79 continue;
80 }
81 for clause in clauses {
82 if !scope_step() {
83 return None;
84 }
85 let Some(target) = clause.child_by_field_name("left") else {
86 continue;
87 };
88 let mut bound = false;
89 collect_binding_targets(target, source, &mut scope_step, |candidate, _| {
90 bound |= candidate == name;
91 })?;
92 if bound {
93 return Some(true);
94 }
95 }
96 }
97 if matches!(
98 parent.kind(),
99 "function_definition" | "lambda" | "class_definition" | "module"
100 ) {
101 break;
102 }
103 current = parent;
104 }
105 Some(false)
106}
107
108pub fn python_type_parameter_binds_name_at(name: &str, reference: Node<'_>, source: &str) -> bool {
115 python_type_parameter_binds_name_at_bounded(name, reference, source, || true)
116 .expect("unbounded type parameter lookup completes")
117}
118
119pub fn python_type_parameter_binds_name_at_bounded(
122 name: &str,
123 reference: Node<'_>,
124 source: &str,
125 mut scope_step: impl FnMut() -> bool,
126) -> Option<bool> {
127 let mut current = reference;
128 let mut enclosing_functions = Vec::new();
129 while let Some(parent) = current.parent() {
130 if !scope_step() {
131 return None;
132 }
133 if parent.kind() == "function_definition" {
134 enclosing_functions.push(parent);
135 }
136 if matches!(
137 parent.kind(),
138 "class_definition" | "function_definition" | "type_alias_statement"
139 ) && let Some(parameters) = parent.child_by_field_name("type_parameters")
140 && !(parameters.start_byte() <= reference.start_byte()
141 && reference.end_byte() <= parameters.end_byte())
142 {
143 let mut cursor = parameters.walk();
144 for parameter in parameters.named_children(&mut cursor) {
145 if !scope_step() {
146 return None;
147 }
148 let binder = first_identifier_bounded(parameter, &mut scope_step)?;
149 if binder.is_some_and(|binder| node_text(binder, source) == name) {
150 return Some(
152 python_name_resolution_at_in_functions_bounded(
153 name,
154 reference,
155 source,
156 enclosing_functions,
157 &mut scope_step,
158 )? != PythonLexicalNameResolution::Global,
159 );
160 }
161 }
162 }
163 current = parent;
164 }
165 Some(false)
166}
167
168pub fn python_name_resolution_at(
172 name: &str,
173 reference: Node<'_>,
174 source: &str,
175) -> PythonLexicalNameResolution {
176 let mut functions = Vec::new();
177 let mut current = reference;
178 while let Some(parent) = current.parent() {
179 if parent.kind() == "function_definition" {
180 functions.push(parent);
181 }
182 current = parent;
183 }
184 python_name_resolution_at_in_functions(name, reference, source, functions)
185}
186
187fn python_name_resolution_at_in_functions(
188 name: &str,
189 reference: Node<'_>,
190 source: &str,
191 functions: Vec<Node<'_>>,
192) -> PythonLexicalNameResolution {
193 python_name_resolution_at_in_functions_bounded(name, reference, source, functions, || true)
194 .expect("unbounded function binding lookup completes")
195}
196
197fn python_name_resolution_at_in_functions_bounded(
198 name: &str,
199 reference: Node<'_>,
200 source: &str,
201 functions: Vec<Node<'_>>,
202 mut scope_step: impl FnMut() -> bool,
203) -> Option<PythonLexicalNameResolution> {
204 for function in functions {
205 if !scope_step() {
206 return None;
207 }
208 let mut parameter_names = Vec::new();
209 if let Some(parameters) = function.child_by_field_name("parameters") {
210 let mut cursor = parameters.walk();
211 for parameter in parameters.named_children(&mut cursor) {
212 if !scope_step() {
213 return None;
214 }
215 if let Some(name) = crate::syntax::python_parameter_name_bounded(
216 parameter,
217 source,
218 &mut scope_step,
219 )? {
220 parameter_names.push(name);
221 }
222 }
223 }
224 let inventory = PythonLexicalScopeInventory::collect_bounded(
225 function,
226 source,
227 parameter_names,
228 &mut scope_step,
229 )?;
230 let resolution = inventory.name_resolution_at_bounded(name, reference, &mut scope_step)?;
231 match resolution {
232 PythonLexicalNameResolution::Global
233 | PythonLexicalNameResolution::Local
234 | PythonLexicalNameResolution::Nonlocal => return Some(resolution),
235 PythonLexicalNameResolution::Unbound => {}
236 }
237 }
238 Some(PythonLexicalNameResolution::Unbound)
239}
240
241pub fn python_is_type_parameter_binder(node: Node<'_>) -> bool {
244 if node.kind() != "identifier" {
245 return false;
246 }
247 let mut current = node;
248 while let Some(parent) = current.parent() {
249 if matches!(
250 parent.kind(),
251 "class_definition" | "function_definition" | "type_alias_statement"
252 ) && let Some(parameters) = parent.child_by_field_name("type_parameters")
253 && parameters.start_byte() <= node.start_byte()
254 && node.end_byte() <= parameters.end_byte()
255 {
256 let mut cursor = parameters.walk();
257 return parameters.named_children(&mut cursor).any(|parameter| {
258 let binder = python_type_parameter_binder_node(parameter);
259 binder.is_some_and(|binder| binder.id() == node.id())
260 });
261 }
262 if matches!(
263 parent.kind(),
264 "module" | "class_definition" | "function_definition" | "type_alias_statement"
265 ) {
266 return false;
267 }
268 current = parent;
269 }
270 false
271}
272
273fn python_type_parameter_binder_node(parameter: Node<'_>) -> Option<Node<'_>> {
274 if parameter.kind() == "identifier" {
275 Some(parameter)
276 } else {
277 first_identifier_bounded(parameter, &mut || true).flatten()
278 }
279}
280
281#[derive(Clone, Copy, Debug)]
282pub struct PythonDirectScopeBinding<'tree> {
283 pub declaration: Node<'tree>,
284 pub kind: PythonDirectScopeBindingKind,
285}
286
287#[derive(Clone, Debug)]
288struct PythonLocalBinding<'tree> {
289 name: Box<str>,
290 declaration: Node<'tree>,
291}
292
293#[derive(Clone, Copy, Debug, PartialEq, Eq)]
294enum PythonLocalBindingKind {
295 FunctionOnly,
296 ClassOnly,
297 Other,
298}
299
300#[derive(Clone, Debug)]
301struct PythonComprehensionBinding {
302 name: Box<str>,
303 start_byte: usize,
304 end_byte: usize,
305 enclosing_iterable_ranges: Vec<(usize, usize)>,
306}
307
308pub struct PythonLexicalScopeInventory<'tree> {
315 locals: Vec<PythonLocalBinding<'tree>>,
316 bindings: PythonLexicalScopeBindings,
317}
318
319pub struct PythonLexicalScopeBindings {
322 parameters: HashSet<Box<str>>,
323 local_names: HashMap<Box<str>, PythonLocalBindingKind>,
324 binding_writes: HashSet<Box<str>>,
325 global_binding_events: HashSet<Box<str>>,
327 globals: HashSet<Box<str>>,
328 nonlocals: HashSet<Box<str>>,
329 comprehensions: Vec<PythonComprehensionBinding>,
330}
331
332impl PythonLexicalScopeBindings {
333 pub fn name_resolution_at(
334 &self,
335 name: &str,
336 reference: Node<'_>,
337 ) -> PythonLexicalNameResolution {
338 self.name_resolution_at_bounded(name, reference, || true)
339 .expect("unbounded lexical name lookup completes")
340 }
341
342 pub fn name_resolution_at_bounded(
345 &self,
346 name: &str,
347 reference: Node<'_>,
348 mut scope_step: impl FnMut() -> bool,
349 ) -> Option<PythonLexicalNameResolution> {
350 if !scope_step() {
351 return None;
352 }
353 if self.comprehension_binds_name_at_bounded(name, reference, &mut scope_step)? {
354 return Some(PythonLexicalNameResolution::Local);
355 }
356 Some(if self.nonlocals.contains(name) {
357 PythonLexicalNameResolution::Nonlocal
358 } else if self.globals.contains(name) {
359 PythonLexicalNameResolution::Global
360 } else if self.parameters.contains(name) || self.local_names.contains_key(name) {
361 PythonLexicalNameResolution::Local
362 } else {
363 PythonLexicalNameResolution::Unbound
364 })
365 }
366
367 fn comprehension_binds_name_at_bounded(
368 &self,
369 name: &str,
370 reference: Node<'_>,
371 mut scope_step: impl FnMut() -> bool,
372 ) -> Option<bool> {
373 let reference_byte = reference.start_byte();
374 for binding in &self.comprehensions {
375 if !scope_step() {
376 return None;
377 }
378 if binding.name.as_ref() == name
379 && binding.start_byte <= reference_byte
380 && reference_byte < binding.end_byte
381 {
382 let mut in_outer_iterable = false;
383 for (start, end) in &binding.enclosing_iterable_ranges {
384 if !scope_step() {
385 return None;
386 }
387 in_outer_iterable |= *start <= reference_byte && reference_byte < *end;
388 }
389 if !in_outer_iterable {
390 return Some(true);
391 }
392 }
393 }
394 Some(false)
395 }
396
397 pub fn has_global_binding_event(&self, name: &str) -> bool {
400 self.global_binding_events.contains(name)
401 }
402
403 pub fn resolves_to_local_function(&self, name: &str, reference: Node<'_>) -> bool {
404 self.resolves_to_local_function_bounded(name, reference, || true)
405 .expect("unbounded local function lookup completes")
406 }
407
408 pub fn resolves_to_local_function_bounded(
409 &self,
410 name: &str,
411 reference: Node<'_>,
412 mut scope_step: impl FnMut() -> bool,
413 ) -> Option<bool> {
414 if !scope_step() {
415 return None;
416 }
417 Some(
418 !self.parameters.contains(name)
419 && !self.comprehension_binds_name_at_bounded(name, reference, &mut scope_step)?
420 && self.local_names.get(name) == Some(&PythonLocalBindingKind::FunctionOnly),
421 )
422 }
423
424 pub fn has_runtime_callable_binding_at(&self, name: &str, reference: Node<'_>) -> bool {
427 self.has_runtime_callable_binding_at_bounded(name, reference, || true)
428 .expect("unbounded runtime binding lookup completes")
429 }
430
431 pub fn has_runtime_callable_binding_at_bounded(
432 &self,
433 name: &str,
434 reference: Node<'_>,
435 mut scope_step: impl FnMut() -> bool,
436 ) -> Option<bool> {
437 if !scope_step() {
438 return None;
439 }
440 Some(
441 self.parameters.contains(name)
442 || self.binding_writes.contains(name)
443 || self.comprehension_binds_name_at_bounded(name, reference, &mut scope_step)?,
444 )
445 }
446}
447
448#[derive(Clone, Copy)]
449struct ScanFrame<'tree> {
450 node: Node<'tree>,
451 in_comprehension: bool,
452}
453
454pub struct PythonClassReferenceScope<'tree> {
459 scope: Node<'tree>,
460 pending_name: Option<Node<'tree>>,
461}
462
463impl<'tree> PythonClassReferenceScope<'tree> {
464 pub fn new_bounded(
465 scope: Node<'tree>,
466 reference: Node<'tree>,
467 mut scope_step: impl FnMut() -> bool,
468 ) -> Option<Self> {
469 assert_eq!(scope.kind(), "class_definition");
470 let mut current = reference;
471 let mut pending_name = None;
472 while current != scope {
473 if !scope_step() {
474 return None;
475 }
476 if current.kind() == "class_definition"
477 && let Some(superclasses) = current.child_by_field_name("superclasses")
478 {
479 for expression in named_children_iter(superclasses) {
480 if !scope_step() {
481 return None;
482 }
483 if expression.start_byte() <= reference.start_byte()
484 && reference.end_byte() <= expression.end_byte()
485 && let Some(path) = crate::syntax::python_static_value_path_bounded(
486 expression,
487 &mut scope_step,
488 )?
489 && (path.first().copied() == Some(reference) || expression == reference)
490 {
491 pending_name = Some(current.child_by_field_name("name")?);
492 }
493 }
494 }
495 current = current.parent()?;
496 }
497 Some(Self {
498 scope,
499 pending_name,
500 })
501 }
502
503 fn includes_binding(&self, declaration: Node<'tree>) -> bool {
504 Some(declaration) != self.pending_name
505 }
506
507 pub fn collect_bounded(
508 &self,
509 source: &str,
510 scope_step: impl FnMut() -> bool,
511 ) -> Option<PythonLexicalScopeInventory<'tree>> {
512 PythonLexicalScopeInventory::collect_with_declaration_filter_bounded(
513 self.scope,
514 source,
515 std::iter::empty::<String>(),
516 |declaration| self.includes_binding(declaration),
517 scope_step,
518 )
519 }
520
521 pub fn binds_name_bounded(
522 &self,
523 name: &str,
524 source: &str,
525 scope_step: impl FnMut() -> bool,
526 ) -> Option<bool> {
527 python_scope_binds_name_with_filter_bounded(
528 self.scope,
529 name,
530 source,
531 |declaration| self.includes_binding(declaration),
532 scope_step,
533 )
534 }
535}
536
537impl<'tree> PythonLexicalScopeInventory<'tree> {
538 pub fn collect_bounded(
545 callable: Node<'tree>,
546 source: &str,
547 parameter_names: impl IntoIterator<Item = String>,
548 scope_step: impl FnMut() -> bool,
549 ) -> Option<Self> {
550 Self::collect_with_declaration_filter_bounded(
551 callable,
552 source,
553 parameter_names,
554 |_| true,
555 scope_step,
556 )
557 }
558
559 fn collect_with_declaration_filter_bounded(
560 callable: Node<'tree>,
561 source: &str,
562 parameter_names: impl IntoIterator<Item = String>,
563 include_declaration: impl Fn(Node<'tree>) -> bool,
564 mut scope_step: impl FnMut() -> bool,
565 ) -> Option<Self> {
566 let mut inventory = Self {
567 locals: Vec::new(),
568 bindings: PythonLexicalScopeBindings {
569 parameters: parameter_names.into_iter().map(Box::<str>::from).collect(),
570 local_names: HashMap::default(),
571 binding_writes: HashSet::default(),
572 global_binding_events: HashSet::default(),
573 globals: HashSet::default(),
574 nonlocals: HashSet::default(),
575 comprehensions: Vec::new(),
576 },
577 };
578 let body = if callable.kind() == "module" {
579 callable
580 } else if let Some(body) = callable.child_by_field_name("body") {
581 body
582 } else {
583 return Some(inventory);
584 };
585 let mut stack = vec![ScanFrame {
586 node: body,
587 in_comprehension: false,
588 }];
589
590 while let Some(frame) = stack.pop() {
591 if !scope_step() {
592 return None;
593 }
594 let node = frame.node;
595 let nested_scope = node != body
596 && matches!(
597 node.kind(),
598 "function_definition" | "lambda" | "class_definition"
599 );
600 if nested_scope {
601 if matches!(node.kind(), "function_definition" | "class_definition")
602 && let Some(name) = node.child_by_field_name("name")
603 && include_declaration(name)
604 {
605 inventory.record_local_node(name, source);
606 }
607 push_nested_scope_header_children(
612 &mut stack,
613 node,
614 frame.in_comprehension,
615 &mut scope_step,
616 )?;
617 continue;
618 }
619
620 match node.kind() {
621 "global_statement" => {
622 if callable.kind() == "module" {
623 continue;
624 }
625 collect_direct_identifier_names(node, source, &mut scope_step, |name| {
626 inventory.bindings.globals.insert(name.into());
627 })?;
628 continue;
629 }
630 "nonlocal_statement" => {
631 collect_direct_identifier_names(node, source, &mut scope_step, |name| {
632 inventory.bindings.nonlocals.insert(name.into());
633 })?;
634 continue;
635 }
636 "import_statement" | "import_from_statement" => {
637 collect_import_bindings(node, &mut scope_step, |binding| {
638 inventory.record_local_node(binding, source)
639 })?;
640 continue;
641 }
642 "assignment" | "augmented_assignment" => {
643 if let Some(target) = node.child_by_field_name("left") {
644 collect_binding_targets(
645 target,
646 source,
647 &mut scope_step,
648 |name, declaration| {
649 inventory.record_binding_write(name, declaration);
650 },
651 )?;
652 }
653 }
654 "type_alias_statement" => {
655 if let Some(target) = node.child_by_field_name("left")
656 && let Some(binding) = first_identifier_bounded(target, &mut scope_step)?
657 {
658 inventory.record_local_node(binding, source);
659 }
660 }
661 "named_expression" => {
662 if let Some(target) = node.child_by_field_name("name") {
665 collect_binding_targets(
666 target,
667 source,
668 &mut scope_step,
669 |name, declaration| {
670 inventory.record_binding_write(name, declaration);
671 },
672 )?;
673 }
674 }
675 "for_statement" => {
676 if let Some(target) = node.child_by_field_name("left") {
677 collect_binding_targets(
678 target,
679 source,
680 &mut scope_step,
681 |name, declaration| {
682 inventory.record_binding_write(name, declaration);
683 },
684 )?;
685 }
686 }
687 "for_in_clause" => {
688 }
691 "delete_statement" => {
692 for target in named_children_bounded(node, &mut scope_step)? {
693 collect_binding_targets(
694 target,
695 source,
696 &mut scope_step,
697 |name, declaration| {
698 inventory.record_binding_write(name, declaration);
699 },
700 )?;
701 }
702 continue;
703 }
704 "as_pattern" => {
705 if let Some(alias) = node.child_by_field_name("alias") {
706 collect_binding_targets(
707 alias,
708 source,
709 &mut scope_step,
710 |name, declaration| {
711 inventory.record_binding_write(name, declaration);
712 },
713 )?;
714 push_named_children_except(
715 &mut stack,
716 node,
717 alias,
718 frame.in_comprehension,
719 &mut scope_step,
720 )?;
721 continue;
722 }
723 }
724 "except_clause" => {
725 if let Some(alias) = node.child_by_field_name("alias") {
729 collect_binding_targets(
730 alias,
731 source,
732 &mut scope_step,
733 |name, declaration| {
734 inventory.record_binding_write(name, declaration);
735 },
736 )?;
737 }
738 }
739 "case_clause" => {
740 let children = named_children_bounded(node, &mut scope_step)?;
741 for child in children.iter().copied() {
742 if child.kind() == "case_pattern" {
743 collect_match_pattern_bindings(
744 child,
745 source,
746 &mut scope_step,
747 |name, declaration| {
748 inventory.record_binding_write(name, declaration);
749 },
750 )?;
751 }
752 }
753 for child in children.into_iter().rev() {
754 if child.kind() != "case_pattern" {
755 stack.push(ScanFrame {
756 node: child,
757 in_comprehension: frame.in_comprehension,
758 });
759 }
760 }
761 continue;
762 }
763 kind if is_comprehension(kind) => {
764 let range = (node.start_byte(), node.end_byte());
765 let children = named_children_bounded(node, &mut scope_step)?;
766 let enclosing_iterable_ranges = if let Some(first_clause) = children
767 .iter()
768 .copied()
769 .find(|child| child.kind() == "for_in_clause")
770 {
771 children_by_field_name_bounded(first_clause, "right", &mut scope_step)?
772 .into_iter()
773 .map(|iterable| (iterable.start_byte(), iterable.end_byte()))
774 .collect()
775 } else {
776 Vec::new()
777 };
778 for clause in children
779 .iter()
780 .copied()
781 .filter(|child| child.kind() == "for_in_clause")
782 {
783 if let Some(target) = clause.child_by_field_name("left") {
784 collect_binding_targets(target, source, &mut scope_step, |name, _| {
785 inventory.bindings.comprehensions.push(
786 PythonComprehensionBinding {
787 name: name.into(),
788 start_byte: range.0,
789 end_byte: range.1,
790 enclosing_iterable_ranges: enclosing_iterable_ranges
791 .clone(),
792 },
793 );
794 })?;
795 }
796 }
797 for child in children.into_iter().rev() {
798 stack.push(ScanFrame {
799 node: child,
800 in_comprehension: true,
801 });
802 }
803 continue;
804 }
805 _ => {}
806 }
807
808 push_named_children(&mut stack, node, frame.in_comprehension, &mut scope_step)?;
809 }
810
811 for name in inventory.bindings.local_names.keys() {
815 if !scope_step() {
816 return None;
817 }
818 if inventory.bindings.globals.contains(name.as_ref()) {
819 inventory
820 .bindings
821 .global_binding_events
822 .insert(name.clone());
823 }
824 }
825
826 inventory.locals.retain(|binding| {
829 !inventory.bindings.globals.contains(binding.name.as_ref())
830 && !inventory.bindings.nonlocals.contains(binding.name.as_ref())
831 });
832 inventory.bindings.local_names.retain(|name, _| {
833 !inventory.bindings.globals.contains(name.as_ref())
834 && !inventory.bindings.nonlocals.contains(name.as_ref())
835 });
836 Some(inventory)
837 }
838
839 pub fn into_bindings(self) -> PythonLexicalScopeBindings {
840 self.bindings
841 }
842
843 pub fn name_resolution_at(
844 &self,
845 name: &str,
846 reference: Node<'_>,
847 ) -> PythonLexicalNameResolution {
848 self.bindings.name_resolution_at(name, reference)
849 }
850
851 pub fn name_resolution_at_bounded(
852 &self,
853 name: &str,
854 reference: Node<'_>,
855 scope_step: impl FnMut() -> bool,
856 ) -> Option<PythonLexicalNameResolution> {
857 self.bindings
858 .name_resolution_at_bounded(name, reference, scope_step)
859 }
860
861 pub fn resolves_to_local_function(&self, name: &str, reference: Node<'_>) -> bool {
862 self.bindings.resolves_to_local_function(name, reference)
863 }
864
865 pub fn has_runtime_callable_binding_at(&self, name: &str, reference: Node<'_>) -> bool {
866 self.bindings
867 .has_runtime_callable_binding_at(name, reference)
868 }
869
870 pub fn has_runtime_callable_binding_at_bounded(
871 &self,
872 name: &str,
873 reference: Node<'_>,
874 scope_step: impl FnMut() -> bool,
875 ) -> Option<bool> {
876 self.bindings
877 .has_runtime_callable_binding_at_bounded(name, reference, scope_step)
878 }
879
880 pub fn local_function_declaration(
881 &self,
882 name: &str,
883 reference: Node<'_>,
884 ) -> Option<Node<'tree>> {
885 self.local_function_declaration_bounded(name, reference, || true)
886 }
887
888 pub fn local_function_declaration_bounded(
889 &self,
890 name: &str,
891 reference: Node<'_>,
892 mut scope_step: impl FnMut() -> bool,
893 ) -> Option<Node<'tree>> {
894 if !self
895 .bindings
896 .resolves_to_local_function_bounded(name, reference, &mut scope_step)?
897 {
898 return None;
899 }
900 for binding in &self.locals {
901 if !scope_step() {
902 return None;
903 }
904 if binding.name.as_ref() == name {
905 return binding
906 .declaration
907 .parent()
908 .filter(|declaration| declaration.kind() == "function_definition");
909 }
910 }
911 None
912 }
913
914 pub fn local_class_declaration_bounded(
917 &self,
918 name: &str,
919 reference: Node<'_>,
920 mut scope_step: impl FnMut() -> bool,
921 ) -> Option<Node<'tree>> {
922 if self.name_resolution_at_bounded(name, reference, &mut scope_step)?
923 != PythonLexicalNameResolution::Local
924 || self.bindings.has_runtime_callable_binding_at_bounded(
925 name,
926 reference,
927 &mut scope_step,
928 )?
929 || self.bindings.local_names.get(name) != Some(&PythonLocalBindingKind::ClassOnly)
930 {
931 return None;
932 }
933 for binding in &self.locals {
934 if !scope_step() {
935 return None;
936 }
937 if binding.name.as_ref() == name {
938 return binding
939 .declaration
940 .parent()
941 .filter(|node| node.kind() == "class_definition");
942 }
943 }
944 None
945 }
946
947 pub fn local_bindings(&self) -> impl Iterator<Item = (&str, Node<'tree>)> + '_ {
948 self.locals
949 .iter()
950 .map(|binding| (binding.name.as_ref(), binding.declaration))
951 }
952
953 fn record_local_node(&mut self, node: Node<'tree>, source: &str) {
954 let name = node_text(node, source);
955 self.record_local(name, node);
956 }
957
958 fn record_local(&mut self, name: &str, declaration: Node<'tree>) {
959 if name.is_empty() {
960 return;
961 }
962 let binding_kind = if is_function_declaration_name(declaration) {
963 PythonLocalBindingKind::FunctionOnly
964 } else if declaration.parent().is_some_and(|parent| {
965 parent.kind() == "class_definition"
966 && brokk_bifrost_core::analyzer::tree_walk::is_field_child(
967 parent,
968 declaration,
969 "name",
970 )
971 }) {
972 PythonLocalBindingKind::ClassOnly
973 } else {
974 PythonLocalBindingKind::Other
975 };
976 match self.bindings.local_names.entry(name.into()) {
977 std::collections::hash_map::Entry::Vacant(entry) => {
978 entry.insert(binding_kind);
979 self.locals.push(PythonLocalBinding {
980 name: name.into(),
981 declaration,
982 });
983 }
984 std::collections::hash_map::Entry::Occupied(mut entry) => {
985 entry.insert(PythonLocalBindingKind::Other);
986 }
987 }
988 }
989
990 fn record_binding_write(&mut self, name: &str, declaration: Node<'tree>) {
991 if !name.is_empty() {
992 self.bindings.binding_writes.insert(name.into());
993 }
994 self.record_local(name, declaration);
995 }
996}
997
998pub fn python_global_binding_is_untouched_bounded(
1004 name: &str,
1005 reference: Node<'_>,
1006 global_scope: Node<'_>,
1007 source: &str,
1008 mut scope_step: impl FnMut() -> bool,
1009) -> Option<bool> {
1010 assert!(matches!(
1011 global_scope.kind(),
1012 "function_definition" | "lambda" | "class_definition"
1013 ));
1014 let mut current = Some(global_scope);
1015 while let Some(scope) = current {
1016 if !scope_step() {
1017 return None;
1018 }
1019 if matches!(
1020 scope.kind(),
1021 "function_definition" | "lambda" | "class_definition"
1022 ) && scope.child_by_field_name("body").is_some_and(|body| {
1023 body.start_byte() <= reference.start_byte() && reference.end_byte() <= body.end_byte()
1024 }) {
1025 let inventory = if scope.kind() == "class_definition" {
1026 PythonClassReferenceScope::new_bounded(scope, reference, &mut scope_step)?
1027 .collect_bounded(source, &mut scope_step)?
1028 } else {
1029 PythonLexicalScopeInventory::collect_bounded(
1030 scope,
1031 source,
1032 std::iter::empty::<String>(),
1033 &mut scope_step,
1034 )?
1035 };
1036 if inventory.bindings.has_global_binding_event(name) {
1037 return Some(false);
1038 }
1039 }
1040 current = scope.parent();
1041 }
1042 Some(true)
1043}
1044
1045fn is_function_declaration_name(node: Node<'_>) -> bool {
1046 node.parent().is_some_and(|parent| {
1047 parent.kind() == "function_definition"
1048 && brokk_bifrost_core::analyzer::tree_walk::is_field_child(parent, node, "name")
1049 })
1050}
1051
1052pub fn python_binding_targets_bounded<'tree>(
1056 target: Node<'tree>,
1057 source: &str,
1058 mut scope_step: impl FnMut() -> bool,
1059) -> Option<Vec<PythonDirectScopeBinding<'tree>>> {
1060 let mut bindings = Vec::new();
1061 collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
1062 bindings.push(PythonDirectScopeBinding {
1063 declaration,
1064 kind: PythonDirectScopeBindingKind::Other,
1065 });
1066 })?;
1067 Some(bindings)
1068}
1069
1070pub fn python_direct_scope_bindings_bounded<'tree>(
1077 node: Node<'tree>,
1078 source: &str,
1079 mut scope_step: impl FnMut() -> bool,
1080) -> Option<Vec<PythonDirectScopeBinding<'tree>>> {
1081 let mut bindings = Vec::new();
1082
1083 match node.kind() {
1084 "function_definition" => {
1085 if let Some(name) = node.child_by_field_name("name") {
1086 bindings.push(PythonDirectScopeBinding {
1087 declaration: name,
1088 kind: if is_direct_module_definition_bounded(node, &mut scope_step)? {
1089 PythonDirectScopeBindingKind::FunctionDeclaration
1090 } else {
1091 PythonDirectScopeBindingKind::Other
1092 },
1093 });
1094 }
1095 }
1096 "class_definition" => {
1097 if let Some(name) = node.child_by_field_name("name") {
1098 bindings.push(PythonDirectScopeBinding {
1099 declaration: name,
1100 kind: if is_direct_module_definition_bounded(node, &mut scope_step)? {
1101 PythonDirectScopeBindingKind::ClassDeclaration
1102 } else {
1103 PythonDirectScopeBindingKind::Other
1104 },
1105 });
1106 }
1107 }
1108 "import_statement" | "import_from_statement" => {
1109 let module_level = is_direct_module_definition_bounded(node, &mut scope_step)?;
1110 collect_import_bindings(node, &mut scope_step, |declaration| {
1111 bindings.push(PythonDirectScopeBinding {
1112 declaration,
1113 kind: if module_level {
1114 PythonDirectScopeBindingKind::Import
1115 } else {
1116 PythonDirectScopeBindingKind::Other
1117 },
1118 });
1119 })?;
1120 }
1121 "assignment" | "annotated_assignment" | "augmented_assignment" => {
1122 if let Some(target) = node.child_by_field_name("left") {
1123 bindings.extend(python_binding_targets_bounded(
1124 target,
1125 source,
1126 &mut scope_step,
1127 )?);
1128 }
1129 }
1130 "type_alias_statement" => {
1131 if let Some(target) = node.child_by_field_name("left")
1132 && let Some(declaration) = first_identifier_bounded(target, &mut scope_step)?
1133 {
1134 bindings.push(PythonDirectScopeBinding {
1135 declaration,
1136 kind: PythonDirectScopeBindingKind::Other,
1137 });
1138 }
1139 }
1140 "named_expression" => {
1141 if let Some(target) = node.child_by_field_name("name") {
1142 bindings.extend(python_binding_targets_bounded(
1143 target,
1144 source,
1145 &mut scope_step,
1146 )?);
1147 }
1148 }
1149 "for_statement" | "async_for_statement" => {
1150 if let Some(target) = node.child_by_field_name("left") {
1151 bindings.extend(python_binding_targets_bounded(
1152 target,
1153 source,
1154 &mut scope_step,
1155 )?);
1156 }
1157 }
1158 "with_statement" => {
1159 let mut cursor = node.walk();
1160 for item in node
1161 .named_children(&mut cursor)
1162 .filter(|item| item.kind() == "with_item")
1163 {
1164 if let Some(alias) = item.child_by_field_name("alias") {
1165 collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
1166 bindings.push(PythonDirectScopeBinding {
1167 declaration,
1168 kind: PythonDirectScopeBindingKind::Other,
1169 });
1170 })?;
1171 }
1172 }
1173 }
1174 "delete_statement" => {
1175 for target in named_children_bounded(node, &mut scope_step)? {
1176 bindings.extend(python_binding_targets_bounded(
1177 target,
1178 source,
1179 &mut scope_step,
1180 )?);
1181 }
1182 }
1183 "as_pattern" => {
1184 if let Some(alias) = node.child_by_field_name("alias") {
1185 collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
1186 bindings.push(PythonDirectScopeBinding {
1187 declaration,
1188 kind: PythonDirectScopeBindingKind::Other,
1189 });
1190 })?;
1191 }
1192 }
1193 "except_clause" => {
1194 if let Some(alias) = node.child_by_field_name("alias") {
1195 collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
1196 bindings.push(PythonDirectScopeBinding {
1197 declaration,
1198 kind: PythonDirectScopeBindingKind::Other,
1199 });
1200 })?;
1201 }
1202 }
1203 "case_clause" => {
1204 for child in named_children_bounded(node, &mut scope_step)? {
1205 if child.kind() == "case_pattern" {
1206 collect_match_pattern_bindings(
1207 child,
1208 source,
1209 &mut scope_step,
1210 |_, declaration| {
1211 bindings.push(PythonDirectScopeBinding {
1212 declaration,
1213 kind: PythonDirectScopeBindingKind::Other,
1214 });
1215 },
1216 )?;
1217 }
1218 }
1219 }
1220 _ => {}
1221 }
1222 Some(bindings)
1223}
1224
1225pub fn python_module_or_class_scope_binds_name_bounded(
1230 scope: Node<'_>,
1231 target_name: &str,
1232 source: &str,
1233 scope_step: impl FnMut() -> bool,
1234) -> Option<bool> {
1235 python_scope_binds_name_with_filter_bounded(scope, target_name, source, |_| true, scope_step)
1236}
1237
1238fn python_scope_binds_name_with_filter_bounded<'tree>(
1239 scope: Node<'tree>,
1240 target_name: &str,
1241 source: &str,
1242 include_binding: impl Fn(Node<'tree>) -> bool,
1243 mut scope_step: impl FnMut() -> bool,
1244) -> Option<bool> {
1245 assert!(matches!(scope.kind(), "module" | "class_definition"));
1246 let body = scope.child_by_field_name("body").unwrap_or(scope);
1247 let mut stack = vec![body];
1248 while let Some(node) = stack.pop() {
1249 if !scope_step() {
1250 return None;
1251 }
1252 if node.kind() == "wildcard_import" {
1253 return Some(true);
1254 }
1255 if python_direct_scope_bindings_bounded(node, source, &mut scope_step)?
1256 .into_iter()
1257 .any(|binding| {
1258 include_binding(binding.declaration)
1259 && node_text(binding.declaration, source) == target_name
1260 })
1261 {
1262 return Some(true);
1263 }
1264 stack.extend(python_executable_scope_children(node));
1265 }
1266 Some(false)
1267}
1268
1269pub fn python_executable_scope_children<'tree>(
1271 node: Node<'tree>,
1272) -> impl Iterator<Item = Node<'tree>> {
1273 let excluded_body = matches!(
1274 node.kind(),
1275 "class_definition" | "function_definition" | "lambda"
1276 )
1277 .then(|| node.child_by_field_name("body").map(|body| body.id()))
1278 .flatten();
1279 named_children_iter(node).filter(move |child| Some(child.id()) != excluded_body)
1280}
1281
1282pub fn python_unambiguous_module_class_binding_bounded(
1283 root: Node<'_>,
1284 source: &str,
1285 target_name: &str,
1286 scope_step: impl FnMut() -> bool,
1287) -> Option<bool> {
1288 Some(
1289 python_unambiguous_module_binding_bounded(root, source, target_name, scope_step)?
1290 == Some(PythonDirectScopeBindingKind::ClassDeclaration),
1291 )
1292}
1293
1294pub fn python_unambiguous_module_binding_bounded(
1301 root: Node<'_>,
1302 source: &str,
1303 target_name: &str,
1304 mut scope_step: impl FnMut() -> bool,
1305) -> Option<Option<PythonDirectScopeBindingKind>> {
1306 let mut matched = None;
1307 let mut stack = vec![root];
1308 while let Some(node) = stack.pop() {
1309 if !scope_step() {
1310 return None;
1311 }
1312 if node.kind() == "wildcard_import" && matched.is_some() {
1315 return Some(None);
1316 }
1317 for binding in python_direct_scope_bindings_bounded(node, source, &mut scope_step)? {
1318 if node_text(binding.declaration, source) != target_name {
1319 continue;
1320 }
1321 if matched.is_some() {
1322 return Some(None);
1323 }
1324 matched = Some(binding.kind);
1325 if binding.kind == PythonDirectScopeBindingKind::Other {
1326 return Some(Some(PythonDirectScopeBindingKind::Other));
1327 }
1328 }
1329
1330 let body = matches!(
1331 node.kind(),
1332 "function_definition" | "class_definition" | "lambda"
1333 )
1334 .then(|| node.child_by_field_name("body").map(|child| child.id()))
1335 .flatten();
1336 let name = matches!(node.kind(), "function_definition" | "class_definition")
1337 .then(|| node.child_by_field_name("name").map(|child| child.id()))
1338 .flatten();
1339 for child in named_children_bounded(node, &mut scope_step)?
1340 .into_iter()
1341 .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
1342 .rev()
1343 {
1344 stack.push(child);
1345 }
1346 }
1347 Some(matched)
1348}
1349
1350fn is_direct_module_definition_bounded(
1351 node: Node<'_>,
1352 scope_step: &mut impl FnMut() -> bool,
1353) -> Option<bool> {
1354 if !scope_step() {
1355 return None;
1356 }
1357 let Some(mut parent) = node.parent() else {
1358 return Some(false);
1359 };
1360 if parent.kind() == "decorated_definition" {
1361 if !scope_step() {
1362 return None;
1363 }
1364 let Some(grandparent) = parent.parent() else {
1365 return Some(false);
1366 };
1367 parent = grandparent;
1368 }
1369 Some(parent.kind() == "module")
1370}
1371
1372fn collect_direct_identifier_names(
1373 node: Node<'_>,
1374 source: &str,
1375 scope_step: &mut impl FnMut() -> bool,
1376 mut record: impl FnMut(&str),
1377) -> Option<()> {
1378 for child in named_children_bounded(node, scope_step)? {
1379 if child.kind() == "identifier" {
1380 let name = node_text(child, source);
1381 if !name.is_empty() {
1382 record(name);
1383 }
1384 }
1385 }
1386 Some(())
1387}
1388
1389fn collect_import_bindings<'tree>(
1390 statement: Node<'tree>,
1391 scope_step: &mut impl FnMut() -> bool,
1392 mut record: impl FnMut(Node<'tree>),
1393) -> Option<()> {
1394 let mut cursor = statement.walk();
1395 let mut imports = Vec::new();
1396 for imported in statement.children_by_field_name("name", &mut cursor) {
1397 if !scope_step() {
1398 return None;
1399 }
1400 imports.push(imported);
1401 }
1402 for imported in imports {
1403 if let Some(alias) = imported.child_by_field_name("alias") {
1404 record(alias);
1405 continue;
1406 }
1407 let name = imported.child_by_field_name("name").unwrap_or(imported);
1408 let binding = if statement.kind() == "import_statement" {
1409 first_identifier_bounded(name, scope_step)?
1410 } else {
1411 last_identifier_bounded(name, scope_step)?
1412 };
1413 if let Some(binding) = binding {
1414 record(binding);
1415 }
1416 }
1417 Some(())
1418}
1419
1420fn first_identifier_bounded<'tree>(
1421 root: Node<'tree>,
1422 scope_step: &mut impl FnMut() -> bool,
1423) -> Option<Option<Node<'tree>>> {
1424 let mut stack = vec![root];
1425 while let Some(node) = stack.pop() {
1426 if !scope_step() {
1427 return None;
1428 }
1429 if node.kind() == "identifier" {
1430 return Some(Some(node));
1431 }
1432 let children = named_children_bounded(node, scope_step)?;
1433 stack.extend(children.into_iter().rev());
1434 }
1435 Some(None)
1436}
1437
1438fn last_identifier_bounded<'tree>(
1439 root: Node<'tree>,
1440 scope_step: &mut impl FnMut() -> bool,
1441) -> Option<Option<Node<'tree>>> {
1442 let mut result = None;
1443 let mut stack = vec![root];
1444 while let Some(node) = stack.pop() {
1445 if !scope_step() {
1446 return None;
1447 }
1448 if node.kind() == "identifier" {
1449 result = Some(node);
1450 continue;
1451 }
1452 let children = named_children_bounded(node, scope_step)?;
1453 stack.extend(children.into_iter().rev());
1454 }
1455 Some(result)
1456}
1457
1458fn collect_binding_targets<'tree>(
1459 target: Node<'tree>,
1460 source: &str,
1461 scope_step: &mut impl FnMut() -> bool,
1462 mut record: impl FnMut(&str, Node<'tree>),
1463) -> Option<()> {
1464 let mut stack = vec![target];
1465 while let Some(node) = stack.pop() {
1466 if !scope_step() {
1467 return None;
1468 }
1469 match node.kind() {
1470 "attribute" | "subscript" => continue,
1473 "identifier" | "keyword_identifier" => {
1474 let name = node_text(node, source);
1475 if !name.is_empty() {
1476 record(name, node);
1477 }
1478 continue;
1479 }
1480 _ => {}
1481 }
1482 let children = named_children_bounded(node, scope_step)?;
1483 if node.kind() == "as_pattern_target" && children.is_empty() {
1484 let name = node_text(node, source);
1485 if !name.is_empty() {
1486 record(name, node);
1487 }
1488 continue;
1489 }
1490 stack.extend(children.into_iter().rev());
1491 }
1492 Some(())
1493}
1494
1495fn collect_match_pattern_bindings<'tree>(
1496 pattern: Node<'tree>,
1497 source: &str,
1498 scope_step: &mut impl FnMut() -> bool,
1499 mut record: impl FnMut(&str, Node<'tree>),
1500) -> Option<()> {
1501 let mut stack = vec![pattern];
1502 while let Some(node) = stack.pop() {
1503 if !scope_step() {
1504 return None;
1505 }
1506 match node.kind() {
1507 "dotted_name" => {
1508 let identifiers = named_children_bounded(node, scope_step)?
1509 .into_iter()
1510 .filter(|child| child.kind() == "identifier")
1511 .collect::<Vec<_>>();
1512 if let [binding] = identifiers.as_slice() {
1513 let name = node_text(*binding, source);
1514 if !name.is_empty() {
1515 record(name, *binding);
1516 }
1517 }
1518 continue;
1519 }
1520 "splat_pattern" => {
1521 for child in named_children_bounded(node, scope_step)? {
1522 if child.kind() == "identifier" {
1523 let name = node_text(child, source);
1524 if !name.is_empty() {
1525 record(name, child);
1526 }
1527 }
1528 }
1529 continue;
1530 }
1531 "class_pattern" => {
1532 let mut children = named_children_bounded(node, scope_step)?;
1533 if children
1534 .first()
1535 .is_some_and(|child| child.kind() == "dotted_name")
1536 {
1537 children.remove(0);
1538 }
1539 stack.extend(children.into_iter().rev());
1540 continue;
1541 }
1542 "keyword_pattern" => {
1543 let mut children = named_children_bounded(node, scope_step)?;
1544 if children
1545 .first()
1546 .is_some_and(|child| child.kind() == "identifier")
1547 {
1548 children.remove(0);
1549 }
1550 stack.extend(children.into_iter().rev());
1551 continue;
1552 }
1553 "dict_pattern" => {
1554 let key_ids = children_by_field_name_bounded(node, "key", scope_step)?
1555 .into_iter()
1556 .map(|key| key.id())
1557 .collect::<HashSet<_>>();
1558 let children = named_children_bounded(node, scope_step)?;
1559 stack.extend(
1560 children
1561 .into_iter()
1562 .filter(|child| !key_ids.contains(&child.id()))
1563 .rev(),
1564 );
1565 continue;
1566 }
1567 "as_pattern" => {
1568 if let Some(alias) = node.child_by_field_name("alias") {
1569 collect_binding_targets(alias, source, scope_step, &mut record)?;
1570 let children = named_children_bounded(node, scope_step)?;
1571 stack.extend(
1572 children
1573 .into_iter()
1574 .filter(|child| child.id() != alias.id())
1575 .rev(),
1576 );
1577 continue;
1578 }
1579 let mut children = named_children_bounded(node, scope_step)?;
1580 if let Some(alias) = children
1581 .last()
1582 .copied()
1583 .filter(|child| child.kind() == "identifier")
1584 {
1585 let name = node_text(alias, source);
1586 if !name.is_empty() {
1587 record(name, alias);
1588 }
1589 children.pop();
1590 }
1591 stack.extend(children.into_iter().rev());
1592 continue;
1593 }
1594 "identifier" => {
1595 continue;
1598 }
1599 kind if is_pattern_literal(kind) => continue,
1600 _ => {}
1601 }
1602 let children = named_children_bounded(node, scope_step)?;
1603 stack.extend(children.into_iter().rev());
1604 }
1605 Some(())
1606}
1607
1608fn push_named_children<'tree>(
1609 stack: &mut Vec<ScanFrame<'tree>>,
1610 node: Node<'tree>,
1611 in_comprehension: bool,
1612 scope_step: &mut impl FnMut() -> bool,
1613) -> Option<()> {
1614 let children = named_children_bounded(node, scope_step)?;
1615 push_bounded_children(stack, children.into_iter(), in_comprehension);
1616 Some(())
1617}
1618
1619fn push_named_children_except<'tree>(
1620 stack: &mut Vec<ScanFrame<'tree>>,
1621 node: Node<'tree>,
1622 excluded: Node<'tree>,
1623 in_comprehension: bool,
1624 scope_step: &mut impl FnMut() -> bool,
1625) -> Option<()> {
1626 let children = named_children_bounded(node, scope_step)?;
1627 push_bounded_children(
1628 stack,
1629 children
1630 .into_iter()
1631 .filter(|child| child.id() != excluded.id()),
1632 in_comprehension,
1633 );
1634 Some(())
1635}
1636
1637fn push_nested_scope_header_children<'tree>(
1638 stack: &mut Vec<ScanFrame<'tree>>,
1639 node: Node<'tree>,
1640 in_comprehension: bool,
1641 scope_step: &mut impl FnMut() -> bool,
1642) -> Option<()> {
1643 let body = node.child_by_field_name("body").map(|child| child.id());
1644 let name = node.child_by_field_name("name").map(|child| child.id());
1645 let mut cursor = node.walk();
1648 let children = node
1649 .named_children(&mut cursor)
1650 .filter(|child| Some(child.id()) != body);
1651 let mut headers = Vec::new();
1652 push_bounded_nodes(children, &mut headers, scope_step).then_some(())?;
1653 push_bounded_children(
1654 stack,
1655 headers.into_iter().filter(|child| Some(child.id()) != name),
1656 in_comprehension,
1657 );
1658 Some(())
1659}
1660
1661fn push_bounded_children<'tree>(
1662 stack: &mut Vec<ScanFrame<'tree>>,
1663 children: impl DoubleEndedIterator<Item = Node<'tree>>,
1664 in_comprehension: bool,
1665) {
1666 for child in children.rev() {
1667 stack.push(ScanFrame {
1668 node: child,
1669 in_comprehension,
1670 });
1671 }
1672}
1673
1674fn named_children_bounded<'tree>(
1675 node: Node<'tree>,
1676 scope_step: &mut impl FnMut() -> bool,
1677) -> Option<Vec<Node<'tree>>> {
1678 let mut cursor = node.walk();
1679 let mut children = Vec::new();
1680 push_bounded_nodes(node.named_children(&mut cursor), &mut children, scope_step)
1681 .then_some(children)
1682}
1683
1684fn children_by_field_name_bounded<'tree>(
1685 node: Node<'tree>,
1686 field: &str,
1687 scope_step: &mut impl FnMut() -> bool,
1688) -> Option<Vec<Node<'tree>>> {
1689 let mut cursor = node.walk();
1690 let mut children = Vec::new();
1691 push_bounded_nodes(
1692 node.children_by_field_name(field, &mut cursor),
1693 &mut children,
1694 scope_step,
1695 )
1696 .then_some(children)
1697}
1698
1699fn is_comprehension(kind: &str) -> bool {
1700 matches!(
1701 kind,
1702 "list_comprehension"
1703 | "set_comprehension"
1704 | "dictionary_comprehension"
1705 | "generator_expression"
1706 )
1707}
1708
1709fn is_pattern_literal(kind: &str) -> bool {
1710 matches!(
1711 kind,
1712 "string"
1713 | "concatenated_string"
1714 | "integer"
1715 | "float"
1716 | "complex_pattern"
1717 | "true"
1718 | "false"
1719 | "none"
1720 )
1721}
1722
1723#[cfg(test)]
1724mod tests {
1725 use brokk_bifrost_test_macros::hardwon;
1726
1727 #[test]
1728 fn only_the_first_comprehension_iterable_uses_the_enclosing_scope() {
1729 let source = "def run(first, second):\n return [second for first in first for second in second]\n";
1730 let mut parser = tree_sitter::Parser::new();
1731 parser
1732 .set_language(&tree_sitter_python::LANGUAGE.into())
1733 .unwrap();
1734 let tree = parser.parse(source, None).unwrap();
1735 let mut stack = vec![tree.root_node()];
1736 let comprehension = loop {
1737 let node = stack.pop().expect("fixture contains a comprehension");
1738 if node.kind() == "list_comprehension" {
1739 break node;
1740 }
1741 let mut cursor = node.walk();
1742 stack.extend(node.named_children(&mut cursor));
1743 };
1744 let mut cursor = comprehension.walk();
1745 let clauses = comprehension
1746 .named_children(&mut cursor)
1747 .filter(|node| node.kind() == "for_in_clause")
1748 .collect::<Vec<_>>();
1749 let outer = clauses[0].child_by_field_name("right").unwrap();
1750 let inner = clauses[1].child_by_field_name("right").unwrap();
1751 assert!(!super::python_comprehension_binds_name_at(
1752 "first", outer, source
1753 ));
1754 assert!(super::python_comprehension_binds_name_at(
1755 "second", inner, source
1756 ));
1757 }
1758
1759 #[test]
1760 #[hardwon(
1761 "#5044+#5511: bounded lexical lookups bypassed budgets or erased lazy class bindings"
1762 )]
1763 fn bounded_comprehension_lookup_preserves_exhaustion() {
1764 let source = "def run(xs):\n class Base:\n pass\n def helper():\n pass\n class Derived(Base):\n pass\n helper()\n return [target for target in xs]\n";
1765 let tree = crate::declarations::parse_python_tree(source).unwrap();
1766 let function = tree.root_node().named_child(0).unwrap();
1767 let inventory = super::PythonLexicalScopeInventory::collect_bounded(
1768 function,
1769 source,
1770 ["xs".to_owned()],
1771 || true,
1772 )
1773 .unwrap();
1774 let mut stack = vec![function];
1775 let comprehension = loop {
1776 let node = stack.pop().expect("fixture contains a comprehension");
1777 if node.kind() == "list_comprehension" {
1778 break node;
1779 }
1780 stack.extend(brokk_bifrost_core::analyzer::tree_walk::named_children_iter(node));
1781 };
1782 let reference = comprehension.named_child(0).unwrap();
1783 for mut remaining in [0_usize, 1] {
1784 assert_eq!(
1785 inventory.name_resolution_at_bounded("target", reference, || {
1786 let available = remaining > 0;
1787 remaining = remaining.saturating_sub(1);
1788 available
1789 }),
1790 None
1791 );
1792 }
1793 assert_eq!(
1794 inventory.name_resolution_at_bounded("target", reference, || true),
1795 Some(super::PythonLexicalNameResolution::Local)
1796 );
1797 let body = function.child_by_field_name("body").unwrap();
1798 let derived = brokk_bifrost_core::analyzer::tree_walk::named_children_iter(body)
1799 .find(|node| node.child_by_field_name("superclasses").is_some())
1800 .unwrap();
1801 let base_reference = derived
1802 .child_by_field_name("superclasses")
1803 .unwrap()
1804 .named_child(0)
1805 .unwrap();
1806 let call = brokk_bifrost_core::analyzer::tree_walk::named_children_iter(body)
1807 .filter(|node| node.kind() == "expression_statement")
1808 .find_map(|node| node.named_child(0).filter(|node| node.kind() == "call"))
1809 .unwrap();
1810 let helper_reference = call.child_by_field_name("function").unwrap();
1811 let one_step = || {
1814 let mut available = true;
1815 move || std::mem::take(&mut available)
1816 };
1817 assert!(
1818 inventory
1819 .has_runtime_callable_binding_at_bounded("helper", helper_reference, one_step())
1820 .is_none()
1821 );
1822 assert!(
1823 inventory
1824 .local_function_declaration_bounded("helper", helper_reference, one_step())
1825 .is_none()
1826 );
1827 assert!(
1828 inventory
1829 .local_class_declaration_bounded("Base", base_reference, one_step())
1830 .is_none()
1831 );
1832 assert_eq!(
1833 inventory
1834 .local_function_declaration_bounded("helper", helper_reference, || true)
1835 .unwrap()
1836 .kind(),
1837 "function_definition"
1838 );
1839 assert_eq!(
1840 inventory
1841 .local_class_declaration_bounded("Base", base_reference, || true)
1842 .unwrap()
1843 .kind(),
1844 "class_definition"
1845 );
1846 let bindings = inventory.into_bindings();
1847 assert_eq!(
1848 bindings.name_resolution_at_bounded("missing", reference, one_step()),
1849 None,
1850 "stopped lookup must not manufacture Unbound"
1851 );
1852 assert_eq!(
1853 bindings.name_resolution_at_bounded("missing", reference, || true),
1854 Some(super::PythonLexicalNameResolution::Unbound)
1855 );
1856 assert_eq!(
1857 bindings.resolves_to_local_function_bounded("helper", helper_reference, one_step()),
1858 None
1859 );
1860 assert_eq!(
1861 bindings.resolves_to_local_function_bounded("helper", helper_reference, || true),
1862 Some(true)
1863 );
1864 assert_eq!(
1865 super::python_comprehension_binds_name_at_bounded(
1866 "target",
1867 reference,
1868 source,
1869 one_step()
1870 ),
1871 None
1872 );
1873 assert_eq!(
1874 super::python_comprehension_binds_name_at_bounded("target", reference, source, || true),
1875 Some(true)
1876 );
1877 let clause = brokk_bifrost_core::analyzer::tree_walk::named_children_iter(comprehension)
1878 .find(|node| node.kind() == "for_in_clause")
1879 .unwrap();
1880 let targets = super::python_binding_targets_bounded(
1881 clause.child_by_field_name("left").unwrap(),
1882 source,
1883 || true,
1884 )
1885 .unwrap();
1886 assert_eq!(targets.len(), 1);
1887 assert_eq!(
1888 Some(targets[0].declaration),
1889 clause.child_by_field_name("left")
1890 );
1891
1892 let source = "import ns_source as ns\nclass Outer:\n class ns[T: ns.Target]((ns.Target)):\n pass\n";
1895 let tree = crate::declarations::parse_python_tree(source).unwrap();
1896 assert!(!tree.root_node().has_error());
1897 let outer = brokk_bifrost_core::analyzer::tree_walk::first_named_child_of_kind(
1898 tree.root_node(),
1899 "class_definition",
1900 )
1901 .unwrap();
1902 let inner = brokk_bifrost_core::analyzer::tree_walk::first_named_child_of_kind(
1903 outer.child_by_field_name("body").unwrap(),
1904 "class_definition",
1905 )
1906 .unwrap();
1907 let base = inner
1908 .child_by_field_name("superclasses")
1909 .unwrap()
1910 .named_child(0)
1911 .unwrap();
1912 let immediate = crate::syntax::python_static_value_path_bounded(base, || true)
1913 .unwrap()
1914 .unwrap()[0];
1915 let bound =
1916 brokk_bifrost_core::analyzer::tree_walk::first_named_descendant_of_kind_inclusive(
1917 inner.child_by_field_name("type_parameters").unwrap(),
1918 "attribute",
1919 )
1920 .unwrap()
1921 .child_by_field_name("object")
1922 .unwrap();
1923 for (reference, resolution, binds) in [
1924 (
1925 immediate,
1926 super::PythonLexicalNameResolution::Unbound,
1927 false,
1928 ),
1929 (bound, super::PythonLexicalNameResolution::Local, true),
1930 ] {
1931 assert!(
1932 super::PythonClassReferenceScope::new_bounded(outer, reference, || false).is_none()
1933 );
1934 let scope =
1935 super::PythonClassReferenceScope::new_bounded(outer, reference, || true).unwrap();
1936 let inventory = scope.collect_bounded(source, || true).unwrap();
1937 assert_eq!(
1938 inventory.name_resolution_at_bounded("ns", reference, || true),
1939 Some(resolution)
1940 );
1941 assert_eq!(scope.binds_name_bounded("ns", source, || true), Some(binds));
1942 }
1943 }
1944}