1use tree_sitter::Node;
2
3use brokk_bifrost_core::hash::{HashMap, HashSet};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum PythonLexicalNameResolution {
7 Unbound,
8 Local,
9 Nonlocal,
10 Global,
11}
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum PythonDirectScopeBindingKind {
15 ClassDeclaration,
16 FunctionDeclaration,
17 Import,
18 Other,
19}
20
21pub fn python_comprehension_binds_name_at(name: &str, reference: Node<'_>, source: &str) -> bool {
29 let mut current = reference;
30 while let Some(parent) = current.parent() {
31 if is_comprehension(parent.kind()) {
32 let mut cursor = parent.walk();
33 let clauses = parent
34 .named_children(&mut cursor)
35 .filter(|child| child.kind() == "for_in_clause")
36 .collect::<Vec<_>>();
37 let in_outer_iterable = clauses.first().is_some_and(|clause| {
38 let mut cursor = clause.walk();
39 clause
40 .children_by_field_name("right", &mut cursor)
41 .any(|right| {
42 right.start_byte() <= reference.start_byte()
43 && reference.end_byte() <= right.end_byte()
44 })
45 });
46 if in_outer_iterable {
47 current = parent;
48 continue;
49 }
50 for clause in clauses {
51 let Some(target) = clause.child_by_field_name("left") else {
52 continue;
53 };
54 let mut bound = false;
55 collect_binding_targets(target, source, &mut || true, |candidate, _| {
56 bound |= candidate == name;
57 })
58 .expect("unbounded comprehension target collection completes");
59 if bound {
60 return true;
61 }
62 }
63 }
64 if matches!(
65 parent.kind(),
66 "function_definition" | "lambda" | "class_definition" | "module"
67 ) {
68 break;
69 }
70 current = parent;
71 }
72 false
73}
74
75pub fn python_type_parameter_binds_name_at(name: &str, reference: Node<'_>, source: &str) -> bool {
82 let mut current = reference;
83 let mut enclosing_functions = Vec::new();
84 while let Some(parent) = current.parent() {
85 if parent.kind() == "function_definition" {
86 enclosing_functions.push(parent);
87 }
88 if matches!(
89 parent.kind(),
90 "class_definition" | "function_definition" | "type_alias_statement"
91 ) && let Some(parameters) = parent.child_by_field_name("type_parameters")
92 && !(parameters.start_byte() <= reference.start_byte()
93 && reference.end_byte() <= parameters.end_byte())
94 {
95 let mut cursor = parameters.walk();
96 for parameter in parameters.named_children(&mut cursor) {
97 let binder = if parameter.kind() == "identifier" {
98 Some(parameter)
99 } else {
100 first_identifier_bounded(parameter, &mut || true).flatten()
101 };
102 if binder.is_some_and(|binder| node_text(binder, source) == name) {
103 return !python_name_declared_global_at_in_functions(
108 name,
109 reference,
110 source,
111 enclosing_functions,
112 );
113 }
114 }
115 }
116 current = parent;
117 }
118 false
119}
120
121pub fn python_name_resolution_at(
125 name: &str,
126 reference: Node<'_>,
127 source: &str,
128) -> PythonLexicalNameResolution {
129 let mut functions = Vec::new();
130 let mut current = reference;
131 while let Some(parent) = current.parent() {
132 if parent.kind() == "function_definition" {
133 functions.push(parent);
134 }
135 current = parent;
136 }
137 python_name_resolution_at_in_functions(name, reference, source, functions)
138}
139
140fn python_name_declared_global_at_in_functions(
141 name: &str,
142 reference: Node<'_>,
143 source: &str,
144 functions: Vec<Node<'_>>,
145) -> bool {
146 python_name_resolution_at_in_functions(name, reference, source, functions)
147 == PythonLexicalNameResolution::Global
148}
149
150fn python_name_resolution_at_in_functions(
151 name: &str,
152 reference: Node<'_>,
153 source: &str,
154 functions: Vec<Node<'_>>,
155) -> PythonLexicalNameResolution {
156 for function in functions {
157 let parameter_names = function
158 .child_by_field_name("parameters")
159 .into_iter()
160 .flat_map(|parameters| {
161 let mut cursor = parameters.walk();
162 parameters
163 .named_children(&mut cursor)
164 .filter_map(|parameter| python_parameter_name(parameter, source))
165 .collect::<Vec<_>>()
166 })
167 .collect::<Vec<_>>();
168 let Some(inventory) =
169 PythonLexicalScopeInventory::collect_bounded(function, source, parameter_names, || {
170 true
171 })
172 else {
173 return PythonLexicalNameResolution::Unbound;
174 };
175 let resolution = inventory.name_resolution_at(name, reference);
176 match resolution {
177 PythonLexicalNameResolution::Global => return PythonLexicalNameResolution::Global,
178 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal => {
179 return resolution;
180 }
181 PythonLexicalNameResolution::Unbound => {}
182 }
183 }
184 PythonLexicalNameResolution::Unbound
185}
186
187fn python_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
188 match node.kind() {
189 "identifier" => Some(node_text(node, source).trim().to_string()),
190 "typed_parameter"
191 | "typed_default_parameter"
192 | "default_parameter"
193 | "list_splat_pattern"
194 | "dictionary_splat_pattern" => node
195 .child_by_field_name("name")
196 .or_else(|| {
197 let mut cursor = node.walk();
198 node.named_children(&mut cursor)
199 .find(|child| child.kind() == "identifier")
200 })
201 .and_then(|name| python_parameter_name(name, source)),
202 _ => None,
203 }
204 .filter(|name| !name.is_empty())
205}
206
207pub fn python_is_type_parameter_binder(node: Node<'_>) -> bool {
210 if node.kind() != "identifier" {
211 return false;
212 }
213 let mut current = node;
214 while let Some(parent) = current.parent() {
215 if matches!(
216 parent.kind(),
217 "class_definition" | "function_definition" | "type_alias_statement"
218 ) && let Some(parameters) = parent.child_by_field_name("type_parameters")
219 && parameters.start_byte() <= node.start_byte()
220 && node.end_byte() <= parameters.end_byte()
221 {
222 let mut cursor = parameters.walk();
223 return parameters.named_children(&mut cursor).any(|parameter| {
224 let binder = if parameter.kind() == "identifier" {
225 Some(parameter)
226 } else {
227 first_identifier_bounded(parameter, &mut || true).flatten()
228 };
229 binder.is_some_and(|binder| binder.id() == node.id())
230 });
231 }
232 if matches!(
233 parent.kind(),
234 "module" | "class_definition" | "function_definition" | "type_alias_statement"
235 ) {
236 return false;
237 }
238 current = parent;
239 }
240 false
241}
242
243#[derive(Clone, Copy, Debug)]
244pub struct PythonDirectScopeBinding<'tree> {
245 pub declaration: Node<'tree>,
246 pub kind: PythonDirectScopeBindingKind,
247}
248
249#[derive(Clone, Debug)]
250struct PythonLocalBinding<'tree> {
251 name: Box<str>,
252 declaration: Node<'tree>,
253}
254
255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256enum PythonLocalBindingKind {
257 FunctionOnly,
258 Other,
259}
260
261#[derive(Clone, Debug)]
262struct PythonComprehensionBinding {
263 name: Box<str>,
264 start_byte: usize,
265 end_byte: usize,
266 enclosing_iterable_ranges: Vec<(usize, usize)>,
267}
268
269pub struct PythonLexicalScopeInventory<'tree> {
276 locals: Vec<PythonLocalBinding<'tree>>,
277 bindings: PythonLexicalScopeBindings,
278}
279
280pub struct PythonLexicalScopeBindings {
283 parameters: HashSet<Box<str>>,
284 local_names: HashMap<Box<str>, PythonLocalBindingKind>,
285 binding_writes: HashSet<Box<str>>,
286 globals: HashSet<Box<str>>,
287 nonlocals: HashSet<Box<str>>,
288 comprehensions: Vec<PythonComprehensionBinding>,
289}
290
291impl PythonLexicalScopeBindings {
292 pub fn name_resolution_at(
293 &self,
294 name: &str,
295 reference: Node<'_>,
296 ) -> PythonLexicalNameResolution {
297 let reference_byte = reference.start_byte();
298 if self.comprehensions.iter().any(|binding| {
299 binding.name.as_ref() == name
300 && binding.start_byte <= reference_byte
301 && reference_byte < binding.end_byte
302 && !binding
303 .enclosing_iterable_ranges
304 .iter()
305 .any(|(start, end)| *start <= reference_byte && reference_byte < *end)
306 }) {
307 return PythonLexicalNameResolution::Local;
308 }
309 if self.nonlocals.contains(name) {
310 return PythonLexicalNameResolution::Nonlocal;
311 }
312 if self.globals.contains(name) {
313 return PythonLexicalNameResolution::Global;
314 }
315 if self.parameters.contains(name) || self.local_names.contains_key(name) {
316 PythonLexicalNameResolution::Local
317 } else {
318 PythonLexicalNameResolution::Unbound
319 }
320 }
321
322 pub fn resolves_to_local_function(&self, name: &str, reference: Node<'_>) -> bool {
323 let reference_byte = reference.start_byte();
324 !self.parameters.contains(name)
325 && !self.comprehensions.iter().any(|binding| {
326 binding.name.as_ref() == name
327 && binding.start_byte <= reference_byte
328 && reference_byte < binding.end_byte
329 && !binding
330 .enclosing_iterable_ranges
331 .iter()
332 .any(|(start, end)| *start <= reference_byte && reference_byte < *end)
333 })
334 && self.local_names.get(name) == Some(&PythonLocalBindingKind::FunctionOnly)
335 }
336
337 pub fn has_runtime_callable_binding_at(&self, name: &str, reference: Node<'_>) -> bool {
340 let reference_byte = reference.start_byte();
341 self.parameters.contains(name)
342 || self.binding_writes.contains(name)
343 || self.comprehensions.iter().any(|binding| {
344 binding.name.as_ref() == name
345 && binding.start_byte <= reference_byte
346 && reference_byte < binding.end_byte
347 && !binding
348 .enclosing_iterable_ranges
349 .iter()
350 .any(|(start, end)| *start <= reference_byte && reference_byte < *end)
351 })
352 }
353}
354
355#[derive(Clone, Copy)]
356struct ScanFrame<'tree> {
357 node: Node<'tree>,
358 in_comprehension: bool,
359}
360
361impl<'tree> PythonLexicalScopeInventory<'tree> {
362 pub fn collect_bounded(
369 callable: Node<'tree>,
370 source: &str,
371 parameter_names: impl IntoIterator<Item = String>,
372 mut scope_step: impl FnMut() -> bool,
373 ) -> Option<Self> {
374 let mut inventory = Self {
375 locals: Vec::new(),
376 bindings: PythonLexicalScopeBindings {
377 parameters: parameter_names.into_iter().map(Box::<str>::from).collect(),
378 local_names: HashMap::default(),
379 binding_writes: HashSet::default(),
380 globals: HashSet::default(),
381 nonlocals: HashSet::default(),
382 comprehensions: Vec::new(),
383 },
384 };
385 let Some(body) = callable.child_by_field_name("body") else {
386 return Some(inventory);
387 };
388 let mut stack = vec![ScanFrame {
389 node: body,
390 in_comprehension: false,
391 }];
392
393 while let Some(frame) = stack.pop() {
394 if !scope_step() {
395 return None;
396 }
397 let node = frame.node;
398 let nested_scope = node != body
399 && matches!(
400 node.kind(),
401 "function_definition" | "lambda" | "class_definition"
402 );
403 if nested_scope {
404 if matches!(node.kind(), "function_definition" | "class_definition")
405 && let Some(name) = node.child_by_field_name("name")
406 {
407 inventory.record_local_node(name, source);
408 }
409 push_nested_scope_header_children(
414 &mut stack,
415 node,
416 frame.in_comprehension,
417 &mut scope_step,
418 )?;
419 continue;
420 }
421
422 match node.kind() {
423 "global_statement" => {
424 collect_direct_identifier_names(node, source, &mut scope_step, |name| {
425 inventory.bindings.globals.insert(name.into());
426 })?;
427 continue;
428 }
429 "nonlocal_statement" => {
430 collect_direct_identifier_names(node, source, &mut scope_step, |name| {
431 inventory.bindings.nonlocals.insert(name.into());
432 })?;
433 continue;
434 }
435 "import_statement" | "import_from_statement" => {
436 collect_import_bindings(node, &mut scope_step, |binding| {
437 inventory.record_local_node(binding, source)
438 })?;
439 continue;
440 }
441 "assignment" | "augmented_assignment" => {
442 if let Some(target) = node.child_by_field_name("left") {
443 collect_binding_targets(
444 target,
445 source,
446 &mut scope_step,
447 |name, declaration| {
448 inventory.record_binding_write(name, declaration);
449 },
450 )?;
451 }
452 }
453 "type_alias_statement" => {
454 if let Some(target) = node.child_by_field_name("left")
455 && let Some(binding) = first_identifier_bounded(target, &mut scope_step)?
456 {
457 inventory.record_local_node(binding, source);
458 }
459 }
460 "named_expression" => {
461 if let Some(target) = node.child_by_field_name("name") {
464 collect_binding_targets(
465 target,
466 source,
467 &mut scope_step,
468 |name, declaration| {
469 inventory.record_binding_write(name, declaration);
470 },
471 )?;
472 }
473 }
474 "for_statement" => {
475 if let Some(target) = node.child_by_field_name("left") {
476 collect_binding_targets(
477 target,
478 source,
479 &mut scope_step,
480 |name, declaration| {
481 inventory.record_binding_write(name, declaration);
482 },
483 )?;
484 }
485 }
486 "for_in_clause" => {
487 }
490 "delete_statement" => {
491 for target in named_children_bounded(node, &mut scope_step)? {
492 collect_binding_targets(
493 target,
494 source,
495 &mut scope_step,
496 |name, declaration| {
497 inventory.record_binding_write(name, declaration);
498 },
499 )?;
500 }
501 continue;
502 }
503 "as_pattern" => {
504 if let Some(alias) = node.child_by_field_name("alias") {
505 collect_binding_targets(
506 alias,
507 source,
508 &mut scope_step,
509 |name, declaration| {
510 inventory.record_binding_write(name, declaration);
511 },
512 )?;
513 push_named_children_except(
514 &mut stack,
515 node,
516 alias,
517 frame.in_comprehension,
518 &mut scope_step,
519 )?;
520 continue;
521 }
522 }
523 "except_clause" => {
524 if let Some(alias) = node.child_by_field_name("alias") {
528 collect_binding_targets(
529 alias,
530 source,
531 &mut scope_step,
532 |name, declaration| {
533 inventory.record_binding_write(name, declaration);
534 },
535 )?;
536 }
537 }
538 "case_clause" => {
539 let children = named_children_bounded(node, &mut scope_step)?;
540 for child in children.iter().copied() {
541 if child.kind() == "case_pattern" {
542 collect_match_pattern_bindings(
543 child,
544 source,
545 &mut scope_step,
546 |name, declaration| {
547 inventory.record_binding_write(name, declaration);
548 },
549 )?;
550 }
551 }
552 for child in children.into_iter().rev() {
553 if child.kind() != "case_pattern" {
554 stack.push(ScanFrame {
555 node: child,
556 in_comprehension: frame.in_comprehension,
557 });
558 }
559 }
560 continue;
561 }
562 kind if is_comprehension(kind) => {
563 let range = (node.start_byte(), node.end_byte());
564 let children = named_children_bounded(node, &mut scope_step)?;
565 let enclosing_iterable_ranges = if let Some(first_clause) = children
566 .iter()
567 .copied()
568 .find(|child| child.kind() == "for_in_clause")
569 {
570 children_by_field_name_bounded(first_clause, "right", &mut scope_step)?
571 .into_iter()
572 .map(|iterable| (iterable.start_byte(), iterable.end_byte()))
573 .collect()
574 } else {
575 Vec::new()
576 };
577 for clause in children
578 .iter()
579 .copied()
580 .filter(|child| child.kind() == "for_in_clause")
581 {
582 if let Some(target) = clause.child_by_field_name("left") {
583 collect_binding_targets(target, source, &mut scope_step, |name, _| {
584 inventory.bindings.comprehensions.push(
585 PythonComprehensionBinding {
586 name: name.into(),
587 start_byte: range.0,
588 end_byte: range.1,
589 enclosing_iterable_ranges: enclosing_iterable_ranges
590 .clone(),
591 },
592 );
593 })?;
594 }
595 }
596 for child in children.into_iter().rev() {
597 stack.push(ScanFrame {
598 node: child,
599 in_comprehension: true,
600 });
601 }
602 continue;
603 }
604 _ => {}
605 }
606
607 push_named_children(&mut stack, node, frame.in_comprehension, &mut scope_step)?;
608 }
609
610 inventory.locals.retain(|binding| {
613 !inventory.bindings.globals.contains(binding.name.as_ref())
614 && !inventory.bindings.nonlocals.contains(binding.name.as_ref())
615 });
616 inventory.bindings.local_names.retain(|name, _| {
617 !inventory.bindings.globals.contains(name.as_ref())
618 && !inventory.bindings.nonlocals.contains(name.as_ref())
619 });
620 Some(inventory)
621 }
622
623 pub fn into_bindings(self) -> PythonLexicalScopeBindings {
624 self.bindings
625 }
626
627 pub fn name_resolution_at(
628 &self,
629 name: &str,
630 reference: Node<'_>,
631 ) -> PythonLexicalNameResolution {
632 self.bindings.name_resolution_at(name, reference)
633 }
634
635 pub fn resolves_to_local_function(&self, name: &str, reference: Node<'_>) -> bool {
636 self.bindings.resolves_to_local_function(name, reference)
637 }
638
639 pub fn has_runtime_callable_binding_at(&self, name: &str, reference: Node<'_>) -> bool {
640 self.bindings
641 .has_runtime_callable_binding_at(name, reference)
642 }
643
644 pub fn local_function_declaration(
645 &self,
646 name: &str,
647 reference: Node<'_>,
648 ) -> Option<Node<'tree>> {
649 self.resolves_to_local_function(name, reference)
650 .then(|| {
651 self.locals
652 .iter()
653 .find(|binding| binding.name.as_ref() == name)
654 .and_then(|binding| binding.declaration.parent())
655 .filter(|declaration| declaration.kind() == "function_definition")
656 })
657 .flatten()
658 }
659
660 pub fn local_bindings(&self) -> impl Iterator<Item = (&str, Node<'tree>)> + '_ {
661 self.locals
662 .iter()
663 .map(|binding| (binding.name.as_ref(), binding.declaration))
664 }
665
666 fn record_local_node(&mut self, node: Node<'tree>, source: &str) {
667 let name = node_text(node, source);
668 self.record_local(name, node);
669 }
670
671 fn record_local(&mut self, name: &str, declaration: Node<'tree>) {
672 if name.is_empty() {
673 return;
674 }
675 let binding_kind = if is_function_declaration_name(declaration) {
676 PythonLocalBindingKind::FunctionOnly
677 } else {
678 PythonLocalBindingKind::Other
679 };
680 match self.bindings.local_names.entry(name.into()) {
681 std::collections::hash_map::Entry::Vacant(entry) => {
682 entry.insert(binding_kind);
683 self.locals.push(PythonLocalBinding {
684 name: name.into(),
685 declaration,
686 });
687 }
688 std::collections::hash_map::Entry::Occupied(mut entry) => {
689 entry.insert(PythonLocalBindingKind::Other);
690 }
691 }
692 }
693
694 fn record_binding_write(&mut self, name: &str, declaration: Node<'tree>) {
695 if !name.is_empty() {
696 self.bindings.binding_writes.insert(name.into());
697 }
698 self.record_local(name, declaration);
699 }
700}
701
702fn is_function_declaration_name(node: Node<'_>) -> bool {
703 node.parent().is_some_and(|parent| {
704 parent.kind() == "function_definition"
705 && parent
706 .child_by_field_name("name")
707 .is_some_and(|name| name.id() == node.id())
708 })
709}
710
711pub fn python_direct_scope_bindings_bounded<'tree>(
718 node: Node<'tree>,
719 source: &str,
720 mut scope_step: impl FnMut() -> bool,
721) -> Option<Vec<PythonDirectScopeBinding<'tree>>> {
722 let mut bindings = Vec::new();
723
724 match node.kind() {
725 "function_definition" => {
726 if let Some(name) = node.child_by_field_name("name") {
727 bindings.push(PythonDirectScopeBinding {
728 declaration: name,
729 kind: if is_direct_module_definition_bounded(node, &mut scope_step)? {
730 PythonDirectScopeBindingKind::FunctionDeclaration
731 } else {
732 PythonDirectScopeBindingKind::Other
733 },
734 });
735 }
736 }
737 "class_definition" => {
738 if let Some(name) = node.child_by_field_name("name") {
739 bindings.push(PythonDirectScopeBinding {
740 declaration: name,
741 kind: if is_direct_module_definition_bounded(node, &mut scope_step)? {
742 PythonDirectScopeBindingKind::ClassDeclaration
743 } else {
744 PythonDirectScopeBindingKind::Other
745 },
746 });
747 }
748 }
749 "import_statement" | "import_from_statement" => {
750 let module_level = is_direct_module_definition_bounded(node, &mut scope_step)?;
751 collect_import_bindings(node, &mut scope_step, |declaration| {
752 bindings.push(PythonDirectScopeBinding {
753 declaration,
754 kind: if module_level {
755 PythonDirectScopeBindingKind::Import
756 } else {
757 PythonDirectScopeBindingKind::Other
758 },
759 });
760 })?;
761 }
762 "assignment" | "augmented_assignment" => {
763 if let Some(target) = node.child_by_field_name("left") {
764 collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
765 bindings.push(PythonDirectScopeBinding {
766 declaration,
767 kind: PythonDirectScopeBindingKind::Other,
768 });
769 })?;
770 }
771 }
772 "type_alias_statement" => {
773 if let Some(target) = node.child_by_field_name("left")
774 && let Some(declaration) = first_identifier_bounded(target, &mut scope_step)?
775 {
776 bindings.push(PythonDirectScopeBinding {
777 declaration,
778 kind: PythonDirectScopeBindingKind::Other,
779 });
780 }
781 }
782 "named_expression" => {
783 if let Some(target) = node.child_by_field_name("name") {
784 collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
785 bindings.push(PythonDirectScopeBinding {
786 declaration,
787 kind: PythonDirectScopeBindingKind::Other,
788 });
789 })?;
790 }
791 }
792 "for_statement" => {
793 if let Some(target) = node.child_by_field_name("left") {
794 collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
795 bindings.push(PythonDirectScopeBinding {
796 declaration,
797 kind: PythonDirectScopeBindingKind::Other,
798 });
799 })?;
800 }
801 }
802 "delete_statement" => {
803 for target in named_children_bounded(node, &mut scope_step)? {
804 collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
805 bindings.push(PythonDirectScopeBinding {
806 declaration,
807 kind: PythonDirectScopeBindingKind::Other,
808 });
809 })?;
810 }
811 }
812 "as_pattern" => {
813 if let Some(alias) = node.child_by_field_name("alias") {
814 collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
815 bindings.push(PythonDirectScopeBinding {
816 declaration,
817 kind: PythonDirectScopeBindingKind::Other,
818 });
819 })?;
820 }
821 }
822 "except_clause" => {
823 if let Some(alias) = node.child_by_field_name("alias") {
824 collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
825 bindings.push(PythonDirectScopeBinding {
826 declaration,
827 kind: PythonDirectScopeBindingKind::Other,
828 });
829 })?;
830 }
831 }
832 "case_clause" => {
833 for child in named_children_bounded(node, &mut scope_step)? {
834 if child.kind() == "case_pattern" {
835 collect_match_pattern_bindings(
836 child,
837 source,
838 &mut scope_step,
839 |_, declaration| {
840 bindings.push(PythonDirectScopeBinding {
841 declaration,
842 kind: PythonDirectScopeBindingKind::Other,
843 });
844 },
845 )?;
846 }
847 }
848 }
849 _ => {}
850 }
851 Some(bindings)
852}
853
854pub fn python_module_or_class_scope_binds_name_bounded(
859 scope: Node<'_>,
860 target_name: &str,
861 source: &str,
862 mut scope_step: impl FnMut() -> bool,
863) -> Option<bool> {
864 assert!(matches!(scope.kind(), "module" | "class_definition"));
865 let body = scope.child_by_field_name("body").unwrap_or(scope);
866 let mut stack = vec![body];
867 while let Some(node) = stack.pop() {
868 if !scope_step() {
869 return None;
870 }
871 if node.kind() == "wildcard_import" {
872 return Some(true);
873 }
874 if python_direct_scope_bindings_bounded(node, source, &mut scope_step)?
875 .into_iter()
876 .any(|binding| node_text(binding.declaration, source) == target_name)
877 {
878 return Some(true);
879 }
880 let excluded_body = matches!(
881 node.kind(),
882 "class_definition" | "function_definition" | "lambda"
883 )
884 .then(|| node.child_by_field_name("body").map(|body| body.id()))
885 .flatten();
886 let mut cursor = node.walk();
887 stack.extend(
888 node.named_children(&mut cursor)
889 .filter(|child| Some(child.id()) != excluded_body),
890 );
891 }
892 Some(false)
893}
894
895pub fn python_unambiguous_module_class_binding_bounded(
896 root: Node<'_>,
897 source: &str,
898 target_name: &str,
899 scope_step: impl FnMut() -> bool,
900) -> Option<bool> {
901 Some(
902 python_unambiguous_module_binding_bounded(root, source, target_name, scope_step)?
903 == Some(PythonDirectScopeBindingKind::ClassDeclaration),
904 )
905}
906
907pub fn python_unambiguous_module_binding_bounded(
914 root: Node<'_>,
915 source: &str,
916 target_name: &str,
917 mut scope_step: impl FnMut() -> bool,
918) -> Option<Option<PythonDirectScopeBindingKind>> {
919 let mut matched = None;
920 let mut stack = vec![root];
921 while let Some(node) = stack.pop() {
922 if !scope_step() {
923 return None;
924 }
925 if node.kind() == "wildcard_import" && matched.is_some() {
928 return Some(None);
929 }
930 for binding in python_direct_scope_bindings_bounded(node, source, &mut scope_step)? {
931 if node_text(binding.declaration, source) != target_name {
932 continue;
933 }
934 if matched.is_some() {
935 return Some(None);
936 }
937 matched = Some(binding.kind);
938 if binding.kind == PythonDirectScopeBindingKind::Other {
939 return Some(Some(PythonDirectScopeBindingKind::Other));
940 }
941 }
942
943 let body = matches!(
944 node.kind(),
945 "function_definition" | "class_definition" | "lambda"
946 )
947 .then(|| node.child_by_field_name("body").map(|child| child.id()))
948 .flatten();
949 let name = matches!(node.kind(), "function_definition" | "class_definition")
950 .then(|| node.child_by_field_name("name").map(|child| child.id()))
951 .flatten();
952 for child in named_children_bounded(node, &mut scope_step)?
953 .into_iter()
954 .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
955 .rev()
956 {
957 stack.push(child);
958 }
959 }
960 Some(matched)
961}
962
963fn is_direct_module_definition_bounded(
964 node: Node<'_>,
965 scope_step: &mut impl FnMut() -> bool,
966) -> Option<bool> {
967 if !scope_step() {
968 return None;
969 }
970 let Some(mut parent) = node.parent() else {
971 return Some(false);
972 };
973 if parent.kind() == "decorated_definition" {
974 if !scope_step() {
975 return None;
976 }
977 let Some(grandparent) = parent.parent() else {
978 return Some(false);
979 };
980 parent = grandparent;
981 }
982 Some(parent.kind() == "module")
983}
984
985fn collect_direct_identifier_names(
986 node: Node<'_>,
987 source: &str,
988 scope_step: &mut impl FnMut() -> bool,
989 mut record: impl FnMut(&str),
990) -> Option<()> {
991 for child in named_children_bounded(node, scope_step)? {
992 if child.kind() == "identifier" {
993 let name = node_text(child, source);
994 if !name.is_empty() {
995 record(name);
996 }
997 }
998 }
999 Some(())
1000}
1001
1002fn collect_import_bindings<'tree>(
1003 statement: Node<'tree>,
1004 scope_step: &mut impl FnMut() -> bool,
1005 mut record: impl FnMut(Node<'tree>),
1006) -> Option<()> {
1007 let mut cursor = statement.walk();
1008 let mut imports = Vec::new();
1009 for imported in statement.children_by_field_name("name", &mut cursor) {
1010 if !scope_step() {
1011 return None;
1012 }
1013 imports.push(imported);
1014 }
1015 for imported in imports {
1016 if let Some(alias) = imported.child_by_field_name("alias") {
1017 record(alias);
1018 continue;
1019 }
1020 let name = imported.child_by_field_name("name").unwrap_or(imported);
1021 let binding = if statement.kind() == "import_statement" {
1022 first_identifier_bounded(name, scope_step)?
1023 } else {
1024 last_identifier_bounded(name, scope_step)?
1025 };
1026 if let Some(binding) = binding {
1027 record(binding);
1028 }
1029 }
1030 Some(())
1031}
1032
1033fn first_identifier_bounded<'tree>(
1034 root: Node<'tree>,
1035 scope_step: &mut impl FnMut() -> bool,
1036) -> Option<Option<Node<'tree>>> {
1037 let mut stack = vec![root];
1038 while let Some(node) = stack.pop() {
1039 if !scope_step() {
1040 return None;
1041 }
1042 if node.kind() == "identifier" {
1043 return Some(Some(node));
1044 }
1045 let children = named_children_bounded(node, scope_step)?;
1046 stack.extend(children.into_iter().rev());
1047 }
1048 Some(None)
1049}
1050
1051fn last_identifier_bounded<'tree>(
1052 root: Node<'tree>,
1053 scope_step: &mut impl FnMut() -> bool,
1054) -> Option<Option<Node<'tree>>> {
1055 let mut result = None;
1056 let mut stack = vec![root];
1057 while let Some(node) = stack.pop() {
1058 if !scope_step() {
1059 return None;
1060 }
1061 if node.kind() == "identifier" {
1062 result = Some(node);
1063 continue;
1064 }
1065 let children = named_children_bounded(node, scope_step)?;
1066 stack.extend(children.into_iter().rev());
1067 }
1068 Some(result)
1069}
1070
1071fn collect_binding_targets<'tree>(
1072 target: Node<'tree>,
1073 source: &str,
1074 scope_step: &mut impl FnMut() -> bool,
1075 mut record: impl FnMut(&str, Node<'tree>),
1076) -> Option<()> {
1077 let mut stack = vec![target];
1078 while let Some(node) = stack.pop() {
1079 if !scope_step() {
1080 return None;
1081 }
1082 match node.kind() {
1083 "attribute" | "subscript" => continue,
1086 "identifier" | "keyword_identifier" => {
1087 let name = node_text(node, source);
1088 if !name.is_empty() {
1089 record(name, node);
1090 }
1091 continue;
1092 }
1093 _ => {}
1094 }
1095 let children = named_children_bounded(node, scope_step)?;
1096 if node.kind() == "as_pattern_target" && children.is_empty() {
1097 let name = node_text(node, source);
1098 if !name.is_empty() {
1099 record(name, node);
1100 }
1101 continue;
1102 }
1103 stack.extend(children.into_iter().rev());
1104 }
1105 Some(())
1106}
1107
1108fn collect_match_pattern_bindings<'tree>(
1109 pattern: Node<'tree>,
1110 source: &str,
1111 scope_step: &mut impl FnMut() -> bool,
1112 mut record: impl FnMut(&str, Node<'tree>),
1113) -> Option<()> {
1114 let mut stack = vec![pattern];
1115 while let Some(node) = stack.pop() {
1116 if !scope_step() {
1117 return None;
1118 }
1119 match node.kind() {
1120 "dotted_name" => {
1121 let identifiers = named_children_bounded(node, scope_step)?
1122 .into_iter()
1123 .filter(|child| child.kind() == "identifier")
1124 .collect::<Vec<_>>();
1125 if let [binding] = identifiers.as_slice() {
1126 let name = node_text(*binding, source);
1127 if !name.is_empty() {
1128 record(name, *binding);
1129 }
1130 }
1131 continue;
1132 }
1133 "splat_pattern" => {
1134 for child in named_children_bounded(node, scope_step)? {
1135 if child.kind() == "identifier" {
1136 let name = node_text(child, source);
1137 if !name.is_empty() {
1138 record(name, child);
1139 }
1140 }
1141 }
1142 continue;
1143 }
1144 "class_pattern" => {
1145 let mut children = named_children_bounded(node, scope_step)?;
1146 if children
1147 .first()
1148 .is_some_and(|child| child.kind() == "dotted_name")
1149 {
1150 children.remove(0);
1151 }
1152 stack.extend(children.into_iter().rev());
1153 continue;
1154 }
1155 "keyword_pattern" => {
1156 let mut children = named_children_bounded(node, scope_step)?;
1157 if children
1158 .first()
1159 .is_some_and(|child| child.kind() == "identifier")
1160 {
1161 children.remove(0);
1162 }
1163 stack.extend(children.into_iter().rev());
1164 continue;
1165 }
1166 "dict_pattern" => {
1167 let key_ids = children_by_field_name_bounded(node, "key", scope_step)?
1168 .into_iter()
1169 .map(|key| key.id())
1170 .collect::<HashSet<_>>();
1171 let children = named_children_bounded(node, scope_step)?;
1172 stack.extend(
1173 children
1174 .into_iter()
1175 .filter(|child| !key_ids.contains(&child.id()))
1176 .rev(),
1177 );
1178 continue;
1179 }
1180 "as_pattern" => {
1181 if let Some(alias) = node.child_by_field_name("alias") {
1182 collect_binding_targets(alias, source, scope_step, &mut record)?;
1183 let children = named_children_bounded(node, scope_step)?;
1184 stack.extend(
1185 children
1186 .into_iter()
1187 .filter(|child| child.id() != alias.id())
1188 .rev(),
1189 );
1190 continue;
1191 }
1192 let mut children = named_children_bounded(node, scope_step)?;
1193 if let Some(alias) = children
1194 .last()
1195 .copied()
1196 .filter(|child| child.kind() == "identifier")
1197 {
1198 let name = node_text(alias, source);
1199 if !name.is_empty() {
1200 record(name, alias);
1201 }
1202 children.pop();
1203 }
1204 stack.extend(children.into_iter().rev());
1205 continue;
1206 }
1207 "identifier" => {
1208 continue;
1211 }
1212 kind if is_pattern_literal(kind) => continue,
1213 _ => {}
1214 }
1215 let children = named_children_bounded(node, scope_step)?;
1216 stack.extend(children.into_iter().rev());
1217 }
1218 Some(())
1219}
1220
1221fn push_named_children<'tree>(
1222 stack: &mut Vec<ScanFrame<'tree>>,
1223 node: Node<'tree>,
1224 in_comprehension: bool,
1225 scope_step: &mut impl FnMut() -> bool,
1226) -> Option<()> {
1227 for child in named_children_bounded(node, scope_step)?.into_iter().rev() {
1228 stack.push(ScanFrame {
1229 node: child,
1230 in_comprehension,
1231 });
1232 }
1233 Some(())
1234}
1235
1236fn push_named_children_except<'tree>(
1237 stack: &mut Vec<ScanFrame<'tree>>,
1238 node: Node<'tree>,
1239 excluded: Node<'tree>,
1240 in_comprehension: bool,
1241 scope_step: &mut impl FnMut() -> bool,
1242) -> Option<()> {
1243 for child in named_children_bounded(node, scope_step)?
1244 .into_iter()
1245 .filter(|child| child.id() != excluded.id())
1246 .rev()
1247 {
1248 stack.push(ScanFrame {
1249 node: child,
1250 in_comprehension,
1251 });
1252 }
1253 Some(())
1254}
1255
1256fn push_nested_scope_header_children<'tree>(
1257 stack: &mut Vec<ScanFrame<'tree>>,
1258 node: Node<'tree>,
1259 in_comprehension: bool,
1260 scope_step: &mut impl FnMut() -> bool,
1261) -> Option<()> {
1262 let body = node.child_by_field_name("body").map(|child| child.id());
1263 let name = node.child_by_field_name("name").map(|child| child.id());
1264 for child in named_children_bounded(node, scope_step)?
1265 .into_iter()
1266 .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
1267 .rev()
1268 {
1269 stack.push(ScanFrame {
1270 node: child,
1271 in_comprehension,
1272 });
1273 }
1274 Some(())
1275}
1276
1277fn named_children_bounded<'tree>(
1278 node: Node<'tree>,
1279 scope_step: &mut impl FnMut() -> bool,
1280) -> Option<Vec<Node<'tree>>> {
1281 let mut cursor = node.walk();
1282 let mut children = Vec::new();
1283 for child in node.named_children(&mut cursor) {
1284 if !scope_step() {
1285 return None;
1286 }
1287 children.push(child);
1288 }
1289 Some(children)
1290}
1291
1292fn children_by_field_name_bounded<'tree>(
1293 node: Node<'tree>,
1294 field: &str,
1295 scope_step: &mut impl FnMut() -> bool,
1296) -> Option<Vec<Node<'tree>>> {
1297 let mut cursor = node.walk();
1298 let mut children = Vec::new();
1299 for child in node.children_by_field_name(field, &mut cursor) {
1300 if !scope_step() {
1301 return None;
1302 }
1303 children.push(child);
1304 }
1305 Some(children)
1306}
1307
1308fn is_comprehension(kind: &str) -> bool {
1309 matches!(
1310 kind,
1311 "list_comprehension"
1312 | "set_comprehension"
1313 | "dictionary_comprehension"
1314 | "generator_expression"
1315 )
1316}
1317
1318fn is_pattern_literal(kind: &str) -> bool {
1319 matches!(
1320 kind,
1321 "string"
1322 | "concatenated_string"
1323 | "integer"
1324 | "float"
1325 | "complex_pattern"
1326 | "true"
1327 | "false"
1328 | "none"
1329 )
1330}
1331
1332fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1333 brokk_bifrost_core::analyzer::common::node_source_text_trimmed(node, source)
1334}
1335
1336#[cfg(test)]
1337mod tests {
1338 #[test]
1339 fn only_the_first_comprehension_iterable_uses_the_enclosing_scope() {
1340 let source = "def run(first, second):\n return [second for first in first for second in second]\n";
1341 let mut parser = tree_sitter::Parser::new();
1342 parser
1343 .set_language(&tree_sitter_python::LANGUAGE.into())
1344 .unwrap();
1345 let tree = parser.parse(source, None).unwrap();
1346 let mut stack = vec![tree.root_node()];
1347 let comprehension = loop {
1348 let node = stack.pop().expect("fixture contains a comprehension");
1349 if node.kind() == "list_comprehension" {
1350 break node;
1351 }
1352 let mut cursor = node.walk();
1353 stack.extend(node.named_children(&mut cursor));
1354 };
1355 let mut cursor = comprehension.walk();
1356 let clauses = comprehension
1357 .named_children(&mut cursor)
1358 .filter(|node| node.kind() == "for_in_clause")
1359 .collect::<Vec<_>>();
1360 let outer = clauses[0].child_by_field_name("right").unwrap();
1361 let inner = clauses[1].child_by_field_name("right").unwrap();
1362 assert!(!super::python_comprehension_binds_name_at(
1363 "first", outer, source
1364 ));
1365 assert!(super::python_comprehension_binds_name_at(
1366 "second", inner, source
1367 ));
1368 }
1369}