Skip to main content

brokk_bifrost_python/
bindings.rs

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
21/// Whether `reference` reads a target introduced by an enclosing comprehension.
22///
23/// Comprehensions create an implicit scope even at module level. The element
24/// expression is textually before its `for` clauses but evaluates after their
25/// binders. Only the first iterable evaluates in the enclosing scope; later
26/// iterables use comprehension-local names, including not-yet-bound targets.
27/// This structured walk models those scopes without source-text parsing.
28pub 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
75/// Whether `reference` reads a PEP 695 type parameter declared by its enclosing
76/// class, function, or type-alias definition.
77///
78/// Type parameters are lexical bindings rather than indexed workspace
79/// declarations. The binder is the first identifier in each structured
80/// parameter node; identifiers in bounds and constraints remain references.
81pub 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                    // Only a name that actually matched an enclosing type
104                    // parameter needs the function-wide directive scans. This
105                    // keeps the hot-path check for ordinary identifiers a
106                    // bounded ancestor walk with no scope-inventory builds.
107                    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
121/// Resolve `name` through the nearest enclosing function scope. This is the
122/// structured fallback for references inside nested functions that do not
123/// have their own indexed `CodeUnit` scope snapshot.
124pub 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
207/// Whether this identifier is the binder (rather than a bound/constraint use)
208/// of one structured PEP 695 type parameter.
209pub 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
269/// Function-scope Python bindings discovered from tree-sitter structure.
270///
271/// The inventory models Python's whole-function symbol-table behavior while
272/// retaining the implicit scope of comprehension targets. Construction is
273/// iterative and every inspected node is gated by `scope_step`; `None` means
274/// the caller stopped discovery and must conservatively avoid module fallback.
275pub struct PythonLexicalScopeInventory<'tree> {
276    locals: Vec<PythonLocalBinding<'tree>>,
277    bindings: PythonLexicalScopeBindings,
278}
279
280/// Completed function binding facts without borrowed tree-sitter nodes.
281/// Retain these only under the identity of the exact source that produced them.
282pub 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    /// Whether the active callable obtains `name` from a runtime binding
338    /// rather than an untouched function or import declaration.
339    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    /// `parameter_names` is the callable's formal-parameter name stream. The
363    /// layout it comes from is resolved by `analyzer/lexical_definitions.rs`,
364    /// which dispatches through the analysis-side language registry and so
365    /// cannot be named here; `analyzer/python/lexical_scope.rs` in
366    /// `brokk-bifrost-analysis` is the one caller that computes it, under the
367    /// same `scope_step` meter this walk uses.
368    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                // Defaults, annotations, bases, and type-parameter expressions
410                // execute in an enclosing scope. Keep scanning those headers
411                // for walrus/comprehension bindings while pruning the nested
412                // callable or class body itself.
413                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                    // PEP 572 binds a comprehension walrus in the containing
462                    // non-comprehension scope, so this remains a function local.
463                    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                    // These occur inside comprehensions and belong to their
488                    // implicit scope. The enclosing comprehension records them.
489                }
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                    // Older grammar shapes expose the alias directly. Current
525                    // tree-sitter-python nests it in an `as_pattern`, handled
526                    // above when the clause's children are scanned.
527                    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        // `global` and `nonlocal` are whole-function directives regardless of
611        // source order. Neither declaration may become a semantic local.
612        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
711/// Return the bindings introduced directly by `node`.
712///
713/// Descendant traversal remains the caller's responsibility. This lets the
714/// semantic file walk build a module binding inventory without adding a
715/// second whole-file scan, while reusing the same structured target handling
716/// as function symbol-table discovery.
717pub 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
854/// Whether a module or class execution scope binds `target_name` anywhere in
855/// its direct scope. Nested callable and class bodies are separate scopes, but
856/// their headers still execute in this scope. A wildcard import keeps the
857/// name's runtime identity open even when it does not expose a static binder.
858pub 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
907/// The kind of the module scope's one binding of `target_name`.
908///
909/// `Some(None)` says the module does not bind the name, binds it more than
910/// once, or leaves its identity open through a wildcard import; the name is
911/// then not a proven reference to one module-level declaration. `None` says the
912/// bounded walk ran out of work.
913pub 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        // A later unconditional declaration replaces an earlier wildcard
926        // binding; a wildcard after it leaves its identity open.
927        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            // These mutate an existing object and do not bind either the
1084            // receiver or member name in the function.
1085            "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                // Direct identifiers are match `as` aliases handled by their
1209                // parent. Keyword names and class heads are likewise skipped.
1210                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}