Skip to main content

brokk_bifrost_python/graph/
extractor.rs

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