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    Other,
17}
18
19/// Whether `reference` reads a target introduced by an enclosing comprehension.
20///
21/// Comprehensions create an implicit scope even at module level. The element
22/// expression is textually before its `for` clauses but evaluates after their
23/// binders, while a clause's own iterable evaluates before that clause binds
24/// its target. This structured walk models that ordering without source-text
25/// parsing.
26pub fn python_comprehension_binds_name_at(name: &str, reference: Node<'_>, source: &str) -> bool {
27    let mut current = reference;
28    while let Some(parent) = current.parent() {
29        if is_comprehension(parent.kind()) {
30            let mut cursor = parent.walk();
31            for clause in parent
32                .named_children(&mut cursor)
33                .filter(|child| child.kind() == "for_in_clause")
34            {
35                let Some(target) = clause.child_by_field_name("left") else {
36                    continue;
37                };
38                let mut bound = false;
39                let _ = collect_binding_targets(target, source, &mut || true, |candidate, _| {
40                    bound |= candidate == name;
41                });
42                if !bound {
43                    continue;
44                }
45                let inside_own_iterable =
46                    clause.child_by_field_name("right").is_some_and(|right| {
47                        right.start_byte() <= reference.start_byte()
48                            && reference.end_byte() <= right.end_byte()
49                    });
50                if !inside_own_iterable {
51                    return true;
52                }
53            }
54        }
55        if matches!(
56            parent.kind(),
57            "function_definition" | "lambda" | "class_definition" | "module"
58        ) {
59            break;
60        }
61        current = parent;
62    }
63    false
64}
65
66/// Whether `reference` reads a PEP 695 type parameter declared by its enclosing
67/// class, function, or type-alias definition.
68///
69/// Type parameters are lexical bindings rather than indexed workspace
70/// declarations. The binder is the first identifier in each structured
71/// parameter node; identifiers in bounds and constraints remain references.
72pub fn python_type_parameter_binds_name_at(name: &str, reference: Node<'_>, source: &str) -> bool {
73    let mut current = reference;
74    let mut enclosing_functions = Vec::new();
75    while let Some(parent) = current.parent() {
76        if parent.kind() == "function_definition" {
77            enclosing_functions.push(parent);
78        }
79        if matches!(
80            parent.kind(),
81            "class_definition" | "function_definition" | "type_alias_statement"
82        ) && let Some(parameters) = parent.child_by_field_name("type_parameters")
83            && !(parameters.start_byte() <= reference.start_byte()
84                && reference.end_byte() <= parameters.end_byte())
85        {
86            let mut cursor = parameters.walk();
87            for parameter in parameters.named_children(&mut cursor) {
88                let binder = if parameter.kind() == "identifier" {
89                    Some(parameter)
90                } else {
91                    first_identifier_bounded(parameter, &mut || true).flatten()
92                };
93                if binder.is_some_and(|binder| node_text(binder, source) == name) {
94                    // Only a name that actually matched an enclosing type
95                    // parameter needs the function-wide directive scans. This
96                    // keeps the hot-path check for ordinary identifiers a
97                    // bounded ancestor walk with no scope-inventory builds.
98                    return !python_name_declared_global_at_in_functions(
99                        name,
100                        reference,
101                        source,
102                        enclosing_functions,
103                    );
104                }
105            }
106        }
107        current = parent;
108    }
109    false
110}
111
112/// Resolve `name` through the nearest enclosing function scope. This is the
113/// structured fallback for references inside nested functions that do not
114/// have their own indexed `CodeUnit` scope snapshot.
115pub fn python_name_resolution_at(
116    name: &str,
117    reference: Node<'_>,
118    source: &str,
119) -> PythonLexicalNameResolution {
120    let mut functions = Vec::new();
121    let mut current = reference;
122    while let Some(parent) = current.parent() {
123        if parent.kind() == "function_definition" {
124            functions.push(parent);
125        }
126        current = parent;
127    }
128    python_name_resolution_at_in_functions(name, reference, source, functions)
129}
130
131fn python_name_declared_global_at_in_functions(
132    name: &str,
133    reference: Node<'_>,
134    source: &str,
135    functions: Vec<Node<'_>>,
136) -> bool {
137    python_name_resolution_at_in_functions(name, reference, source, functions)
138        == PythonLexicalNameResolution::Global
139}
140
141fn python_name_resolution_at_in_functions(
142    name: &str,
143    reference: Node<'_>,
144    source: &str,
145    functions: Vec<Node<'_>>,
146) -> PythonLexicalNameResolution {
147    for function in functions {
148        let parameter_names = function
149            .child_by_field_name("parameters")
150            .into_iter()
151            .flat_map(|parameters| {
152                let mut cursor = parameters.walk();
153                parameters
154                    .named_children(&mut cursor)
155                    .filter_map(|parameter| python_parameter_name(parameter, source))
156                    .collect::<Vec<_>>()
157            })
158            .collect::<Vec<_>>();
159        let Some(inventory) =
160            PythonLexicalScopeInventory::collect_bounded(function, source, parameter_names, || {
161                true
162            })
163        else {
164            return PythonLexicalNameResolution::Unbound;
165        };
166        let resolution = inventory.name_resolution_at(name, reference);
167        match resolution {
168            PythonLexicalNameResolution::Global => return PythonLexicalNameResolution::Global,
169            PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal => {
170                return resolution;
171            }
172            PythonLexicalNameResolution::Unbound => {}
173        }
174    }
175    PythonLexicalNameResolution::Unbound
176}
177
178fn python_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
179    match node.kind() {
180        "identifier" => Some(node_text(node, source).trim().to_string()),
181        "typed_parameter"
182        | "typed_default_parameter"
183        | "default_parameter"
184        | "list_splat_pattern"
185        | "dictionary_splat_pattern" => node
186            .child_by_field_name("name")
187            .or_else(|| {
188                let mut cursor = node.walk();
189                node.named_children(&mut cursor)
190                    .find(|child| child.kind() == "identifier")
191            })
192            .and_then(|name| python_parameter_name(name, source)),
193        _ => None,
194    }
195    .filter(|name| !name.is_empty())
196}
197
198/// Whether this identifier is the binder (rather than a bound/constraint use)
199/// of one structured PEP 695 type parameter.
200pub fn python_is_type_parameter_binder(node: Node<'_>) -> bool {
201    if node.kind() != "identifier" {
202        return false;
203    }
204    let mut current = node;
205    while let Some(parent) = current.parent() {
206        if matches!(
207            parent.kind(),
208            "class_definition" | "function_definition" | "type_alias_statement"
209        ) && let Some(parameters) = parent.child_by_field_name("type_parameters")
210            && parameters.start_byte() <= node.start_byte()
211            && node.end_byte() <= parameters.end_byte()
212        {
213            let mut cursor = parameters.walk();
214            return parameters.named_children(&mut cursor).any(|parameter| {
215                let binder = if parameter.kind() == "identifier" {
216                    Some(parameter)
217                } else {
218                    first_identifier_bounded(parameter, &mut || true).flatten()
219                };
220                binder.is_some_and(|binder| binder.id() == node.id())
221            });
222        }
223        if matches!(
224            parent.kind(),
225            "module" | "class_definition" | "function_definition" | "type_alias_statement"
226        ) {
227            return false;
228        }
229        current = parent;
230    }
231    false
232}
233
234#[derive(Clone, Copy, Debug)]
235pub struct PythonDirectScopeBinding<'tree> {
236    pub declaration: Node<'tree>,
237    pub kind: PythonDirectScopeBindingKind,
238}
239
240#[derive(Clone, Debug)]
241struct PythonLocalBinding<'tree> {
242    name: Box<str>,
243    declaration: Node<'tree>,
244}
245
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
247enum PythonLocalBindingKind {
248    FunctionOnly,
249    Other,
250}
251
252#[derive(Clone, Debug)]
253struct PythonComprehensionBinding {
254    name: Box<str>,
255    start_byte: usize,
256    end_byte: usize,
257    enclosing_iterable_ranges: Vec<(usize, usize)>,
258}
259
260/// Function-scope Python bindings discovered from tree-sitter structure.
261///
262/// The inventory models Python's whole-function symbol-table behavior while
263/// retaining the implicit scope of comprehension targets. Construction is
264/// iterative and every inspected node is gated by `scope_step`; `None` means
265/// the caller stopped discovery and must conservatively avoid module fallback.
266pub struct PythonLexicalScopeInventory<'tree> {
267    parameters: HashSet<Box<str>>,
268    locals: Vec<PythonLocalBinding<'tree>>,
269    local_names: HashMap<Box<str>, PythonLocalBindingKind>,
270    binding_writes: HashSet<Box<str>>,
271    globals: HashSet<Box<str>>,
272    nonlocals: HashSet<Box<str>>,
273    comprehensions: Vec<PythonComprehensionBinding>,
274}
275
276#[derive(Clone, Copy)]
277struct ScanFrame<'tree> {
278    node: Node<'tree>,
279    in_comprehension: bool,
280}
281
282impl<'tree> PythonLexicalScopeInventory<'tree> {
283    /// `parameter_names` is the callable's formal-parameter name stream. The
284    /// layout it comes from is resolved by `analyzer/lexical_definitions.rs`,
285    /// which dispatches through the analysis-side language registry and so
286    /// cannot be named here; `analyzer/python/lexical_scope.rs` in
287    /// `brokk-bifrost-analysis` is the one caller that computes it, under the
288    /// same `scope_step` meter this walk uses.
289    pub fn collect_bounded(
290        callable: Node<'tree>,
291        source: &str,
292        parameter_names: impl IntoIterator<Item = String>,
293        mut scope_step: impl FnMut() -> bool,
294    ) -> Option<Self> {
295        let mut inventory = Self {
296            parameters: parameter_names.into_iter().map(Box::<str>::from).collect(),
297            locals: Vec::new(),
298            local_names: HashMap::default(),
299            binding_writes: HashSet::default(),
300            globals: HashSet::default(),
301            nonlocals: HashSet::default(),
302            comprehensions: Vec::new(),
303        };
304        let Some(body) = callable.child_by_field_name("body") else {
305            return Some(inventory);
306        };
307        let mut stack = vec![ScanFrame {
308            node: body,
309            in_comprehension: false,
310        }];
311
312        while let Some(frame) = stack.pop() {
313            if !scope_step() {
314                return None;
315            }
316            let node = frame.node;
317            let nested_scope = node != body
318                && matches!(
319                    node.kind(),
320                    "function_definition" | "lambda" | "class_definition"
321                );
322            if nested_scope {
323                if matches!(node.kind(), "function_definition" | "class_definition")
324                    && let Some(name) = node.child_by_field_name("name")
325                {
326                    inventory.record_local_node(name, source);
327                }
328                // Defaults, annotations, bases, and type-parameter expressions
329                // execute in an enclosing scope. Keep scanning those headers
330                // for walrus/comprehension bindings while pruning the nested
331                // callable or class body itself.
332                push_nested_scope_header_children(
333                    &mut stack,
334                    node,
335                    frame.in_comprehension,
336                    &mut scope_step,
337                )?;
338                continue;
339            }
340
341            match node.kind() {
342                "global_statement" => {
343                    collect_direct_identifier_names(node, source, &mut scope_step, |name| {
344                        inventory.globals.insert(name.into());
345                    })?;
346                    continue;
347                }
348                "nonlocal_statement" => {
349                    collect_direct_identifier_names(node, source, &mut scope_step, |name| {
350                        inventory.nonlocals.insert(name.into());
351                    })?;
352                    continue;
353                }
354                "import_statement" | "import_from_statement" => {
355                    collect_import_bindings(node, &mut scope_step, |binding| {
356                        inventory.record_local_node(binding, source)
357                    })?;
358                    continue;
359                }
360                "assignment" | "augmented_assignment" => {
361                    if let Some(target) = node.child_by_field_name("left") {
362                        collect_binding_targets(
363                            target,
364                            source,
365                            &mut scope_step,
366                            |name, declaration| {
367                                inventory.record_binding_write(name, declaration);
368                            },
369                        )?;
370                    }
371                }
372                "type_alias_statement" => {
373                    if let Some(target) = node.child_by_field_name("left")
374                        && let Some(binding) = first_identifier_bounded(target, &mut scope_step)?
375                    {
376                        inventory.record_local_node(binding, source);
377                    }
378                }
379                "named_expression" => {
380                    // PEP 572 binds a comprehension walrus in the containing
381                    // non-comprehension scope, so this remains a function local.
382                    if let Some(target) = node.child_by_field_name("name") {
383                        collect_binding_targets(
384                            target,
385                            source,
386                            &mut scope_step,
387                            |name, declaration| {
388                                inventory.record_binding_write(name, declaration);
389                            },
390                        )?;
391                    }
392                }
393                "for_statement" => {
394                    if let Some(target) = node.child_by_field_name("left") {
395                        collect_binding_targets(
396                            target,
397                            source,
398                            &mut scope_step,
399                            |name, declaration| {
400                                inventory.record_binding_write(name, declaration);
401                            },
402                        )?;
403                    }
404                }
405                "for_in_clause" => {
406                    // These occur inside comprehensions and belong to their
407                    // implicit scope. The enclosing comprehension records them.
408                }
409                "delete_statement" => {
410                    for target in named_children_bounded(node, &mut scope_step)? {
411                        collect_binding_targets(
412                            target,
413                            source,
414                            &mut scope_step,
415                            |name, declaration| {
416                                inventory.record_binding_write(name, declaration);
417                            },
418                        )?;
419                    }
420                    continue;
421                }
422                "as_pattern" => {
423                    if let Some(alias) = node.child_by_field_name("alias") {
424                        collect_binding_targets(
425                            alias,
426                            source,
427                            &mut scope_step,
428                            |name, declaration| {
429                                inventory.record_binding_write(name, declaration);
430                            },
431                        )?;
432                        push_named_children_except(
433                            &mut stack,
434                            node,
435                            alias,
436                            frame.in_comprehension,
437                            &mut scope_step,
438                        )?;
439                        continue;
440                    }
441                }
442                "except_clause" => {
443                    // Older grammar shapes expose the alias directly. Current
444                    // tree-sitter-python nests it in an `as_pattern`, handled
445                    // above when the clause's children are scanned.
446                    if let Some(alias) = node.child_by_field_name("alias") {
447                        collect_binding_targets(
448                            alias,
449                            source,
450                            &mut scope_step,
451                            |name, declaration| {
452                                inventory.record_binding_write(name, declaration);
453                            },
454                        )?;
455                    }
456                }
457                "case_clause" => {
458                    let children = named_children_bounded(node, &mut scope_step)?;
459                    for child in children.iter().copied() {
460                        if child.kind() == "case_pattern" {
461                            collect_match_pattern_bindings(
462                                child,
463                                source,
464                                &mut scope_step,
465                                |name, declaration| {
466                                    inventory.record_binding_write(name, declaration);
467                                },
468                            )?;
469                        }
470                    }
471                    for child in children.into_iter().rev() {
472                        if child.kind() != "case_pattern" {
473                            stack.push(ScanFrame {
474                                node: child,
475                                in_comprehension: frame.in_comprehension,
476                            });
477                        }
478                    }
479                    continue;
480                }
481                kind if is_comprehension(kind) => {
482                    let range = (node.start_byte(), node.end_byte());
483                    let children = named_children_bounded(node, &mut scope_step)?;
484                    let enclosing_iterable_ranges = if let Some(first_clause) = children
485                        .iter()
486                        .copied()
487                        .find(|child| child.kind() == "for_in_clause")
488                    {
489                        children_by_field_name_bounded(first_clause, "right", &mut scope_step)?
490                            .into_iter()
491                            .map(|iterable| (iterable.start_byte(), iterable.end_byte()))
492                            .collect()
493                    } else {
494                        Vec::new()
495                    };
496                    for clause in children
497                        .iter()
498                        .copied()
499                        .filter(|child| child.kind() == "for_in_clause")
500                    {
501                        if let Some(target) = clause.child_by_field_name("left") {
502                            collect_binding_targets(target, source, &mut scope_step, |name, _| {
503                                inventory.comprehensions.push(PythonComprehensionBinding {
504                                    name: name.into(),
505                                    start_byte: range.0,
506                                    end_byte: range.1,
507                                    enclosing_iterable_ranges: enclosing_iterable_ranges.clone(),
508                                });
509                            })?;
510                        }
511                    }
512                    for child in children.into_iter().rev() {
513                        stack.push(ScanFrame {
514                            node: child,
515                            in_comprehension: true,
516                        });
517                    }
518                    continue;
519                }
520                _ => {}
521            }
522
523            push_named_children(&mut stack, node, frame.in_comprehension, &mut scope_step)?;
524        }
525
526        // `global` and `nonlocal` are whole-function directives regardless of
527        // source order. Neither declaration may become a semantic local.
528        inventory.locals.retain(|binding| {
529            !inventory.globals.contains(binding.name.as_ref())
530                && !inventory.nonlocals.contains(binding.name.as_ref())
531        });
532        inventory.local_names.retain(|name, _| {
533            !inventory.globals.contains(name.as_ref())
534                && !inventory.nonlocals.contains(name.as_ref())
535        });
536        Some(inventory)
537    }
538
539    pub fn name_resolution_at(
540        &self,
541        name: &str,
542        reference: Node<'_>,
543    ) -> PythonLexicalNameResolution {
544        let reference_byte = reference.start_byte();
545        if self.comprehensions.iter().any(|binding| {
546            binding.name.as_ref() == name
547                && binding.start_byte <= reference_byte
548                && reference_byte < binding.end_byte
549                && !binding
550                    .enclosing_iterable_ranges
551                    .iter()
552                    .any(|(start, end)| *start <= reference_byte && reference_byte < *end)
553        }) {
554            return PythonLexicalNameResolution::Local;
555        }
556        if self.nonlocals.contains(name) {
557            return PythonLexicalNameResolution::Nonlocal;
558        }
559        if self.globals.contains(name) {
560            return PythonLexicalNameResolution::Global;
561        }
562        if self.parameters.contains(name) || self.local_names.contains_key(name) {
563            PythonLexicalNameResolution::Local
564        } else {
565            PythonLexicalNameResolution::Unbound
566        }
567    }
568
569    pub fn resolves_to_local_function(&self, name: &str, reference: Node<'_>) -> bool {
570        let reference_byte = reference.start_byte();
571        !self.parameters.contains(name)
572            && !self.comprehensions.iter().any(|binding| {
573                binding.name.as_ref() == name
574                    && binding.start_byte <= reference_byte
575                    && reference_byte < binding.end_byte
576                    && !binding
577                        .enclosing_iterable_ranges
578                        .iter()
579                        .any(|(start, end)| *start <= reference_byte && reference_byte < *end)
580            })
581            && self.local_names.get(name) == Some(&PythonLocalBindingKind::FunctionOnly)
582    }
583
584    /// Whether the active callable obtains `name` from a runtime binding
585    /// rather than an untouched function or import declaration.
586    pub fn has_runtime_callable_binding_at(&self, name: &str, reference: Node<'_>) -> bool {
587        let reference_byte = reference.start_byte();
588        self.parameters.contains(name)
589            || self.binding_writes.contains(name)
590            || self.comprehensions.iter().any(|binding| {
591                binding.name.as_ref() == name
592                    && binding.start_byte <= reference_byte
593                    && reference_byte < binding.end_byte
594                    && !binding
595                        .enclosing_iterable_ranges
596                        .iter()
597                        .any(|(start, end)| *start <= reference_byte && reference_byte < *end)
598            })
599    }
600
601    pub fn local_function_declaration(
602        &self,
603        name: &str,
604        reference: Node<'_>,
605    ) -> Option<Node<'tree>> {
606        self.resolves_to_local_function(name, reference)
607            .then(|| {
608                self.locals
609                    .iter()
610                    .find(|binding| binding.name.as_ref() == name)
611                    .and_then(|binding| binding.declaration.parent())
612                    .filter(|declaration| declaration.kind() == "function_definition")
613            })
614            .flatten()
615    }
616
617    pub fn local_bindings(&self) -> impl Iterator<Item = (&str, Node<'tree>)> + '_ {
618        self.locals
619            .iter()
620            .map(|binding| (binding.name.as_ref(), binding.declaration))
621    }
622
623    fn record_local_node(&mut self, node: Node<'tree>, source: &str) {
624        let name = node_text(node, source);
625        self.record_local(name, node);
626    }
627
628    fn record_local(&mut self, name: &str, declaration: Node<'tree>) {
629        if name.is_empty() {
630            return;
631        }
632        let binding_kind = if is_function_declaration_name(declaration) {
633            PythonLocalBindingKind::FunctionOnly
634        } else {
635            PythonLocalBindingKind::Other
636        };
637        match self.local_names.entry(name.into()) {
638            std::collections::hash_map::Entry::Vacant(entry) => {
639                entry.insert(binding_kind);
640                self.locals.push(PythonLocalBinding {
641                    name: name.into(),
642                    declaration,
643                });
644            }
645            std::collections::hash_map::Entry::Occupied(mut entry) => {
646                entry.insert(PythonLocalBindingKind::Other);
647            }
648        }
649    }
650
651    fn record_binding_write(&mut self, name: &str, declaration: Node<'tree>) {
652        if !name.is_empty() {
653            self.binding_writes.insert(name.into());
654        }
655        self.record_local(name, declaration);
656    }
657}
658
659fn is_function_declaration_name(node: Node<'_>) -> bool {
660    node.parent().is_some_and(|parent| {
661        parent.kind() == "function_definition"
662            && parent
663                .child_by_field_name("name")
664                .is_some_and(|name| name.id() == node.id())
665    })
666}
667
668/// Return the bindings introduced directly by `node`.
669///
670/// Descendant traversal remains the caller's responsibility. This lets the
671/// semantic file walk build a module binding inventory without adding a
672/// second whole-file scan, while reusing the same structured target handling
673/// as function symbol-table discovery.
674pub fn python_direct_scope_bindings_bounded<'tree>(
675    node: Node<'tree>,
676    source: &str,
677    mut scope_step: impl FnMut() -> bool,
678) -> Option<Vec<PythonDirectScopeBinding<'tree>>> {
679    let mut bindings = Vec::new();
680
681    match node.kind() {
682        "function_definition" => {
683            if let Some(name) = node.child_by_field_name("name") {
684                bindings.push(PythonDirectScopeBinding {
685                    declaration: name,
686                    kind: PythonDirectScopeBindingKind::Other,
687                });
688            }
689        }
690        "class_definition" => {
691            if let Some(name) = node.child_by_field_name("name") {
692                bindings.push(PythonDirectScopeBinding {
693                    declaration: name,
694                    kind: if is_direct_module_definition_bounded(node, &mut scope_step)? {
695                        PythonDirectScopeBindingKind::ClassDeclaration
696                    } else {
697                        PythonDirectScopeBindingKind::Other
698                    },
699                });
700            }
701        }
702        "import_statement" | "import_from_statement" => {
703            collect_import_bindings(node, &mut scope_step, |declaration| {
704                bindings.push(PythonDirectScopeBinding {
705                    declaration,
706                    kind: PythonDirectScopeBindingKind::Other,
707                });
708            })?;
709        }
710        "assignment" | "augmented_assignment" => {
711            if let Some(target) = node.child_by_field_name("left") {
712                collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
713                    bindings.push(PythonDirectScopeBinding {
714                        declaration,
715                        kind: PythonDirectScopeBindingKind::Other,
716                    });
717                })?;
718            }
719        }
720        "type_alias_statement" => {
721            if let Some(target) = node.child_by_field_name("left")
722                && let Some(declaration) = first_identifier_bounded(target, &mut scope_step)?
723            {
724                bindings.push(PythonDirectScopeBinding {
725                    declaration,
726                    kind: PythonDirectScopeBindingKind::Other,
727                });
728            }
729        }
730        "named_expression" => {
731            if let Some(target) = node.child_by_field_name("name") {
732                collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
733                    bindings.push(PythonDirectScopeBinding {
734                        declaration,
735                        kind: PythonDirectScopeBindingKind::Other,
736                    });
737                })?;
738            }
739        }
740        "for_statement" => {
741            if let Some(target) = node.child_by_field_name("left") {
742                collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
743                    bindings.push(PythonDirectScopeBinding {
744                        declaration,
745                        kind: PythonDirectScopeBindingKind::Other,
746                    });
747                })?;
748            }
749        }
750        "delete_statement" => {
751            for target in named_children_bounded(node, &mut scope_step)? {
752                collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
753                    bindings.push(PythonDirectScopeBinding {
754                        declaration,
755                        kind: PythonDirectScopeBindingKind::Other,
756                    });
757                })?;
758            }
759        }
760        "as_pattern" => {
761            if let Some(alias) = node.child_by_field_name("alias") {
762                collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
763                    bindings.push(PythonDirectScopeBinding {
764                        declaration,
765                        kind: PythonDirectScopeBindingKind::Other,
766                    });
767                })?;
768            }
769        }
770        "except_clause" => {
771            if let Some(alias) = node.child_by_field_name("alias") {
772                collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
773                    bindings.push(PythonDirectScopeBinding {
774                        declaration,
775                        kind: PythonDirectScopeBindingKind::Other,
776                    });
777                })?;
778            }
779        }
780        "case_clause" => {
781            for child in named_children_bounded(node, &mut scope_step)? {
782                if child.kind() == "case_pattern" {
783                    collect_match_pattern_bindings(
784                        child,
785                        source,
786                        &mut scope_step,
787                        |_, declaration| {
788                            bindings.push(PythonDirectScopeBinding {
789                                declaration,
790                                kind: PythonDirectScopeBindingKind::Other,
791                            });
792                        },
793                    )?;
794                }
795            }
796        }
797        _ => {}
798    }
799    Some(bindings)
800}
801
802/// Whether a module or class execution scope binds `target_name` anywhere in
803/// its direct scope. Nested callable and class bodies are separate scopes, but
804/// their headers still execute in this scope. A wildcard import keeps the
805/// name's runtime identity open even when it does not expose a static binder.
806pub fn python_module_or_class_scope_binds_name_bounded(
807    scope: Node<'_>,
808    target_name: &str,
809    source: &str,
810    mut scope_step: impl FnMut() -> bool,
811) -> Option<bool> {
812    assert!(matches!(scope.kind(), "module" | "class_definition"));
813    let body = scope.child_by_field_name("body").unwrap_or(scope);
814    let mut stack = vec![body];
815    while let Some(node) = stack.pop() {
816        if !scope_step() {
817            return None;
818        }
819        if node.kind() == "wildcard_import" {
820            return Some(true);
821        }
822        if python_direct_scope_bindings_bounded(node, source, &mut scope_step)?
823            .into_iter()
824            .any(|binding| node_text(binding.declaration, source) == target_name)
825        {
826            return Some(true);
827        }
828        let excluded_body = matches!(
829            node.kind(),
830            "class_definition" | "function_definition" | "lambda"
831        )
832        .then(|| node.child_by_field_name("body").map(|body| body.id()))
833        .flatten();
834        let mut cursor = node.walk();
835        stack.extend(
836            node.named_children(&mut cursor)
837                .filter(|child| Some(child.id()) != excluded_body),
838        );
839    }
840    Some(false)
841}
842
843pub fn python_unambiguous_module_class_binding_bounded(
844    root: Node<'_>,
845    source: &str,
846    target_name: &str,
847    mut scope_step: impl FnMut() -> bool,
848) -> Option<bool> {
849    let mut matched = None;
850    let mut stack = vec![root];
851    while let Some(node) = stack.pop() {
852        if !scope_step() {
853            return None;
854        }
855        // A later unconditional class declaration replaces an earlier
856        // wildcard binding; a wildcard after the class leaves its identity open.
857        if node.kind() == "wildcard_import" && matched.is_some() {
858            return Some(false);
859        }
860        for binding in python_direct_scope_bindings_bounded(node, source, &mut scope_step)? {
861            if node_text(binding.declaration, source) != target_name {
862                continue;
863            }
864            if matched.is_some() {
865                return Some(false);
866            }
867            matched = Some(binding.kind);
868            if binding.kind == PythonDirectScopeBindingKind::Other {
869                return Some(false);
870            }
871        }
872
873        let body = matches!(
874            node.kind(),
875            "function_definition" | "class_definition" | "lambda"
876        )
877        .then(|| node.child_by_field_name("body").map(|child| child.id()))
878        .flatten();
879        let name = matches!(node.kind(), "function_definition" | "class_definition")
880            .then(|| node.child_by_field_name("name").map(|child| child.id()))
881            .flatten();
882        for child in named_children_bounded(node, &mut scope_step)?
883            .into_iter()
884            .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
885            .rev()
886        {
887            stack.push(child);
888        }
889    }
890    Some(matched == Some(PythonDirectScopeBindingKind::ClassDeclaration))
891}
892
893fn is_direct_module_definition_bounded(
894    node: Node<'_>,
895    scope_step: &mut impl FnMut() -> bool,
896) -> Option<bool> {
897    if !scope_step() {
898        return None;
899    }
900    let Some(mut parent) = node.parent() else {
901        return Some(false);
902    };
903    if parent.kind() == "decorated_definition" {
904        if !scope_step() {
905            return None;
906        }
907        let Some(grandparent) = parent.parent() else {
908            return Some(false);
909        };
910        parent = grandparent;
911    }
912    Some(parent.kind() == "module")
913}
914
915fn collect_direct_identifier_names(
916    node: Node<'_>,
917    source: &str,
918    scope_step: &mut impl FnMut() -> bool,
919    mut record: impl FnMut(&str),
920) -> Option<()> {
921    for child in named_children_bounded(node, scope_step)? {
922        if child.kind() == "identifier" {
923            let name = node_text(child, source);
924            if !name.is_empty() {
925                record(name);
926            }
927        }
928    }
929    Some(())
930}
931
932fn collect_import_bindings<'tree>(
933    statement: Node<'tree>,
934    scope_step: &mut impl FnMut() -> bool,
935    mut record: impl FnMut(Node<'tree>),
936) -> Option<()> {
937    let mut cursor = statement.walk();
938    let mut imports = Vec::new();
939    for imported in statement.children_by_field_name("name", &mut cursor) {
940        if !scope_step() {
941            return None;
942        }
943        imports.push(imported);
944    }
945    for imported in imports {
946        if let Some(alias) = imported.child_by_field_name("alias") {
947            record(alias);
948            continue;
949        }
950        let name = imported.child_by_field_name("name").unwrap_or(imported);
951        let binding = if statement.kind() == "import_statement" {
952            first_identifier_bounded(name, scope_step)?
953        } else {
954            last_identifier_bounded(name, scope_step)?
955        };
956        if let Some(binding) = binding {
957            record(binding);
958        }
959    }
960    Some(())
961}
962
963fn first_identifier_bounded<'tree>(
964    root: Node<'tree>,
965    scope_step: &mut impl FnMut() -> bool,
966) -> Option<Option<Node<'tree>>> {
967    let mut stack = vec![root];
968    while let Some(node) = stack.pop() {
969        if !scope_step() {
970            return None;
971        }
972        if node.kind() == "identifier" {
973            return Some(Some(node));
974        }
975        let children = named_children_bounded(node, scope_step)?;
976        stack.extend(children.into_iter().rev());
977    }
978    Some(None)
979}
980
981fn last_identifier_bounded<'tree>(
982    root: Node<'tree>,
983    scope_step: &mut impl FnMut() -> bool,
984) -> Option<Option<Node<'tree>>> {
985    let mut result = None;
986    let mut stack = vec![root];
987    while let Some(node) = stack.pop() {
988        if !scope_step() {
989            return None;
990        }
991        if node.kind() == "identifier" {
992            result = Some(node);
993            continue;
994        }
995        let children = named_children_bounded(node, scope_step)?;
996        stack.extend(children.into_iter().rev());
997    }
998    Some(result)
999}
1000
1001fn collect_binding_targets<'tree>(
1002    target: Node<'tree>,
1003    source: &str,
1004    scope_step: &mut impl FnMut() -> bool,
1005    mut record: impl FnMut(&str, Node<'tree>),
1006) -> Option<()> {
1007    let mut stack = vec![target];
1008    while let Some(node) = stack.pop() {
1009        if !scope_step() {
1010            return None;
1011        }
1012        match node.kind() {
1013            // These mutate an existing object and do not bind either the
1014            // receiver or member name in the function.
1015            "attribute" | "subscript" => continue,
1016            "identifier" | "keyword_identifier" => {
1017                let name = node_text(node, source);
1018                if !name.is_empty() {
1019                    record(name, node);
1020                }
1021                continue;
1022            }
1023            _ => {}
1024        }
1025        let children = named_children_bounded(node, scope_step)?;
1026        if node.kind() == "as_pattern_target" && children.is_empty() {
1027            let name = node_text(node, source);
1028            if !name.is_empty() {
1029                record(name, node);
1030            }
1031            continue;
1032        }
1033        stack.extend(children.into_iter().rev());
1034    }
1035    Some(())
1036}
1037
1038fn collect_match_pattern_bindings<'tree>(
1039    pattern: Node<'tree>,
1040    source: &str,
1041    scope_step: &mut impl FnMut() -> bool,
1042    mut record: impl FnMut(&str, Node<'tree>),
1043) -> Option<()> {
1044    let mut stack = vec![pattern];
1045    while let Some(node) = stack.pop() {
1046        if !scope_step() {
1047            return None;
1048        }
1049        match node.kind() {
1050            "dotted_name" => {
1051                let identifiers = named_children_bounded(node, scope_step)?
1052                    .into_iter()
1053                    .filter(|child| child.kind() == "identifier")
1054                    .collect::<Vec<_>>();
1055                if let [binding] = identifiers.as_slice() {
1056                    let name = node_text(*binding, source);
1057                    if !name.is_empty() {
1058                        record(name, *binding);
1059                    }
1060                }
1061                continue;
1062            }
1063            "splat_pattern" => {
1064                for child in named_children_bounded(node, scope_step)? {
1065                    if child.kind() == "identifier" {
1066                        let name = node_text(child, source);
1067                        if !name.is_empty() {
1068                            record(name, child);
1069                        }
1070                    }
1071                }
1072                continue;
1073            }
1074            "class_pattern" => {
1075                let mut children = named_children_bounded(node, scope_step)?;
1076                if children
1077                    .first()
1078                    .is_some_and(|child| child.kind() == "dotted_name")
1079                {
1080                    children.remove(0);
1081                }
1082                stack.extend(children.into_iter().rev());
1083                continue;
1084            }
1085            "keyword_pattern" => {
1086                let mut children = named_children_bounded(node, scope_step)?;
1087                if children
1088                    .first()
1089                    .is_some_and(|child| child.kind() == "identifier")
1090                {
1091                    children.remove(0);
1092                }
1093                stack.extend(children.into_iter().rev());
1094                continue;
1095            }
1096            "dict_pattern" => {
1097                let key_ids = children_by_field_name_bounded(node, "key", scope_step)?
1098                    .into_iter()
1099                    .map(|key| key.id())
1100                    .collect::<HashSet<_>>();
1101                let children = named_children_bounded(node, scope_step)?;
1102                stack.extend(
1103                    children
1104                        .into_iter()
1105                        .filter(|child| !key_ids.contains(&child.id()))
1106                        .rev(),
1107                );
1108                continue;
1109            }
1110            "as_pattern" => {
1111                if let Some(alias) = node.child_by_field_name("alias") {
1112                    collect_binding_targets(alias, source, scope_step, &mut record)?;
1113                    let children = named_children_bounded(node, scope_step)?;
1114                    stack.extend(
1115                        children
1116                            .into_iter()
1117                            .filter(|child| child.id() != alias.id())
1118                            .rev(),
1119                    );
1120                    continue;
1121                }
1122                let mut children = named_children_bounded(node, scope_step)?;
1123                if let Some(alias) = children
1124                    .last()
1125                    .copied()
1126                    .filter(|child| child.kind() == "identifier")
1127                {
1128                    let name = node_text(alias, source);
1129                    if !name.is_empty() {
1130                        record(name, alias);
1131                    }
1132                    children.pop();
1133                }
1134                stack.extend(children.into_iter().rev());
1135                continue;
1136            }
1137            "identifier" => {
1138                // Direct identifiers are match `as` aliases handled by their
1139                // parent. Keyword names and class heads are likewise skipped.
1140                continue;
1141            }
1142            kind if is_pattern_literal(kind) => continue,
1143            _ => {}
1144        }
1145        let children = named_children_bounded(node, scope_step)?;
1146        stack.extend(children.into_iter().rev());
1147    }
1148    Some(())
1149}
1150
1151fn push_named_children<'tree>(
1152    stack: &mut Vec<ScanFrame<'tree>>,
1153    node: Node<'tree>,
1154    in_comprehension: bool,
1155    scope_step: &mut impl FnMut() -> bool,
1156) -> Option<()> {
1157    for child in named_children_bounded(node, scope_step)?.into_iter().rev() {
1158        stack.push(ScanFrame {
1159            node: child,
1160            in_comprehension,
1161        });
1162    }
1163    Some(())
1164}
1165
1166fn push_named_children_except<'tree>(
1167    stack: &mut Vec<ScanFrame<'tree>>,
1168    node: Node<'tree>,
1169    excluded: Node<'tree>,
1170    in_comprehension: bool,
1171    scope_step: &mut impl FnMut() -> bool,
1172) -> Option<()> {
1173    for child in named_children_bounded(node, scope_step)?
1174        .into_iter()
1175        .filter(|child| child.id() != excluded.id())
1176        .rev()
1177    {
1178        stack.push(ScanFrame {
1179            node: child,
1180            in_comprehension,
1181        });
1182    }
1183    Some(())
1184}
1185
1186fn push_nested_scope_header_children<'tree>(
1187    stack: &mut Vec<ScanFrame<'tree>>,
1188    node: Node<'tree>,
1189    in_comprehension: bool,
1190    scope_step: &mut impl FnMut() -> bool,
1191) -> Option<()> {
1192    let body = node.child_by_field_name("body").map(|child| child.id());
1193    let name = node.child_by_field_name("name").map(|child| child.id());
1194    for child in named_children_bounded(node, scope_step)?
1195        .into_iter()
1196        .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
1197        .rev()
1198    {
1199        stack.push(ScanFrame {
1200            node: child,
1201            in_comprehension,
1202        });
1203    }
1204    Some(())
1205}
1206
1207fn named_children_bounded<'tree>(
1208    node: Node<'tree>,
1209    scope_step: &mut impl FnMut() -> bool,
1210) -> Option<Vec<Node<'tree>>> {
1211    let mut cursor = node.walk();
1212    let mut children = Vec::new();
1213    for child in node.named_children(&mut cursor) {
1214        if !scope_step() {
1215            return None;
1216        }
1217        children.push(child);
1218    }
1219    Some(children)
1220}
1221
1222fn children_by_field_name_bounded<'tree>(
1223    node: Node<'tree>,
1224    field: &str,
1225    scope_step: &mut impl FnMut() -> bool,
1226) -> Option<Vec<Node<'tree>>> {
1227    let mut cursor = node.walk();
1228    let mut children = Vec::new();
1229    for child in node.children_by_field_name(field, &mut cursor) {
1230        if !scope_step() {
1231            return None;
1232        }
1233        children.push(child);
1234    }
1235    Some(children)
1236}
1237
1238fn is_comprehension(kind: &str) -> bool {
1239    matches!(
1240        kind,
1241        "list_comprehension"
1242            | "set_comprehension"
1243            | "dictionary_comprehension"
1244            | "generator_expression"
1245    )
1246}
1247
1248fn is_pattern_literal(kind: &str) -> bool {
1249    matches!(
1250        kind,
1251        "string"
1252            | "concatenated_string"
1253            | "integer"
1254            | "float"
1255            | "complex_pattern"
1256            | "true"
1257            | "false"
1258            | "none"
1259    )
1260}
1261
1262fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1263    brokk_bifrost_core::analyzer::common::node_source_text_trimmed(node, source)
1264}