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