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