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 Other,
17}
18
19#[derive(Clone, Copy, Debug)]
20pub struct PythonDirectScopeBinding<'tree> {
21 pub declaration: Node<'tree>,
22 pub kind: PythonDirectScopeBindingKind,
23}
24
25#[derive(Clone, Debug)]
26struct PythonLocalBinding<'tree> {
27 name: Box<str>,
28 declaration: Node<'tree>,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32enum PythonLocalBindingKind {
33 FunctionOnly,
34 Other,
35}
36
37#[derive(Clone, Debug)]
38struct PythonComprehensionBinding {
39 name: Box<str>,
40 start_byte: usize,
41 end_byte: usize,
42 enclosing_iterable_ranges: Vec<(usize, usize)>,
43}
44
45pub struct PythonLexicalScopeInventory<'tree> {
52 parameters: HashSet<Box<str>>,
53 locals: Vec<PythonLocalBinding<'tree>>,
54 local_names: HashMap<Box<str>, PythonLocalBindingKind>,
55 globals: HashSet<Box<str>>,
56 nonlocals: HashSet<Box<str>>,
57 comprehensions: Vec<PythonComprehensionBinding>,
58}
59
60#[derive(Clone, Copy)]
61struct ScanFrame<'tree> {
62 node: Node<'tree>,
63 in_comprehension: bool,
64}
65
66impl<'tree> PythonLexicalScopeInventory<'tree> {
67 pub fn collect_bounded(
74 callable: Node<'tree>,
75 source: &str,
76 parameter_names: impl IntoIterator<Item = String>,
77 mut scope_step: impl FnMut() -> bool,
78 ) -> Option<Self> {
79 let mut inventory = Self {
80 parameters: parameter_names.into_iter().map(Box::<str>::from).collect(),
81 locals: Vec::new(),
82 local_names: HashMap::default(),
83 globals: HashSet::default(),
84 nonlocals: HashSet::default(),
85 comprehensions: Vec::new(),
86 };
87 let Some(body) = callable.child_by_field_name("body") else {
88 return Some(inventory);
89 };
90 let mut stack = vec![ScanFrame {
91 node: body,
92 in_comprehension: false,
93 }];
94
95 while let Some(frame) = stack.pop() {
96 if !scope_step() {
97 return None;
98 }
99 let node = frame.node;
100 let nested_scope = node != body
101 && matches!(
102 node.kind(),
103 "function_definition" | "lambda" | "class_definition"
104 );
105 if nested_scope {
106 if matches!(node.kind(), "function_definition" | "class_definition")
107 && let Some(name) = node.child_by_field_name("name")
108 {
109 inventory.record_local_node(name, source);
110 }
111 push_nested_scope_header_children(
116 &mut stack,
117 node,
118 frame.in_comprehension,
119 &mut scope_step,
120 )?;
121 continue;
122 }
123
124 match node.kind() {
125 "global_statement" => {
126 collect_direct_identifier_names(node, source, &mut scope_step, |name| {
127 inventory.globals.insert(name.into());
128 })?;
129 continue;
130 }
131 "nonlocal_statement" => {
132 collect_direct_identifier_names(node, source, &mut scope_step, |name| {
133 inventory.nonlocals.insert(name.into());
134 })?;
135 continue;
136 }
137 "import_statement" | "import_from_statement" => {
138 collect_import_bindings(node, &mut scope_step, |binding| {
139 inventory.record_local_node(binding, source)
140 })?;
141 continue;
142 }
143 "assignment" | "augmented_assignment" => {
144 if let Some(target) = node.child_by_field_name("left") {
145 collect_binding_targets(
146 target,
147 source,
148 &mut scope_step,
149 |name, declaration| {
150 inventory.record_local(name, declaration);
151 },
152 )?;
153 }
154 }
155 "type_alias_statement" => {
156 if let Some(target) = node.child_by_field_name("left")
157 && let Some(binding) = first_identifier_bounded(target, &mut scope_step)?
158 {
159 inventory.record_local_node(binding, source);
160 }
161 }
162 "named_expression" => {
163 if let Some(target) = node.child_by_field_name("name") {
166 collect_binding_targets(
167 target,
168 source,
169 &mut scope_step,
170 |name, declaration| {
171 inventory.record_local(name, declaration);
172 },
173 )?;
174 }
175 }
176 "for_statement" => {
177 if let Some(target) = node.child_by_field_name("left") {
178 collect_binding_targets(
179 target,
180 source,
181 &mut scope_step,
182 |name, declaration| {
183 inventory.record_local(name, declaration);
184 },
185 )?;
186 }
187 }
188 "for_in_clause" => {
189 }
192 "delete_statement" => {
193 for target in named_children_bounded(node, &mut scope_step)? {
194 collect_binding_targets(
195 target,
196 source,
197 &mut scope_step,
198 |name, declaration| {
199 inventory.record_local(name, declaration);
200 },
201 )?;
202 }
203 continue;
204 }
205 "as_pattern" => {
206 if let Some(alias) = node.child_by_field_name("alias") {
207 collect_binding_targets(
208 alias,
209 source,
210 &mut scope_step,
211 |name, declaration| {
212 inventory.record_local(name, declaration);
213 },
214 )?;
215 push_named_children_except(
216 &mut stack,
217 node,
218 alias,
219 frame.in_comprehension,
220 &mut scope_step,
221 )?;
222 continue;
223 }
224 }
225 "except_clause" => {
226 if let Some(alias) = node.child_by_field_name("alias") {
230 collect_binding_targets(
231 alias,
232 source,
233 &mut scope_step,
234 |name, declaration| {
235 inventory.record_local(name, declaration);
236 },
237 )?;
238 }
239 }
240 "case_clause" => {
241 let children = named_children_bounded(node, &mut scope_step)?;
242 for child in children.iter().copied() {
243 if child.kind() == "case_pattern" {
244 collect_match_pattern_bindings(
245 child,
246 source,
247 &mut scope_step,
248 |name, declaration| {
249 inventory.record_local(name, declaration);
250 },
251 )?;
252 }
253 }
254 for child in children.into_iter().rev() {
255 if child.kind() != "case_pattern" {
256 stack.push(ScanFrame {
257 node: child,
258 in_comprehension: frame.in_comprehension,
259 });
260 }
261 }
262 continue;
263 }
264 kind if is_comprehension(kind) => {
265 let range = (node.start_byte(), node.end_byte());
266 let children = named_children_bounded(node, &mut scope_step)?;
267 let enclosing_iterable_ranges = if let Some(first_clause) = children
268 .iter()
269 .copied()
270 .find(|child| child.kind() == "for_in_clause")
271 {
272 children_by_field_name_bounded(first_clause, "right", &mut scope_step)?
273 .into_iter()
274 .map(|iterable| (iterable.start_byte(), iterable.end_byte()))
275 .collect()
276 } else {
277 Vec::new()
278 };
279 for clause in children
280 .iter()
281 .copied()
282 .filter(|child| child.kind() == "for_in_clause")
283 {
284 if let Some(target) = clause.child_by_field_name("left") {
285 collect_binding_targets(target, source, &mut scope_step, |name, _| {
286 inventory.comprehensions.push(PythonComprehensionBinding {
287 name: name.into(),
288 start_byte: range.0,
289 end_byte: range.1,
290 enclosing_iterable_ranges: enclosing_iterable_ranges.clone(),
291 });
292 })?;
293 }
294 }
295 for child in children.into_iter().rev() {
296 stack.push(ScanFrame {
297 node: child,
298 in_comprehension: true,
299 });
300 }
301 continue;
302 }
303 _ => {}
304 }
305
306 push_named_children(&mut stack, node, frame.in_comprehension, &mut scope_step)?;
307 }
308
309 inventory.locals.retain(|binding| {
312 !inventory.globals.contains(binding.name.as_ref())
313 && !inventory.nonlocals.contains(binding.name.as_ref())
314 });
315 inventory.local_names.retain(|name, _| {
316 !inventory.globals.contains(name.as_ref())
317 && !inventory.nonlocals.contains(name.as_ref())
318 });
319 Some(inventory)
320 }
321
322 pub fn name_resolution_at(
323 &self,
324 name: &str,
325 reference: Node<'_>,
326 ) -> PythonLexicalNameResolution {
327 let reference_byte = reference.start_byte();
328 if self.comprehensions.iter().any(|binding| {
329 binding.name.as_ref() == name
330 && binding.start_byte <= reference_byte
331 && reference_byte < binding.end_byte
332 && !binding
333 .enclosing_iterable_ranges
334 .iter()
335 .any(|(start, end)| *start <= reference_byte && reference_byte < *end)
336 }) {
337 return PythonLexicalNameResolution::Local;
338 }
339 if self.nonlocals.contains(name) {
340 return PythonLexicalNameResolution::Nonlocal;
341 }
342 if self.globals.contains(name) {
343 return PythonLexicalNameResolution::Global;
344 }
345 if self.parameters.contains(name) || self.local_names.contains_key(name) {
346 PythonLexicalNameResolution::Local
347 } else {
348 PythonLexicalNameResolution::Unbound
349 }
350 }
351
352 pub fn resolves_to_local_function(&self, name: &str, reference: Node<'_>) -> bool {
353 let reference_byte = reference.start_byte();
354 !self.parameters.contains(name)
355 && !self.comprehensions.iter().any(|binding| {
356 binding.name.as_ref() == name
357 && binding.start_byte <= reference_byte
358 && reference_byte < binding.end_byte
359 && !binding
360 .enclosing_iterable_ranges
361 .iter()
362 .any(|(start, end)| *start <= reference_byte && reference_byte < *end)
363 })
364 && self.local_names.get(name) == Some(&PythonLocalBindingKind::FunctionOnly)
365 }
366
367 pub fn local_function_declaration(
368 &self,
369 name: &str,
370 reference: Node<'_>,
371 ) -> Option<Node<'tree>> {
372 self.resolves_to_local_function(name, reference)
373 .then(|| {
374 self.locals
375 .iter()
376 .find(|binding| binding.name.as_ref() == name)
377 .and_then(|binding| binding.declaration.parent())
378 .filter(|declaration| declaration.kind() == "function_definition")
379 })
380 .flatten()
381 }
382
383 pub fn local_bindings(&self) -> impl Iterator<Item = (&str, Node<'tree>)> + '_ {
384 self.locals
385 .iter()
386 .map(|binding| (binding.name.as_ref(), binding.declaration))
387 }
388
389 fn record_local_node(&mut self, node: Node<'tree>, source: &str) {
390 let name = node_text(node, source);
391 self.record_local(name, node);
392 }
393
394 fn record_local(&mut self, name: &str, declaration: Node<'tree>) {
395 if name.is_empty() {
396 return;
397 }
398 let binding_kind = if is_function_declaration_name(declaration) {
399 PythonLocalBindingKind::FunctionOnly
400 } else {
401 PythonLocalBindingKind::Other
402 };
403 match self.local_names.entry(name.into()) {
404 std::collections::hash_map::Entry::Vacant(entry) => {
405 entry.insert(binding_kind);
406 self.locals.push(PythonLocalBinding {
407 name: name.into(),
408 declaration,
409 });
410 }
411 std::collections::hash_map::Entry::Occupied(mut entry) => {
412 entry.insert(PythonLocalBindingKind::Other);
413 }
414 }
415 }
416}
417
418fn is_function_declaration_name(node: Node<'_>) -> bool {
419 node.parent().is_some_and(|parent| {
420 parent.kind() == "function_definition"
421 && parent
422 .child_by_field_name("name")
423 .is_some_and(|name| name.id() == node.id())
424 })
425}
426
427pub fn python_direct_scope_bindings_bounded<'tree>(
434 node: Node<'tree>,
435 source: &str,
436 mut scope_step: impl FnMut() -> bool,
437) -> Option<Vec<PythonDirectScopeBinding<'tree>>> {
438 let mut bindings = Vec::new();
439
440 match node.kind() {
441 "function_definition" => {
442 if let Some(name) = node.child_by_field_name("name") {
443 bindings.push(PythonDirectScopeBinding {
444 declaration: name,
445 kind: PythonDirectScopeBindingKind::Other,
446 });
447 }
448 }
449 "class_definition" => {
450 if let Some(name) = node.child_by_field_name("name") {
451 bindings.push(PythonDirectScopeBinding {
452 declaration: name,
453 kind: if is_direct_module_definition_bounded(node, &mut scope_step)? {
454 PythonDirectScopeBindingKind::ClassDeclaration
455 } else {
456 PythonDirectScopeBindingKind::Other
457 },
458 });
459 }
460 }
461 "import_statement" | "import_from_statement" => {
462 collect_import_bindings(node, &mut scope_step, |declaration| {
463 bindings.push(PythonDirectScopeBinding {
464 declaration,
465 kind: PythonDirectScopeBindingKind::Other,
466 });
467 })?;
468 }
469 "assignment" | "augmented_assignment" => {
470 if let Some(target) = node.child_by_field_name("left") {
471 collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
472 bindings.push(PythonDirectScopeBinding {
473 declaration,
474 kind: PythonDirectScopeBindingKind::Other,
475 });
476 })?;
477 }
478 }
479 "type_alias_statement" => {
480 if let Some(target) = node.child_by_field_name("left")
481 && let Some(declaration) = first_identifier_bounded(target, &mut scope_step)?
482 {
483 bindings.push(PythonDirectScopeBinding {
484 declaration,
485 kind: PythonDirectScopeBindingKind::Other,
486 });
487 }
488 }
489 "named_expression" => {
490 if let Some(target) = node.child_by_field_name("name") {
491 collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
492 bindings.push(PythonDirectScopeBinding {
493 declaration,
494 kind: PythonDirectScopeBindingKind::Other,
495 });
496 })?;
497 }
498 }
499 "for_statement" => {
500 if let Some(target) = node.child_by_field_name("left") {
501 collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
502 bindings.push(PythonDirectScopeBinding {
503 declaration,
504 kind: PythonDirectScopeBindingKind::Other,
505 });
506 })?;
507 }
508 }
509 "delete_statement" => {
510 for target in named_children_bounded(node, &mut scope_step)? {
511 collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
512 bindings.push(PythonDirectScopeBinding {
513 declaration,
514 kind: PythonDirectScopeBindingKind::Other,
515 });
516 })?;
517 }
518 }
519 "as_pattern" => {
520 if let Some(alias) = node.child_by_field_name("alias") {
521 collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
522 bindings.push(PythonDirectScopeBinding {
523 declaration,
524 kind: PythonDirectScopeBindingKind::Other,
525 });
526 })?;
527 }
528 }
529 "except_clause" => {
530 if let Some(alias) = node.child_by_field_name("alias") {
531 collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
532 bindings.push(PythonDirectScopeBinding {
533 declaration,
534 kind: PythonDirectScopeBindingKind::Other,
535 });
536 })?;
537 }
538 }
539 "case_clause" => {
540 for child in named_children_bounded(node, &mut scope_step)? {
541 if child.kind() == "case_pattern" {
542 collect_match_pattern_bindings(
543 child,
544 source,
545 &mut scope_step,
546 |_, declaration| {
547 bindings.push(PythonDirectScopeBinding {
548 declaration,
549 kind: PythonDirectScopeBindingKind::Other,
550 });
551 },
552 )?;
553 }
554 }
555 }
556 _ => {}
557 }
558 Some(bindings)
559}
560
561pub fn python_unambiguous_module_class_binding_bounded(
562 root: Node<'_>,
563 source: &str,
564 target_name: &str,
565 mut scope_step: impl FnMut() -> bool,
566) -> Option<bool> {
567 let mut matched = None;
568 let mut stack = vec![root];
569 while let Some(node) = stack.pop() {
570 if !scope_step() {
571 return None;
572 }
573 for binding in python_direct_scope_bindings_bounded(node, source, &mut scope_step)? {
574 if node_text(binding.declaration, source) != target_name {
575 continue;
576 }
577 if matched.is_some() {
578 return Some(false);
579 }
580 matched = Some(binding.kind);
581 if binding.kind == PythonDirectScopeBindingKind::Other {
582 return Some(false);
583 }
584 }
585
586 let body = matches!(
587 node.kind(),
588 "function_definition" | "class_definition" | "lambda"
589 )
590 .then(|| node.child_by_field_name("body").map(|child| child.id()))
591 .flatten();
592 let name = matches!(node.kind(), "function_definition" | "class_definition")
593 .then(|| node.child_by_field_name("name").map(|child| child.id()))
594 .flatten();
595 for child in named_children_bounded(node, &mut scope_step)?
596 .into_iter()
597 .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
598 .rev()
599 {
600 stack.push(child);
601 }
602 }
603 Some(matched == Some(PythonDirectScopeBindingKind::ClassDeclaration))
604}
605
606fn is_direct_module_definition_bounded(
607 node: Node<'_>,
608 scope_step: &mut impl FnMut() -> bool,
609) -> Option<bool> {
610 if !scope_step() {
611 return None;
612 }
613 let Some(mut parent) = node.parent() else {
614 return Some(false);
615 };
616 if parent.kind() == "decorated_definition" {
617 if !scope_step() {
618 return None;
619 }
620 let Some(grandparent) = parent.parent() else {
621 return Some(false);
622 };
623 parent = grandparent;
624 }
625 Some(parent.kind() == "module")
626}
627
628fn collect_direct_identifier_names(
629 node: Node<'_>,
630 source: &str,
631 scope_step: &mut impl FnMut() -> bool,
632 mut record: impl FnMut(&str),
633) -> Option<()> {
634 for child in named_children_bounded(node, scope_step)? {
635 if child.kind() == "identifier" {
636 let name = node_text(child, source);
637 if !name.is_empty() {
638 record(name);
639 }
640 }
641 }
642 Some(())
643}
644
645fn collect_import_bindings<'tree>(
646 statement: Node<'tree>,
647 scope_step: &mut impl FnMut() -> bool,
648 mut record: impl FnMut(Node<'tree>),
649) -> Option<()> {
650 let mut cursor = statement.walk();
651 let mut imports = Vec::new();
652 for imported in statement.children_by_field_name("name", &mut cursor) {
653 if !scope_step() {
654 return None;
655 }
656 imports.push(imported);
657 }
658 for imported in imports {
659 if let Some(alias) = imported.child_by_field_name("alias") {
660 record(alias);
661 continue;
662 }
663 let name = imported.child_by_field_name("name").unwrap_or(imported);
664 let binding = if statement.kind() == "import_statement" {
665 first_identifier_bounded(name, scope_step)?
666 } else {
667 last_identifier_bounded(name, scope_step)?
668 };
669 if let Some(binding) = binding {
670 record(binding);
671 }
672 }
673 Some(())
674}
675
676fn first_identifier_bounded<'tree>(
677 root: Node<'tree>,
678 scope_step: &mut impl FnMut() -> bool,
679) -> Option<Option<Node<'tree>>> {
680 let mut stack = vec![root];
681 while let Some(node) = stack.pop() {
682 if !scope_step() {
683 return None;
684 }
685 if node.kind() == "identifier" {
686 return Some(Some(node));
687 }
688 let children = named_children_bounded(node, scope_step)?;
689 stack.extend(children.into_iter().rev());
690 }
691 Some(None)
692}
693
694fn last_identifier_bounded<'tree>(
695 root: Node<'tree>,
696 scope_step: &mut impl FnMut() -> bool,
697) -> Option<Option<Node<'tree>>> {
698 let mut result = None;
699 let mut stack = vec![root];
700 while let Some(node) = stack.pop() {
701 if !scope_step() {
702 return None;
703 }
704 if node.kind() == "identifier" {
705 result = Some(node);
706 continue;
707 }
708 let children = named_children_bounded(node, scope_step)?;
709 stack.extend(children.into_iter().rev());
710 }
711 Some(result)
712}
713
714fn collect_binding_targets<'tree>(
715 target: Node<'tree>,
716 source: &str,
717 scope_step: &mut impl FnMut() -> bool,
718 mut record: impl FnMut(&str, Node<'tree>),
719) -> Option<()> {
720 let mut stack = vec![target];
721 while let Some(node) = stack.pop() {
722 if !scope_step() {
723 return None;
724 }
725 match node.kind() {
726 "attribute" | "subscript" => continue,
729 "identifier" | "keyword_identifier" => {
730 let name = node_text(node, source);
731 if !name.is_empty() {
732 record(name, node);
733 }
734 continue;
735 }
736 _ => {}
737 }
738 let children = named_children_bounded(node, scope_step)?;
739 if node.kind() == "as_pattern_target" && children.is_empty() {
740 let name = node_text(node, source);
741 if !name.is_empty() {
742 record(name, node);
743 }
744 continue;
745 }
746 stack.extend(children.into_iter().rev());
747 }
748 Some(())
749}
750
751fn collect_match_pattern_bindings<'tree>(
752 pattern: Node<'tree>,
753 source: &str,
754 scope_step: &mut impl FnMut() -> bool,
755 mut record: impl FnMut(&str, Node<'tree>),
756) -> Option<()> {
757 let mut stack = vec![pattern];
758 while let Some(node) = stack.pop() {
759 if !scope_step() {
760 return None;
761 }
762 match node.kind() {
763 "dotted_name" => {
764 let identifiers = named_children_bounded(node, scope_step)?
765 .into_iter()
766 .filter(|child| child.kind() == "identifier")
767 .collect::<Vec<_>>();
768 if let [binding] = identifiers.as_slice() {
769 let name = node_text(*binding, source);
770 if !name.is_empty() {
771 record(name, *binding);
772 }
773 }
774 continue;
775 }
776 "splat_pattern" => {
777 for child in named_children_bounded(node, scope_step)? {
778 if child.kind() == "identifier" {
779 let name = node_text(child, source);
780 if !name.is_empty() {
781 record(name, child);
782 }
783 }
784 }
785 continue;
786 }
787 "class_pattern" => {
788 let mut children = named_children_bounded(node, scope_step)?;
789 if children
790 .first()
791 .is_some_and(|child| child.kind() == "dotted_name")
792 {
793 children.remove(0);
794 }
795 stack.extend(children.into_iter().rev());
796 continue;
797 }
798 "keyword_pattern" => {
799 let mut children = named_children_bounded(node, scope_step)?;
800 if children
801 .first()
802 .is_some_and(|child| child.kind() == "identifier")
803 {
804 children.remove(0);
805 }
806 stack.extend(children.into_iter().rev());
807 continue;
808 }
809 "dict_pattern" => {
810 let key_ids = children_by_field_name_bounded(node, "key", scope_step)?
811 .into_iter()
812 .map(|key| key.id())
813 .collect::<HashSet<_>>();
814 let children = named_children_bounded(node, scope_step)?;
815 stack.extend(
816 children
817 .into_iter()
818 .filter(|child| !key_ids.contains(&child.id()))
819 .rev(),
820 );
821 continue;
822 }
823 "as_pattern" => {
824 if let Some(alias) = node.child_by_field_name("alias") {
825 collect_binding_targets(alias, source, scope_step, &mut record)?;
826 let children = named_children_bounded(node, scope_step)?;
827 stack.extend(
828 children
829 .into_iter()
830 .filter(|child| child.id() != alias.id())
831 .rev(),
832 );
833 continue;
834 }
835 let mut children = named_children_bounded(node, scope_step)?;
836 if let Some(alias) = children
837 .last()
838 .copied()
839 .filter(|child| child.kind() == "identifier")
840 {
841 let name = node_text(alias, source);
842 if !name.is_empty() {
843 record(name, alias);
844 }
845 children.pop();
846 }
847 stack.extend(children.into_iter().rev());
848 continue;
849 }
850 "identifier" => {
851 continue;
854 }
855 kind if is_pattern_literal(kind) => continue,
856 _ => {}
857 }
858 let children = named_children_bounded(node, scope_step)?;
859 stack.extend(children.into_iter().rev());
860 }
861 Some(())
862}
863
864fn push_named_children<'tree>(
865 stack: &mut Vec<ScanFrame<'tree>>,
866 node: Node<'tree>,
867 in_comprehension: bool,
868 scope_step: &mut impl FnMut() -> bool,
869) -> Option<()> {
870 for child in named_children_bounded(node, scope_step)?.into_iter().rev() {
871 stack.push(ScanFrame {
872 node: child,
873 in_comprehension,
874 });
875 }
876 Some(())
877}
878
879fn push_named_children_except<'tree>(
880 stack: &mut Vec<ScanFrame<'tree>>,
881 node: Node<'tree>,
882 excluded: Node<'tree>,
883 in_comprehension: bool,
884 scope_step: &mut impl FnMut() -> bool,
885) -> Option<()> {
886 for child in named_children_bounded(node, scope_step)?
887 .into_iter()
888 .filter(|child| child.id() != excluded.id())
889 .rev()
890 {
891 stack.push(ScanFrame {
892 node: child,
893 in_comprehension,
894 });
895 }
896 Some(())
897}
898
899fn push_nested_scope_header_children<'tree>(
900 stack: &mut Vec<ScanFrame<'tree>>,
901 node: Node<'tree>,
902 in_comprehension: bool,
903 scope_step: &mut impl FnMut() -> bool,
904) -> Option<()> {
905 let body = node.child_by_field_name("body").map(|child| child.id());
906 let name = node.child_by_field_name("name").map(|child| child.id());
907 for child in named_children_bounded(node, scope_step)?
908 .into_iter()
909 .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
910 .rev()
911 {
912 stack.push(ScanFrame {
913 node: child,
914 in_comprehension,
915 });
916 }
917 Some(())
918}
919
920fn named_children_bounded<'tree>(
921 node: Node<'tree>,
922 scope_step: &mut impl FnMut() -> bool,
923) -> Option<Vec<Node<'tree>>> {
924 let mut cursor = node.walk();
925 let mut children = Vec::new();
926 for child in node.named_children(&mut cursor) {
927 if !scope_step() {
928 return None;
929 }
930 children.push(child);
931 }
932 Some(children)
933}
934
935fn children_by_field_name_bounded<'tree>(
936 node: Node<'tree>,
937 field: &str,
938 scope_step: &mut impl FnMut() -> bool,
939) -> Option<Vec<Node<'tree>>> {
940 let mut cursor = node.walk();
941 let mut children = Vec::new();
942 for child in node.children_by_field_name(field, &mut cursor) {
943 if !scope_step() {
944 return None;
945 }
946 children.push(child);
947 }
948 Some(children)
949}
950
951fn is_comprehension(kind: &str) -> bool {
952 matches!(
953 kind,
954 "list_comprehension"
955 | "set_comprehension"
956 | "dictionary_comprehension"
957 | "generator_expression"
958 )
959}
960
961fn is_pattern_literal(kind: &str) -> bool {
962 matches!(
963 kind,
964 "string"
965 | "concatenated_string"
966 | "integer"
967 | "float"
968 | "complex_pattern"
969 | "true"
970 | "false"
971 | "none"
972 )
973}
974
975fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
976 brokk_bifrost_core::analyzer::common::node_source_text_trimmed(node, source)
977}