Skip to main content

brokk_bifrost_python/graph/
resolver.rs

1//! Python's reference resolution: export-name and seed inference, receiver-type
2//! resolution, and annotation candidates.
3//!
4//! `index` arguments below are the *dispatching* analyzer's
5//! [`CodeUnitIndex`] (see [`PythonGraphSource`]); `python` is the Python
6//! analyzer's memoized products.
7
8use crate::graph::PythonGraphSource;
9use crate::graph_support::PythonUsageSource;
10use crate::imports::resolve_fqn_candidates;
11use crate::syntax::{
12    python_deferred_annotation_identifier_ranges, python_deferred_annotation_tree,
13    python_node_is_in_annotation,
14};
15use crate::usage_index::{
16    ModuleBindingEventKind, ModuleBindingTimeline, usage_resolve_module_files, usage_seeds,
17};
18use brokk_bifrost_core::analyzer::fq_name::{FqName, SegmentKind, segment_interner};
19use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path;
20use brokk_bifrost_core::analyzer::usages::model::{ExportEntry, ImportBinder, ImportKind};
21use brokk_bifrost_core::analyzer::usages::{ImportEdge, ImportEdgeKind};
22use brokk_bifrost_core::analyzer::{
23    CodeUnit, CodeUnitIndex, DefinitionLanguageScope, Language, ProjectFile, Range,
24    RelationalDefinitionQuery, RelationalDefinitionQuestion, RelationalDefinitionValue,
25    RelationalName,
26};
27use brokk_bifrost_core::hash::HashSet;
28use std::collections::BTreeSet;
29use tree_sitter::Node;
30
31const MAX_MEMBER_OWNER_FRONTIER: usize = 512;
32
33/// Resolve the declarations selected by Python member lookup for one concrete
34/// receiver type. A declaration on the receiver (or on a nearer base layer)
35/// overrides every same-named declaration farther up the hierarchy.
36pub(crate) fn resolved_member_declarations(
37    graph: &PythonGraphSource<'_>,
38    receiver: &CodeUnit,
39    member: &str,
40) -> Vec<CodeUnit> {
41    let mut visited = HashSet::default();
42    let mut frontier = vec![receiver.clone()];
43    while !frontier.is_empty() && visited.len() < MAX_MEMBER_OWNER_FRONTIER {
44        let mut declarations = Vec::new();
45        let mut next = Vec::new();
46        for owner in frontier {
47            if !visited.insert(owner.clone()) {
48                continue;
49            }
50            let fqn = format!("{}.{member}", owner.fq_name());
51            declarations.extend(
52                graph
53                    .index
54                    .definitions(&fqn)
55                    .filter(|candidate| graph.index.parent_of(candidate).as_ref() == Some(&owner)),
56            );
57            if let Some(hierarchy) = graph.hierarchy {
58                next.extend(hierarchy.get_direct_ancestors(&owner));
59            }
60        }
61        declarations.sort();
62        declarations.dedup();
63        if !declarations.is_empty() {
64            return declarations;
65        }
66        next.sort();
67        next.dedup();
68        frontier = next;
69    }
70    Vec::new()
71}
72
73pub fn infer_export_names(python: &dyn PythonUsageSource, target: &CodeUnit) -> BTreeSet<String> {
74    if target_owner_code_unit(python, target).is_some() {
75        let owner_name = top_level_identifier(python, target);
76        let owner_exports =
77            infer_export_names_for_local(python, target, target.source(), &owner_name);
78        if !owner_exports.is_empty() {
79            return owner_exports;
80        }
81    }
82
83    infer_export_names_for_local(python, target, target.source(), target.identifier())
84}
85
86pub fn infer_usage_seeds(
87    python: &dyn PythonUsageSource,
88    target: &CodeUnit,
89    seed_names: BTreeSet<String>,
90) -> BTreeSet<(ProjectFile, String)> {
91    let mut seeds = BTreeSet::new();
92    for seed_name in &seed_names {
93        seeds.extend(usage_seeds(python, target.source(), seed_name));
94    }
95    if seeds.is_empty()
96        && seed_names.contains(target.identifier())
97        && is_module_level_target_identifier(python, target, target.source(), target.identifier())
98    {
99        seeds.insert((target.source().clone(), target.identifier().to_string()));
100    }
101    seeds
102}
103
104fn infer_export_names_for_local(
105    python: &dyn PythonUsageSource,
106    target: &CodeUnit,
107    file: &ProjectFile,
108    local_name: &str,
109) -> BTreeSet<String> {
110    let index = python.export_index_of(file);
111    let mut export_names = BTreeSet::new();
112    if index.exports_by_name.contains_key(local_name) {
113        export_names.insert(local_name.to_string());
114    }
115    for (export_name, entry) in &index.exports_by_name {
116        if matches!(entry, ExportEntry::Local { local_name: name } if name == local_name) {
117            export_names.insert(export_name.clone());
118        }
119    }
120    if export_names.is_empty()
121        && is_module_level_target_identifier(python, target, file, local_name)
122    {
123        export_names.insert(local_name.to_string());
124    }
125    export_names
126}
127
128fn is_module_level_target_identifier(
129    python: &dyn PythonUsageSource,
130    target: &CodeUnit,
131    file: &ProjectFile,
132    local_name: &str,
133) -> bool {
134    target.source() == file
135        && target.identifier() == local_name
136        && python
137            .parent_of(target)
138            .is_some_and(|parent| parent.is_module() && parent.source() == file)
139}
140
141pub fn top_level_identifier(index: &dyn CodeUnitIndex, target: &CodeUnit) -> String {
142    let mut current = target.clone();
143    while let Some(parent) = index.parent_of(&current) {
144        if parent.is_module() {
145            break;
146        }
147        current = parent;
148    }
149    current.identifier().to_string()
150}
151
152pub fn member_name(index: &dyn CodeUnitIndex, target: &CodeUnit) -> Option<String> {
153    target_owner_code_unit(index, target).map(|_| target.identifier().to_string())
154}
155
156pub fn target_owner_code_unit(index: &dyn CodeUnitIndex, target: &CodeUnit) -> Option<CodeUnit> {
157    index
158        .parent_of(target)
159        .filter(|parent| parent.source() == target.source() && parent.is_class())
160}
161
162pub fn resolve_receiver_type(
163    graph: &PythonGraphSource<'_>,
164    python: &dyn PythonUsageSource,
165    file: &ProjectFile,
166    raw_type: &str,
167    target_self_file: bool,
168) -> Option<CodeUnit> {
169    let raw_type = raw_type.trim();
170    if raw_type.is_empty() || raw_type.contains('|') {
171        return None;
172    }
173    if raw_type.contains('.') {
174        let candidates = resolve_fqn_candidates(python, raw_type, |name| {
175            graph.index.definitions(name).collect()
176        })
177        .into_iter()
178        .filter(CodeUnit::is_class)
179        .collect::<Vec<_>>();
180        return (candidates.len() == 1)
181            .then(|| candidates.into_iter().next())
182            .flatten();
183    }
184
185    if let Some(binding) = python.import_binder_of(file).bindings.get(raw_type)
186        && binding.kind == ImportKind::Named
187        && let Some(imported) = binding.imported_name.as_ref()
188    {
189        let fqn = format!("{}.{}", binding.module_specifier, imported);
190        if let Some(class) =
191            resolve_fqn_candidates(python, &fqn, |name| graph.index.definitions(name).collect())
192                .into_iter()
193                .find(CodeUnit::is_class)
194        {
195            return Some(class);
196        }
197    }
198
199    if let Some(provider) = graph.imports
200        && let Some(imported) = provider
201            .imported_code_units_of(file)
202            .iter()
203            .find(|code_unit| code_unit.identifier() == raw_type && code_unit.is_class())
204    {
205        return Some(imported.clone());
206    }
207
208    graph
209        .index
210        .declarations(file)
211        .into_iter()
212        .find(|code_unit| code_unit.identifier() == raw_type && code_unit.is_class())
213        .or_else(|| {
214            if !target_self_file {
215                return None;
216            }
217            resolve_indexed_receiver_type(graph, file, raw_type)
218        })
219}
220
221fn resolve_bare_annotation_symbol(
222    graph: &PythonGraphSource<'_>,
223    python: &dyn PythonUsageSource,
224    file: &ProjectFile,
225    source: &str,
226    node: Node<'_>,
227    raw_symbol: &str,
228) -> Vec<CodeUnit> {
229    let raw_symbol = raw_symbol.trim();
230    if raw_symbol.is_empty() {
231        return Vec::new();
232    }
233
234    if let Some(owner) = annotation_scope_owner_class(graph, file, source, node) {
235        let owner_candidates: Vec<_> = exact_owner_annotation_members(graph, &owner, raw_symbol)
236            .into_iter()
237            .filter(|candidate| !candidate.is_function())
238            .collect();
239        if !owner_candidates.is_empty() {
240            return owner_candidates;
241        }
242    }
243
244    let mut candidates = Vec::new();
245    if let Some(binding) = python.import_binder_of(file).bindings.get(raw_symbol)
246        && binding.kind == ImportKind::Named
247        && let Some(imported) = binding.imported_name.as_ref()
248    {
249        let fqn = format!("{}.{}", binding.module_specifier, imported);
250        let mut imported_candidates = resolve_fqn_candidates(python, &fqn, |name| {
251            graph
252                .index
253                .definitions(name)
254                .filter(|candidate| candidate.source().language() == Language::Python)
255                .collect()
256        });
257        imported_candidates.retain(|candidate| {
258            !candidate.is_module() || candidate.fq_name() != binding.module_specifier
259        });
260        candidates.extend(imported_candidates);
261    }
262
263    candidates.extend(
264        graph
265            .index
266            .top_level_declarations(file)
267            .into_iter()
268            .filter(|code_unit| {
269                !code_unit.is_module()
270                    && code_unit.identifier() == raw_symbol
271                    && graph
272                        .index
273                        .parent_of(code_unit)
274                        .is_some_and(|parent| parent.is_module())
275            }),
276    );
277
278    candidates.retain(|candidate| !candidate.is_function());
279    candidates.sort();
280    candidates.dedup();
281    candidates
282}
283
284/// Resolve a structured Python annotation reference.
285///
286/// Only AST nodes that occur inside a function return type, parameter type, or
287/// annotated-assignment type are considered. In particular, string contents are
288/// accepted only in those annotation positions; arbitrary string literals are
289/// never interpreted as type expressions.
290pub fn annotation_reference_candidates(
291    graph: &PythonGraphSource<'_>,
292    python: &dyn PythonUsageSource,
293    file: &ProjectFile,
294    source: &str,
295    node: Node<'_>,
296    target_self_file: bool,
297) -> Option<Vec<CodeUnit>> {
298    if !is_annotation_reference_node(node, source) {
299        return None;
300    }
301
302    let mut candidates = match node.kind() {
303        "identifier" => {
304            let mut candidates = resolve_bare_annotation_symbol(
305                graph,
306                python,
307                file,
308                source,
309                node,
310                node_text(node, source),
311            );
312            if candidates.is_empty() {
313                candidates.extend(resolve_receiver_type(
314                    graph,
315                    python,
316                    file,
317                    node_text(node, source),
318                    target_self_file,
319                ));
320            }
321            candidates
322        }
323        "string_content" => {
324            let Some(string) = node.parent() else {
325                return Some(Vec::new());
326            };
327            let Some(ranges) = python_deferred_annotation_identifier_ranges(string, source, None)
328            else {
329                return Some(Vec::new());
330            };
331            let mut candidates = Vec::new();
332            for range in ranges {
333                let Some(symbol) = source.get(range.start_byte..range.end_byte) else {
334                    continue;
335                };
336                let mut symbol_candidates =
337                    resolve_bare_annotation_symbol(graph, python, file, source, node, symbol);
338                if symbol_candidates.is_empty() {
339                    symbol_candidates.extend(resolve_receiver_type(
340                        graph,
341                        python,
342                        file,
343                        symbol,
344                        target_self_file,
345                    ));
346                }
347                candidates.extend(symbol_candidates);
348            }
349            candidates
350        }
351        "attribute" => resolve_annotation_attribute_types(graph, python, file, source, node),
352        _ => Vec::new(),
353    };
354    candidates.sort();
355    candidates.dedup();
356    Some(candidates)
357}
358
359/// Resolve only the annotation identifier selected by one definition request.
360/// Quoted annotations are reparsed with original byte coordinates, while the
361/// original syntax node remains the lexical class-scope anchor.
362#[allow(clippy::too_many_arguments)]
363pub fn annotation_reference_candidates_at_focus(
364    graph: &PythonGraphSource<'_>,
365    python: &dyn PythonUsageSource,
366    file: &ProjectFile,
367    source: &str,
368    node: Node<'_>,
369    focus_start: usize,
370    focus_end: usize,
371    target_self_file: bool,
372) -> Option<Vec<CodeUnit>> {
373    if !is_annotation_reference_node(node, source) {
374        return None;
375    }
376
377    let deferred_tree = if node.kind() == "string_content" {
378        Some(python_deferred_annotation_tree(
379            node.parent()?,
380            source,
381            None,
382        )?)
383    } else {
384        None
385    };
386    let search_root = deferred_tree.as_ref().map_or(node, |tree| tree.root_node());
387    let focused = search_root.descendant_for_byte_range(focus_start, focus_end)?;
388    if focused.kind() != "identifier"
389        || focused.start_byte() != focus_start
390        || focused.end_byte() != focus_end
391    {
392        return Some(Vec::new());
393    }
394
395    let mut path = focused;
396    while let Some(parent) = path.parent() {
397        if parent.kind() != "attribute" {
398            break;
399        }
400        path = parent;
401    }
402    if path.kind() == "attribute" {
403        return Some(focused_annotation_attribute_candidates(
404            graph, python, file, source, node, path, focused,
405        ));
406    }
407
408    let symbol = node_text(focused, source);
409    let mut candidates = resolve_bare_annotation_symbol(graph, python, file, source, node, symbol);
410    if candidates.is_empty() {
411        candidates.extend(resolve_receiver_type(
412            graph,
413            python,
414            file,
415            symbol,
416            target_self_file,
417        ));
418    }
419    candidates.sort();
420    candidates.dedup();
421    Some(candidates)
422}
423
424fn focused_annotation_attribute_candidates(
425    graph: &PythonGraphSource<'_>,
426    python: &dyn PythonUsageSource,
427    file: &ProjectFile,
428    source: &str,
429    scope_node: Node<'_>,
430    path: Node<'_>,
431    focused: Node<'_>,
432) -> Vec<CodeUnit> {
433    let Some((root, attributes)) = annotation_attribute_chain(path) else {
434        return Vec::new();
435    };
436    if root.id() == focused.id() {
437        return resolve_bare_annotation_symbol(
438            graph,
439            python,
440            file,
441            source,
442            scope_node,
443            node_text(root, source),
444        );
445    }
446
447    if attributes
448        .last()
449        .is_some_and(|node| node.id() == focused.id())
450    {
451        let candidates = namespace_qualified_declarations(graph, python, file, source, path);
452        if !candidates.is_empty() {
453            return candidates;
454        }
455    }
456
457    let owners: Vec<_> = resolve_bare_annotation_symbol(
458        graph,
459        python,
460        file,
461        source,
462        scope_node,
463        node_text(root, source),
464    )
465    .into_iter()
466    .filter(CodeUnit::is_class)
467    .collect();
468    let [owner] = owners.as_slice() else {
469        return Vec::new();
470    };
471    let mut owner = owner.clone();
472    for attribute in attributes {
473        let candidates = exact_nested_annotation_class(graph, &owner, node_text(attribute, source));
474        if attribute.id() == focused.id() {
475            return candidates;
476        }
477        let [next] = candidates.as_slice() else {
478            return Vec::new();
479        };
480        owner = next.clone();
481    }
482    Vec::new()
483}
484
485/// Return the exact qualifier token when a class target owns part of a
486/// structured annotation attribute chain.
487pub fn annotation_class_qualifier_site<'tree>(
488    graph: &PythonGraphSource<'_>,
489    python: &dyn PythonUsageSource,
490    file: &ProjectFile,
491    source: &str,
492    node: Node<'tree>,
493    target: &CodeUnit,
494) -> Option<Node<'tree>> {
495    if node.kind() != "attribute"
496        || !target.is_class()
497        || !is_annotation_reference_node(node, source)
498    {
499        return None;
500    }
501
502    let (root, attributes) = annotation_attribute_chain(node)?;
503    let owners: Vec<_> =
504        resolve_bare_annotation_symbol(graph, python, file, source, root, node_text(root, source))
505            .into_iter()
506            .filter(CodeUnit::is_class)
507            .collect();
508    let [owner] = owners.as_slice() else {
509        return None;
510    };
511    let mut owner = owner.clone();
512    if &owner == target {
513        return Some(root);
514    }
515
516    // The final attribute is the annotation declaration itself. Only the
517    // preceding segments are class qualifiers.
518    let qualifier_count = attributes.len().saturating_sub(1);
519    for attribute in attributes.into_iter().take(qualifier_count) {
520        let next_candidates =
521            exact_nested_annotation_class(graph, &owner, node_text(attribute, source));
522        let [next] = next_candidates.as_slice() else {
523            return None;
524        };
525        owner = next.clone();
526        if &owner == target {
527            return Some(attribute);
528        }
529    }
530
531    None
532}
533
534fn resolve_annotation_attribute_types(
535    graph: &PythonGraphSource<'_>,
536    python: &dyn PythonUsageSource,
537    file: &ProjectFile,
538    source: &str,
539    node: Node<'_>,
540) -> Vec<CodeUnit> {
541    // Preserve the established namespace-import path (`module.Type`) while
542    // adding owner-qualified nested classes (`Outer.Inner`). The namespace walk
543    // already understands module/re-export bindings; it simply cannot interpret
544    // a class as the namespace for another class.
545    let mut candidates = namespace_qualified_declarations(graph, python, file, source, node);
546    let Some((root, attributes)) = annotation_attribute_chain(node) else {
547        return candidates;
548    };
549    let root_text = node_text(root, source);
550    let owners: Vec<_> =
551        resolve_bare_annotation_symbol(graph, python, file, source, root, root_text)
552            .into_iter()
553            .filter(CodeUnit::is_class)
554            .collect();
555    let [owner] = owners.as_slice() else {
556        return candidates;
557    };
558    let mut owner = owner.clone();
559
560    for attribute in attributes {
561        let segment = node_text(attribute, source);
562        let next_candidates = exact_nested_annotation_class(graph, &owner, segment);
563        let [next] = next_candidates.as_slice() else {
564            return candidates;
565        };
566        owner = next.clone();
567    }
568
569    candidates.push(owner);
570    candidates
571}
572
573fn annotation_attribute_chain(node: Node<'_>) -> Option<(Node<'_>, Vec<Node<'_>>)> {
574    let mut attributes = Vec::new();
575    let mut current = node;
576    while current.kind() == "attribute" {
577        attributes.push(current.child_by_field_name("attribute")?);
578        current = current.child_by_field_name("object")?;
579    }
580    if current.kind() != "identifier" || attributes.is_empty() {
581        return None;
582    }
583    attributes.reverse();
584    Some((current, attributes))
585}
586
587fn exact_nested_annotation_class(
588    graph: &PythonGraphSource<'_>,
589    owner: &CodeUnit,
590    segment: &str,
591) -> Vec<CodeUnit> {
592    let mut candidates: Vec<_> = exact_owner_annotation_members(graph, owner, segment)
593        .into_iter()
594        .filter(CodeUnit::is_class)
595        .collect();
596    candidates.sort();
597    candidates.dedup();
598    candidates
599}
600
601fn exact_owner_annotation_members(
602    graph: &PythonGraphSource<'_>,
603    owner: &CodeUnit,
604    segment: &str,
605) -> Vec<CodeUnit> {
606    let mut candidates: Vec<_> = graph
607        .index
608        .declarations(owner.source())
609        .into_iter()
610        .filter(|unit| {
611            unit.identifier() == segment
612                && graph
613                    .index
614                    .parent_of(unit)
615                    .is_some_and(|parent| parent.fq_name() == owner.fq_name())
616        })
617        .collect();
618    candidates.sort();
619    candidates.dedup();
620    candidates
621}
622
623fn annotation_scope_owner_class(
624    graph: &PythonGraphSource<'_>,
625    file: &ProjectFile,
626    source: &str,
627    node: Node<'_>,
628) -> Option<CodeUnit> {
629    if !annotation_expression_is_class_scoped(node) {
630        return None;
631    }
632    let range = Range {
633        start_byte: node.start_byte(),
634        end_byte: node.end_byte(),
635        start_line: 0,
636        end_line: 0,
637    };
638    if let Some(enclosing) = graph.index.enclosing_code_unit(file, &range) {
639        if enclosing.is_class() {
640            return Some(enclosing);
641        }
642        if let Some(owner) = target_owner_code_unit(graph.index, &enclosing) {
643            return Some(owner);
644        }
645    }
646    structural_annotation_owner_class(graph, file, source, node)
647}
648
649fn annotation_expression_is_class_scoped(node: Node<'_>) -> bool {
650    let site_start = node.start_byte();
651    let site_end = node.end_byte();
652    let mut current = node;
653    while let Some(parent) = current.parent() {
654        if matches!(parent.kind(), "function_definition" | "lambda")
655            && parent
656                .child_by_field_name("body")
657                .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
658        {
659            return false;
660        }
661        if parent.kind() == "class_definition" {
662            return true;
663        }
664        current = parent;
665    }
666    false
667}
668
669fn structural_annotation_owner_class(
670    graph: &PythonGraphSource<'_>,
671    file: &ProjectFile,
672    source: &str,
673    node: Node<'_>,
674) -> Option<CodeUnit> {
675    let mut current = node;
676    while let Some(parent) = current.parent() {
677        if parent.kind() == "class_definition" {
678            let name = node_text(parent.child_by_field_name("name")?, source).trim();
679            if name.is_empty() {
680                return None;
681            }
682            let class_range = Range {
683                start_byte: parent.start_byte(),
684                end_byte: parent.end_byte(),
685                start_line: 0,
686                end_line: 0,
687            };
688            let mut matches: Vec<_> = graph
689                .index
690                .declarations(file)
691                .into_iter()
692                .filter(|unit| unit.is_class() && unit.identifier() == name)
693                .filter(|unit| {
694                    graph
695                        .index
696                        .ranges(unit)
697                        .into_iter()
698                        .any(|range| range.contains(&class_range))
699                })
700                .collect();
701            matches.sort();
702            matches.dedup();
703            let [owner] = matches.as_slice() else {
704                return None;
705            };
706            return Some(owner.clone());
707        }
708        current = parent;
709    }
710    None
711}
712
713fn is_annotation_reference_node(node: Node<'_>, source: &str) -> bool {
714    if !matches!(node.kind(), "identifier" | "attribute" | "string_content") {
715        return false;
716    }
717    if !python_node_is_in_annotation(node) {
718        return false;
719    }
720
721    let mut current = node;
722    while let Some(parent) = current.parent() {
723        if parent.kind() == "call"
724            && parent
725                .child_by_field_name("arguments")
726                .is_some_and(|arguments| {
727                    arguments.start_byte() <= node.start_byte()
728                        && node.end_byte() <= arguments.end_byte()
729                })
730        {
731            return false;
732        }
733        if parent.kind() == "subscript"
734            && let Some(value) = parent.child_by_field_name("value")
735            && value.kind() == "identifier"
736        {
737            match node_text(value, source) {
738                // Literal parameters are values, including invalid values a
739                // type checker will diagnose. They are still ordinary Python
740                // references for definition and usage resolution.
741                "Literal" => return false,
742                // Only Annotated's first parameter is a type. Every following
743                // metadata expression is evaluated as a value.
744                "Annotated" => {
745                    let Some(arguments) = parent.child_by_field_name("subscript") else {
746                        return false;
747                    };
748                    let type_argument = if arguments.kind() == "expression_list" {
749                        arguments.named_child(0)
750                    } else {
751                        Some(arguments)
752                    };
753                    if !type_argument.is_some_and(|argument| {
754                        argument.start_byte() <= node.start_byte()
755                            && node.end_byte() <= argument.end_byte()
756                    }) {
757                        return false;
758                    }
759                }
760                _ => {}
761            }
762        }
763        if parent.kind() == "type_parameter"
764            && let Some(generic) = parent
765                .parent()
766                .filter(|parent| parent.kind() == "generic_type")
767        {
768            let generic_name = generic
769                .child_by_field_name("name")
770                .or_else(|| generic.named_child(0))
771                .map(|name| node_text(name, source));
772            match generic_name {
773                Some("Literal") => return false,
774                Some("Annotated") => {
775                    let type_argument = parent.named_child(0);
776                    if !type_argument.is_some_and(|argument| {
777                        argument.start_byte() <= node.start_byte()
778                            && node.end_byte() <= argument.end_byte()
779                    }) {
780                        return false;
781                    }
782                }
783                _ => {}
784            }
785        }
786        current = parent;
787    }
788    true
789}
790
791/// Resolve every visible named-import binding for one local symbol.
792///
793/// The ordinary import binder intentionally stores one effective binding per
794/// local name. A conditional `if TYPE_CHECKING: from .m import T; else: from m
795/// import T` has two possible bindings, however, and usage resolution must keep
796/// every workspace candidate rather than whichever arm the binder visited
797/// last.
798pub fn resolve_visible_named_import_candidates(
799    graph: &PythonGraphSource<'_>,
800    python: &dyn PythonUsageSource,
801    file: &ProjectFile,
802    timeline: &ModuleBindingTimeline,
803    local: &str,
804    cutoff: usize,
805) -> Vec<CodeUnit> {
806    let Some(events) = timeline.get(local) else {
807        return Vec::new();
808    };
809    let visible: Vec<_> = events
810        .iter()
811        .filter(|event| event.visible_from <= cutoff)
812        .collect();
813    let start = visible
814        .iter()
815        .rposition(|event| !event.conditional)
816        .unwrap_or(0);
817    let mut candidates = Vec::new();
818    for event in &visible[start..] {
819        let ModuleBindingEventKind::FromImport {
820            module,
821            imported_name,
822        } = &event.kind
823        else {
824            continue;
825        };
826        let mut resolved_module = false;
827        for module_file in usage_resolve_module_files(python, file, module) {
828            let Some(module_fqn) = graph
829                .index
830                .declarations(&module_file)
831                .into_iter()
832                .find(CodeUnit::is_module)
833                .map(|unit| unit.fq_name())
834            else {
835                continue;
836            };
837            resolved_module = true;
838            let fqn = format!("{module_fqn}.{imported_name}");
839            candidates.extend(resolve_fqn_candidates(python, &fqn, |name| {
840                graph.index.definitions(name).collect()
841            }));
842        }
843        if !resolved_module {
844            let fqn = if module.ends_with('.') {
845                format!("{module}{imported_name}")
846            } else {
847                format!("{module}.{imported_name}")
848            };
849            candidates.extend(resolve_fqn_candidates(python, &fqn, |name| {
850                graph.index.definitions(name).collect()
851            }));
852        }
853    }
854    candidates.sort();
855    candidates.dedup();
856    candidates
857}
858
859/// Resolve the class constructed by a Python call callee without interpreting
860/// source text. Bare callees use the import binder or same-file declarations;
861/// qualified callees walk tree-sitter's `attribute` fields back to a namespace
862/// import and append each attribute component structurally.
863pub fn resolve_constructor_types(
864    graph: &PythonGraphSource<'_>,
865    python: &dyn PythonUsageSource,
866    file: &ProjectFile,
867    source: &str,
868    function: Node<'_>,
869) -> Vec<CodeUnit> {
870    let candidates = match function.kind() {
871        "identifier" => {
872            let local = node_text(function, source);
873            if local.is_empty() {
874                return Vec::new();
875            }
876            let binder = python.import_binder_of(file);
877            let fqn = match binder.bindings.get(local) {
878                Some(binding) if binding.kind == ImportKind::Named => binding
879                    .imported_name
880                    .as_ref()
881                    .map(|imported| format!("{}.{}", binding.module_specifier, imported)),
882                _ => graph
883                    .index
884                    .declarations(file)
885                    .into_iter()
886                    .find(|unit| unit.is_class() && unit.identifier() == local)
887                    .map(|unit| unit.fq_name()),
888            };
889            let Some(fqn) = fqn else {
890                return Vec::new();
891            };
892            resolve_fqn_candidates(python, &fqn, |name| graph.index.definitions(name).collect())
893        }
894        "attribute" => namespace_qualified_declarations(graph, python, file, source, function),
895        _ => Vec::new(),
896    };
897    // A call callee names something constructible; an annotation does not, so
898    // the kind filter belongs here rather than in the shared namespace walk.
899    let mut classes: Vec<CodeUnit> = candidates.into_iter().filter(CodeUnit::is_class).collect();
900    classes.sort();
901    classes.dedup();
902    classes
903}
904
905/// Resolve the class in the nearest enclosing callable parameter default.
906///
907/// For `def run(Foo: type = Foo): Foo(bar=1)`, the body binding shadows the
908/// imported class name. The structured default still proves which class the
909/// keyword belongs to when the default callable is used.
910pub fn resolve_callable_parameter_default_types(
911    graph: &PythonGraphSource<'_>,
912    python: &dyn PythonUsageSource,
913    file: &ProjectFile,
914    source: &str,
915    reference: Node<'_>,
916    local_name: &str,
917) -> Vec<CodeUnit> {
918    let site_start = reference.start_byte();
919    let site_end = reference.end_byte();
920    let mut current = reference;
921    while let Some(parent) = current.parent() {
922        current = parent;
923        if !matches!(current.kind(), "function_definition" | "lambda") {
924            continue;
925        }
926        if current
927            .child_by_field_name("body")
928            .is_none_or(|body| !(body.start_byte() <= site_start && site_end <= body.end_byte()))
929        {
930            continue;
931        }
932        let Some(parameters) = current.child_by_field_name("parameters") else {
933            return Vec::new();
934        };
935        let mut cursor = parameters.walk();
936        for parameter in parameters.named_children(&mut cursor) {
937            let name = if parameter.kind() == "identifier" {
938                Some(parameter)
939            } else {
940                parameter.child_by_field_name("name")
941            };
942            if name.is_none_or(|name| node_text(name, source) != local_name) {
943                continue;
944            }
945            let Some(value) = parameter.child_by_field_name("value") else {
946                return Vec::new();
947            };
948            return resolve_constructor_types(graph, python, file, source, value);
949        }
950        return Vec::new();
951    }
952    Vec::new()
953}
954
955/// The declarations a namespace-qualified attribute path names (`module.Name`,
956/// `pkg.module.Name`), walked structurally through the import binder.
957///
958/// No kind filter: a Python annotation legitimately names a module-level type
959/// alias, `TypeAlias`, `NewType` or `TypeVar` value, all of which the analyzer
960/// models as fields (issue #1763).
961fn namespace_qualified_declarations(
962    graph: &PythonGraphSource<'_>,
963    python: &dyn PythonUsageSource,
964    file: &ProjectFile,
965    source: &str,
966    node: Node<'_>,
967) -> Vec<CodeUnit> {
968    let binder = python.import_binder_of(file);
969    let Some(fqn) = namespace_constructor_fqn(&binder, source, node) else {
970        return Vec::new();
971    };
972    let mut candidates =
973        resolve_fqn_candidates(python, &fqn, |name| graph.index.definitions(name).collect());
974    candidates.sort();
975    candidates.dedup();
976    candidates
977}
978
979fn namespace_constructor_fqn(
980    binder: &ImportBinder,
981    source: &str,
982    function: Node<'_>,
983) -> Option<String> {
984    let mut attributes = Vec::new();
985    let mut current = function;
986    while current.kind() == "attribute" {
987        let attribute = current.child_by_field_name("attribute")?;
988        let text = node_text(attribute, source);
989        if text.is_empty() {
990            return None;
991        }
992        attributes.push(text);
993        current = current.child_by_field_name("object")?;
994    }
995    if current.kind() != "identifier" {
996        return None;
997    }
998    let root = node_text(current, source);
999    let binding = binder.bindings.get(root)?;
1000    if binding.kind != ImportKind::Namespace {
1001        return None;
1002    }
1003    let mut fqn = binding.module_specifier.clone();
1004    for attribute in attributes.into_iter().rev() {
1005        fqn.push('.');
1006        fqn.push_str(attribute);
1007    }
1008    Some(fqn)
1009}
1010
1011fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1012    brokk_bifrost_core::analyzer::common::node_source_text(node, source)
1013}
1014
1015fn resolve_indexed_receiver_type(
1016    graph: &PythonGraphSource<'_>,
1017    file: &ProjectFile,
1018    raw_type: &str,
1019) -> Option<CodeUnit> {
1020    let package_matches = module_fq_for_file(graph.index, file).map_or_else(Vec::new, |module| {
1021        relational_definitions(
1022            graph,
1023            RelationalName::stable(module),
1024            RelationalDefinitionQuery::PackageTypes {
1025                simple_name: raw_type.to_string(),
1026            },
1027        )
1028    });
1029    let mut root_name = FqName::new();
1030    root_name.push(segment_interner().intern(raw_type, SegmentKind::Type));
1031    let exact_matches = relational_definitions(
1032        graph,
1033        RelationalName::stable(root_name.clone()),
1034        RelationalDefinitionQuery::ExactName,
1035    );
1036    let normalized_matches = relational_definitions(
1037        graph,
1038        RelationalName::stable(root_name),
1039        RelationalDefinitionQuery::NormalizedName,
1040    );
1041    package_matches
1042        .into_iter()
1043        .chain(exact_matches)
1044        .chain(normalized_matches)
1045        .find(|code_unit| code_unit.identifier() == raw_type && code_unit.is_class())
1046}
1047
1048fn relational_definitions(
1049    graph: &PythonGraphSource<'_>,
1050    name: RelationalName,
1051    query: RelationalDefinitionQuery,
1052) -> Vec<CodeUnit> {
1053    let question = RelationalDefinitionQuestion {
1054        // Python's graph owns Python declarations. Cross-language interop is
1055        // an explicit exact-FQN decision in the dispatching definition layer;
1056        // a workspace-wide normalized/simple-name query here would let an
1057        // unresolved Python annotation borrow an unrelated declaration from
1058        // another language.
1059        language_scope: DefinitionLanguageScope::Language(Language::Python),
1060        name,
1061        query,
1062    };
1063    match graph.definitions.ask(&question) {
1064        RelationalDefinitionValue::Definitions(units) => units,
1065        _ => panic!("definition question returned the wrong result shape"),
1066    }
1067}
1068
1069fn module_fq_for_file(index: &dyn CodeUnitIndex, file: &ProjectFile) -> Option<FqName> {
1070    index
1071        .declarations(file)
1072        .into_iter()
1073        .find(|code_unit| code_unit.is_module())
1074        .map(|code_unit| code_unit.fq().clone())
1075        .or_else(|| {
1076            index
1077                .declarations(file)
1078                .into_iter()
1079                .find(|code_unit| !code_unit.package_name().is_empty())
1080                .map(|code_unit| code_unit.package_fq())
1081        })
1082}
1083
1084pub fn normalized_receiver_type(annotation: &str) -> Option<String> {
1085    let annotation = unwrap_python_string_annotation(annotation.trim());
1086    let annotation = unwrap_supported_receiver_wrapper(annotation);
1087    if annotation.is_empty()
1088        || annotation.contains('|')
1089        || annotation.contains('[')
1090        || annotation.contains(']')
1091        || annotation.contains(',')
1092        || annotation.contains('(')
1093        || annotation.contains(')')
1094        || annotation.contains('{')
1095        || annotation.contains('}')
1096        || annotation.contains(':')
1097    {
1098        return None;
1099    }
1100    Some(annotation.to_string())
1101}
1102
1103fn unwrap_python_string_annotation(annotation: &str) -> &str {
1104    if annotation.len() >= 2 {
1105        let bytes = annotation.as_bytes();
1106        let first = bytes[0];
1107        let last = bytes[annotation.len() - 1];
1108        if (first == b'\'' || first == b'"') && first == last {
1109            return annotation[1..annotation.len() - 1].trim();
1110        }
1111    }
1112    annotation
1113}
1114
1115fn unwrap_supported_receiver_wrapper(annotation: &str) -> &str {
1116    let mut current = annotation.trim();
1117    loop {
1118        let next = current
1119            .strip_prefix("Optional[")
1120            .or_else(|| current.strip_prefix("typing.Optional["))
1121            .and_then(|inner| inner.strip_suffix(']'))
1122            .map(str::trim);
1123        let Some(unwrapped) = next else {
1124            return current;
1125        };
1126        current = unwrapped;
1127    }
1128}
1129
1130pub fn receiver_annotation_matches_target(
1131    annotation: &str,
1132    edges: &[ImportEdge],
1133    target_short: &str,
1134    target_self_file: bool,
1135) -> bool {
1136    let annotation = annotation.trim();
1137    if annotation.is_empty() {
1138        return false;
1139    }
1140    if annotation.contains('|')
1141        || annotation.contains('[')
1142        || annotation.contains(']')
1143        || annotation.contains(',')
1144        || annotation.contains('(')
1145        || annotation.contains(')')
1146    {
1147        return false;
1148    }
1149    if annotation == target_short {
1150        return target_self_file || edges.iter().any(|edge| edge.local_name == target_short);
1151    }
1152
1153    // `annotation` was already filtered above to exclude generics/unions/calls, so
1154    // it is a bare dotted qualifier (Python identifiers never embed a literal
1155    // `.`); re-tokenizing with the shared structured splitter and rejoining
1156    // every part but the last with `.` reproduces `rsplit_once('.')`'s
1157    // (qualifier, member) split exactly.
1158    let segments = parse_symbol_path(Language::Python, annotation);
1159    let Some((member, qualifier_parts)) = segments.split_last() else {
1160        return false;
1161    };
1162    if qualifier_parts.is_empty() {
1163        return false;
1164    }
1165    let qualifier = qualifier_parts.join(".");
1166    let member = member.as_str();
1167    if member != target_short {
1168        return false;
1169    }
1170    edges.iter().any(|edge| {
1171        matches!(edge.kind, ImportEdgeKind::Namespace)
1172            && (edge.local_name == qualifier
1173                || qualifier.ends_with(&format!(".{}", edge.local_name)))
1174    })
1175}
1176
1177// Python module-name and relative-import resolution were lifted to the analyzer
1178// (`PythonAnalyzer::python_module_name` / `resolve_module_files`, see
1179// `analyzer::python::usage_index`); both usage paths now resolve through there.