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