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