Skip to main content

brokk_bifrost_python/graph/
extractor.rs

1//! Python's forward, per-target usage scan: the scoped import closure, the
2//! per-file walk that proves a reference, and the receiver-type facts both this
3//! and the inverted walk resolve through.
4
5use crate::graph::PythonGraphSource;
6use crate::graph::hits::{
7    record_hit, record_import_hit, record_self_receiver_hit, record_unproven_hit,
8};
9use crate::graph::resolver::{
10    annotation_class_qualifier_site, annotation_reference_candidates, member_name,
11    normalized_receiver_type, receiver_annotation_matches_target,
12    resolve_callable_parameter_default_types, resolve_constructor_types, resolve_receiver_type,
13    target_owner_code_unit, top_level_identifier,
14};
15use crate::graph_support::{PythonSource, PythonUsageSource};
16use crate::imports::{PythonImportBinding, parse_python_import_bindings, resolve_fqn_candidates};
17use crate::usage_index::{
18    ModuleBindingEvent, ModuleBindingEventKind, ModuleBindingTimeline, PythonScopeFacts,
19    usage_matching_edges, usage_module_binding_timeline, usage_resolve_module_files,
20    usage_scope_facts,
21};
22use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path;
23use brokk_bifrost_core::analyzer::usages::local_inference::{
24    LocalBindingsSnapshot, LocalInferenceConfig, LocalInferenceEngine, SymbolResolution,
25};
26use brokk_bifrost_core::analyzer::usages::model::{ImportKind, UsageHit};
27use brokk_bifrost_core::analyzer::usages::{ImportEdge, ImportEdgeKind};
28use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, Language, ProjectFile, Range};
29use brokk_bifrost_core::cancellation::CancellationToken;
30use brokk_bifrost_core::hash::{HashMap, HashSet};
31use brokk_bifrost_core::text_utils::compute_line_starts;
32use rayon::prelude::*;
33use std::collections::BTreeSet;
34use std::sync::{Arc, Mutex};
35use tree_sitter::{Node, Parser, Tree};
36
37pub struct ParsedFile {
38    pub source: Arc<String>,
39    pub tree: Tree,
40}
41
42pub struct PythonProjectGraph {
43    parsed: HashMap<ProjectFile, ParsedFile>,
44}
45
46impl PythonProjectGraph {
47    pub fn scan_files(
48        &self,
49        candidate_files: &HashSet<ProjectFile>,
50        target_file: &ProjectFile,
51    ) -> HashSet<ProjectFile> {
52        candidate_files
53            .iter()
54            .cloned()
55            .chain(std::iter::once(target_file.clone()))
56            .collect()
57    }
58}
59
60pub fn build_python_graph(
61    candidate_files: &HashSet<ProjectFile>,
62    target_file: &ProjectFile,
63    cancellation: Option<&CancellationToken>,
64) -> PythonProjectGraph {
65    let parser_language = tree_sitter_python::LANGUAGE.into();
66    let files: HashSet<ProjectFile> = candidate_files
67        .iter()
68        .cloned()
69        .chain(std::iter::once(target_file.clone()))
70        .collect();
71    let mut parsed = HashMap::default();
72
73    for file in files {
74        if cancellation.is_some_and(CancellationToken::is_cancelled) {
75            break;
76        }
77        let Ok(source) = file.read_to_string() else {
78            continue;
79        };
80        if cancellation.is_some_and(CancellationToken::is_cancelled) {
81            break;
82        }
83        if source.is_empty() {
84            continue;
85        }
86        let mut parser = Parser::new();
87        if parser.set_language(&parser_language).is_err() {
88            continue;
89        }
90        let Some(tree) = parser.parse(source.as_str(), None) else {
91            continue;
92        };
93        if cancellation.is_some_and(CancellationToken::is_cancelled) {
94            break;
95        }
96        parsed.insert(
97            file,
98            ParsedFile {
99                source: Arc::new(source),
100                tree,
101            },
102        );
103    }
104
105    PythonProjectGraph { parsed }
106}
107
108pub fn scan_files_for_seeds(
109    graph: &PythonGraphSource<'_>,
110    python: &dyn PythonUsageSource,
111    project_graph: &PythonProjectGraph,
112    files: &HashSet<ProjectFile>,
113    target: &CodeUnit,
114    seeds: &BTreeSet<(ProjectFile, String)>,
115    cancellation: Option<&CancellationToken>,
116) -> ScanResult {
117    let collected: Mutex<BTreeSet<UsageHit>> = Mutex::new(BTreeSet::new());
118    let unproven_collected: Mutex<BTreeSet<UsageHit>> = Mutex::new(BTreeSet::new());
119    let target_short = top_level_identifier(graph.index, target);
120    let target_member = member_name(graph.index, target);
121    let target_owner = target_owner_code_unit(graph.index, target);
122    // A same-file best-effort for unresolvable receivers is only safe when the
123    // member name is unambiguous in the target's file (exactly one class there
124    // declares it), so `recv.member` can only mean the target.
125    let member_unique_in_target_file = target_member.as_deref().is_some_and(|member| {
126        let owners: HashSet<CodeUnit> = graph
127            .index
128            .declarations(target.source())
129            .into_iter()
130            .filter(|decl| {
131                decl.identifier() == member && target_owner_code_unit(graph.index, decl).is_some()
132            })
133            .filter_map(|decl| target_owner_code_unit(graph.index, &decl))
134            .collect();
135        owners.len() == 1
136    });
137    let files_vec: Vec<&ProjectFile> = files.iter().collect();
138    let parser_language = tree_sitter_python::LANGUAGE.into();
139
140    files_vec.par_iter().for_each(|file| {
141        if cancellation.is_some_and(CancellationToken::is_cancelled) {
142            return;
143        }
144        let owned_source: Option<Arc<String>>;
145        let owned_tree: Option<Tree>;
146        let (source_str, tree_ref) = if let Some(parsed) = project_graph.parsed.get(*file) {
147            (parsed.source.as_str(), &parsed.tree)
148        } else {
149            let Ok(source) = file.read_to_string() else {
150                return;
151            };
152            if source.is_empty() {
153                return;
154            }
155            let mut parser = Parser::new();
156            if parser.set_language(&parser_language).is_err() {
157                return;
158            }
159            let Some(tree) = parser.parse(source.as_str(), None) else {
160                return;
161            };
162            owned_source = Some(Arc::new(source));
163            owned_tree = Some(tree);
164            (
165                owned_source.as_deref().unwrap().as_str(),
166                owned_tree.as_ref().unwrap(),
167            )
168        };
169        if cancellation.is_some_and(CancellationToken::is_cancelled) {
170            return;
171        }
172
173        let edges = {
174            let _scope = brokk_bifrost_core::profiling::scope("python_graph::matching_edges");
175            usage_matching_edges(python, file, seeds)
176        };
177        // This is an AST-name gate, not a source-text resolver. Every usage
178        // accepted by the later structural walk has a matching identifier or
179        // can occur inside an annotation string. Skip files that lack both
180        // before building their scope facts and performing that expensive walk.
181        if !file_may_reference_target(
182            tree_ref.root_node(),
183            source_str,
184            target,
185            target_short.as_str(),
186            target_member.as_deref(),
187            &edges,
188        ) {
189            return;
190        }
191        let raw_module_bindings = {
192            let _scope =
193                brokk_bifrost_core::profiling::scope("python_graph::module_binding_timeline");
194            usage_module_binding_timeline(python, file, || {
195                collect_module_binding_timeline(tree_ref.root_node(), source_str)
196            })
197        };
198        let module_bindings = classify_module_binding_timeline(
199            python,
200            file,
201            raw_module_bindings.as_ref(),
202            seeds,
203            &edges,
204        );
205        let scoped_import_bindings = parse_python_import_bindings(source_str);
206        let target_self_file = *file == target.source();
207        let scope_facts = {
208            let _scope = brokk_bifrost_core::profiling::scope("python_graph::scope_facts");
209            usage_scope_facts(python, file, || {
210                collect_scope_facts_from_parsed_source(
211                    graph,
212                    python,
213                    file,
214                    source_str,
215                    tree_ref.root_node(),
216                )
217            })
218        };
219        let scope_range_index = build_scope_range_index(graph, scope_facts.as_ref());
220
221        let mut local_hits = BTreeSet::new();
222        let mut local_unproven_hits = BTreeSet::new();
223        let line_starts = compute_line_starts(source_str);
224
225        let mut scan_ctx = ScanCtx {
226            python,
227            file,
228            source: source_str,
229            line_starts: &line_starts,
230            graph,
231            target,
232            target_short: &target_short,
233            target_member: target_member.as_deref(),
234            target_owner: target_owner.clone(),
235            target_is_module: target.is_module(),
236            target_source: target.source(),
237            seeds,
238            edges: &edges,
239            target_self_file,
240            member_best_effort_unique: target_self_file && member_unique_in_target_file,
241            raw_module_bindings: raw_module_bindings.as_ref(),
242            module_bindings: &module_bindings,
243            scoped_import_bindings: &scoped_import_bindings,
244            scope_facts: scope_facts.as_ref(),
245            scope_range_index: &scope_range_index,
246            hits: &mut local_hits,
247            unproven_hits: &mut local_unproven_hits,
248        };
249
250        {
251            let _scope = brokk_bifrost_core::profiling::scope("python_graph::scan_tree");
252            scan_node(tree_ref.root_node(), &mut scan_ctx);
253        }
254
255        if !local_hits.is_empty() {
256            let mut sink = collected
257                .lock()
258                .expect("usage hit collector mutex poisoned");
259            sink.extend(local_hits);
260        }
261        if !local_unproven_hits.is_empty() {
262            let mut sink = unproven_collected
263                .lock()
264                .expect("usage unproven hit collector mutex poisoned");
265            sink.extend(local_unproven_hits);
266        }
267    });
268
269    ScanResult {
270        hits: collected
271            .into_inner()
272            .expect("usage hit collector mutex poisoned"),
273        unproven_hits: unproven_collected
274            .into_inner()
275            .expect("usage unproven hit collector mutex poisoned"),
276    }
277}
278
279fn file_may_reference_target(
280    root: Node<'_>,
281    source: &str,
282    target: &CodeUnit,
283    target_short: &str,
284    target_member: Option<&str>,
285    edges: &[ImportEdge],
286) -> bool {
287    if target.is_module() {
288        return true;
289    }
290
291    let mut stack = vec![root];
292    while let Some(node) = stack.pop() {
293        if node.kind() == "string" {
294            // Annotation strings can use an FQN or a re-export alias that is
295            // not an exact identifier node. Keep them for the structured
296            // annotation resolver below.
297            return true;
298        }
299        if node.kind() == "identifier" {
300            let name = slice(node, source);
301            if name == target_short
302                || target_member.is_some_and(|member| name == member)
303                || edges.iter().any(|edge| edge.local_name == name)
304            {
305                return true;
306            }
307        }
308
309        let mut cursor = node.walk();
310        stack.extend(node.named_children(&mut cursor));
311    }
312    false
313}
314
315pub struct ScanResult {
316    pub hits: BTreeSet<UsageHit>,
317    pub unproven_hits: BTreeSet<UsageHit>,
318}
319
320pub struct ScanCtx<'a> {
321    python: &'a dyn PythonUsageSource,
322    pub file: &'a ProjectFile,
323    pub source: &'a str,
324    pub line_starts: &'a [usize],
325    pub graph: &'a PythonGraphSource<'a>,
326    target: &'a CodeUnit,
327    target_short: &'a str,
328    target_member: Option<&'a str>,
329    target_owner: Option<CodeUnit>,
330    target_is_module: bool,
331    target_source: &'a ProjectFile,
332    seeds: &'a BTreeSet<(ProjectFile, String)>,
333    edges: &'a [ImportEdge],
334    target_self_file: bool,
335    /// True when a same-file best-effort is justified for an unresolvable
336    /// receiver: the target is a member, its owner is in this file, and exactly
337    /// one class in this file declares that member name (so `recv.member` with
338    /// an un-inferrable `recv` unambiguously means the target). Cross-file
339    /// untyped receivers stay conservative.
340    member_best_effort_unique: bool,
341    raw_module_bindings: &'a ModuleBindingTimeline,
342    module_bindings: &'a HashMap<String, Vec<ClassifiedModuleBindingEvent>>,
343    scoped_import_bindings: &'a [PythonImportBinding],
344    scope_facts: &'a HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
345    scope_range_index: &'a [ScopeRangeEntry],
346    pub hits: &'a mut BTreeSet<UsageHit>,
347    pub unproven_hits: &'a mut BTreeSet<UsageHit>,
348}
349
350struct ScopeRangeEntry {
351    range: Range,
352    scope: CodeUnit,
353    prefix_max_end: usize,
354}
355
356fn build_scope_range_index(
357    graph: &PythonGraphSource<'_>,
358    scope_facts: &HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
359) -> Vec<ScopeRangeEntry> {
360    let mut entries = scope_facts
361        .keys()
362        .flat_map(|scope| {
363            graph
364                .index
365                .ranges(scope)
366                .into_iter()
367                .map(|range| ScopeRangeEntry {
368                    range,
369                    scope: scope.clone(),
370                    prefix_max_end: 0,
371                })
372        })
373        .collect::<Vec<_>>();
374    entries.sort_by(|left, right| {
375        left.range
376            .start_byte
377            .cmp(&right.range.start_byte)
378            .then_with(|| right.range.end_byte.cmp(&left.range.end_byte))
379            .then_with(|| left.scope.cmp(&right.scope))
380    });
381    let mut max_end = 0;
382    for entry in &mut entries {
383        max_end = max_end.max(entry.range.end_byte);
384        entry.prefix_max_end = max_end;
385    }
386    entries
387}
388
389fn indexed_scope_entry<'entry, 'facts>(
390    scope_range_index: &'entry [ScopeRangeEntry],
391    scope_facts: &'facts HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
392    node: Node<'_>,
393    mut skip_innermost: usize,
394) -> Option<(&'entry CodeUnit, &'facts LocalBindingsSnapshot<String>)> {
395    let mut cursor =
396        scope_range_index.partition_point(|entry| entry.range.start_byte <= node.start_byte());
397    while cursor > 0 {
398        cursor -= 1;
399        let entry = &scope_range_index[cursor];
400        if entry.prefix_max_end < node.end_byte() {
401            return None;
402        }
403        if entry.range.end_byte >= node.end_byte() {
404            if skip_innermost > 0 {
405                skip_innermost -= 1;
406                continue;
407            }
408            return scope_facts
409                .get(&entry.scope)
410                .map(|facts| (&entry.scope, facts));
411        }
412    }
413    None
414}
415
416/// The per-function receiver-type facts enclosing `node`, if any. Shared by the
417/// forward scan ([`ScanCtx`]) and the inverted builder (`PyScan`) so the two
418/// paths resolve a receiver's scope through one place.
419pub fn enclosing_scope_facts<'a>(
420    index: &dyn CodeUnitIndex,
421    file: &ProjectFile,
422    scope_facts: &'a HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
423    node: Node<'_>,
424) -> Option<&'a LocalBindingsSnapshot<String>> {
425    let range = Range {
426        start_byte: node.start_byte(),
427        end_byte: node.end_byte(),
428        start_line: 0,
429        end_line: 0,
430    };
431    let enclosing = index.enclosing_code_unit(file, &range)?;
432    scope_facts.get(&enclosing)
433}
434
435impl ScanCtx<'_> {
436    fn scope_entry_for_node(
437        &self,
438        node: Node<'_>,
439    ) -> Option<(&CodeUnit, &LocalBindingsSnapshot<String>)> {
440        indexed_scope_entry(
441            self.scope_range_index,
442            self.scope_facts,
443            node,
444            usize::from(function_declaration_expression_is_outer_scoped(node)),
445        )
446    }
447
448    fn scope_facts_for_node(&self, node: Node<'_>) -> Option<&LocalBindingsSnapshot<String>> {
449        self.scope_entry_for_node(node).map(|(_, facts)| facts)
450    }
451
452    fn binds_target(&self, ident: &str, node: Node<'_>) -> bool {
453        let scope_entry = self.scope_entry_for_node(node);
454        if self.target_self_file
455            && ident == self.target_short
456            && scope_entry
457                .is_none_or(|(scope, facts)| scope.is_module() || !facts.is_shadowed(ident))
458        {
459            return true;
460        }
461        if scope_entry.is_some_and(|(scope, facts)| !scope.is_module() && facts.is_shadowed(ident))
462        {
463            return false;
464        }
465        self.module_binding_targets_query(ident, node)
466    }
467
468    fn receiver_binds_target(&self, expr: &str, node: Node<'_>) -> bool {
469        if self.binds_target(expr, node) {
470            return true;
471        }
472
473        if self.target_member.is_some() && self.import_edge_visible_for(expr, node) {
474            return true;
475        }
476
477        // `self`/`cls` is implicitly typed as the enclosing class, so a same-file
478        // `self.member` access is a usage of that class's member even though the
479        // receiver is never assigned a type the way a local or parameter is.
480        if matches!(expr, "self" | "cls") && self.self_receiver_matches_target(node) {
481            return true;
482        }
483
484        match enclosing_runtime_parameter_type(expr, node, self.source) {
485            EnclosingParameterType::Typed(raw_type) => {
486                return self.receiver_type_matches_target(&raw_type);
487            }
488            EnclosingParameterType::Untyped => return false,
489            EnclosingParameterType::NotDeclared => {}
490        }
491
492        let Some(scope_facts) = self.scope_facts_for_node(node) else {
493            return false;
494        };
495        let resolution = scope_facts.resolution_for(expr);
496        let Some(raw_type) = resolution
497            .as_precise()
498            .and_then(|targets| targets.iter().next())
499        else {
500            return false;
501        };
502        self.receiver_type_matches_target(raw_type)
503    }
504
505    /// Whether `node` is evaluated in the target member owner's class namespace.
506    /// This includes class-level field initializers and the decorators,
507    /// annotations, and defaults of a method declaration. The method body itself
508    /// executes later with ordinary function scoping, where a bare member name
509    /// does not reach the class namespace.
510    fn node_directly_in_owner_class_body(&self, node: Node<'_>) -> bool {
511        let Some(target_owner) = self.target_owner.as_ref() else {
512            return false;
513        };
514        let range = Range {
515            start_byte: node.start_byte(),
516            end_byte: node.end_byte(),
517            start_line: 0,
518            end_line: 0,
519        };
520        let Some(enclosing) = self.graph.index.enclosing_code_unit(self.file, &range) else {
521            return false;
522        };
523        if &enclosing == target_owner {
524            return true;
525        }
526        if enclosing.is_function() {
527            return target_owner_code_unit(self.graph.index, &enclosing).as_ref()
528                == Some(target_owner)
529                && function_declaration_expression_is_outer_scoped(node);
530        }
531        target_owner_code_unit(self.graph.index, &enclosing).as_ref() == Some(target_owner)
532    }
533
534    /// Whether `expr`'s type is genuinely un-inferrable in `node`'s scope (an
535    /// unseeded receiver such as an unannotated parameter), as opposed to a
536    /// receiver we resolved to some specific — possibly different — type.
537    fn receiver_type_is_unknown(&self, expr: &str, node: Node<'_>) -> bool {
538        match enclosing_runtime_parameter_type(expr, node, self.source) {
539            EnclosingParameterType::Typed(_) => return false,
540            EnclosingParameterType::Untyped => return true,
541            EnclosingParameterType::NotDeclared => {}
542        }
543        match self.scope_facts_for_node(node) {
544            Some(facts) => facts.resolution_for(expr).is_unknown(),
545            None => true,
546        }
547    }
548
549    fn import_edge_visible_for(&self, ident: &str, node: Node<'_>) -> bool {
550        if let Some(scope_facts) = self.scope_facts_for_node(node)
551            && scope_facts.is_shadowed(ident)
552        {
553            return false;
554        }
555        self.module_binding_targets_query(ident, node)
556    }
557
558    fn module_binding_targets_query(&self, ident: &str, node: Node<'_>) -> bool {
559        // A member receiver must bind the owner symbol itself. A namespace
560        // import only binds the module that contains the owner; treating that
561        // module as the owner conflates `from pkg import child` (the
562        // `pkg.child` module) with a same-named `child` class exported by that
563        // module. Top-level targets still accept either binding kind below.
564        if self.target_member.is_some() {
565            return self.module_binding_targets_symbol(ident, node);
566        }
567        if let Some(matches) = self.function_import_binding_targets_query(ident, node) {
568            return matches;
569        }
570        self.module_binding_matches_query(ident, node, true, |kind| {
571            kind != ModuleBindingKind::Other
572        })
573    }
574
575    fn module_binding_targets_symbol(&self, ident: &str, node: Node<'_>) -> bool {
576        if let Some(matches) = self.function_import_binding_targets_query(ident, node) {
577            return matches;
578        }
579        let unclassified_named_import = self.edges.iter().any(|edge| {
580            edge.local_name == ident && !matches!(edge.kind, ImportEdgeKind::Namespace)
581        });
582        self.module_binding_matches_query(ident, node, unclassified_named_import, |kind| {
583            kind == ModuleBindingKind::TargetSymbolImport
584        })
585    }
586
587    /// Resolve the nearest function-local import before the module timeline.
588    ///
589    /// Candidate discovery retains all imports. A local import with the same
590    /// binder must still override the module binding only inside its function.
591    fn function_import_binding_targets_query(&self, ident: &str, node: Node<'_>) -> Option<bool> {
592        let binding = self.scoped_import_bindings.iter().rev().find(|binding| {
593            binding.is_function_scoped()
594                && binding.start_byte <= node.start_byte()
595                && binding.scope_start_byte <= node.start_byte()
596                && node.end_byte() <= binding.scope_end_byte
597                && binding.local_name == ident
598        })?;
599        let candidates = resolve_fqn_candidates(self.python, &binding.qualified_name, |name| {
600            self.graph.index.definitions(name).collect()
601        });
602        let imported_target = self.target_owner.as_ref().unwrap_or(self.target);
603        Some(
604            candidates
605                .iter()
606                .any(|candidate| candidate == imported_target),
607        )
608    }
609
610    fn module_binding_matches_query(
611        &self,
612        ident: &str,
613        node: Node<'_>,
614        unclassified: bool,
615        matches: impl Fn(ModuleBindingKind) -> bool,
616    ) -> bool {
617        if !self.edges.iter().any(|edge| edge.local_name == ident) {
618            return false;
619        }
620        let Some(events) = self.module_bindings.get(ident) else {
621            return unclassified;
622        };
623        let cutoff = if reference_is_deferred_function_body(node) {
624            usize::MAX
625        } else {
626            node.start_byte()
627        };
628        let visible: Vec<_> = events
629            .iter()
630            .filter(|event| event.visible_from <= cutoff)
631            .collect();
632        let start = visible
633            .iter()
634            .rposition(|event| !event.conditional)
635            .unwrap_or(0);
636        visible[start..].iter().any(|event| matches(event.kind))
637    }
638
639    /// Whether the class enclosing `node` is the target member's owner (or a
640    /// subclass of it, for inherited members). Used to resolve `self`/`cls`
641    /// receivers, whose type is the lexically enclosing class.
642    fn self_receiver_matches_target(&self, node: Node<'_>) -> bool {
643        let Some(target_owner) = self.target_owner.as_ref() else {
644            return false;
645        };
646        let range = Range {
647            start_byte: node.start_byte(),
648            end_byte: node.end_byte(),
649            start_line: 0,
650            end_line: 0,
651        };
652        let Some(enclosing) = self.graph.index.enclosing_code_unit(self.file, &range) else {
653            return false;
654        };
655        let enclosing_class = if enclosing.is_class() {
656            enclosing
657        } else {
658            match target_owner_code_unit(self.graph.index, &enclosing) {
659                Some(class) => class,
660                None => return false,
661            }
662        };
663        if &enclosing_class == target_owner {
664            return true;
665        }
666        self.graph
667            .hierarchy
668            .map(|provider| provider.get_ancestors(&enclosing_class))
669            .unwrap_or_default()
670            .into_iter()
671            .any(|ancestor| ancestor == *target_owner)
672    }
673
674    fn receiver_type_matches_target(&self, raw_type: &str) -> bool {
675        let Some(target_owner) = self.target_owner.as_ref() else {
676            return false;
677        };
678        if let Some(receiver_type) = resolve_receiver_type(
679            self.graph,
680            self.python,
681            self.file,
682            raw_type,
683            self.target_self_file,
684        ) {
685            if &receiver_type == target_owner {
686                return true;
687            }
688            return self
689                .graph
690                .hierarchy
691                .map(|provider| provider.get_ancestors(&receiver_type))
692                .unwrap_or_default()
693                .into_iter()
694                .any(|ancestor| ancestor == *target_owner);
695        }
696
697        // Preserve the annotation-edge/name fallback only when structured
698        // resolution has no answer. When it identifies a concrete owner, even
699        // a negative answer is authoritative: otherwise identically named
700        // vendored package copies widen into one another.
701        receiver_annotation_matches_target(
702            raw_type,
703            self.edges,
704            self.target_short,
705            self.target_self_file,
706        )
707    }
708}
709
710pub(crate) fn function_declaration_expression_is_outer_scoped(node: Node<'_>) -> bool {
711    let site_start = node.start_byte();
712    let site_end = node.end_byte();
713    let mut current = node;
714    while let Some(parent) = current.parent() {
715        if parent.kind() == "function_definition" {
716            if parent
717                .child_by_field_name("body")
718                .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
719            {
720                return false;
721            }
722            if parent
723                .child_by_field_name("name")
724                .is_some_and(|name| name.id() == node.id())
725            {
726                return false;
727            }
728            if let Some(parameters) = parent.child_by_field_name("parameters")
729                && parameters.start_byte() <= site_start
730                && site_end <= parameters.end_byte()
731            {
732                let mut parameter = node;
733                while parameter.parent() != Some(parameters) {
734                    let Some(next) = parameter.parent() else {
735                        return false;
736                    };
737                    parameter = next;
738                }
739                let binder = if parameter.kind() == "identifier" {
740                    Some(parameter)
741                } else {
742                    parameter.child_by_field_name("name").or_else(|| {
743                        parameter
744                            .named_child(0)
745                            .filter(|child| child.kind() == "identifier")
746                    })
747                };
748                return binder.is_none_or(|binder| binder.id() != node.id());
749            }
750            return true;
751        }
752        if parent.kind() == "decorated_definition" {
753            return current.kind() == "decorator";
754        }
755        if parent.kind() == "class_definition" {
756            break;
757        }
758        current = parent;
759    }
760    false
761}
762
763fn scan_node(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
764    let mut stack = vec![node];
765    while let Some(node) = stack.pop() {
766        match node.kind() {
767            "import_statement" | "import_from_statement" => {
768                handle_import_candidate(node, ctx);
769                continue;
770            }
771            "identifier" => {
772                if handle_annotation_reference_candidate(node, ctx) {
773                    continue;
774                }
775                handle_identifier_candidate(node, ctx);
776            }
777            "attribute" => {
778                if handle_annotation_reference_candidate(node, ctx) {
779                    continue;
780                }
781                handle_attribute_candidate(node, ctx);
782            }
783            "string_content" => {
784                handle_annotation_reference_candidate(node, ctx);
785            }
786            "keyword_argument" => {
787                handle_keyword_argument_candidate(node, ctx);
788                if let Some(value) = node.child_by_field_name("value") {
789                    stack.push(value);
790                }
791                continue;
792            }
793            _ => {}
794        }
795
796        let mut cursor = node.walk();
797        let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
798        children.reverse();
799        stack.extend(children);
800    }
801}
802
803fn handle_annotation_reference_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) -> bool {
804    let Some(candidates) = annotation_reference_candidates(
805        ctx.graph,
806        ctx.python,
807        ctx.file,
808        ctx.source,
809        node,
810        ctx.target_self_file,
811    ) else {
812        return false;
813    };
814
815    // Method annotations are part of the surrounding class declaration. A
816    // qualifier such as `types.Handler` therefore uses the class field
817    // `types` when that field was defined earlier in the class body, even
818    // though resolving the complete annotation yields `Handler`.
819    if node.kind() == "attribute"
820        && ctx.target.is_field()
821        && let Some(root) = leftmost_identifier(node)
822        && ctx.target_member == Some(slice(root, ctx.source))
823        && ctx.node_directly_in_owner_class_body(root)
824    {
825        record_hit(root, ctx);
826    }
827
828    if (ctx.target.is_class() || ctx.target.is_field() || ctx.target_member.is_none())
829        && candidates.iter().all(|candidate| *candidate == *ctx.target)
830        && candidates.iter().any(|candidate| *candidate == *ctx.target)
831    {
832        let site = if node.kind() == "attribute" {
833            node.child_by_field_name("attribute").unwrap_or(node)
834        } else {
835            node
836        };
837        record_hit(site, ctx);
838    }
839
840    if let Some(site) = annotation_class_qualifier_site(
841        ctx.graph, ctx.python, ctx.file, ctx.source, node, ctx.target,
842    ) {
843        record_hit(site, ctx);
844    }
845
846    // An attribute annotation the resolver could not turn into a candidate is
847    // not consumed here: the namespace-attribute path still has to see it, and
848    // so do the node's children. A module target also needs that path because
849    // annotation resolution names the terminal declaration, not its qualifier.
850    // `inverted.rs` always falls through for these module qualifier references.
851    if node.kind() == "attribute" && (candidates.is_empty() || ctx.target_is_module) {
852        return false;
853    }
854
855    true
856}
857
858fn handle_keyword_argument_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
859    let (Some(target_member), Some(name), Some(arguments)) = (
860        ctx.target_member,
861        node.child_by_field_name("name"),
862        node.parent(),
863    ) else {
864        return;
865    };
866    if name.kind() != "identifier"
867        || slice(name, ctx.source) != target_member
868        || arguments.kind() != "argument_list"
869    {
870        return;
871    }
872    let Some(call) = arguments.parent().filter(|parent| parent.kind() == "call") else {
873        return;
874    };
875    let Some(function) = call.child_by_field_name("function") else {
876        return;
877    };
878    if function.kind() == "identifier" && slice(function, ctx.source) == "cls" {
879        if ctx.self_receiver_matches_target(function) {
880            record_hit(name, ctx);
881        }
882        return;
883    }
884    let Some(target_owner) = ctx.target_owner.as_ref() else {
885        return;
886    };
887    let scoped_callee_matches = if function.kind() == "identifier" {
888        ctx.scope_facts_for_node(function)
889            .and_then(|facts| {
890                facts
891                    .resolution_for(slice(function, ctx.source))
892                    .as_precise()
893                    .and_then(|targets| targets.iter().next().cloned())
894            })
895            .is_some_and(|raw_type| ctx.receiver_type_matches_target(&raw_type))
896    } else {
897        false
898    };
899    let default_callee_matches = if function.kind() == "identifier" {
900        resolve_callable_parameter_default_types(
901            ctx.graph,
902            ctx.python,
903            ctx.file,
904            ctx.source,
905            function,
906            slice(function, ctx.source),
907        )
908        .into_iter()
909        .any(|class| {
910            &class == target_owner
911                || ctx
912                    .graph
913                    .hierarchy
914                    .map(|provider| provider.get_ancestors(&class))
915                    .unwrap_or_default()
916                    .into_iter()
917                    .any(|ancestor| &ancestor == target_owner)
918        })
919    } else {
920        false
921    };
922    let root_shadowed = leftmost_identifier(function).is_some_and(|root| {
923        ctx.scope_facts_for_node(function)
924            .is_some_and(|facts| facts.is_shadowed(slice(root, ctx.source)))
925    });
926    if root_shadowed && !scoped_callee_matches && !default_callee_matches {
927        return;
928    }
929    let matches = scoped_callee_matches
930        || default_callee_matches
931        || (!root_shadowed
932            && resolve_constructor_types(ctx.graph, ctx.python, ctx.file, ctx.source, function)
933                .into_iter()
934                .any(|class| {
935                    &class == target_owner
936                        || ctx
937                            .graph
938                            .hierarchy
939                            .map(|provider| provider.get_ancestors(&class))
940                            .unwrap_or_default()
941                            .into_iter()
942                            .any(|ancestor| &ancestor == target_owner)
943                }));
944    if matches {
945        record_hit(name, ctx);
946    }
947}
948
949fn leftmost_identifier(mut node: Node<'_>) -> Option<Node<'_>> {
950    loop {
951        match node.kind() {
952            "identifier" => return Some(node),
953            "attribute" => node = node.child_by_field_name("object")?,
954            _ => return None,
955        }
956    }
957}
958
959/// Emit an `Import`-binding hit for `from <mod> import <target>` (the token that
960/// brings the target into this file). Gated on there being an import edge whose
961/// local name is the target — so a same-named symbol imported from a different
962/// module is not falsely counted. Only top-level symbols (not members) are
963/// imported by their own name.
964fn handle_import_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
965    if ctx.target_member.is_some() {
966        return;
967    }
968    if !ctx
969        .edges
970        .iter()
971        .any(|edge| edge.local_name == ctx.target_short)
972    {
973        return;
974    }
975    let mut stack = vec![node];
976    while let Some(node) = stack.pop() {
977        if node.kind() == "identifier" && slice(node, ctx.source) == ctx.target_short {
978            record_import_hit(node, ctx);
979            return;
980        }
981        let mut cursor = node.walk();
982        for child in node.named_children(&mut cursor) {
983            stack.push(child);
984        }
985    }
986}
987
988fn handle_identifier_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
989    if node
990        .parent()
991        .is_some_and(|parent| parent.kind() == "attribute")
992    {
993        return;
994    }
995    let text = slice(node, ctx.source);
996    if text.is_empty() || is_declaration_identifier(node) || decorates_the_target(node, ctx) {
997        return;
998    }
999    if let Some(member) = ctx.target_member {
1000        // A constructor call `Owner(...)` invokes the class's `__init__`, so it
1001        // is a usage of `__init__` even though `__init__` never appears.
1002        if member == "__init__" && is_call_callee(node) && ctx.binds_target(text, node) {
1003            record_hit(node, ctx);
1004            return;
1005        }
1006        // For a member target, a bare identifier is a usage only when it names
1007        // the member directly in the owner class body (the class namespace).
1008        if text == member && ctx.node_directly_in_owner_class_body(node) {
1009            record_hit(node, ctx);
1010        }
1011        return;
1012    }
1013    if !ctx.binds_target(text, node) {
1014        return;
1015    }
1016    if !ctx.target_is_module
1017        && ctx.edges.iter().any(|edge| edge.local_name == text)
1018        && !ctx.module_binding_targets_symbol(text, node)
1019    {
1020        return;
1021    }
1022    record_hit(node, ctx);
1023}
1024
1025/// Whether `node` sits in a decorator of the very definition the scan is
1026/// looking for.
1027///
1028/// Python evaluates a decorator expression before the decorated name is bound,
1029/// so `@foo` above `def foo()` names an *outer* `foo` and never the definition
1030/// it decorates. Without this the decorated definition reads as a user of
1031/// itself, which the enclosing-equals-target drop used to hide.
1032fn decorates_the_target(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
1033    if ctx.file != ctx.target_source {
1034        return false;
1035    }
1036    let mut current = node;
1037    while let Some(parent) = current.parent() {
1038        if parent.kind() == "decorated_definition" && current.kind() == "decorator" {
1039            let Some(definition) = parent.child_by_field_name("definition") else {
1040                return false;
1041            };
1042            return ctx.graph.index.ranges(ctx.target).iter().any(|range| {
1043                range.start_byte <= definition.start_byte()
1044                    && definition.end_byte() <= range.end_byte
1045            });
1046        }
1047        current = parent;
1048    }
1049    false
1050}
1051
1052fn handle_attribute_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
1053    let Some(object) = node.child_by_field_name("object") else {
1054        return;
1055    };
1056    let Some(attribute) = node.child_by_field_name("attribute") else {
1057        return;
1058    };
1059    let object_text = slice(object, ctx.source);
1060    let attribute_text = slice(attribute, ctx.source);
1061    if let Some(member) = ctx.target_member
1062        && attribute_text == member
1063    {
1064        // A `self.member` / `cls.member` receiver is the current instance / own
1065        // class — a same-owner site, excluded from external usage counts (#1014
1066        // facet B). A typed local of the same type is a different instance and
1067        // stays an external hit.
1068        let is_same_owner_receiver =
1069            matches!(object_text, "self" | "cls") && ctx.self_receiver_matches_target(node);
1070        if is_same_owner_receiver {
1071            record_self_receiver_hit(attribute, ctx);
1072        } else if ctx.receiver_binds_target(object_text, node)
1073            || (object.kind() == "call" && call_result_matches_target(object, ctx))
1074        {
1075            record_hit(attribute, ctx);
1076        } else if member_receiver_match_is_unproven(object, object_text, node, ctx) {
1077            record_unproven_hit(attribute, ctx);
1078        }
1079    }
1080
1081    let object_binds_target = if ctx.target_is_module {
1082        imported_root_targets_module(ctx, object, node)
1083    } else {
1084        ctx.binds_target(object_text, node)
1085    };
1086    if object.kind() == "identifier"
1087        && object_binds_target
1088        && (ctx.target_is_module
1089            || (ctx.target_member.is_none()
1090                && !ctx.edges.iter().any(|edge| {
1091                    matches!(edge.kind, ImportEdgeKind::Namespace) && edge.local_name == object_text
1092                })))
1093    {
1094        record_hit(object, ctx);
1095    }
1096
1097    if ctx.target_is_module
1098        && let Some(module_qualifier) = module_attribute_target_hit(node, ctx)
1099    {
1100        record_hit(module_qualifier, ctx);
1101    }
1102
1103    // A bare member name used as the *object* of an attribute access in the
1104    // owner class body — e.g. the `x` in `@x.setter`/`@x.deleter` decorating a
1105    // property `x` — is a usage of that member.
1106    if let Some(member) = ctx.target_member
1107        && object.kind() == "identifier"
1108        && object_text == member
1109        && ctx.node_directly_in_owner_class_body(object)
1110    {
1111        record_hit(object, ctx);
1112    }
1113
1114    // Best-effort for an un-inferrable receiver: `recv.member` where `recv`'s
1115    // type cannot be resolved is attributed to the target when the target's
1116    // owner is in this file and the member name is unique among local classes
1117    // (so `recv.member` can only mean the target). `self`/`cls` are handled
1118    // structurally above; cross-file untyped receivers stay conservative.
1119    if ctx.member_best_effort_unique
1120        && let Some(member) = ctx.target_member
1121        && attribute_text == member
1122        && object.kind() == "identifier"
1123        && !matches!(object_text, "self" | "cls")
1124        && !ctx.receiver_binds_target(object_text, node)
1125        && ctx.receiver_type_is_unknown(object_text, node)
1126    {
1127        record_hit(attribute, ctx);
1128    }
1129
1130    if let Some(module_binding_target) = module_binding_attribute_target_hit(node, ctx) {
1131        record_hit(module_binding_target, ctx);
1132    }
1133}
1134
1135/// Resolve a top-level symbol written through an imported module binding and
1136/// any number of intermediate modules (`K.feature.DISK`,
1137/// `pkg.core.ops.eye_like`). This includes both `import pkg as K` and
1138/// `from pkg import submodule`; the latter is a named import syntactically but
1139/// still introduces a module binding when the structured module index proves
1140/// that `pkg.submodule` is a workspace module.
1141///
1142/// The written path is assembled only from tree-sitter's `attribute` fields.
1143/// The analyzer's canonical export resolver then proves that the whole path
1144/// names the exact physical target, so re-export aliases remain supported
1145/// without comparing rendered source paths or broadening same-name candidates.
1146fn module_binding_attribute_target_hit<'a>(node: Node<'a>, ctx: &ScanCtx<'_>) -> Option<Node<'a>> {
1147    if ctx.target_member.is_some() {
1148        return None;
1149    }
1150    let (root, attributes) = attribute_chain(node)?;
1151    let terminal = *attributes.last()?;
1152    let terminal_name = slice(terminal, ctx.source);
1153    if terminal_name.is_empty()
1154        || !ctx
1155            .seeds
1156            .iter()
1157            .any(|(_, seed_name)| seed_name == terminal_name)
1158    {
1159        return None;
1160    }
1161
1162    for binding in imported_module_bindings(ctx, root, node) {
1163        let mut written_module = binding.module.clone();
1164        for attribute in attributes
1165            [binding.consumed_attributes.min(attributes.len() - 1)..attributes.len() - 1]
1166            .iter()
1167        {
1168            let segment = slice(*attribute, ctx.source);
1169            if segment.is_empty() {
1170                return None;
1171            }
1172            written_module.push('.');
1173            written_module.push_str(segment);
1174        }
1175        if usage_resolve_module_files(ctx.python, ctx.file, &written_module)
1176            .iter()
1177            .any(|resolved| {
1178                ctx.seeds
1179                    .contains(&(resolved.clone(), terminal_name.to_string()))
1180            })
1181        {
1182            return Some(terminal);
1183        }
1184
1185        let mut written_fqn = binding.module;
1186        for attribute in attributes.iter().skip(binding.consumed_attributes) {
1187            let segment = slice(*attribute, ctx.source);
1188            if segment.is_empty() {
1189                return None;
1190            }
1191            written_fqn.push('.');
1192            written_fqn.push_str(segment);
1193        }
1194        if resolve_fqn_candidates(ctx.python, &written_fqn, |name| {
1195            ctx.graph.index.definitions(name).collect()
1196        })
1197        .into_iter()
1198        .any(|candidate| &candidate == ctx.target)
1199        {
1200            return Some(terminal);
1201        }
1202    }
1203    None
1204}
1205
1206fn imported_root_targets_module(ctx: &ScanCtx<'_>, root: Node<'_>, reference: Node<'_>) -> bool {
1207    imported_module_bindings(ctx, root, reference)
1208        .into_iter()
1209        .any(|binding| {
1210            usage_resolve_module_files(ctx.python, ctx.file, &binding.module)
1211                .into_iter()
1212                .any(|resolved_file| &resolved_file == ctx.target_source)
1213        })
1214}
1215
1216fn module_attribute_target_hit<'a>(node: Node<'a>, ctx: &ScanCtx<'_>) -> Option<Node<'a>> {
1217    let (root, attributes) = attribute_chain(node)?;
1218    if attributes.is_empty() {
1219        return None;
1220    }
1221    for binding in imported_module_bindings(ctx, root, node) {
1222        let mut module_fqn = binding.module;
1223        for attribute in attributes.iter().skip(binding.consumed_attributes) {
1224            let segment = slice(*attribute, ctx.source);
1225            if segment.is_empty() {
1226                return None;
1227            }
1228            if module_fqn.ends_with('.') {
1229                module_fqn.push_str(segment);
1230            } else {
1231                module_fqn.push('.');
1232                module_fqn.push_str(segment);
1233            }
1234            let resolved = usage_resolve_module_files(ctx.python, ctx.file, &module_fqn);
1235            if resolved.is_empty() {
1236                break;
1237            }
1238            if resolved
1239                .iter()
1240                .any(|resolved_file| resolved_file == ctx.target_source)
1241            {
1242                return Some(*attribute);
1243            }
1244        }
1245    }
1246    None
1247}
1248
1249fn call_result_matches_target(call: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
1250    let Some(target_owner) = ctx.target_owner.as_ref() else {
1251        return false;
1252    };
1253    let scope_facts = ctx.scope_facts_for_node(call);
1254    call_result_types(
1255        ctx.graph,
1256        ctx.python,
1257        ctx.file,
1258        ctx.source,
1259        call,
1260        scope_facts,
1261    )
1262    .into_iter()
1263    .any(|class| {
1264        &class == target_owner
1265            || ctx
1266                .graph
1267                .hierarchy
1268                .map(|provider| provider.get_ancestors(&class))
1269                .unwrap_or_default()
1270                .into_iter()
1271                .any(|ancestor| &ancestor == target_owner)
1272    })
1273}
1274
1275pub fn call_result_types(
1276    graph: &PythonGraphSource<'_>,
1277    python: &dyn PythonUsageSource,
1278    file: &ProjectFile,
1279    source: &str,
1280    call: Node<'_>,
1281    scope_facts: Option<&LocalBindingsSnapshot<String>>,
1282) -> Vec<CodeUnit> {
1283    let Some(function) = call.child_by_field_name("function") else {
1284        return Vec::new();
1285    };
1286    let constructed = resolve_constructor_types(graph, python, file, source, function);
1287    if !constructed.is_empty() {
1288        return constructed;
1289    }
1290    let callable_fqns = resolve_callable_fqns(graph, python, file, source, function, scope_facts);
1291    if callable_fqns.is_empty() {
1292        return Vec::new();
1293    }
1294    let callables = callable_fqns
1295        .into_iter()
1296        .flat_map(|callable_fqn| {
1297            resolve_fqn_candidates(python, &callable_fqn, |name| {
1298                graph.index.definitions(name).collect()
1299            })
1300        })
1301        .collect::<Vec<_>>();
1302    let mut classes = Vec::new();
1303    for callable in callables.into_iter().filter(CodeUnit::is_function) {
1304        let raw_type = callable_return_type_name(graph, python, &callable).or_else(|| {
1305            if callable.source() != file {
1306                return None;
1307            }
1308            let prepared = python.prepared_syntax(graph.token, file)?;
1309            let key = factory_function_key(graph, &callable);
1310            collect_factory_return_types_from_root(prepared.tree().root_node(), prepared.source())
1311                .remove(&key)
1312        });
1313        let Some(raw_type) = raw_type else {
1314            continue;
1315        };
1316        if let Some(class) =
1317            resolve_receiver_type(graph, python, callable.source(), &raw_type, true)
1318        {
1319            classes.push(class);
1320        }
1321    }
1322    classes.sort();
1323    classes.dedup();
1324    classes
1325}
1326
1327fn resolve_callable_fqns(
1328    graph: &PythonGraphSource<'_>,
1329    python: &dyn PythonUsageSource,
1330    file: &ProjectFile,
1331    source: &str,
1332    function: Node<'_>,
1333    scope_facts: Option<&LocalBindingsSnapshot<String>>,
1334) -> Vec<String> {
1335    match function.kind() {
1336        "identifier" => {
1337            resolve_identifier_callable_fqns(graph, python, file, source, function, scope_facts)
1338        }
1339        "attribute" => {
1340            resolve_attribute_callable_fqns(graph, python, file, source, function, scope_facts)
1341        }
1342        _ => Vec::new(),
1343    }
1344}
1345
1346fn resolve_identifier_callable_fqns(
1347    graph: &PythonGraphSource<'_>,
1348    python: &dyn PythonUsageSource,
1349    file: &ProjectFile,
1350    source: &str,
1351    function: Node<'_>,
1352    scope_facts: Option<&LocalBindingsSnapshot<String>>,
1353) -> Vec<String> {
1354    let local = slice(function, source);
1355    if local.is_empty() || scope_facts.is_some_and(|facts| facts.is_shadowed(local)) {
1356        return Vec::new();
1357    }
1358    let binder = python.import_binder_of(file);
1359    match binder.bindings.get(local) {
1360        Some(binding) if binding.kind == ImportKind::Named => binding
1361            .imported_name
1362            .as_ref()
1363            .map(|imported| vec![format!("{}.{}", binding.module_specifier, imported)])
1364            .unwrap_or_default(),
1365        _ => graph
1366            .index
1367            .declarations(file)
1368            .into_iter()
1369            .find(|unit| unit.is_function() && unit.identifier() == local)
1370            .map(|unit| vec![unit.fq_name()])
1371            .unwrap_or_default(),
1372    }
1373}
1374
1375fn resolve_attribute_callable_fqns(
1376    graph: &PythonGraphSource<'_>,
1377    python: &dyn PythonUsageSource,
1378    file: &ProjectFile,
1379    source: &str,
1380    function: Node<'_>,
1381    scope_facts: Option<&LocalBindingsSnapshot<String>>,
1382) -> Vec<String> {
1383    let Some(receiver) = function.child_by_field_name("object") else {
1384        return Vec::new();
1385    };
1386    let Some(method) = function.child_by_field_name("attribute") else {
1387        return Vec::new();
1388    };
1389    let method = slice(method, source);
1390    if method.is_empty() {
1391        return Vec::new();
1392    }
1393    let mut fqns = attribute_receiver_classes(graph, python, file, source, receiver, scope_facts)
1394        .into_iter()
1395        .map(|class| format!("{}.{}", class.fq_name(), method))
1396        .collect::<Vec<_>>();
1397    fqns.sort();
1398    fqns.dedup();
1399    fqns
1400}
1401
1402fn attribute_receiver_classes(
1403    graph: &PythonGraphSource<'_>,
1404    python: &dyn PythonUsageSource,
1405    file: &ProjectFile,
1406    source: &str,
1407    receiver: Node<'_>,
1408    scope_facts: Option<&LocalBindingsSnapshot<String>>,
1409) -> Vec<CodeUnit> {
1410    let mut classes = match receiver.kind() {
1411        "identifier" => {
1412            identifier_receiver_classes(graph, python, file, source, receiver, scope_facts)
1413        }
1414        "attribute" => {
1415            if let Some(root) = leftmost_identifier(receiver)
1416                && scope_facts.is_some_and(|facts| facts.is_shadowed(slice(root, source)))
1417            {
1418                Vec::new()
1419            } else {
1420                resolve_constructor_types(graph, python, file, source, receiver)
1421            }
1422        }
1423        _ => Vec::new(),
1424    };
1425    classes.sort();
1426    classes.dedup();
1427    classes
1428}
1429
1430fn identifier_receiver_classes(
1431    graph: &PythonGraphSource<'_>,
1432    python: &dyn PythonUsageSource,
1433    file: &ProjectFile,
1434    source: &str,
1435    receiver: Node<'_>,
1436    scope_facts: Option<&LocalBindingsSnapshot<String>>,
1437) -> Vec<CodeUnit> {
1438    let ident = slice(receiver, source);
1439    if ident.is_empty() {
1440        return Vec::new();
1441    }
1442    if matches!(ident, "self" | "cls")
1443        && let Some(class) = enclosing_class_for_node(graph, file, receiver)
1444    {
1445        return vec![class];
1446    }
1447    if let Some(facts) = scope_facts {
1448        if let Some(raw_type) = facts
1449            .resolution_for(ident)
1450            .as_precise()
1451            .and_then(|targets| targets.iter().next())
1452            && let Some(class) = resolve_receiver_type(graph, python, file, raw_type, false)
1453        {
1454            return vec![class];
1455        }
1456        if facts.is_shadowed(ident) {
1457            return Vec::new();
1458        }
1459    }
1460    resolve_receiver_type(graph, python, file, ident, false)
1461        .into_iter()
1462        .collect()
1463}
1464
1465fn enclosing_class_for_node(
1466    graph: &PythonGraphSource<'_>,
1467    file: &ProjectFile,
1468    node: Node<'_>,
1469) -> Option<CodeUnit> {
1470    let range = Range {
1471        start_byte: node.start_byte(),
1472        end_byte: node.end_byte(),
1473        start_line: 0,
1474        end_line: 0,
1475    };
1476    let enclosing = graph.index.enclosing_code_unit(file, &range)?;
1477    // Python's structural model never separately indexes nested
1478    // function/lambda scopes as their own CodeUnit, so `self`/`cls` can only
1479    // ever need the enclosing unit itself or (a method's owner) exactly one
1480    // `parent_of` hop up — `.take(2)` keeps that bound explicit rather than
1481    // an unbounded walk that could climb past the same-file owner class.
1482    brokk_bifrost_core::analyzer::usages::common::enclosing_owner_chain(enclosing, |unit| {
1483        graph.index.parent_of(unit)
1484    })
1485    .take(2)
1486    .find(|unit| unit.is_class() && unit.source() == file)
1487}
1488
1489fn attribute_chain<'a>(node: Node<'a>) -> Option<(Node<'a>, Vec<Node<'a>>)> {
1490    let mut attributes = Vec::new();
1491    let mut current = node;
1492    loop {
1493        if current.kind() != "attribute" {
1494            return None;
1495        }
1496        attributes.push(current.child_by_field_name("attribute")?);
1497        current = current.child_by_field_name("object")?;
1498        if current.kind() == "identifier" {
1499            attributes.reverse();
1500            return Some((current, attributes));
1501        }
1502    }
1503}
1504
1505struct ImportedModuleBinding {
1506    module: String,
1507    consumed_attributes: usize,
1508}
1509
1510fn imported_module_bindings(
1511    ctx: &ScanCtx<'_>,
1512    root: Node<'_>,
1513    reference: Node<'_>,
1514) -> Vec<ImportedModuleBinding> {
1515    let root_text = slice(root, ctx.source);
1516    if root_text.is_empty() || import_root_shadowed(ctx, root_text, root, reference) {
1517        return Vec::new();
1518    }
1519
1520    if let Some(binding) = ctx.scoped_import_bindings.iter().rev().find(|binding| {
1521        binding.is_function_scoped()
1522            && binding.start_byte <= reference.start_byte()
1523            && binding.scope_start_byte <= reference.start_byte()
1524            && reference.end_byte() <= binding.scope_end_byte
1525            && binding.local_name == root_text
1526    }) {
1527        return if usage_resolve_module_files(ctx.python, ctx.file, &binding.qualified_name)
1528            .is_empty()
1529        {
1530            Vec::new()
1531        } else {
1532            vec![ImportedModuleBinding {
1533                module: binding.qualified_name.clone(),
1534                consumed_attributes: binding.consumed_attributes,
1535            }]
1536        };
1537    }
1538
1539    let Some(events) = ctx.raw_module_bindings.get(root_text) else {
1540        return Vec::new();
1541    };
1542    let cutoff = if reference_is_deferred_function_body(reference) {
1543        usize::MAX
1544    } else {
1545        reference.start_byte()
1546    };
1547    let visible: Vec<_> = events
1548        .iter()
1549        .filter(|event| event.visible_from <= cutoff)
1550        .collect();
1551    // Unaliased dotted imports sharing a root are cumulative in Python:
1552    // `import pkg.a; import pkg.b` leaves both attributes on `pkg`. Only a
1553    // binding of this local to some other value cuts off those earlier module
1554    // imports.
1555    let start = visible
1556        .iter()
1557        .rposition(|event| {
1558            if event.conditional {
1559                return false;
1560            }
1561            match &event.kind {
1562                ModuleBindingEventKind::ImportModule {
1563                    module,
1564                    consumed_attributes,
1565                } => *consumed_attributes == 0 && module != root_text,
1566                ModuleBindingEventKind::FromImport { .. } | ModuleBindingEventKind::Other => true,
1567            }
1568        })
1569        .unwrap_or(0);
1570    let mut modules = visible[start..]
1571        .iter()
1572        .filter_map(|event| match &event.kind {
1573            ModuleBindingEventKind::ImportModule {
1574                module,
1575                consumed_attributes,
1576            } => {
1577                let mut segments = parse_symbol_path(Language::Python, module);
1578                segments.truncate(segments.len().saturating_sub(*consumed_attributes));
1579                Some(ImportedModuleBinding {
1580                    module: segments.join("."),
1581                    consumed_attributes: 0,
1582                })
1583            }
1584            ModuleBindingEventKind::FromImport {
1585                module,
1586                imported_name,
1587            } => {
1588                let submodule = if module.ends_with('.') {
1589                    format!("{module}{imported_name}")
1590                } else {
1591                    format!("{module}.{imported_name}")
1592                };
1593                (!usage_resolve_module_files(ctx.python, ctx.file, &submodule).is_empty())
1594                    .then_some(ImportedModuleBinding {
1595                        module: submodule,
1596                        consumed_attributes: 0,
1597                    })
1598            }
1599            ModuleBindingEventKind::Other => None,
1600        })
1601        .collect::<Vec<_>>();
1602    modules.sort_by(|left, right| {
1603        left.module
1604            .cmp(&right.module)
1605            .then_with(|| left.consumed_attributes.cmp(&right.consumed_attributes))
1606    });
1607    modules.dedup_by(|left, right| {
1608        left.module == right.module && left.consumed_attributes == right.consumed_attributes
1609    });
1610    modules
1611}
1612
1613fn import_root_shadowed(
1614    ctx: &ScanCtx<'_>,
1615    root_text: &str,
1616    root: Node<'_>,
1617    reference: Node<'_>,
1618) -> bool {
1619    ctx.scope_entry_for_node(root)
1620        .or_else(|| ctx.scope_entry_for_node(reference))
1621        .is_some_and(|(scope, facts)| !scope.is_module() && facts.is_shadowed(root_text))
1622        || enclosing_parameters_shadow(root_text, reference, ctx.source)
1623}
1624
1625fn enclosing_parameters_shadow(root_text: &str, reference: Node<'_>, source: &str) -> bool {
1626    let mut current = reference;
1627    while let Some(parent) = current.parent() {
1628        if matches!(parent.kind(), "function_definition" | "lambda") {
1629            let Some(parameters) = parent.child_by_field_name("parameters") else {
1630                return false;
1631            };
1632            let mut cursor = parameters.walk();
1633            return parameters.named_children(&mut cursor).any(|parameter| {
1634                parameter_symbol(parameter, source).as_deref() == Some(root_text)
1635            });
1636        }
1637        current = parent;
1638    }
1639    false
1640}
1641
1642enum EnclosingParameterType {
1643    NotDeclared,
1644    Untyped,
1645    Typed(String),
1646}
1647
1648fn enclosing_runtime_parameter_type(
1649    name: &str,
1650    reference: Node<'_>,
1651    source: &str,
1652) -> EnclosingParameterType {
1653    let site_start = reference.start_byte();
1654    let site_end = reference.end_byte();
1655    let mut current = reference;
1656    while let Some(parent) = current.parent() {
1657        if matches!(parent.kind(), "function_definition" | "lambda")
1658            && parent
1659                .child_by_field_name("body")
1660                .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
1661            && let Some(parameters) = parent.child_by_field_name("parameters")
1662        {
1663            let mut cursor = parameters.walk();
1664            for parameter in parameters.named_children(&mut cursor) {
1665                if parameter_symbol(parameter, source).as_deref() != Some(name) {
1666                    continue;
1667                }
1668                return parameter
1669                    .child_by_field_name("type")
1670                    .and_then(|annotation| normalized_receiver_type(slice(annotation, source)))
1671                    .map_or(EnclosingParameterType::Untyped, |raw_type| {
1672                        EnclosingParameterType::Typed(raw_type)
1673                    });
1674            }
1675        }
1676        current = parent;
1677    }
1678    EnclosingParameterType::NotDeclared
1679}
1680
1681fn member_receiver_match_is_unproven(
1682    object: Node<'_>,
1683    object_text: &str,
1684    node: Node<'_>,
1685    ctx: &ScanCtx<'_>,
1686) -> bool {
1687    if matches!(object_text, "self" | "cls") {
1688        return false;
1689    }
1690    match object.kind() {
1691        "identifier" => {
1692            ctx.receiver_type_is_unknown(object_text, node) && !ctx.member_best_effort_unique
1693        }
1694        "attribute" => true,
1695        _ => false,
1696    }
1697}
1698
1699pub fn slice<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1700    brokk_bifrost_core::analyzer::common::node_source_text(node, source)
1701}
1702
1703/// Whether `node` is the `function` callee of a call expression (`node(...)`).
1704fn is_call_callee(node: Node<'_>) -> bool {
1705    node.parent().is_some_and(|parent| {
1706        parent.kind() == "call"
1707            && parent
1708                .child_by_field_name("function")
1709                .is_some_and(|function| function.id() == node.id())
1710    })
1711}
1712
1713pub fn is_declaration_identifier(node: Node<'_>) -> bool {
1714    let Some(parent) = node.parent() else {
1715        return false;
1716    };
1717    let contains = |container: Node<'_>| {
1718        container.start_byte() <= node.start_byte() && node.end_byte() <= container.end_byte()
1719    };
1720    match parent.kind() {
1721        "class_definition" | "function_definition" => parent
1722            .child_by_field_name("name")
1723            .is_some_and(|name| name.id() == node.id()),
1724        "parameters" | "lambda_parameters" | "list_splat_pattern" | "dictionary_splat_pattern" => {
1725            true
1726        }
1727        "default_parameter" | "typed_parameter" | "typed_default_parameter" => {
1728            parent.child_by_field_name("name").is_some_and(contains)
1729        }
1730        "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => {
1731            parent.child_by_field_name("left").is_some_and(contains)
1732        }
1733        "named_expression" => parent.child_by_field_name("name").is_some_and(contains),
1734        "aliased_import" | "import_from_statement" | "import_statement" => true,
1735        _ => false,
1736    }
1737}
1738
1739#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1740enum ModuleBindingKind {
1741    TargetSymbolImport,
1742    TargetModuleImport,
1743    Other,
1744}
1745
1746#[derive(Clone, Copy, Debug)]
1747struct ClassifiedModuleBindingEvent {
1748    visible_from: usize,
1749    conditional: bool,
1750    kind: ModuleBindingKind,
1751}
1752
1753pub fn collect_module_binding_timeline(root: Node<'_>, source: &str) -> ModuleBindingTimeline {
1754    let mut timeline = ModuleBindingTimeline::default();
1755    let mut stack = vec![root];
1756    while let Some(node) = stack.pop() {
1757        match node.kind() {
1758            "function_definition" | "class_definition" => {
1759                if let Some(name) = node.child_by_field_name("name") {
1760                    record_module_binding(
1761                        &mut timeline,
1762                        slice(name, source),
1763                        node.end_byte(),
1764                        binding_is_conditional(node),
1765                        ModuleBindingEventKind::Other,
1766                    );
1767                }
1768                continue;
1769            }
1770            "import_statement" | "import_from_statement" => {
1771                collect_import_binding_events(node, source, &mut timeline);
1772                continue;
1773            }
1774            "assignment" | "augmented_assignment" | "named_expression" => {
1775                if let Some(left) = node.child_by_field_name("left") {
1776                    record_local_binding_targets(
1777                        left,
1778                        source,
1779                        node.end_byte(),
1780                        binding_is_conditional(node),
1781                        &mut timeline,
1782                    );
1783                }
1784                continue;
1785            }
1786            "for_statement" => {
1787                if let Some(left) = node.child_by_field_name("left") {
1788                    record_local_binding_targets(
1789                        left,
1790                        source,
1791                        left.end_byte(),
1792                        true,
1793                        &mut timeline,
1794                    );
1795                }
1796            }
1797            _ => {}
1798        }
1799
1800        let mut cursor = node.walk();
1801        let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
1802        children.reverse();
1803        stack.extend(children);
1804    }
1805    for events in timeline.values_mut() {
1806        events.sort_by_key(|event| event.visible_from);
1807    }
1808    timeline
1809}
1810
1811fn collect_import_binding_events(
1812    node: Node<'_>,
1813    source: &str,
1814    timeline: &mut ModuleBindingTimeline,
1815) {
1816    if node.kind() == "import_statement" {
1817        let mut cursor = node.walk();
1818        for imported in node.children_by_field_name("name", &mut cursor) {
1819            let name = imported.child_by_field_name("name").unwrap_or(imported);
1820            let Some(local) = imported
1821                .child_by_field_name("alias")
1822                .or_else(|| first_identifier(name))
1823            else {
1824                continue;
1825            };
1826            let module = slice(name, source).trim();
1827            let consumed_attributes = if imported.child_by_field_name("alias").is_some() {
1828                0
1829            } else {
1830                parse_symbol_path(Language::Python, module)
1831                    .len()
1832                    .saturating_sub(1)
1833            };
1834            record_module_binding(
1835                timeline,
1836                slice(local, source),
1837                node.end_byte(),
1838                binding_is_conditional(node),
1839                ModuleBindingEventKind::ImportModule {
1840                    module: module.to_string(),
1841                    consumed_attributes,
1842                },
1843            );
1844        }
1845        return;
1846    }
1847
1848    let Some(module_node) = node.child_by_field_name("module_name") else {
1849        return;
1850    };
1851    let module = slice(module_node, source).trim();
1852    let mut cursor = node.walk();
1853    for imported in node.children_by_field_name("name", &mut cursor) {
1854        if imported.kind() == "wildcard_import" {
1855            continue;
1856        }
1857        let name = imported.child_by_field_name("name").unwrap_or(imported);
1858        let Some(imported_identifier) = last_identifier(name) else {
1859            continue;
1860        };
1861        let imported_name = slice(imported_identifier, source).trim();
1862        let Some(local) = imported
1863            .child_by_field_name("alias")
1864            .or_else(|| last_identifier(name))
1865        else {
1866            continue;
1867        };
1868        record_module_binding(
1869            timeline,
1870            slice(local, source),
1871            node.end_byte(),
1872            binding_is_conditional(node),
1873            ModuleBindingEventKind::FromImport {
1874                module: module.to_string(),
1875                imported_name: imported_name.to_string(),
1876            },
1877        );
1878    }
1879}
1880
1881fn classify_module_binding_timeline(
1882    python: &dyn PythonUsageSource,
1883    file: &ProjectFile,
1884    timeline: &ModuleBindingTimeline,
1885    seeds: &BTreeSet<(ProjectFile, String)>,
1886    edges: &[ImportEdge],
1887) -> HashMap<String, Vec<ClassifiedModuleBindingEvent>> {
1888    let mut classified = HashMap::default();
1889    let mut module_targets: HashMap<String, bool> = HashMap::default();
1890    let relevant_locals: HashSet<&str> =
1891        edges.iter().map(|edge| edge.local_name.as_str()).collect();
1892    for (local, events) in timeline {
1893        if !relevant_locals.contains(local.as_str()) {
1894            continue;
1895        }
1896        let classified_events = events
1897            .iter()
1898            .map(|event| {
1899                let kind = match &event.kind {
1900                    ModuleBindingEventKind::ImportModule { module, .. } => {
1901                        if *module_targets
1902                            .entry(module.clone())
1903                            .or_insert_with(|| module_contains_seed(python, file, module, seeds))
1904                        {
1905                            ModuleBindingKind::TargetModuleImport
1906                        } else {
1907                            ModuleBindingKind::Other
1908                        }
1909                    }
1910                    ModuleBindingEventKind::FromImport {
1911                        module,
1912                        imported_name,
1913                    } => {
1914                        let direct = usage_resolve_module_files(python, file, module).iter().any(
1915                            |resolved| seeds.contains(&(resolved.clone(), imported_name.clone())),
1916                        );
1917                        let submodule = if module.ends_with('.') {
1918                            format!("{module}{imported_name}")
1919                        } else {
1920                            format!("{module}.{imported_name}")
1921                        };
1922                        let imports_target_module =
1923                            *module_targets.entry(submodule.clone()).or_insert_with(|| {
1924                                module_contains_seed(python, file, &submodule, seeds)
1925                            });
1926                        if direct {
1927                            ModuleBindingKind::TargetSymbolImport
1928                        } else if imports_target_module {
1929                            ModuleBindingKind::TargetModuleImport
1930                        } else {
1931                            ModuleBindingKind::Other
1932                        }
1933                    }
1934                    ModuleBindingEventKind::Other => ModuleBindingKind::Other,
1935                };
1936                ClassifiedModuleBindingEvent {
1937                    visible_from: event.visible_from,
1938                    conditional: event.conditional,
1939                    kind,
1940                }
1941            })
1942            .collect();
1943        classified.insert(local.clone(), classified_events);
1944    }
1945    classified
1946}
1947
1948fn module_contains_seed(
1949    python: &dyn PythonUsageSource,
1950    file: &ProjectFile,
1951    module: &str,
1952    seeds: &BTreeSet<(ProjectFile, String)>,
1953) -> bool {
1954    usage_resolve_module_files(python, file, module)
1955        .iter()
1956        .any(|resolved| seeds.iter().any(|(seed_file, _)| seed_file == resolved))
1957}
1958
1959fn record_module_binding(
1960    timeline: &mut ModuleBindingTimeline,
1961    name: &str,
1962    visible_from: usize,
1963    conditional: bool,
1964    kind: ModuleBindingEventKind,
1965) {
1966    let name = name.trim();
1967    if name.is_empty() {
1968        return;
1969    }
1970    timeline
1971        .entry(name.to_string())
1972        .or_default()
1973        .push(ModuleBindingEvent {
1974            visible_from,
1975            conditional,
1976            kind,
1977        });
1978}
1979
1980fn record_local_binding_targets(
1981    target: Node<'_>,
1982    source: &str,
1983    visible_from: usize,
1984    conditional: bool,
1985    timeline: &mut ModuleBindingTimeline,
1986) {
1987    let mut stack = vec![target];
1988    while let Some(node) = stack.pop() {
1989        if node.kind() == "identifier" {
1990            record_module_binding(
1991                timeline,
1992                slice(node, source),
1993                visible_from,
1994                conditional,
1995                ModuleBindingEventKind::Other,
1996            );
1997            continue;
1998        }
1999        if matches!(node.kind(), "attribute" | "subscript") {
2000            continue;
2001        }
2002        let mut cursor = node.walk();
2003        let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2004        children.reverse();
2005        stack.extend(children);
2006    }
2007}
2008
2009fn binding_is_conditional(mut node: Node<'_>) -> bool {
2010    while let Some(parent) = node.parent() {
2011        if matches!(
2012            parent.kind(),
2013            "if_statement"
2014                | "try_statement"
2015                | "except_clause"
2016                | "match_statement"
2017                | "case_clause"
2018                | "for_statement"
2019                | "while_statement"
2020        ) {
2021            return true;
2022        }
2023        if matches!(
2024            parent.kind(),
2025            "module" | "function_definition" | "class_definition"
2026        ) {
2027            return false;
2028        }
2029        node = parent;
2030    }
2031    false
2032}
2033
2034fn first_identifier(node: Node<'_>) -> Option<Node<'_>> {
2035    identifier_extreme(node, false)
2036}
2037
2038fn last_identifier(node: Node<'_>) -> Option<Node<'_>> {
2039    identifier_extreme(node, true)
2040}
2041
2042fn identifier_extreme(node: Node<'_>, last: bool) -> Option<Node<'_>> {
2043    let mut best = None;
2044    let mut stack = vec![node];
2045    while let Some(node) = stack.pop() {
2046        if node.kind() == "identifier" {
2047            if best.is_none_or(|current: Node<'_>| {
2048                if last {
2049                    node.start_byte() > current.start_byte()
2050                } else {
2051                    node.start_byte() < current.start_byte()
2052                }
2053            }) {
2054                best = Some(node);
2055            }
2056            continue;
2057        }
2058        let mut cursor = node.walk();
2059        stack.extend(node.named_children(&mut cursor));
2060    }
2061    best
2062}
2063
2064fn reference_is_deferred_function_body(node: Node<'_>) -> bool {
2065    let site_start = node.start_byte();
2066    let site_end = node.end_byte();
2067    let mut current = node;
2068    while let Some(parent) = current.parent() {
2069        if matches!(parent.kind(), "function_definition" | "lambda")
2070            && parent
2071                .child_by_field_name("body")
2072                .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
2073        {
2074            return true;
2075        }
2076        current = parent;
2077    }
2078    false
2079}
2080
2081/// Collect the names an assignment or `for` target *binds*: plain identifiers
2082/// and identifiers nested in destructuring patterns. An `attribute`,
2083/// `subscript`, or `call` target is evaluated, not bound (`recv.member = v`
2084/// and `make().member = v` bind no local names), so its identifiers stay
2085/// references and must not shadow same-named imports or declarations (#898).
2086pub fn collect_assigned_identifiers(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
2087    let mut stack = vec![node];
2088    while let Some(node) = stack.pop() {
2089        match node.kind() {
2090            "attribute" | "subscript" | "call" => continue,
2091            "identifier" => {
2092                let text = slice(node, source).trim();
2093                if !text.is_empty() {
2094                    out.insert(text.to_string());
2095                }
2096                continue;
2097            }
2098            _ => {}
2099        }
2100
2101        let mut cursor = node.walk();
2102        let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2103        children.reverse();
2104        stack.extend(children);
2105    }
2106}
2107
2108pub fn collect_scope_facts_from_parsed_source(
2109    graph: &PythonGraphSource<'_>,
2110    python: &dyn PythonUsageSource,
2111    file: &ProjectFile,
2112    source: &str,
2113    root: Node<'_>,
2114) -> PythonScopeFacts {
2115    let mut factory_return_types = collect_factory_return_types_from_root(root, source);
2116    collect_imported_factory_return_types(graph, python, file, &mut factory_return_types);
2117    collect_scope_facts_with_factory_returns(graph, file, source, &factory_return_types)
2118}
2119
2120fn collect_imported_factory_return_types(
2121    graph: &PythonGraphSource<'_>,
2122    python: &dyn PythonUsageSource,
2123    file: &ProjectFile,
2124    factory_return_types: &mut HashMap<String, String>,
2125) {
2126    let binder = python.import_binder_of(file);
2127    for (local, binding) in &binder.bindings {
2128        if !matches!(binding.kind, ImportKind::Named) {
2129            continue;
2130        }
2131        let Some(imported) = binding.imported_name.as_deref() else {
2132            continue;
2133        };
2134        let fqn = format!("{}.{}", binding.module_specifier, imported);
2135        let units =
2136            resolve_fqn_candidates(python, &fqn, |name| graph.index.definitions(name).collect());
2137        for unit in units {
2138            if unit.is_function() {
2139                if let Some(return_type) = callable_return_type_name(graph, python, &unit) {
2140                    factory_return_types
2141                        .entry(local.clone())
2142                        .or_insert(return_type);
2143                }
2144                continue;
2145            }
2146            if !unit.is_class() {
2147                continue;
2148            }
2149            factory_return_types
2150                .entry(local.clone())
2151                .or_insert_with(|| unit.identifier().to_string());
2152            collect_imported_class_method_return_types(
2153                graph,
2154                python,
2155                local,
2156                &unit,
2157                factory_return_types,
2158            );
2159        }
2160    }
2161}
2162
2163fn collect_imported_class_method_return_types(
2164    graph: &PythonGraphSource<'_>,
2165    python: &dyn PythonSource,
2166    local_class_name: &str,
2167    class_unit: &CodeUnit,
2168    factory_return_types: &mut HashMap<String, String>,
2169) {
2170    for member in graph.index.direct_children(class_unit) {
2171        if !member.is_function() {
2172            continue;
2173        }
2174        let Some(return_type) = callable_return_type_name(graph, python, &member) else {
2175            continue;
2176        };
2177        factory_return_types
2178            .entry(format!("{}.{}", local_class_name, member.identifier()))
2179            .or_insert(return_type);
2180    }
2181}
2182
2183fn callable_return_type_name(
2184    graph: &PythonGraphSource<'_>,
2185    python: &dyn PythonSource,
2186    callable: &CodeUnit,
2187) -> Option<String> {
2188    // The analyzer's already-parsed whole-file tree when it has one. This runs
2189    // once per imported class member, and the fallback below clones the entire
2190    // file source and builds a fresh `Parser` per declaration range. Same
2191    // prepared-syntax fast path C++ resolution uses for the same reason.
2192    if let Some(prepared) = python.prepared_syntax(graph.token, callable.source()) {
2193        #[cfg(any(test, feature = "test-support"))]
2194        note_callable_return_type_lookup_for_test(true);
2195        return callable_return_type_name_in_tree(
2196            graph,
2197            callable,
2198            prepared.source(),
2199            prepared.tree().root_node(),
2200        );
2201    }
2202    #[cfg(any(test, feature = "test-support"))]
2203    note_callable_return_type_lookup_for_test(false);
2204    let source = graph.index.indexed_source(callable.source())?;
2205    declaration_source_slices(graph, callable, &source)
2206        .into_iter()
2207        .find_map(|declaration_source| {
2208            let mut parser = Parser::new();
2209            parser
2210                .set_language(&tree_sitter_python::LANGUAGE.into())
2211                .ok()?;
2212            let tree = parser.parse(declaration_source, None)?;
2213            let function = first_function_definition(tree.root_node())?;
2214            factory_return_type(function, declaration_source)
2215        })
2216}
2217
2218/// How `callable_return_type_name` answered, counted per arm.
2219///
2220/// Both arms are counted, not just the slow one: a test that asserted only
2221/// "no reparses" would pass vacuously on a fixture that never reaches this
2222/// function at all.
2223#[cfg(any(test, feature = "test-support"))]
2224#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
2225pub struct CallableReturnTypeLookupCounts {
2226    /// Answered from the analyzer's already-parsed whole-file tree.
2227    pub prepared: usize,
2228    /// Fell back to cloning the file source and building a fresh parser.
2229    pub reparsed: usize,
2230}
2231
2232#[cfg(any(test, feature = "test-support"))]
2233thread_local! {
2234    static CALLABLE_RETURN_TYPE_LOOKUPS_FOR_TEST: std::cell::Cell<CallableReturnTypeLookupCounts> =
2235        const { std::cell::Cell::new(CallableReturnTypeLookupCounts { prepared: 0, reparsed: 0 }) };
2236}
2237
2238#[cfg(any(test, feature = "test-support"))]
2239fn note_callable_return_type_lookup_for_test(from_prepared_syntax: bool) {
2240    CALLABLE_RETURN_TYPE_LOOKUPS_FOR_TEST.with(|counts| {
2241        let mut observed = counts.get();
2242        if from_prepared_syntax {
2243            observed.prepared += 1;
2244        } else {
2245            observed.reparsed += 1;
2246        }
2247        counts.set(observed);
2248    });
2249}
2250
2251/// Runs `body` and reports which arm each `callable_return_type_name` call
2252/// took. An analyzer that holds prepared syntax must report zero reparses.
2253#[cfg(any(test, feature = "test-support"))]
2254pub fn with_callable_return_type_lookup_counter_for_test<T>(
2255    body: impl FnOnce() -> T,
2256) -> (T, CallableReturnTypeLookupCounts) {
2257    CALLABLE_RETURN_TYPE_LOOKUPS_FOR_TEST.with(|counts| {
2258        counts.set(CallableReturnTypeLookupCounts::default());
2259        let result = body();
2260        let observed = counts.get();
2261        counts.set(CallableReturnTypeLookupCounts::default());
2262        (result, observed)
2263    })
2264}
2265
2266/// The declaration walk both arms of [`callable_return_type_name`] share.
2267///
2268/// `source` and `root` must be the same snapshot: `factory_return_type` slices
2269/// `source` at the node's byte offsets, so a whole-file tree needs whole-file
2270/// source and a per-range reparse needs that range's slice.
2271fn callable_return_type_name_in_tree(
2272    graph: &PythonGraphSource<'_>,
2273    callable: &CodeUnit,
2274    source: &str,
2275    root: Node<'_>,
2276) -> Option<String> {
2277    let mut ranges = graph.index.ranges(callable);
2278    ranges.sort_by_key(|range| range.start_byte);
2279    ranges.into_iter().find_map(|range| {
2280        let declaration = root.descendant_for_byte_range(range.start_byte, range.end_byte)?;
2281        let function = first_function_definition(declaration)?;
2282        factory_return_type(function, source)
2283    })
2284}
2285
2286fn factory_function_key(graph: &PythonGraphSource<'_>, callable: &CodeUnit) -> String {
2287    graph
2288        .index
2289        .parent_of(callable)
2290        .filter(CodeUnit::is_class)
2291        .map(|owner| format!("{}.{}", owner.identifier(), callable.identifier()))
2292        .unwrap_or_else(|| callable.identifier().to_string())
2293}
2294
2295fn first_function_definition(root: Node<'_>) -> Option<Node<'_>> {
2296    let mut stack = vec![root];
2297    while let Some(node) = stack.pop() {
2298        if node.kind() == "function_definition" {
2299            return Some(node);
2300        }
2301        let mut cursor = node.walk();
2302        let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2303        children.reverse();
2304        stack.extend(children);
2305    }
2306    None
2307}
2308
2309fn collect_scope_facts_with_factory_returns(
2310    graph: &PythonGraphSource<'_>,
2311    file: &ProjectFile,
2312    source: &str,
2313    factory_return_types: &HashMap<String, String>,
2314) -> PythonScopeFacts {
2315    let declarations = graph.index.declarations(file);
2316    let mut class_facts_by_name: HashMap<String, LocalBindingsSnapshot<String>> =
2317        HashMap::default();
2318    for declaration in declarations
2319        .iter()
2320        .filter(|declaration| declaration.is_class())
2321    {
2322        let Some(declaration_source) = declaration_source(graph, declaration, source) else {
2323            continue;
2324        };
2325        let facts = collect_scope_facts_from_source(
2326            &declaration_source,
2327            ScopeFactTraversal::Class,
2328            true,
2329            Some(declaration.short_name()),
2330            factory_return_types,
2331        );
2332        class_facts_by_name.insert(
2333            declaration.short_name().to_string(),
2334            facts.filtered_visible_bindings(|symbol, _| symbol.starts_with("self.")),
2335        );
2336    }
2337
2338    let mut scope_facts = HashMap::default();
2339    for declaration in declarations
2340        .iter()
2341        .filter(|declaration| declaration.is_function())
2342    {
2343        let Some(declaration_source) = declaration_source(graph, declaration, source) else {
2344            continue;
2345        };
2346        // fqname-M4: package-less short_name owner, matched below against
2347        // `class_facts_by_name` keys built from `short_name()`; `fq.parent()`
2348        // (`default_parent_fq_name`) would render the package-qualified owner,
2349        // a different string that would never hit in that map.
2350        let owner = declaration
2351            .short_name()
2352            .rsplit_once('.')
2353            .map(|(owner, _)| owner);
2354        let mut facts = collect_scope_facts_from_source(
2355            &declaration_source,
2356            ScopeFactTraversal::Function,
2357            false,
2358            owner,
2359            factory_return_types,
2360        );
2361        if let Some(owner) = owner
2362            && let Some(class_facts) = class_facts_by_name.get(owner)
2363        {
2364            facts = facts.merged_with_visible(class_facts);
2365        }
2366        scope_facts.insert(declaration.clone(), facts);
2367    }
2368
2369    // Module-level statements (e.g. a top-level `f = Foo()`) form their own scope.
2370    // `enclosing_code_unit` resolves a top-level usage to the module CodeUnit, so
2371    // its bindings must be recorded too, otherwise constructed-local receivers
2372    // used at module scope resolve to no type.
2373    for declaration in declarations.iter().filter(|d| d.is_module()) {
2374        let Some(declaration_source) = declaration_source(graph, declaration, source) else {
2375            continue;
2376        };
2377        let facts = collect_scope_facts_from_source(
2378            &declaration_source,
2379            ScopeFactTraversal::Module,
2380            false,
2381            None,
2382            factory_return_types,
2383        );
2384        scope_facts.insert(declaration.clone(), facts);
2385    }
2386    scope_facts
2387}
2388
2389fn declaration_source(
2390    graph: &PythonGraphSource<'_>,
2391    declaration: &CodeUnit,
2392    file_source: &str,
2393) -> Option<String> {
2394    let slices = declaration_source_slices(graph, declaration, file_source);
2395    (!slices.is_empty()).then(|| slices.join("\n\n"))
2396}
2397
2398fn declaration_source_slices<'a>(
2399    graph: &PythonGraphSource<'_>,
2400    declaration: &CodeUnit,
2401    file_source: &'a str,
2402) -> Vec<&'a str> {
2403    let mut ranges = graph.index.ranges(declaration);
2404    ranges.sort_by_key(|range| range.start_byte);
2405    ranges
2406        .into_iter()
2407        .filter_map(|range| file_source.get(range.start_byte..range.end_byte))
2408        .collect()
2409}
2410
2411fn collect_scope_facts_from_source(
2412    source: &str,
2413    traversal: ScopeFactTraversal,
2414    allow_self_receivers: bool,
2415    current_class: Option<&str>,
2416    factory_return_types: &HashMap<String, String>,
2417) -> LocalBindingsSnapshot<String> {
2418    let events = collect_scope_fact_events(source, traversal);
2419    collect_scope_facts_from_events(
2420        &events,
2421        allow_self_receivers,
2422        current_class,
2423        factory_return_types,
2424    )
2425}
2426
2427pub fn collect_function_scope_facts_from_node(
2428    function: Node<'_>,
2429    source: &str,
2430) -> LocalBindingsSnapshot<String> {
2431    let mut events = Vec::new();
2432    if function.kind() == "lambda" {
2433        if let Some(parameters) = function.child_by_field_name("parameters") {
2434            collect_parameter_events(parameters, source, &mut events);
2435        }
2436    } else {
2437        collect_scope_fact_events_from_node(
2438            function,
2439            source,
2440            ScopeFactTraversal::Function,
2441            &mut events,
2442        );
2443    }
2444    collect_scope_facts_from_events(&events, false, None, &HashMap::default())
2445}
2446
2447fn collect_scope_facts_from_events(
2448    events: &[ScopeFactEvent],
2449    allow_self_receivers: bool,
2450    current_class: Option<&str>,
2451    factory_return_types: &HashMap<String, String>,
2452) -> LocalBindingsSnapshot<String> {
2453    let mut engine = LocalInferenceEngine::new(LocalInferenceConfig::default());
2454    let globals: HashSet<&str> = events
2455        .iter()
2456        .filter_map(|event| match event {
2457            ScopeFactEvent::Global { symbol } => Some(symbol.as_str()),
2458            _ => None,
2459        })
2460        .collect();
2461    let nonlocals: HashSet<&str> = events
2462        .iter()
2463        .filter_map(|event| match event {
2464            ScopeFactEvent::Nonlocal { symbol } => Some(symbol.as_str()),
2465            _ => None,
2466        })
2467        .collect();
2468    for symbol in &nonlocals {
2469        engine.declare_shadow((*symbol).to_string());
2470    }
2471    for event in events {
2472        if let ScopeFactEvent::Parameter { symbol, .. } = event
2473            && !globals.contains(symbol.as_str())
2474            && !nonlocals.contains(symbol.as_str())
2475            && !engine.is_shadowed(symbol)
2476        {
2477            engine.declare_shadow(symbol.clone());
2478        }
2479    }
2480
2481    let mut changed = true;
2482    while changed {
2483        changed = false;
2484        let mut aliases = Vec::new();
2485        for event in events {
2486            match event {
2487                ScopeFactEvent::Parameter {
2488                    symbol,
2489                    annotation: Some(annotation),
2490                }
2491                | ScopeFactEvent::Annotation { symbol, annotation } => {
2492                    if globals.contains(symbol.as_str()) || nonlocals.contains(symbol.as_str()) {
2493                        continue;
2494                    }
2495                    apply_annotation_event(
2496                        symbol,
2497                        annotation,
2498                        allow_self_receivers,
2499                        &mut engine,
2500                        &mut changed,
2501                    );
2502                }
2503                ScopeFactEvent::Parameter {
2504                    annotation: None, ..
2505                } => {}
2506                ScopeFactEvent::Assignment { lhs, rhs } => {
2507                    if globals.contains(lhs.as_str()) {
2508                        continue;
2509                    }
2510                    if !engine.is_shadowed(lhs) {
2511                        engine.declare_shadow(lhs.clone());
2512                    }
2513                    if lhs.starts_with("self.") && !allow_self_receivers {
2514                        continue;
2515                    }
2516
2517                    match rhs {
2518                        AssignmentRhs::Call(callee) => {
2519                            if !engine.is_shadowed(callee) {
2520                                if let Some(receiver_type) = factory_return_type_for_callee(
2521                                    callee,
2522                                    current_class,
2523                                    factory_return_types,
2524                                ) && engine.resolve_symbol(lhs).is_unknown()
2525                                {
2526                                    engine.seed_symbol(lhs.clone(), receiver_type.clone());
2527                                    changed = true;
2528                                    continue;
2529                                }
2530
2531                                if let Some(receiver_type) = normalized_receiver_type(callee)
2532                                    && engine.resolve_symbol(lhs).is_unknown()
2533                                {
2534                                    engine.seed_symbol(lhs.clone(), receiver_type);
2535                                    changed = true;
2536                                    continue;
2537                                }
2538                            }
2539                        }
2540                        AssignmentRhs::Symbol(rhs_symbol) => {
2541                            if !engine.is_shadowed(rhs_symbol)
2542                                && let Some(receiver_type) = normalized_receiver_type(rhs_symbol)
2543                                && engine.resolve_symbol(lhs).is_unknown()
2544                            {
2545                                engine.seed_symbol(lhs.clone(), receiver_type);
2546                                changed = true;
2547                                continue;
2548                            }
2549
2550                            if let SymbolResolution::Precise(targets) =
2551                                engine.resolve_symbol(rhs_symbol)
2552                                && !targets.is_empty()
2553                            {
2554                                aliases.push((lhs.clone(), rhs_symbol.clone()));
2555                            }
2556                        }
2557                        AssignmentRhs::Unknown => {}
2558                    }
2559                }
2560                ScopeFactEvent::Global { .. } | ScopeFactEvent::Nonlocal { .. } => {}
2561            }
2562        }
2563        let before = engine.snapshot();
2564        engine.apply_aliases_until_stable(aliases);
2565        if engine.snapshot() != before {
2566            changed = true;
2567        }
2568    }
2569
2570    engine.snapshot()
2571}
2572
2573fn factory_return_type_for_callee<'a>(
2574    callee: &str,
2575    current_class: Option<&str>,
2576    factory_return_types: &'a HashMap<String, String>,
2577) -> Option<&'a String> {
2578    if let Some(receiver_type) = factory_return_types.get(callee) {
2579        return Some(receiver_type);
2580    }
2581    let class_name = current_class?;
2582    let method = callee
2583        .strip_prefix("self.")
2584        .or_else(|| callee.strip_prefix("cls."))?;
2585    factory_return_types.get(&format!("{class_name}.{method}"))
2586}
2587
2588fn apply_annotation_event(
2589    symbol: &str,
2590    annotation: &str,
2591    allow_self_receivers: bool,
2592    engine: &mut LocalInferenceEngine<String>,
2593    changed: &mut bool,
2594) {
2595    if symbol.starts_with("self.") && !allow_self_receivers {
2596        return;
2597    }
2598    if let Some(receiver_type) = normalized_receiver_type(annotation)
2599        && engine.resolve_symbol(symbol).is_unknown()
2600    {
2601        engine.seed_symbol(symbol.to_string(), receiver_type);
2602        *changed = true;
2603    }
2604}
2605
2606enum ScopeFactEvent {
2607    Global {
2608        symbol: String,
2609    },
2610    Nonlocal {
2611        symbol: String,
2612    },
2613    Parameter {
2614        symbol: String,
2615        annotation: Option<String>,
2616    },
2617    Annotation {
2618        symbol: String,
2619        annotation: String,
2620    },
2621    Assignment {
2622        lhs: String,
2623        rhs: AssignmentRhs,
2624    },
2625}
2626
2627enum AssignmentRhs {
2628    Symbol(String),
2629    Call(String),
2630    Unknown,
2631}
2632
2633#[derive(Clone, Copy)]
2634enum ScopeFactTraversal {
2635    Module,
2636    Function,
2637    Class,
2638}
2639
2640fn collect_scope_fact_events(source: &str, traversal: ScopeFactTraversal) -> Vec<ScopeFactEvent> {
2641    if source.trim().is_empty() {
2642        return Vec::new();
2643    }
2644
2645    let mut parser = Parser::new();
2646    if parser
2647        .set_language(&tree_sitter_python::LANGUAGE.into())
2648        .is_err()
2649    {
2650        return Vec::new();
2651    }
2652    let Some(tree) = parser.parse(source, None) else {
2653        return Vec::new();
2654    };
2655
2656    let mut events = Vec::new();
2657    collect_scope_fact_events_from_node(tree.root_node(), source, traversal, &mut events);
2658    events
2659}
2660
2661fn collect_scope_fact_events_from_node(
2662    root: Node<'_>,
2663    source: &str,
2664    traversal: ScopeFactTraversal,
2665    events: &mut Vec<ScopeFactEvent>,
2666) {
2667    let mut stack = vec![(root, false)];
2668    while let Some((node, inside_function)) = stack.pop() {
2669        let next_inside_function = match traversal {
2670            ScopeFactTraversal::Module => {
2671                if matches!(
2672                    node.kind(),
2673                    "function_definition" | "class_definition" | "lambda"
2674                ) {
2675                    continue;
2676                }
2677                false
2678            }
2679            ScopeFactTraversal::Function => match node.kind() {
2680                "function_definition" if inside_function => continue,
2681                "function_definition" => true,
2682                "class_definition" | "lambda" => continue,
2683                _ => inside_function,
2684            },
2685            ScopeFactTraversal::Class => inside_function,
2686        };
2687        if matches!(traversal, ScopeFactTraversal::Function) && !next_inside_function {
2688            let mut cursor = node.walk();
2689            let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2690            children.reverse();
2691            stack.extend(children.into_iter().map(|child| (child, false)));
2692            continue;
2693        }
2694        match node.kind() {
2695            "global_statement" => collect_scope_directive_events(node, source, events, |symbol| {
2696                ScopeFactEvent::Global { symbol }
2697            }),
2698            "nonlocal_statement" => {
2699                collect_scope_directive_events(node, source, events, |symbol| {
2700                    ScopeFactEvent::Nonlocal { symbol }
2701                })
2702            }
2703            "parameters" | "lambda_parameters" => collect_parameter_events(node, source, events),
2704            "assignment" => collect_assignment_events(node, source, events),
2705            _ => {}
2706        }
2707
2708        let mut cursor = node.walk();
2709        let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2710        children.reverse();
2711        stack.extend(
2712            children
2713                .into_iter()
2714                .map(|child| (child, next_inside_function)),
2715        );
2716    }
2717}
2718
2719fn collect_scope_directive_events(
2720    node: Node<'_>,
2721    source: &str,
2722    events: &mut Vec<ScopeFactEvent>,
2723    make_event: impl Fn(String) -> ScopeFactEvent,
2724) {
2725    let mut cursor = node.walk();
2726    for identifier in node
2727        .named_children(&mut cursor)
2728        .filter(|child| child.kind() == "identifier")
2729    {
2730        let Some(symbol) = non_empty_node_text(identifier, source) else {
2731            continue;
2732        };
2733        events.push(make_event(symbol));
2734    }
2735}
2736
2737fn collect_parameter_events(node: Node<'_>, source: &str, events: &mut Vec<ScopeFactEvent>) {
2738    let mut cursor = node.walk();
2739    for child in node.named_children(&mut cursor) {
2740        if child.kind() == "type_parameter" {
2741            continue;
2742        }
2743        let Some(symbol) = parameter_symbol(child, source) else {
2744            continue;
2745        };
2746        if matches!(symbol.as_str(), "self" | "cls" | "/") {
2747            continue;
2748        }
2749        let annotation = child
2750            .child_by_field_name("type")
2751            .map(|annotation| slice(annotation, source).trim().to_string())
2752            .filter(|annotation| !annotation.is_empty());
2753        events.push(ScopeFactEvent::Parameter { symbol, annotation });
2754    }
2755}
2756
2757fn parameter_symbol(node: Node<'_>, source: &str) -> Option<String> {
2758    if node.kind() == "identifier" {
2759        return non_empty_node_text(node, source);
2760    }
2761    if let Some(name) = node.child_by_field_name("name") {
2762        return non_empty_node_text(name, source);
2763    }
2764    let mut cursor = node.walk();
2765    node.named_children(&mut cursor)
2766        .find(|child| child.kind() == "identifier")
2767        .and_then(|identifier| non_empty_node_text(identifier, source))
2768}
2769
2770fn collect_assignment_events(node: Node<'_>, source: &str, events: &mut Vec<ScopeFactEvent>) {
2771    let Some(left) = node.child_by_field_name("left") else {
2772        return;
2773    };
2774    let Some(lhs) = receiver_symbol(left, source) else {
2775        return;
2776    };
2777
2778    if let Some(annotation) = node
2779        .child_by_field_name("type")
2780        .map(|annotation| slice(annotation, source).trim().to_string())
2781        .filter(|annotation| !annotation.is_empty())
2782    {
2783        events.push(ScopeFactEvent::Annotation {
2784            symbol: lhs,
2785            annotation,
2786        });
2787        return;
2788    }
2789
2790    let rhs = node
2791        .child_by_field_name("right")
2792        .and_then(|right| rhs_symbol(right, source))
2793        .unwrap_or(AssignmentRhs::Unknown);
2794    events.push(ScopeFactEvent::Assignment { lhs, rhs });
2795}
2796
2797fn receiver_symbol(node: Node<'_>, source: &str) -> Option<String> {
2798    match node.kind() {
2799        "identifier" | "attribute" => non_empty_node_text(node, source),
2800        _ => None,
2801    }
2802}
2803
2804fn rhs_symbol(node: Node<'_>, source: &str) -> Option<AssignmentRhs> {
2805    match node.kind() {
2806        "identifier" | "attribute" => non_empty_node_text(node, source).map(AssignmentRhs::Symbol),
2807        "call" => node
2808            .child_by_field_name("function")
2809            .or_else(|| node.named_child(0))
2810            .and_then(|callee| receiver_symbol(callee, source))
2811            .map(AssignmentRhs::Call),
2812        _ => None,
2813    }
2814}
2815
2816fn non_empty_node_text(node: Node<'_>, source: &str) -> Option<String> {
2817    let text = slice(node, source).trim();
2818    (!text.is_empty()).then(|| text.to_string())
2819}
2820
2821fn collect_factory_return_types_from_root(root: Node<'_>, source: &str) -> HashMap<String, String> {
2822    let mut returns = HashMap::default();
2823    let mut functions = Vec::new();
2824    let mut stack = vec![(root, None::<String>)];
2825    while let Some((node, class_name)) = stack.pop() {
2826        match node.kind() {
2827            "class_definition" => {
2828                let next_class = node
2829                    .child_by_field_name("name")
2830                    .and_then(|name| non_empty_node_text(name, source))
2831                    .or(class_name);
2832                push_factory_index_children(node, next_class, &mut stack);
2833            }
2834            "function_definition" => {
2835                if let Some(name) = node
2836                    .child_by_field_name("name")
2837                    .and_then(|name| non_empty_node_text(name, source))
2838                {
2839                    let key = class_name
2840                        .as_ref()
2841                        .map(|class| format!("{class}.{name}"))
2842                        .unwrap_or(name);
2843                    if let Some(return_type) = factory_return_type(node, source) {
2844                        returns.insert(key.clone(), return_type);
2845                    }
2846                    functions.push((key, class_name, node));
2847                }
2848            }
2849            _ => push_factory_index_children(node, class_name, &mut stack),
2850        }
2851    }
2852    // A factory can delegate one branch to another local factory and construct
2853    // the same class directly on another branch. Canonicalize those AST-derived
2854    // return callees to a stable terminal type before deciding the branches
2855    // conflict. Iteration is bounded by the number of functions; cycles retain
2856    // the conservative raw result instead of recursing.
2857    for _ in 0..functions.len() {
2858        let mut changed = false;
2859        for (key, class_name, function) in &functions {
2860            let Some(return_type) =
2861                factory_return_type_with_known(*function, source, class_name.as_deref(), &returns)
2862            else {
2863                continue;
2864            };
2865            if returns.get(key) != Some(&return_type) {
2866                returns.insert(key.clone(), return_type);
2867                changed = true;
2868            }
2869        }
2870        if !changed {
2871            break;
2872        }
2873    }
2874    returns
2875}
2876
2877fn push_factory_index_children<'tree>(
2878    node: Node<'tree>,
2879    class_name: Option<String>,
2880    stack: &mut Vec<(Node<'tree>, Option<String>)>,
2881) {
2882    let mut cursor = node.walk();
2883    let mut children: Vec<Node<'tree>> = node.named_children(&mut cursor).collect();
2884    children.reverse();
2885    stack.extend(
2886        children
2887            .into_iter()
2888            .map(|child| (child, class_name.clone())),
2889    );
2890}
2891
2892fn factory_return_type(function: Node<'_>, source: &str) -> Option<String> {
2893    factory_return_type_with_known(function, source, None, &HashMap::default())
2894}
2895
2896fn factory_return_type_with_known(
2897    function: Node<'_>,
2898    source: &str,
2899    current_class: Option<&str>,
2900    known: &HashMap<String, String>,
2901) -> Option<String> {
2902    if let Some(return_type) = function.child_by_field_name("return_type") {
2903        return receiver_type_from_annotation_node(return_type, source);
2904    }
2905
2906    let body = function.child_by_field_name("body")?;
2907    let mut candidates = HashSet::default();
2908    let mut saw_return = false;
2909    let mut saw_unknown_return = false;
2910    let mut stack = vec![body];
2911    while let Some(node) = stack.pop() {
2912        if node != body && matches!(node.kind(), "function_definition" | "class_definition") {
2913            continue;
2914        }
2915        if node.kind() == "return_statement" {
2916            saw_return = true;
2917            match node
2918                .named_child(0)
2919                .and_then(|value| returned_receiver_type(value, source))
2920            {
2921                Some(returned_type) => {
2922                    candidates.insert(
2923                        canonical_factory_return(&returned_type, current_class, known)
2924                            .unwrap_or(returned_type),
2925                    );
2926                }
2927                None => saw_unknown_return = true,
2928            }
2929        }
2930        let mut cursor = node.walk();
2931        let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2932        children.reverse();
2933        stack.extend(children);
2934    }
2935    if !saw_return || saw_unknown_return {
2936        return None;
2937    }
2938    (candidates.len() == 1)
2939        .then(|| candidates.into_iter().next())
2940        .flatten()
2941}
2942
2943fn canonical_factory_return(
2944    raw: &str,
2945    current_class: Option<&str>,
2946    known: &HashMap<String, String>,
2947) -> Option<String> {
2948    let mut current = raw;
2949    let mut seen = HashSet::default();
2950    while let Some(next) = factory_return_type_for_callee(current, current_class, known) {
2951        if !seen.insert(current.to_string()) {
2952            return None;
2953        }
2954        current = next;
2955    }
2956    Some(current.to_string())
2957}
2958
2959/// Return the runtime class named by a structured Python return annotation.
2960///
2961/// For `Manager[A, B]`, the constructed class is the subscript base `Manager`.
2962/// `Optional[T]` is different: it denotes `T | None`, so retain the existing
2963/// supported-wrapper behavior and inspect its structured type argument.
2964fn receiver_type_from_annotation_node(annotation: Node<'_>, source: &str) -> Option<String> {
2965    match annotation.kind() {
2966        "type" => receiver_type_from_annotation_node(annotation.named_child(0)?, source),
2967        "identifier" | "attribute" | "member_type" | "string" => {
2968            normalized_receiver_type(slice(annotation, source).trim())
2969        }
2970        "generic_type" => {
2971            let base = annotation.named_child(0)?;
2972            if optional_annotation_wrapper(base, source) {
2973                let parameter = annotation.named_child(1)?;
2974                return receiver_type_from_annotation_node(parameter.named_child(0)?, source);
2975            }
2976            receiver_type_from_annotation_node(base, source)
2977        }
2978        "subscript" => {
2979            let value = annotation.child_by_field_name("value")?;
2980            if optional_annotation_wrapper(value, source) {
2981                let inner = annotation.child_by_field_name("subscript")?;
2982                return receiver_type_from_annotation_node(inner, source);
2983            }
2984            normalized_receiver_type(slice(value, source).trim())
2985        }
2986        _ => None,
2987    }
2988}
2989
2990fn optional_annotation_wrapper(node: Node<'_>, source: &str) -> bool {
2991    match node.kind() {
2992        "identifier" => slice(node, source) == "Optional",
2993        "attribute" => {
2994            let (Some(object), Some(attribute)) = (
2995                node.child_by_field_name("object"),
2996                node.child_by_field_name("attribute"),
2997            ) else {
2998                return false;
2999            };
3000            object.kind() == "identifier"
3001                && attribute.kind() == "identifier"
3002                && slice(object, source) == "typing"
3003                && slice(attribute, source) == "Optional"
3004        }
3005        _ => false,
3006    }
3007}
3008
3009fn returned_receiver_type(node: Node<'_>, source: &str) -> Option<String> {
3010    let raw = match node.kind() {
3011        "identifier" => non_empty_node_text(node, source),
3012        "call" => node
3013            .child_by_field_name("function")
3014            .or_else(|| node.named_child(0))
3015            .filter(|callee| callee.kind() == "identifier")
3016            .and_then(|callee| non_empty_node_text(callee, source)),
3017        _ => None,
3018    }?;
3019    normalized_receiver_type(&raw)
3020}
3021
3022#[cfg(test)]
3023mod tests {
3024    use super::*;
3025    use std::path::PathBuf;
3026
3027    #[test]
3028    fn lambda_scope_facts_preserve_untyped_parameter_shadowing() {
3029        let source = "shadowed = lambda method: method.signature\n";
3030        let mut parser = Parser::new();
3031        parser
3032            .set_language(&tree_sitter_python::LANGUAGE.into())
3033            .unwrap();
3034        let tree = parser.parse(source, None).unwrap();
3035        let mut nodes = vec![tree.root_node()];
3036        let lambda = loop {
3037            let node = nodes.pop().unwrap();
3038            if node.kind() == "lambda" {
3039                break node;
3040            }
3041            let mut cursor = node.walk();
3042            nodes.extend(node.named_children(&mut cursor));
3043        };
3044
3045        let facts = collect_function_scope_facts_from_node(lambda, source);
3046
3047        assert!(facts.is_shadowed("method"));
3048        assert!(facts.resolution_for("method").is_unknown());
3049    }
3050
3051    #[test]
3052    fn pre_cancelled_graph_build_skips_python_file_parsing() {
3053        let temp = tempfile::tempdir().unwrap();
3054        let root = temp.path().canonicalize().unwrap();
3055        std::fs::write(root.join("target.py"), "def target():\n    pass\n").unwrap();
3056        let file = ProjectFile::new(root.clone(), PathBuf::from("target.py"));
3057        let files = [file.clone()].into_iter().collect();
3058        let cancellation = CancellationToken::default();
3059        cancellation.cancel();
3060
3061        let graph = build_python_graph(&files, &file, Some(&cancellation));
3062
3063        assert!(graph.parsed.is_empty());
3064    }
3065
3066    #[test]
3067    fn graph_build_parses_only_candidates_and_target_not_transitive_imports() {
3068        let temp = tempfile::tempdir().unwrap();
3069        let root = temp.path().canonicalize().unwrap();
3070        std::fs::write(root.join("target.py"), "from dependency import value\n").unwrap();
3071        std::fs::write(
3072            root.join("candidate.py"),
3073            "from transitively_imported import value\n",
3074        )
3075        .unwrap();
3076        std::fs::write(root.join("dependency.py"), "value = 1\n").unwrap();
3077        std::fs::write(root.join("transitively_imported.py"), "value = 2\n").unwrap();
3078        let target = ProjectFile::new(root.clone(), PathBuf::from("target.py"));
3079        let candidate = ProjectFile::new(root.clone(), PathBuf::from("candidate.py"));
3080        let dependency = ProjectFile::new(root.clone(), PathBuf::from("dependency.py"));
3081        let transitive = ProjectFile::new(root.clone(), PathBuf::from("transitively_imported.py"));
3082        let candidates = [candidate.clone()].into_iter().collect();
3083
3084        let graph = build_python_graph(&candidates, &target, None);
3085
3086        assert_eq!(graph.parsed.len(), 2);
3087        assert!(graph.parsed.contains_key(&target));
3088        assert!(graph.parsed.contains_key(&candidate));
3089        assert!(!graph.parsed.contains_key(&dependency));
3090        assert!(!graph.parsed.contains_key(&transitive));
3091    }
3092}