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) = python_type_parameters(parent)
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(crate) fn python_type_parameters(owner: Node<'_>) -> Option<Node<'_>> {
244 owner.child_by_field_name("type_parameters").or_else(|| {
245 if owner.kind() != "type_alias_statement" {
246 return None;
247 }
248 let left = owner.child_by_field_name("left")?;
249 let generic = left.named_child(0)?;
250 (generic.kind() == "generic_type")
251 .then(|| named_children_iter(generic).find(|node| node.kind() == "type_parameter"))
252 .flatten()
253 })
254}
255
256pub fn python_is_type_parameter_binder(node: Node<'_>) -> bool {
259 if node.kind() != "identifier" {
260 return false;
261 }
262 let mut current = node;
263 while let Some(parent) = current.parent() {
264 if matches!(
265 parent.kind(),
266 "class_definition" | "function_definition" | "type_alias_statement"
267 ) && let Some(parameters) = python_type_parameters(parent)
268 && parameters.start_byte() <= node.start_byte()
269 && node.end_byte() <= parameters.end_byte()
270 {
271 let mut cursor = parameters.walk();
272 return parameters.named_children(&mut cursor).any(|parameter| {
273 let binder = python_type_parameter_binder_node(parameter);
274 binder.is_some_and(|binder| binder.id() == node.id())
275 });
276 }
277 if matches!(
278 parent.kind(),
279 "module" | "class_definition" | "function_definition" | "type_alias_statement"
280 ) {
281 return false;
282 }
283 current = parent;
284 }
285 false
286}
287
288pub(crate) fn python_type_parameter_binder_node(parameter: Node<'_>) -> Option<Node<'_>> {
289 if parameter.kind() == "identifier" {
290 Some(parameter)
291 } else {
292 first_identifier_bounded(parameter, &mut || true).flatten()
293 }
294}
295
296#[derive(Clone, Copy, Debug)]
297pub struct PythonDirectScopeBinding<'tree> {
298 pub declaration: Node<'tree>,
299 pub kind: PythonDirectScopeBindingKind,
300}
301
302#[derive(Clone, Debug)]
303struct PythonLocalBinding<'tree> {
304 name: Box<str>,
305 declaration: Node<'tree>,
306}
307
308#[derive(Clone, Copy, Debug, PartialEq, Eq)]
309enum PythonLocalBindingKind {
310 FunctionOnly,
311 ClassOnly,
312 ImportOnly,
313 Other,
314}
315
316#[derive(Clone, Debug)]
317struct PythonComprehensionBinding {
318 name: Box<str>,
319 start_byte: usize,
320 end_byte: usize,
321 enclosing_iterable_ranges: Vec<(usize, usize)>,
322}
323
324pub struct PythonLexicalScopeInventory<'tree> {
331 binding_nodes: Vec<Node<'tree>>,
332 locals: Vec<PythonLocalBinding<'tree>>,
333 bindings: PythonLexicalScopeBindings,
334}
335
336pub struct PythonLexicalScopeBindings {
339 parameters: HashSet<Box<str>>,
340 local_names: HashMap<Box<str>, PythonLocalBindingKind>,
341 binding_writes: HashSet<Box<str>>,
342 global_binding_events: HashSet<Box<str>>,
344 globals: HashSet<Box<str>>,
345 nonlocals: HashSet<Box<str>>,
346 comprehensions: Vec<PythonComprehensionBinding>,
347}
348
349impl PythonLexicalScopeBindings {
350 pub fn name_resolution_at(
351 &self,
352 name: &str,
353 reference: Node<'_>,
354 ) -> PythonLexicalNameResolution {
355 self.name_resolution_at_bounded(name, reference, || true)
356 .expect("unbounded lexical name lookup completes")
357 }
358
359 pub fn name_resolution_at_bounded(
362 &self,
363 name: &str,
364 reference: Node<'_>,
365 mut scope_step: impl FnMut() -> bool,
366 ) -> Option<PythonLexicalNameResolution> {
367 if !scope_step() {
368 return None;
369 }
370 if self.comprehension_binds_name_at_bounded(name, reference, &mut scope_step)? {
371 return Some(PythonLexicalNameResolution::Local);
372 }
373 Some(if self.nonlocals.contains(name) {
374 PythonLexicalNameResolution::Nonlocal
375 } else if self.globals.contains(name) {
376 PythonLexicalNameResolution::Global
377 } else if self.parameters.contains(name) || self.local_names.contains_key(name) {
378 PythonLexicalNameResolution::Local
379 } else {
380 PythonLexicalNameResolution::Unbound
381 })
382 }
383
384 fn comprehension_binds_name_at_bounded(
385 &self,
386 name: &str,
387 reference: Node<'_>,
388 mut scope_step: impl FnMut() -> bool,
389 ) -> Option<bool> {
390 let reference_byte = reference.start_byte();
391 for binding in &self.comprehensions {
392 if !scope_step() {
393 return None;
394 }
395 if binding.name.as_ref() == name
396 && binding.start_byte <= reference_byte
397 && reference_byte < binding.end_byte
398 {
399 let mut in_outer_iterable = false;
400 for (start, end) in &binding.enclosing_iterable_ranges {
401 if !scope_step() {
402 return None;
403 }
404 in_outer_iterable |= *start <= reference_byte && reference_byte < *end;
405 }
406 if !in_outer_iterable {
407 return Some(true);
408 }
409 }
410 }
411 Some(false)
412 }
413
414 pub fn has_global_binding_event(&self, name: &str) -> bool {
417 self.global_binding_events.contains(name)
418 }
419
420 pub fn global_binding_events(&self) -> impl Iterator<Item = &str> {
423 self.global_binding_events.iter().map(|name| name.as_ref())
424 }
425
426 pub fn resolves_to_local_function_bounded(
427 &self,
428 name: &str,
429 reference: Node<'_>,
430 mut scope_step: impl FnMut() -> bool,
431 ) -> Option<bool> {
432 if !scope_step() {
433 return None;
434 }
435 Some(
436 !self.parameters.contains(name)
437 && !self.comprehension_binds_name_at_bounded(name, reference, &mut scope_step)?
438 && self.local_names.get(name) == Some(&PythonLocalBindingKind::FunctionOnly),
439 )
440 }
441
442 pub fn has_runtime_callable_binding_at(&self, name: &str, reference: Node<'_>) -> bool {
445 self.has_runtime_callable_binding_at_bounded(name, reference, || true)
446 .expect("unbounded runtime binding lookup completes")
447 }
448
449 pub fn has_runtime_callable_binding_at_bounded(
450 &self,
451 name: &str,
452 reference: Node<'_>,
453 mut scope_step: impl FnMut() -> bool,
454 ) -> Option<bool> {
455 if !scope_step() {
456 return None;
457 }
458 Some(
459 self.parameters.contains(name)
460 || self.binding_writes.contains(name)
461 || self.comprehension_binds_name_at_bounded(name, reference, &mut scope_step)?,
462 )
463 }
464}
465
466pub struct PythonClassReferenceScope<'tree> {
471 scope: Node<'tree>,
472 pending_name: Option<Node<'tree>>,
473}
474
475impl<'tree> PythonClassReferenceScope<'tree> {
476 pub fn new_bounded(
477 scope: Node<'tree>,
478 reference: Node<'tree>,
479 mut scope_step: impl FnMut() -> bool,
480 ) -> Option<Self> {
481 assert_eq!(scope.kind(), "class_definition");
482 let mut current = reference;
483 let mut pending_name = None;
484 while current != scope {
485 if !scope_step() {
486 return None;
487 }
488 if current.kind() == "class_definition"
489 && let Some(superclasses) = current.child_by_field_name("superclasses")
490 {
491 for expression in named_children_iter(superclasses) {
492 if !scope_step() {
493 return None;
494 }
495 if expression.start_byte() <= reference.start_byte()
496 && reference.end_byte() <= expression.end_byte()
497 && let Some(path) = crate::syntax::python_static_value_path_bounded(
498 expression,
499 &mut scope_step,
500 )?
501 && (path.first().copied() == Some(reference) || expression == reference)
502 {
503 pending_name = Some(current.child_by_field_name("name")?);
504 }
505 }
506 }
507 current = current.parent()?;
508 }
509 Some(Self {
510 scope,
511 pending_name,
512 })
513 }
514
515 fn includes_binding(&self, declaration: Node<'tree>) -> bool {
516 Some(declaration) != self.pending_name
517 }
518
519 pub fn collect_bounded(
520 &self,
521 source: &str,
522 scope_step: impl FnMut() -> bool,
523 ) -> Option<PythonLexicalScopeInventory<'tree>> {
524 PythonLexicalScopeInventory::collect_with_declaration_filter_bounded(
525 self.scope,
526 source,
527 std::iter::empty::<String>(),
528 |declaration| self.includes_binding(declaration),
529 scope_step,
530 )
531 }
532
533 pub fn binds_name_bounded(
534 &self,
535 name: &str,
536 source: &str,
537 scope_step: impl FnMut() -> bool,
538 ) -> Option<bool> {
539 python_scope_binds_name_with_filter_bounded(
540 self.scope,
541 name,
542 source,
543 |declaration| self.includes_binding(declaration),
544 scope_step,
545 )
546 }
547}
548
549impl<'tree> PythonLexicalScopeInventory<'tree> {
550 pub fn collect_bounded(
557 callable: Node<'tree>,
558 source: &str,
559 parameter_names: impl IntoIterator<Item = String>,
560 scope_step: impl FnMut() -> bool,
561 ) -> Option<Self> {
562 Self::collect_with_declaration_filter_bounded(
563 callable,
564 source,
565 parameter_names,
566 |_| true,
567 scope_step,
568 )
569 }
570
571 fn collect_with_declaration_filter_bounded(
572 callable: Node<'tree>,
573 source: &str,
574 parameter_names: impl IntoIterator<Item = String>,
575 include_declaration: impl Fn(Node<'tree>) -> bool,
576 mut scope_step: impl FnMut() -> bool,
577 ) -> Option<Self> {
578 let mut inventory = Self {
579 binding_nodes: Vec::new(),
580 locals: Vec::new(),
581 bindings: PythonLexicalScopeBindings {
582 parameters: parameter_names.into_iter().map(Box::<str>::from).collect(),
583 local_names: HashMap::default(),
584 binding_writes: HashSet::default(),
585 global_binding_events: HashSet::default(),
586 globals: HashSet::default(),
587 nonlocals: HashSet::default(),
588 comprehensions: Vec::new(),
589 },
590 };
591 let module_scope = callable.parent().is_none();
594 let body = if module_scope {
595 callable
596 } else if let Some(body) = callable.child_by_field_name("body") {
597 body
598 } else {
599 return Some(inventory);
600 };
601 let mut stack = vec![body];
602
603 while let Some(node) = stack.pop() {
604 if !scope_step() {
605 return None;
606 }
607 let nested_scope = node != body
608 && matches!(
609 node.kind(),
610 "function_definition" | "lambda" | "class_definition"
611 );
612 if nested_scope {
613 if matches!(node.kind(), "function_definition" | "class_definition")
614 && let Some(name) = node.child_by_field_name("name")
615 && include_declaration(name)
616 {
617 inventory.record_local_node(name, source);
618 }
619 push_nested_scope_header_children(&mut stack, node, &mut scope_step)?;
624 continue;
625 }
626
627 match node.kind() {
628 "global_statement" => {
629 if module_scope {
630 continue;
631 }
632 collect_direct_identifier_names(node, source, &mut scope_step, |name| {
633 inventory.bindings.globals.insert(name.into());
634 })?;
635 continue;
636 }
637 "nonlocal_statement" => {
638 collect_direct_identifier_names(node, source, &mut scope_step, |name| {
639 inventory.bindings.nonlocals.insert(name.into());
640 })?;
641 continue;
642 }
643 "import_statement" | "import_from_statement" => {
644 collect_import_bindings(node, &mut scope_step, |binding| {
645 inventory.record_local_kind(
646 node_text(binding, source),
647 binding,
648 PythonLocalBindingKind::ImportOnly,
649 )
650 })?;
651 continue;
652 }
653 "assignment" | "augmented_assignment" => {
654 if let Some(target) = node.child_by_field_name("left") {
655 collect_binding_targets(
656 target,
657 source,
658 &mut scope_step,
659 |name, declaration| {
660 inventory.record_binding_write(name, declaration);
661 },
662 )?;
663 }
664 }
665 "type_alias_statement" => {
666 if let Some(target) = node.child_by_field_name("left")
667 && let Some(binding) = first_identifier_bounded(target, &mut scope_step)?
668 {
669 inventory.record_local_node(binding, source);
670 }
671 }
672 "named_expression" => {
673 if let Some(target) = node.child_by_field_name("name") {
676 collect_binding_targets(
677 target,
678 source,
679 &mut scope_step,
680 |name, declaration| {
681 inventory.record_binding_write(name, declaration);
682 },
683 )?;
684 }
685 }
686 "for_statement" => {
687 if let Some(target) = node.child_by_field_name("left") {
688 collect_binding_targets(
689 target,
690 source,
691 &mut scope_step,
692 |name, declaration| {
693 inventory.record_binding_write(name, declaration);
694 },
695 )?;
696 }
697 }
698 "for_in_clause" => {
699 }
702 "delete_statement" => {
703 for target in named_children_bounded(node, &mut scope_step)? {
704 collect_binding_targets(
705 target,
706 source,
707 &mut scope_step,
708 |name, declaration| {
709 inventory.record_binding_write(name, declaration);
710 },
711 )?;
712 }
713 continue;
714 }
715 "as_pattern" => {
716 if let Some(alias) = node.child_by_field_name("alias") {
717 collect_binding_targets(
718 alias,
719 source,
720 &mut scope_step,
721 |name, declaration| {
722 inventory.record_binding_write(name, declaration);
723 },
724 )?;
725 push_named_children_except(&mut stack, node, alias, &mut scope_step)?;
726 continue;
727 }
728 }
729 "except_clause" => {
730 if let Some(alias) = node.child_by_field_name("alias") {
734 collect_binding_targets(
735 alias,
736 source,
737 &mut scope_step,
738 |name, declaration| {
739 inventory.record_binding_write(name, declaration);
740 },
741 )?;
742 }
743 }
744 "case_clause" => {
745 let children = named_children_bounded(node, &mut scope_step)?;
746 for child in children.iter().copied() {
747 if child.kind() == "case_pattern" {
748 collect_match_pattern_bindings(
749 child,
750 source,
751 &mut scope_step,
752 |name, declaration| {
753 inventory.record_binding_write(name, declaration);
754 },
755 )?;
756 }
757 }
758 for child in children.into_iter().rev() {
759 if child.kind() != "case_pattern" {
760 stack.push(child);
761 }
762 }
763 continue;
764 }
765 kind if is_comprehension(kind) => {
766 let range = (node.start_byte(), node.end_byte());
767 let children = named_children_bounded(node, &mut scope_step)?;
768 let enclosing_iterable_ranges = if let Some(first_clause) = children
769 .iter()
770 .copied()
771 .find(|child| child.kind() == "for_in_clause")
772 {
773 children_by_field_name_bounded(first_clause, "right", &mut scope_step)?
774 .into_iter()
775 .map(|iterable| (iterable.start_byte(), iterable.end_byte()))
776 .collect()
777 } else {
778 Vec::new()
779 };
780 for clause in children
781 .iter()
782 .copied()
783 .filter(|child| child.kind() == "for_in_clause")
784 {
785 if let Some(target) = clause.child_by_field_name("left") {
786 collect_binding_targets(target, source, &mut scope_step, |name, _| {
787 inventory.bindings.comprehensions.push(
788 PythonComprehensionBinding {
789 name: name.into(),
790 start_byte: range.0,
791 end_byte: range.1,
792 enclosing_iterable_ranges: enclosing_iterable_ranges
793 .clone(),
794 },
795 );
796 })?;
797 }
798 }
799 for child in children.into_iter().rev() {
800 stack.push(child);
801 }
802 continue;
803 }
804 _ => {}
805 }
806
807 push_named_children(&mut stack, node, &mut scope_step)?;
808 }
809
810 for name in inventory.bindings.local_names.keys() {
814 if !scope_step() {
815 return None;
816 }
817 if inventory.bindings.globals.contains(name.as_ref()) {
818 inventory
819 .bindings
820 .global_binding_events
821 .insert(name.clone());
822 }
823 }
824
825 inventory.locals.retain(|binding| {
828 !inventory.bindings.globals.contains(binding.name.as_ref())
829 && !inventory.bindings.nonlocals.contains(binding.name.as_ref())
830 });
831 inventory.bindings.local_names.retain(|name, _| {
832 !inventory.bindings.globals.contains(name.as_ref())
833 && !inventory.bindings.nonlocals.contains(name.as_ref())
834 });
835 Some(inventory)
836 }
837
838 pub fn into_bindings(self) -> PythonLexicalScopeBindings {
839 self.bindings
840 }
841
842 pub fn name_resolution_at(
843 &self,
844 name: &str,
845 reference: Node<'_>,
846 ) -> PythonLexicalNameResolution {
847 self.bindings.name_resolution_at(name, reference)
848 }
849
850 pub fn name_resolution_at_bounded(
851 &self,
852 name: &str,
853 reference: Node<'_>,
854 scope_step: impl FnMut() -> bool,
855 ) -> Option<PythonLexicalNameResolution> {
856 self.bindings
857 .name_resolution_at_bounded(name, reference, scope_step)
858 }
859
860 pub fn has_runtime_callable_binding_at(&self, name: &str, reference: Node<'_>) -> bool {
861 self.bindings
862 .has_runtime_callable_binding_at(name, reference)
863 }
864
865 pub fn has_runtime_callable_binding_at_bounded(
866 &self,
867 name: &str,
868 reference: Node<'_>,
869 scope_step: impl FnMut() -> bool,
870 ) -> Option<bool> {
871 self.bindings
872 .has_runtime_callable_binding_at_bounded(name, reference, scope_step)
873 }
874
875 pub fn local_function_declaration(
876 &self,
877 name: &str,
878 reference: Node<'_>,
879 ) -> Option<Node<'tree>> {
880 self.local_function_declaration_bounded(name, reference, || true)
881 }
882
883 pub fn local_function_declaration_bounded(
884 &self,
885 name: &str,
886 reference: Node<'_>,
887 mut scope_step: impl FnMut() -> bool,
888 ) -> Option<Node<'tree>> {
889 if !self
890 .bindings
891 .resolves_to_local_function_bounded(name, reference, &mut scope_step)?
892 {
893 return None;
894 }
895 for binding in &self.locals {
896 if !scope_step() {
897 return None;
898 }
899 if binding.name.as_ref() == name {
900 return binding
901 .declaration
902 .parent()
903 .filter(|declaration| declaration.kind() == "function_definition");
904 }
905 }
906 None
907 }
908
909 pub fn local_class_declaration_bounded(
912 &self,
913 name: &str,
914 reference: Node<'_>,
915 mut scope_step: impl FnMut() -> bool,
916 ) -> Option<Node<'tree>> {
917 if self.name_resolution_at_bounded(name, reference, &mut scope_step)?
918 != PythonLexicalNameResolution::Local
919 || self.bindings.has_runtime_callable_binding_at_bounded(
920 name,
921 reference,
922 &mut scope_step,
923 )?
924 || self.bindings.local_names.get(name) != Some(&PythonLocalBindingKind::ClassOnly)
925 {
926 return None;
927 }
928 for binding in &self.locals {
929 if !scope_step() {
930 return None;
931 }
932 if binding.name.as_ref() == name {
933 return binding
934 .declaration
935 .parent()
936 .filter(|node| node.kind() == "class_definition");
937 }
938 }
939 None
940 }
941
942 pub fn unique_import_declaration_bounded(
946 &self,
947 name: &str,
948 reference: Node<'_>,
949 mut scope_step: impl FnMut() -> bool,
950 ) -> Option<Option<Node<'tree>>> {
951 if self.name_resolution_at_bounded(name, reference, &mut scope_step)?
952 != PythonLexicalNameResolution::Local
953 || self.bindings.has_runtime_callable_binding_at_bounded(
954 name,
955 reference,
956 &mut scope_step,
957 )?
958 || self.bindings.local_names.get(name) != Some(&PythonLocalBindingKind::ImportOnly)
959 {
960 return Some(None);
961 }
962 for binding in &self.locals {
963 if !scope_step() {
964 return None;
965 }
966 if binding.name.as_ref() == name {
967 return Some(Some(binding.declaration));
968 }
969 }
970 unreachable!("ImportOnly is constructed with its declaration token")
971 }
972
973 pub fn binding_nodes(&self) -> impl Iterator<Item = Node<'tree>> + '_ {
976 self.binding_nodes.iter().copied()
977 }
978
979 pub fn global_names(&self) -> impl Iterator<Item = &str> {
980 self.bindings.globals.iter().map(AsRef::as_ref)
981 }
982
983 pub fn nonlocal_names(&self) -> impl Iterator<Item = &str> {
984 self.bindings.nonlocals.iter().map(AsRef::as_ref)
985 }
986
987 pub fn local_bindings(&self) -> impl Iterator<Item = (&str, Node<'tree>)> + '_ {
988 self.locals
989 .iter()
990 .map(|binding| (binding.name.as_ref(), binding.declaration))
991 }
992
993 fn record_local_node(&mut self, node: Node<'tree>, source: &str) {
994 let name = node_text(node, source);
995 self.record_local(name, node);
996 }
997
998 fn record_local(&mut self, name: &str, declaration: Node<'tree>) {
999 if name.is_empty() {
1000 return;
1001 }
1002 let binding_kind = if is_function_declaration_name(declaration) {
1003 PythonLocalBindingKind::FunctionOnly
1004 } else if declaration.parent().is_some_and(|parent| {
1005 parent.kind() == "class_definition"
1006 && brokk_bifrost_core::analyzer::tree_walk::is_field_child(
1007 parent,
1008 declaration,
1009 "name",
1010 )
1011 }) {
1012 PythonLocalBindingKind::ClassOnly
1013 } else {
1014 PythonLocalBindingKind::Other
1015 };
1016 self.record_local_kind(name, declaration, binding_kind);
1017 }
1018
1019 fn record_local_kind(
1020 &mut self,
1021 name: &str,
1022 declaration: Node<'tree>,
1023 binding_kind: PythonLocalBindingKind,
1024 ) {
1025 if name.is_empty() {
1026 return;
1027 }
1028 self.binding_nodes.push(declaration);
1029 match self.bindings.local_names.entry(name.into()) {
1030 std::collections::hash_map::Entry::Vacant(entry) => {
1031 entry.insert(binding_kind);
1032 self.locals.push(PythonLocalBinding {
1033 name: name.into(),
1034 declaration,
1035 });
1036 }
1037 std::collections::hash_map::Entry::Occupied(mut entry) => {
1038 entry.insert(PythonLocalBindingKind::Other);
1039 }
1040 }
1041 }
1042
1043 fn record_binding_write(&mut self, name: &str, declaration: Node<'tree>) {
1044 if !name.is_empty() {
1045 self.bindings.binding_writes.insert(name.into());
1046 }
1047 self.record_local(name, declaration);
1048 }
1049}
1050
1051pub fn python_global_binding_is_untouched_bounded(
1057 name: &str,
1058 reference: Node<'_>,
1059 global_scope: Node<'_>,
1060 source: &str,
1061 mut scope_step: impl FnMut() -> bool,
1062) -> Option<bool> {
1063 assert!(matches!(
1064 global_scope.kind(),
1065 "function_definition" | "lambda" | "class_definition"
1066 ));
1067 let mut current = Some(global_scope);
1068 while let Some(scope) = current {
1069 if !scope_step() {
1070 return None;
1071 }
1072 if matches!(
1073 scope.kind(),
1074 "function_definition" | "lambda" | "class_definition"
1075 ) && scope.child_by_field_name("body").is_some_and(|body| {
1076 body.start_byte() <= reference.start_byte() && reference.end_byte() <= body.end_byte()
1077 }) {
1078 let inventory = if scope.kind() == "class_definition" {
1079 PythonClassReferenceScope::new_bounded(scope, reference, &mut scope_step)?
1080 .collect_bounded(source, &mut scope_step)?
1081 } else {
1082 PythonLexicalScopeInventory::collect_bounded(
1083 scope,
1084 source,
1085 std::iter::empty::<String>(),
1086 &mut scope_step,
1087 )?
1088 };
1089 if inventory.bindings.has_global_binding_event(name) {
1090 return Some(false);
1091 }
1092 }
1093 current = scope.parent();
1094 }
1095 Some(true)
1096}
1097
1098fn is_function_declaration_name(node: Node<'_>) -> bool {
1099 node.parent().is_some_and(|parent| {
1100 parent.kind() == "function_definition"
1101 && brokk_bifrost_core::analyzer::tree_walk::is_field_child(parent, node, "name")
1102 })
1103}
1104
1105pub fn python_binding_targets_bounded<'tree>(
1109 target: Node<'tree>,
1110 source: &str,
1111 mut scope_step: impl FnMut() -> bool,
1112) -> Option<Vec<PythonDirectScopeBinding<'tree>>> {
1113 let mut bindings = Vec::new();
1114 collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
1115 bindings.push(PythonDirectScopeBinding {
1116 declaration,
1117 kind: PythonDirectScopeBindingKind::Other,
1118 });
1119 })?;
1120 Some(bindings)
1121}
1122
1123pub fn python_direct_scope_bindings_bounded<'tree>(
1130 node: Node<'tree>,
1131 source: &str,
1132 mut scope_step: impl FnMut() -> bool,
1133) -> Option<Vec<PythonDirectScopeBinding<'tree>>> {
1134 let mut bindings = Vec::new();
1135
1136 match node.kind() {
1137 "function_definition" => {
1138 if let Some(name) = node.child_by_field_name("name") {
1139 bindings.push(PythonDirectScopeBinding {
1140 declaration: name,
1141 kind: if is_direct_module_definition_bounded(node, &mut scope_step)? {
1142 PythonDirectScopeBindingKind::FunctionDeclaration
1143 } else {
1144 PythonDirectScopeBindingKind::Other
1145 },
1146 });
1147 }
1148 }
1149 "class_definition" => {
1150 if let Some(name) = node.child_by_field_name("name") {
1151 bindings.push(PythonDirectScopeBinding {
1152 declaration: name,
1153 kind: if is_direct_module_definition_bounded(node, &mut scope_step)? {
1154 PythonDirectScopeBindingKind::ClassDeclaration
1155 } else {
1156 PythonDirectScopeBindingKind::Other
1157 },
1158 });
1159 }
1160 }
1161 "import_statement" | "import_from_statement" => {
1162 let module_level = is_direct_module_definition_bounded(node, &mut scope_step)?;
1163 collect_import_bindings(node, &mut scope_step, |declaration| {
1164 bindings.push(PythonDirectScopeBinding {
1165 declaration,
1166 kind: if module_level {
1167 PythonDirectScopeBindingKind::Import
1168 } else {
1169 PythonDirectScopeBindingKind::Other
1170 },
1171 });
1172 })?;
1173 }
1174 "assignment" | "annotated_assignment" | "augmented_assignment" => {
1175 if let Some(target) = node.child_by_field_name("left") {
1176 bindings.extend(python_binding_targets_bounded(
1177 target,
1178 source,
1179 &mut scope_step,
1180 )?);
1181 }
1182 }
1183 "type_alias_statement" => {
1184 if let Some(target) = node.child_by_field_name("left")
1185 && let Some(declaration) = first_identifier_bounded(target, &mut scope_step)?
1186 {
1187 bindings.push(PythonDirectScopeBinding {
1188 declaration,
1189 kind: PythonDirectScopeBindingKind::Other,
1190 });
1191 }
1192 }
1193 "named_expression" => {
1194 if let Some(target) = node.child_by_field_name("name") {
1195 bindings.extend(python_binding_targets_bounded(
1196 target,
1197 source,
1198 &mut scope_step,
1199 )?);
1200 }
1201 }
1202 "for_statement" | "async_for_statement" => {
1203 if let Some(target) = node.child_by_field_name("left") {
1204 bindings.extend(python_binding_targets_bounded(
1205 target,
1206 source,
1207 &mut scope_step,
1208 )?);
1209 }
1210 }
1211 "with_statement" => {
1212 let mut cursor = node.walk();
1213 for item in node
1214 .named_children(&mut cursor)
1215 .filter(|item| item.kind() == "with_item")
1216 {
1217 if let Some(alias) = item.child_by_field_name("alias") {
1218 collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
1219 bindings.push(PythonDirectScopeBinding {
1220 declaration,
1221 kind: PythonDirectScopeBindingKind::Other,
1222 });
1223 })?;
1224 }
1225 }
1226 }
1227 "delete_statement" => {
1228 for target in named_children_bounded(node, &mut scope_step)? {
1229 bindings.extend(python_binding_targets_bounded(
1230 target,
1231 source,
1232 &mut scope_step,
1233 )?);
1234 }
1235 }
1236 "as_pattern" => {
1237 if let Some(alias) = node.child_by_field_name("alias") {
1238 collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
1239 bindings.push(PythonDirectScopeBinding {
1240 declaration,
1241 kind: PythonDirectScopeBindingKind::Other,
1242 });
1243 })?;
1244 }
1245 }
1246 "except_clause" => {
1247 if let Some(alias) = node.child_by_field_name("alias") {
1248 collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
1249 bindings.push(PythonDirectScopeBinding {
1250 declaration,
1251 kind: PythonDirectScopeBindingKind::Other,
1252 });
1253 })?;
1254 }
1255 }
1256 "case_clause" => {
1257 for child in named_children_bounded(node, &mut scope_step)? {
1258 if child.kind() == "case_pattern" {
1259 collect_match_pattern_bindings(
1260 child,
1261 source,
1262 &mut scope_step,
1263 |_, declaration| {
1264 bindings.push(PythonDirectScopeBinding {
1265 declaration,
1266 kind: PythonDirectScopeBindingKind::Other,
1267 });
1268 },
1269 )?;
1270 }
1271 }
1272 }
1273 _ => {}
1274 }
1275 Some(bindings)
1276}
1277
1278pub fn python_module_or_class_scope_binds_name_bounded(
1283 scope: Node<'_>,
1284 target_name: &str,
1285 source: &str,
1286 scope_step: impl FnMut() -> bool,
1287) -> Option<bool> {
1288 python_scope_binds_name_with_filter_bounded(scope, target_name, source, |_| true, scope_step)
1289}
1290
1291fn python_scope_binds_name_with_filter_bounded<'tree>(
1292 scope: Node<'tree>,
1293 target_name: &str,
1294 source: &str,
1295 include_binding: impl Fn(Node<'tree>) -> bool,
1296 mut scope_step: impl FnMut() -> bool,
1297) -> Option<bool> {
1298 assert!(matches!(scope.kind(), "module" | "class_definition"));
1299 let body = scope.child_by_field_name("body").unwrap_or(scope);
1300 let mut stack = vec![body];
1301 while let Some(node) = stack.pop() {
1302 if !scope_step() {
1303 return None;
1304 }
1305 if node.kind() == "wildcard_import" {
1306 return Some(true);
1307 }
1308 if python_direct_scope_bindings_bounded(node, source, &mut scope_step)?
1309 .into_iter()
1310 .any(|binding| {
1311 include_binding(binding.declaration)
1312 && node_text(binding.declaration, source) == target_name
1313 })
1314 {
1315 return Some(true);
1316 }
1317 stack.extend(python_executable_scope_children(node));
1318 }
1319 Some(false)
1320}
1321
1322pub fn python_executable_scope_children<'tree>(
1324 node: Node<'tree>,
1325) -> impl Iterator<Item = Node<'tree>> {
1326 let excluded_body = matches!(
1327 node.kind(),
1328 "class_definition" | "function_definition" | "lambda"
1329 )
1330 .then(|| node.child_by_field_name("body").map(|body| body.id()))
1331 .flatten();
1332 named_children_iter(node).filter(move |child| Some(child.id()) != excluded_body)
1333}
1334
1335pub fn python_unambiguous_module_class_binding_bounded(
1336 root: Node<'_>,
1337 source: &str,
1338 target_name: &str,
1339 scope_step: impl FnMut() -> bool,
1340) -> Option<bool> {
1341 Some(
1342 python_unambiguous_module_binding_bounded(root, source, target_name, scope_step)?
1343 == Some(PythonDirectScopeBindingKind::ClassDeclaration),
1344 )
1345}
1346
1347pub fn python_unambiguous_module_binding_bounded(
1354 root: Node<'_>,
1355 source: &str,
1356 target_name: &str,
1357 scope_step: impl FnMut() -> bool,
1358) -> Option<Option<PythonDirectScopeBindingKind>> {
1359 Some(
1360 python_unambiguous_module_binding_node_bounded(root, source, target_name, scope_step)?
1361 .map(|binding| binding.kind),
1362 )
1363}
1364
1365pub fn python_unambiguous_module_binding_node_bounded<'tree>(
1372 root: Node<'tree>,
1373 source: &str,
1374 target_name: &str,
1375 mut scope_step: impl FnMut() -> bool,
1376) -> Option<Option<PythonDirectScopeBinding<'tree>>> {
1377 let mut matched = None;
1378 let mut stack = vec![root];
1379 while let Some(node) = stack.pop() {
1380 if !scope_step() {
1381 return None;
1382 }
1383 if node.kind() == "wildcard_import" && matched.is_some() {
1386 return Some(None);
1387 }
1388 for binding in python_direct_scope_bindings_bounded(node, source, &mut scope_step)? {
1389 if node_text(binding.declaration, source) != target_name {
1390 continue;
1391 }
1392 if matched.is_some() {
1393 return Some(None);
1394 }
1395 matched = Some(binding);
1396 }
1397
1398 let body = matches!(
1399 node.kind(),
1400 "function_definition" | "class_definition" | "lambda"
1401 )
1402 .then(|| node.child_by_field_name("body").map(|child| child.id()))
1403 .flatten();
1404 let name = matches!(node.kind(), "function_definition" | "class_definition")
1405 .then(|| node.child_by_field_name("name").map(|child| child.id()))
1406 .flatten();
1407 for child in named_children_bounded(node, &mut scope_step)?
1408 .into_iter()
1409 .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
1410 .rev()
1411 {
1412 stack.push(child);
1413 }
1414 }
1415 Some(matched)
1416}
1417
1418fn is_direct_module_definition_bounded(
1419 node: Node<'_>,
1420 scope_step: &mut impl FnMut() -> bool,
1421) -> Option<bool> {
1422 if !scope_step() {
1423 return None;
1424 }
1425 let Some(mut parent) = node.parent() else {
1426 return Some(false);
1427 };
1428 if parent.kind() == "decorated_definition" {
1429 if !scope_step() {
1430 return None;
1431 }
1432 let Some(grandparent) = parent.parent() else {
1433 return Some(false);
1434 };
1435 parent = grandparent;
1436 }
1437 Some(parent.kind() == "module")
1438}
1439
1440fn collect_direct_identifier_names(
1441 node: Node<'_>,
1442 source: &str,
1443 scope_step: &mut impl FnMut() -> bool,
1444 mut record: impl FnMut(&str),
1445) -> Option<()> {
1446 for child in named_children_bounded(node, scope_step)? {
1447 if child.kind() == "identifier" {
1448 let name = node_text(child, source);
1449 if !name.is_empty() {
1450 record(name);
1451 }
1452 }
1453 }
1454 Some(())
1455}
1456
1457fn collect_import_bindings<'tree>(
1458 statement: Node<'tree>,
1459 scope_step: &mut impl FnMut() -> bool,
1460 mut record: impl FnMut(Node<'tree>),
1461) -> Option<()> {
1462 let mut cursor = statement.walk();
1463 let mut imports = Vec::new();
1464 for imported in statement.children_by_field_name("name", &mut cursor) {
1465 if !scope_step() {
1466 return None;
1467 }
1468 imports.push(imported);
1469 }
1470 for imported in imports {
1471 if let Some(alias) = imported.child_by_field_name("alias") {
1472 record(alias);
1473 continue;
1474 }
1475 let name = imported.child_by_field_name("name").unwrap_or(imported);
1476 let binding = if statement.kind() == "import_statement" {
1477 first_identifier_bounded(name, scope_step)?
1478 } else {
1479 last_identifier_bounded(name, scope_step)?
1480 };
1481 if let Some(binding) = binding {
1482 record(binding);
1483 }
1484 }
1485 Some(())
1486}
1487
1488fn first_identifier_bounded<'tree>(
1489 root: Node<'tree>,
1490 scope_step: &mut impl FnMut() -> bool,
1491) -> Option<Option<Node<'tree>>> {
1492 let mut stack = vec![root];
1493 while let Some(node) = stack.pop() {
1494 if !scope_step() {
1495 return None;
1496 }
1497 if node.kind() == "identifier" {
1498 return Some(Some(node));
1499 }
1500 let children = named_children_bounded(node, scope_step)?;
1501 stack.extend(children.into_iter().rev());
1502 }
1503 Some(None)
1504}
1505
1506fn last_identifier_bounded<'tree>(
1507 root: Node<'tree>,
1508 scope_step: &mut impl FnMut() -> bool,
1509) -> Option<Option<Node<'tree>>> {
1510 let mut result = None;
1511 let mut stack = vec![root];
1512 while let Some(node) = stack.pop() {
1513 if !scope_step() {
1514 return None;
1515 }
1516 if node.kind() == "identifier" {
1517 result = Some(node);
1518 continue;
1519 }
1520 let children = named_children_bounded(node, scope_step)?;
1521 stack.extend(children.into_iter().rev());
1522 }
1523 Some(result)
1524}
1525
1526pub(crate) fn collect_binding_targets<'tree>(
1527 target: Node<'tree>,
1528 source: &str,
1529 scope_step: &mut impl FnMut() -> bool,
1530 mut record: impl FnMut(&str, Node<'tree>),
1531) -> Option<()> {
1532 let mut stack = vec![target];
1533 while let Some(node) = stack.pop() {
1534 if !scope_step() {
1535 return None;
1536 }
1537 match node.kind() {
1538 "attribute" | "subscript" => continue,
1541 "identifier" | "keyword_identifier" => {
1542 let name = node_text(node, source);
1543 if !name.is_empty() {
1544 record(name, node);
1545 }
1546 continue;
1547 }
1548 _ => {}
1549 }
1550 let children = named_children_bounded(node, scope_step)?;
1551 if node.kind() == "as_pattern_target" && children.is_empty() {
1552 let name = node_text(node, source);
1553 if !name.is_empty() {
1554 record(name, node);
1555 }
1556 continue;
1557 }
1558 stack.extend(children.into_iter().rev());
1559 }
1560 Some(())
1561}
1562
1563fn collect_match_pattern_bindings<'tree>(
1564 pattern: Node<'tree>,
1565 source: &str,
1566 scope_step: &mut impl FnMut() -> bool,
1567 mut record: impl FnMut(&str, Node<'tree>),
1568) -> Option<()> {
1569 let mut stack = vec![pattern];
1570 while let Some(node) = stack.pop() {
1571 if !scope_step() {
1572 return None;
1573 }
1574 match node.kind() {
1575 "dotted_name" => {
1576 let identifiers = named_children_bounded(node, scope_step)?
1577 .into_iter()
1578 .filter(|child| child.kind() == "identifier")
1579 .collect::<Vec<_>>();
1580 if let [binding] = identifiers.as_slice() {
1581 let name = node_text(*binding, source);
1582 if !name.is_empty() {
1583 record(name, *binding);
1584 }
1585 }
1586 continue;
1587 }
1588 "splat_pattern" => {
1589 for child in named_children_bounded(node, scope_step)? {
1590 if child.kind() == "identifier" {
1591 let name = node_text(child, source);
1592 if !name.is_empty() {
1593 record(name, child);
1594 }
1595 }
1596 }
1597 continue;
1598 }
1599 "class_pattern" => {
1600 let mut children = named_children_bounded(node, scope_step)?;
1601 if children
1602 .first()
1603 .is_some_and(|child| child.kind() == "dotted_name")
1604 {
1605 children.remove(0);
1606 }
1607 stack.extend(children.into_iter().rev());
1608 continue;
1609 }
1610 "keyword_pattern" => {
1611 let mut children = named_children_bounded(node, scope_step)?;
1612 if children
1613 .first()
1614 .is_some_and(|child| child.kind() == "identifier")
1615 {
1616 children.remove(0);
1617 }
1618 stack.extend(children.into_iter().rev());
1619 continue;
1620 }
1621 "dict_pattern" => {
1622 let key_ids = children_by_field_name_bounded(node, "key", scope_step)?
1623 .into_iter()
1624 .map(|key| key.id())
1625 .collect::<HashSet<_>>();
1626 let children = named_children_bounded(node, scope_step)?;
1627 stack.extend(
1628 children
1629 .into_iter()
1630 .filter(|child| !key_ids.contains(&child.id()))
1631 .rev(),
1632 );
1633 continue;
1634 }
1635 "as_pattern" => {
1636 if let Some(alias) = node.child_by_field_name("alias") {
1637 collect_binding_targets(alias, source, scope_step, &mut record)?;
1638 let children = named_children_bounded(node, scope_step)?;
1639 stack.extend(
1640 children
1641 .into_iter()
1642 .filter(|child| child.id() != alias.id())
1643 .rev(),
1644 );
1645 continue;
1646 }
1647 let mut children = named_children_bounded(node, scope_step)?;
1648 if let Some(alias) = children
1649 .last()
1650 .copied()
1651 .filter(|child| child.kind() == "identifier")
1652 {
1653 let name = node_text(alias, source);
1654 if !name.is_empty() {
1655 record(name, alias);
1656 }
1657 children.pop();
1658 }
1659 stack.extend(children.into_iter().rev());
1660 continue;
1661 }
1662 "identifier" => {
1663 continue;
1666 }
1667 kind if is_pattern_literal(kind) => continue,
1668 _ => {}
1669 }
1670 let children = named_children_bounded(node, scope_step)?;
1671 stack.extend(children.into_iter().rev());
1672 }
1673 Some(())
1674}
1675
1676fn push_named_children<'tree>(
1677 stack: &mut Vec<Node<'tree>>,
1678 node: Node<'tree>,
1679 scope_step: &mut impl FnMut() -> bool,
1680) -> Option<()> {
1681 let children = named_children_bounded(node, scope_step)?;
1682 push_bounded_children(stack, children.into_iter());
1683 Some(())
1684}
1685
1686fn push_named_children_except<'tree>(
1687 stack: &mut Vec<Node<'tree>>,
1688 node: Node<'tree>,
1689 excluded: Node<'tree>,
1690 scope_step: &mut impl FnMut() -> bool,
1691) -> Option<()> {
1692 let children = named_children_bounded(node, scope_step)?;
1693 push_bounded_children(
1694 stack,
1695 children
1696 .into_iter()
1697 .filter(|child| child.id() != excluded.id()),
1698 );
1699 Some(())
1700}
1701
1702fn push_nested_scope_header_children<'tree>(
1703 stack: &mut Vec<Node<'tree>>,
1704 node: Node<'tree>,
1705 scope_step: &mut impl FnMut() -> bool,
1706) -> Option<()> {
1707 let body = node.child_by_field_name("body").map(|child| child.id());
1708 let name = node.child_by_field_name("name").map(|child| child.id());
1709 let mut cursor = node.walk();
1712 let children = node
1713 .named_children(&mut cursor)
1714 .filter(|child| Some(child.id()) != body);
1715 let mut headers = Vec::new();
1716 push_bounded_nodes(children, &mut headers, scope_step).then_some(())?;
1717 push_bounded_children(
1718 stack,
1719 headers.into_iter().filter(|child| Some(child.id()) != name),
1720 );
1721 Some(())
1722}
1723
1724fn push_bounded_children<'tree>(
1725 stack: &mut Vec<Node<'tree>>,
1726 children: impl DoubleEndedIterator<Item = Node<'tree>>,
1727) {
1728 for child in children.rev() {
1729 stack.push(child);
1730 }
1731}
1732
1733fn named_children_bounded<'tree>(
1734 node: Node<'tree>,
1735 scope_step: &mut impl FnMut() -> bool,
1736) -> Option<Vec<Node<'tree>>> {
1737 let mut cursor = node.walk();
1738 let mut children = Vec::new();
1739 push_bounded_nodes(node.named_children(&mut cursor), &mut children, scope_step)
1740 .then_some(children)
1741}
1742
1743fn children_by_field_name_bounded<'tree>(
1744 node: Node<'tree>,
1745 field: &str,
1746 scope_step: &mut impl FnMut() -> bool,
1747) -> Option<Vec<Node<'tree>>> {
1748 let mut cursor = node.walk();
1749 let mut children = Vec::new();
1750 push_bounded_nodes(
1751 node.children_by_field_name(field, &mut cursor),
1752 &mut children,
1753 scope_step,
1754 )
1755 .then_some(children)
1756}
1757
1758pub(crate) fn is_comprehension(kind: &str) -> bool {
1759 matches!(
1760 kind,
1761 "list_comprehension"
1762 | "set_comprehension"
1763 | "dictionary_comprehension"
1764 | "generator_expression"
1765 )
1766}
1767
1768fn is_pattern_literal(kind: &str) -> bool {
1769 matches!(
1770 kind,
1771 "string"
1772 | "concatenated_string"
1773 | "integer"
1774 | "float"
1775 | "complex_pattern"
1776 | "true"
1777 | "false"
1778 | "none"
1779 )
1780}
1781
1782#[cfg(test)]
1783mod tests {
1784 use brokk_bifrost_test_macros::hardwon;
1785
1786 #[test]
1787 fn only_the_first_comprehension_iterable_uses_the_enclosing_scope() {
1788 let source = "def run(first, second):\n return [second for first in first for second in second]\n";
1789 let mut parser = tree_sitter::Parser::new();
1790 parser
1791 .set_language(&tree_sitter_python::LANGUAGE.into())
1792 .unwrap();
1793 let tree = parser.parse(source, None).unwrap();
1794 let mut stack = vec![tree.root_node()];
1795 let comprehension = loop {
1796 let node = stack.pop().expect("fixture contains a comprehension");
1797 if node.kind() == "list_comprehension" {
1798 break node;
1799 }
1800 let mut cursor = node.walk();
1801 stack.extend(node.named_children(&mut cursor));
1802 };
1803 let mut cursor = comprehension.walk();
1804 let clauses = comprehension
1805 .named_children(&mut cursor)
1806 .filter(|node| node.kind() == "for_in_clause")
1807 .collect::<Vec<_>>();
1808 let outer = clauses[0].child_by_field_name("right").unwrap();
1809 let inner = clauses[1].child_by_field_name("right").unwrap();
1810 assert!(!super::python_comprehension_binds_name_at(
1811 "first", outer, source
1812 ));
1813 assert!(super::python_comprehension_binds_name_at(
1814 "second", inner, source
1815 ));
1816 }
1817
1818 #[test]
1819 #[hardwon(
1820 "#5044+#5511+#5751: bounded lexical proofs bypassed stops, lazy bindings or competing events"
1821 )]
1822 fn bounded_comprehension_lookup_preserves_exhaustion() {
1823 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";
1824 let tree = crate::declarations::parse_python_tree(source).unwrap();
1825 let function = tree.root_node().named_child(0).unwrap();
1826 let inventory = super::PythonLexicalScopeInventory::collect_bounded(
1827 function,
1828 source,
1829 ["xs".to_owned()],
1830 || true,
1831 )
1832 .unwrap();
1833 let mut stack = vec![function];
1834 let comprehension = loop {
1835 let node = stack.pop().expect("fixture contains a comprehension");
1836 if node.kind() == "list_comprehension" {
1837 break node;
1838 }
1839 stack.extend(brokk_bifrost_core::analyzer::tree_walk::named_children_iter(node));
1840 };
1841 let reference = comprehension.named_child(0).unwrap();
1842 for mut remaining in [0_usize, 1] {
1843 assert_eq!(
1844 inventory.name_resolution_at_bounded("target", reference, || {
1845 let available = remaining > 0;
1846 remaining = remaining.saturating_sub(1);
1847 available
1848 }),
1849 None
1850 );
1851 }
1852 assert_eq!(
1853 inventory.name_resolution_at_bounded("target", reference, || true),
1854 Some(super::PythonLexicalNameResolution::Local)
1855 );
1856 let body = function.child_by_field_name("body").unwrap();
1857 let derived = brokk_bifrost_core::analyzer::tree_walk::named_children_iter(body)
1858 .find(|node| node.child_by_field_name("superclasses").is_some())
1859 .unwrap();
1860 let base_reference = derived
1861 .child_by_field_name("superclasses")
1862 .unwrap()
1863 .named_child(0)
1864 .unwrap();
1865 let call = brokk_bifrost_core::analyzer::tree_walk::named_children_iter(body)
1866 .filter(|node| node.kind() == "expression_statement")
1867 .find_map(|node| node.named_child(0).filter(|node| node.kind() == "call"))
1868 .unwrap();
1869 let helper_reference = call.child_by_field_name("function").unwrap();
1870 let one_step = || {
1873 let mut available = true;
1874 move || std::mem::take(&mut available)
1875 };
1876 assert!(
1877 inventory
1878 .has_runtime_callable_binding_at_bounded("helper", helper_reference, one_step())
1879 .is_none()
1880 );
1881 assert!(
1882 inventory
1883 .local_function_declaration_bounded("helper", helper_reference, one_step())
1884 .is_none()
1885 );
1886 assert!(
1887 inventory
1888 .local_class_declaration_bounded("Base", base_reference, one_step())
1889 .is_none()
1890 );
1891 assert_eq!(
1892 inventory
1893 .local_function_declaration_bounded("helper", helper_reference, || true)
1894 .unwrap()
1895 .kind(),
1896 "function_definition"
1897 );
1898 assert_eq!(
1899 inventory
1900 .local_class_declaration_bounded("Base", base_reference, || true)
1901 .unwrap()
1902 .kind(),
1903 "class_definition"
1904 );
1905 let bindings = inventory.into_bindings();
1906 assert_eq!(
1907 bindings.name_resolution_at_bounded("missing", reference, one_step()),
1908 None,
1909 "stopped lookup must not manufacture Unbound"
1910 );
1911 assert_eq!(
1912 bindings.name_resolution_at_bounded("missing", reference, || true),
1913 Some(super::PythonLexicalNameResolution::Unbound)
1914 );
1915 assert_eq!(
1916 bindings.resolves_to_local_function_bounded("helper", helper_reference, one_step()),
1917 None
1918 );
1919 assert_eq!(
1920 bindings.resolves_to_local_function_bounded("helper", helper_reference, || true),
1921 Some(true)
1922 );
1923 assert_eq!(
1924 super::python_comprehension_binds_name_at_bounded(
1925 "target",
1926 reference,
1927 source,
1928 one_step()
1929 ),
1930 None
1931 );
1932 assert_eq!(
1933 super::python_comprehension_binds_name_at_bounded("target", reference, source, || true),
1934 Some(true)
1935 );
1936 let clause = brokk_bifrost_core::analyzer::tree_walk::named_children_iter(comprehension)
1937 .find(|node| node.kind() == "for_in_clause")
1938 .unwrap();
1939 let targets = super::python_binding_targets_bounded(
1940 clause.child_by_field_name("left").unwrap(),
1941 source,
1942 || true,
1943 )
1944 .unwrap();
1945 assert_eq!(targets.len(), 1);
1946 assert_eq!(
1947 Some(targets[0].declaration),
1948 clause.child_by_field_name("left")
1949 );
1950
1951 use super::PythonDirectScopeBindingKind::{ClassDeclaration, Other};
1955 for (source, expected_kind) in [
1956 ("Alias = Base\ntrailing = None\n", Some(Other)),
1957 ("Alias = Base\nAlias = Other\n", None),
1958 ("Alias = Base\ndel Alias\n", None),
1959 ("Alias = Base\nfrom pkg import Target as Alias\n", None),
1960 ("Alias = Base\nfrom pkg import *\n", None),
1961 ("from pkg import *\nAlias = Base\n", Some(Other)),
1962 (
1963 "Alias = Base\ndef inner():\n Alias = Other\nclass Container:\n Alias = Other\n",
1964 Some(Other),
1965 ),
1966 (
1967 "Alias = Base\ndef inner(value=(Alias := Other)):\n pass\n",
1968 None,
1969 ),
1970 ("class Alias:\n pass\n", Some(ClassDeclaration)),
1971 (
1972 "from pkg import *\nclass Alias:\n pass\n",
1973 Some(ClassDeclaration),
1974 ),
1975 ("class Alias:\n pass\nfrom pkg import *\n", None),
1976 ("Other = Base\n", None),
1977 ] {
1978 let tree = crate::declarations::parse_python_tree(source).unwrap();
1979 let root = tree.root_node();
1980 assert!(!root.has_error(), "{source}");
1981 let mut steps = 0_usize;
1982 let binding = super::python_unambiguous_module_binding_node_bounded(
1983 root,
1984 source,
1985 "Alias",
1986 || {
1987 steps += 1;
1988 true
1989 },
1990 )
1991 .expect("uninterrupted binding proof completes");
1992 assert_eq!(
1993 binding.map(|binding| binding.kind),
1994 expected_kind,
1995 "{source}"
1996 );
1997 if let Some(binding) = binding {
1998 assert_eq!(binding.declaration.kind(), "identifier");
1999 assert_eq!(super::node_text(binding.declaration, source), "Alias");
2000 let parent = binding.declaration.parent().unwrap();
2001 let field = if binding.kind == ClassDeclaration {
2002 "name"
2003 } else {
2004 "left"
2005 };
2006 assert_eq!(parent.child_by_field_name(field), Some(binding.declaration));
2007 let statement = if binding.kind == ClassDeclaration {
2008 parent
2009 } else {
2010 parent.parent().unwrap()
2011 };
2012 assert_eq!(statement.parent(), Some(root), "exact module-owned binder");
2013 }
2014 assert_eq!(
2015 super::python_unambiguous_module_binding_bounded(root, source, "Alias", || true),
2016 Some(expected_kind),
2017 "kind-only consumers retain the same positive requirement: {source}"
2018 );
2019 assert_eq!(
2020 super::python_unambiguous_module_class_binding_bounded(
2021 root,
2022 source,
2023 "Alias",
2024 || true
2025 ),
2026 Some(expected_kind == Some(ClassDeclaration))
2027 );
2028 for mut remaining in 0..steps {
2029 assert!(
2030 super::python_unambiguous_module_binding_node_bounded(
2031 root,
2032 source,
2033 "Alias",
2034 || {
2035 let available = remaining > 0;
2036 remaining = remaining.saturating_sub(1);
2037 available
2038 },
2039 )
2040 .is_none(),
2041 "a stopped proof cannot publish its early binder: {source}"
2042 );
2043 }
2044 }
2045
2046 let source = "import ns_source as ns\nclass Outer:\n class ns[T: ns.Target]((ns.Target)):\n pass\n";
2049 let tree = crate::declarations::parse_python_tree(source).unwrap();
2050 assert!(!tree.root_node().has_error());
2051 let outer = brokk_bifrost_core::analyzer::tree_walk::first_named_child_of_kind(
2052 tree.root_node(),
2053 "class_definition",
2054 )
2055 .unwrap();
2056 let inner = brokk_bifrost_core::analyzer::tree_walk::first_named_child_of_kind(
2057 outer.child_by_field_name("body").unwrap(),
2058 "class_definition",
2059 )
2060 .unwrap();
2061 let base = inner
2062 .child_by_field_name("superclasses")
2063 .unwrap()
2064 .named_child(0)
2065 .unwrap();
2066 let immediate = crate::syntax::python_static_value_path_bounded(base, || true)
2067 .unwrap()
2068 .unwrap()[0];
2069 let bound =
2070 brokk_bifrost_core::analyzer::tree_walk::first_named_descendant_of_kind_inclusive(
2071 inner.child_by_field_name("type_parameters").unwrap(),
2072 "attribute",
2073 )
2074 .unwrap()
2075 .child_by_field_name("object")
2076 .unwrap();
2077 for (reference, resolution, binds) in [
2078 (
2079 immediate,
2080 super::PythonLexicalNameResolution::Unbound,
2081 false,
2082 ),
2083 (bound, super::PythonLexicalNameResolution::Local, true),
2084 ] {
2085 assert!(
2086 super::PythonClassReferenceScope::new_bounded(outer, reference, || false).is_none()
2087 );
2088 let scope =
2089 super::PythonClassReferenceScope::new_bounded(outer, reference, || true).unwrap();
2090 let inventory = scope.collect_bounded(source, || true).unwrap();
2091 assert_eq!(
2092 inventory.name_resolution_at_bounded("ns", reference, || true),
2093 Some(resolution)
2094 );
2095 assert_eq!(scope.binds_name_bounded("ns", source, || true), Some(binds));
2096 }
2097 }
2098}