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
802pub fn python_unambiguous_module_class_binding_bounded(
803    root: Node<'_>,
804    source: &str,
805    target_name: &str,
806    mut scope_step: impl FnMut() -> bool,
807) -> Option<bool> {
808    let mut matched = None;
809    let mut stack = vec![root];
810    while let Some(node) = stack.pop() {
811        if !scope_step() {
812            return None;
813        }
814        for binding in python_direct_scope_bindings_bounded(node, source, &mut scope_step)? {
815            if node_text(binding.declaration, source) != target_name {
816                continue;
817            }
818            if matched.is_some() {
819                return Some(false);
820            }
821            matched = Some(binding.kind);
822            if binding.kind == PythonDirectScopeBindingKind::Other {
823                return Some(false);
824            }
825        }
826
827        let body = matches!(
828            node.kind(),
829            "function_definition" | "class_definition" | "lambda"
830        )
831        .then(|| node.child_by_field_name("body").map(|child| child.id()))
832        .flatten();
833        let name = matches!(node.kind(), "function_definition" | "class_definition")
834            .then(|| node.child_by_field_name("name").map(|child| child.id()))
835            .flatten();
836        for child in named_children_bounded(node, &mut scope_step)?
837            .into_iter()
838            .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
839            .rev()
840        {
841            stack.push(child);
842        }
843    }
844    Some(matched == Some(PythonDirectScopeBindingKind::ClassDeclaration))
845}
846
847fn is_direct_module_definition_bounded(
848    node: Node<'_>,
849    scope_step: &mut impl FnMut() -> bool,
850) -> Option<bool> {
851    if !scope_step() {
852        return None;
853    }
854    let Some(mut parent) = node.parent() else {
855        return Some(false);
856    };
857    if parent.kind() == "decorated_definition" {
858        if !scope_step() {
859            return None;
860        }
861        let Some(grandparent) = parent.parent() else {
862            return Some(false);
863        };
864        parent = grandparent;
865    }
866    Some(parent.kind() == "module")
867}
868
869fn collect_direct_identifier_names(
870    node: Node<'_>,
871    source: &str,
872    scope_step: &mut impl FnMut() -> bool,
873    mut record: impl FnMut(&str),
874) -> Option<()> {
875    for child in named_children_bounded(node, scope_step)? {
876        if child.kind() == "identifier" {
877            let name = node_text(child, source);
878            if !name.is_empty() {
879                record(name);
880            }
881        }
882    }
883    Some(())
884}
885
886fn collect_import_bindings<'tree>(
887    statement: Node<'tree>,
888    scope_step: &mut impl FnMut() -> bool,
889    mut record: impl FnMut(Node<'tree>),
890) -> Option<()> {
891    let mut cursor = statement.walk();
892    let mut imports = Vec::new();
893    for imported in statement.children_by_field_name("name", &mut cursor) {
894        if !scope_step() {
895            return None;
896        }
897        imports.push(imported);
898    }
899    for imported in imports {
900        if let Some(alias) = imported.child_by_field_name("alias") {
901            record(alias);
902            continue;
903        }
904        let name = imported.child_by_field_name("name").unwrap_or(imported);
905        let binding = if statement.kind() == "import_statement" {
906            first_identifier_bounded(name, scope_step)?
907        } else {
908            last_identifier_bounded(name, scope_step)?
909        };
910        if let Some(binding) = binding {
911            record(binding);
912        }
913    }
914    Some(())
915}
916
917fn first_identifier_bounded<'tree>(
918    root: Node<'tree>,
919    scope_step: &mut impl FnMut() -> bool,
920) -> Option<Option<Node<'tree>>> {
921    let mut stack = vec![root];
922    while let Some(node) = stack.pop() {
923        if !scope_step() {
924            return None;
925        }
926        if node.kind() == "identifier" {
927            return Some(Some(node));
928        }
929        let children = named_children_bounded(node, scope_step)?;
930        stack.extend(children.into_iter().rev());
931    }
932    Some(None)
933}
934
935fn last_identifier_bounded<'tree>(
936    root: Node<'tree>,
937    scope_step: &mut impl FnMut() -> bool,
938) -> Option<Option<Node<'tree>>> {
939    let mut result = None;
940    let mut stack = vec![root];
941    while let Some(node) = stack.pop() {
942        if !scope_step() {
943            return None;
944        }
945        if node.kind() == "identifier" {
946            result = Some(node);
947            continue;
948        }
949        let children = named_children_bounded(node, scope_step)?;
950        stack.extend(children.into_iter().rev());
951    }
952    Some(result)
953}
954
955fn collect_binding_targets<'tree>(
956    target: Node<'tree>,
957    source: &str,
958    scope_step: &mut impl FnMut() -> bool,
959    mut record: impl FnMut(&str, Node<'tree>),
960) -> Option<()> {
961    let mut stack = vec![target];
962    while let Some(node) = stack.pop() {
963        if !scope_step() {
964            return None;
965        }
966        match node.kind() {
967            // These mutate an existing object and do not bind either the
968            // receiver or member name in the function.
969            "attribute" | "subscript" => continue,
970            "identifier" | "keyword_identifier" => {
971                let name = node_text(node, source);
972                if !name.is_empty() {
973                    record(name, node);
974                }
975                continue;
976            }
977            _ => {}
978        }
979        let children = named_children_bounded(node, scope_step)?;
980        if node.kind() == "as_pattern_target" && children.is_empty() {
981            let name = node_text(node, source);
982            if !name.is_empty() {
983                record(name, node);
984            }
985            continue;
986        }
987        stack.extend(children.into_iter().rev());
988    }
989    Some(())
990}
991
992fn collect_match_pattern_bindings<'tree>(
993    pattern: Node<'tree>,
994    source: &str,
995    scope_step: &mut impl FnMut() -> bool,
996    mut record: impl FnMut(&str, Node<'tree>),
997) -> Option<()> {
998    let mut stack = vec![pattern];
999    while let Some(node) = stack.pop() {
1000        if !scope_step() {
1001            return None;
1002        }
1003        match node.kind() {
1004            "dotted_name" => {
1005                let identifiers = named_children_bounded(node, scope_step)?
1006                    .into_iter()
1007                    .filter(|child| child.kind() == "identifier")
1008                    .collect::<Vec<_>>();
1009                if let [binding] = identifiers.as_slice() {
1010                    let name = node_text(*binding, source);
1011                    if !name.is_empty() {
1012                        record(name, *binding);
1013                    }
1014                }
1015                continue;
1016            }
1017            "splat_pattern" => {
1018                for child in named_children_bounded(node, scope_step)? {
1019                    if child.kind() == "identifier" {
1020                        let name = node_text(child, source);
1021                        if !name.is_empty() {
1022                            record(name, child);
1023                        }
1024                    }
1025                }
1026                continue;
1027            }
1028            "class_pattern" => {
1029                let mut children = named_children_bounded(node, scope_step)?;
1030                if children
1031                    .first()
1032                    .is_some_and(|child| child.kind() == "dotted_name")
1033                {
1034                    children.remove(0);
1035                }
1036                stack.extend(children.into_iter().rev());
1037                continue;
1038            }
1039            "keyword_pattern" => {
1040                let mut children = named_children_bounded(node, scope_step)?;
1041                if children
1042                    .first()
1043                    .is_some_and(|child| child.kind() == "identifier")
1044                {
1045                    children.remove(0);
1046                }
1047                stack.extend(children.into_iter().rev());
1048                continue;
1049            }
1050            "dict_pattern" => {
1051                let key_ids = children_by_field_name_bounded(node, "key", scope_step)?
1052                    .into_iter()
1053                    .map(|key| key.id())
1054                    .collect::<HashSet<_>>();
1055                let children = named_children_bounded(node, scope_step)?;
1056                stack.extend(
1057                    children
1058                        .into_iter()
1059                        .filter(|child| !key_ids.contains(&child.id()))
1060                        .rev(),
1061                );
1062                continue;
1063            }
1064            "as_pattern" => {
1065                if let Some(alias) = node.child_by_field_name("alias") {
1066                    collect_binding_targets(alias, source, scope_step, &mut record)?;
1067                    let children = named_children_bounded(node, scope_step)?;
1068                    stack.extend(
1069                        children
1070                            .into_iter()
1071                            .filter(|child| child.id() != alias.id())
1072                            .rev(),
1073                    );
1074                    continue;
1075                }
1076                let mut children = named_children_bounded(node, scope_step)?;
1077                if let Some(alias) = children
1078                    .last()
1079                    .copied()
1080                    .filter(|child| child.kind() == "identifier")
1081                {
1082                    let name = node_text(alias, source);
1083                    if !name.is_empty() {
1084                        record(name, alias);
1085                    }
1086                    children.pop();
1087                }
1088                stack.extend(children.into_iter().rev());
1089                continue;
1090            }
1091            "identifier" => {
1092                // Direct identifiers are match `as` aliases handled by their
1093                // parent. Keyword names and class heads are likewise skipped.
1094                continue;
1095            }
1096            kind if is_pattern_literal(kind) => continue,
1097            _ => {}
1098        }
1099        let children = named_children_bounded(node, scope_step)?;
1100        stack.extend(children.into_iter().rev());
1101    }
1102    Some(())
1103}
1104
1105fn push_named_children<'tree>(
1106    stack: &mut Vec<ScanFrame<'tree>>,
1107    node: Node<'tree>,
1108    in_comprehension: bool,
1109    scope_step: &mut impl FnMut() -> bool,
1110) -> Option<()> {
1111    for child in named_children_bounded(node, scope_step)?.into_iter().rev() {
1112        stack.push(ScanFrame {
1113            node: child,
1114            in_comprehension,
1115        });
1116    }
1117    Some(())
1118}
1119
1120fn push_named_children_except<'tree>(
1121    stack: &mut Vec<ScanFrame<'tree>>,
1122    node: Node<'tree>,
1123    excluded: Node<'tree>,
1124    in_comprehension: bool,
1125    scope_step: &mut impl FnMut() -> bool,
1126) -> Option<()> {
1127    for child in named_children_bounded(node, scope_step)?
1128        .into_iter()
1129        .filter(|child| child.id() != excluded.id())
1130        .rev()
1131    {
1132        stack.push(ScanFrame {
1133            node: child,
1134            in_comprehension,
1135        });
1136    }
1137    Some(())
1138}
1139
1140fn push_nested_scope_header_children<'tree>(
1141    stack: &mut Vec<ScanFrame<'tree>>,
1142    node: Node<'tree>,
1143    in_comprehension: bool,
1144    scope_step: &mut impl FnMut() -> bool,
1145) -> Option<()> {
1146    let body = node.child_by_field_name("body").map(|child| child.id());
1147    let name = node.child_by_field_name("name").map(|child| child.id());
1148    for child in named_children_bounded(node, scope_step)?
1149        .into_iter()
1150        .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
1151        .rev()
1152    {
1153        stack.push(ScanFrame {
1154            node: child,
1155            in_comprehension,
1156        });
1157    }
1158    Some(())
1159}
1160
1161fn named_children_bounded<'tree>(
1162    node: Node<'tree>,
1163    scope_step: &mut impl FnMut() -> bool,
1164) -> Option<Vec<Node<'tree>>> {
1165    let mut cursor = node.walk();
1166    let mut children = Vec::new();
1167    for child in node.named_children(&mut cursor) {
1168        if !scope_step() {
1169            return None;
1170        }
1171        children.push(child);
1172    }
1173    Some(children)
1174}
1175
1176fn children_by_field_name_bounded<'tree>(
1177    node: Node<'tree>,
1178    field: &str,
1179    scope_step: &mut impl FnMut() -> bool,
1180) -> Option<Vec<Node<'tree>>> {
1181    let mut cursor = node.walk();
1182    let mut children = Vec::new();
1183    for child in node.children_by_field_name(field, &mut cursor) {
1184        if !scope_step() {
1185            return None;
1186        }
1187        children.push(child);
1188    }
1189    Some(children)
1190}
1191
1192fn is_comprehension(kind: &str) -> bool {
1193    matches!(
1194        kind,
1195        "list_comprehension"
1196            | "set_comprehension"
1197            | "dictionary_comprehension"
1198            | "generator_expression"
1199    )
1200}
1201
1202fn is_pattern_literal(kind: &str) -> bool {
1203    matches!(
1204        kind,
1205        "string"
1206            | "concatenated_string"
1207            | "integer"
1208            | "float"
1209            | "complex_pattern"
1210            | "true"
1211            | "false"
1212            | "none"
1213    )
1214}
1215
1216fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1217    brokk_bifrost_core::analyzer::common::node_source_text_trimmed(node, source)
1218}