1use crate::graph::PythonGraphSource;
6use crate::graph::hits::{
7 record_hit, record_import_hit, record_self_receiver_hit, record_unproven_hit,
8};
9use crate::graph::resolver::{
10 annotation_reference_candidates, member_name, normalized_receiver_type,
11 receiver_annotation_matches_target, resolve_constructor_types, resolve_receiver_type,
12 target_owner_code_unit, top_level_identifier,
13};
14use crate::graph_support::{PythonSource, PythonUsageSource};
15use crate::imports::resolve_fqn_candidates;
16use crate::usage_index::{
17 ModuleBindingEvent, ModuleBindingEventKind, ModuleBindingTimeline, PythonScopeFacts,
18 usage_matching_edges, usage_module_binding_timeline, usage_resolve_module_files,
19 usage_scope_facts,
20};
21use brokk_bifrost_core::analyzer::usages::local_inference::{
22 LocalBindingsSnapshot, LocalInferenceConfig, LocalInferenceEngine, SymbolResolution,
23};
24use brokk_bifrost_core::analyzer::usages::model::{ImportKind, UsageHit};
25use brokk_bifrost_core::analyzer::usages::{ImportEdge, ImportEdgeKind};
26use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, ProjectFile, Range};
27use brokk_bifrost_core::cancellation::CancellationToken;
28use brokk_bifrost_core::hash::{HashMap, HashSet};
29use brokk_bifrost_core::text_utils::compute_line_starts;
30use rayon::prelude::*;
31use std::collections::BTreeSet;
32use std::sync::{Arc, Mutex};
33use tree_sitter::{Node, Parser, Tree};
34
35pub struct ParsedFile {
36 pub source: Arc<String>,
37 pub tree: Tree,
38}
39
40pub struct PythonProjectGraph {
41 parsed: HashMap<ProjectFile, ParsedFile>,
42}
43
44impl PythonProjectGraph {
45 pub fn scan_files(
46 &self,
47 candidate_files: &HashSet<ProjectFile>,
48 target_file: &ProjectFile,
49 ) -> HashSet<ProjectFile> {
50 candidate_files
51 .iter()
52 .cloned()
53 .chain(std::iter::once(target_file.clone()))
54 .collect()
55 }
56}
57
58pub fn build_python_graph(
59 candidate_files: &HashSet<ProjectFile>,
60 target_file: &ProjectFile,
61 cancellation: Option<&CancellationToken>,
62) -> PythonProjectGraph {
63 let parser_language = tree_sitter_python::LANGUAGE.into();
64 let files: HashSet<ProjectFile> = candidate_files
65 .iter()
66 .cloned()
67 .chain(std::iter::once(target_file.clone()))
68 .collect();
69 let mut parsed = HashMap::default();
70
71 for file in files {
72 if cancellation.is_some_and(CancellationToken::is_cancelled) {
73 break;
74 }
75 let Ok(source) = file.read_to_string() else {
76 continue;
77 };
78 if cancellation.is_some_and(CancellationToken::is_cancelled) {
79 break;
80 }
81 if source.is_empty() {
82 continue;
83 }
84 let mut parser = Parser::new();
85 if parser.set_language(&parser_language).is_err() {
86 continue;
87 }
88 let Some(tree) = parser.parse(source.as_str(), None) else {
89 continue;
90 };
91 if cancellation.is_some_and(CancellationToken::is_cancelled) {
92 break;
93 }
94 parsed.insert(
95 file,
96 ParsedFile {
97 source: Arc::new(source),
98 tree,
99 },
100 );
101 }
102
103 PythonProjectGraph { parsed }
104}
105
106pub fn scan_files_for_seeds(
107 graph: &PythonGraphSource<'_>,
108 python: &dyn PythonUsageSource,
109 project_graph: &PythonProjectGraph,
110 files: &HashSet<ProjectFile>,
111 target: &CodeUnit,
112 seeds: &BTreeSet<(ProjectFile, String)>,
113 cancellation: Option<&CancellationToken>,
114) -> ScanResult {
115 let collected: Mutex<BTreeSet<UsageHit>> = Mutex::new(BTreeSet::new());
116 let unproven_collected: Mutex<BTreeSet<UsageHit>> = Mutex::new(BTreeSet::new());
117 let target_short = top_level_identifier(graph.index, target);
118 let target_member = member_name(graph.index, target);
119 let target_owner = target_owner_code_unit(graph.index, target);
120 let member_unique_in_target_file = target_member.as_deref().is_some_and(|member| {
124 let owners: HashSet<CodeUnit> = graph
125 .index
126 .declarations(target.source())
127 .into_iter()
128 .filter(|decl| {
129 decl.identifier() == member && target_owner_code_unit(graph.index, decl).is_some()
130 })
131 .filter_map(|decl| target_owner_code_unit(graph.index, &decl))
132 .collect();
133 owners.len() == 1
134 });
135 let files_vec: Vec<&ProjectFile> = files.iter().collect();
136 let parser_language = tree_sitter_python::LANGUAGE.into();
137
138 files_vec.par_iter().for_each(|file| {
139 if cancellation.is_some_and(CancellationToken::is_cancelled) {
140 return;
141 }
142 let owned_source: Option<Arc<String>>;
143 let owned_tree: Option<Tree>;
144 let (source_str, tree_ref) = if let Some(parsed) = project_graph.parsed.get(*file) {
145 (parsed.source.as_str(), &parsed.tree)
146 } else {
147 let Ok(source) = file.read_to_string() else {
148 return;
149 };
150 if source.is_empty() {
151 return;
152 }
153 let mut parser = Parser::new();
154 if parser.set_language(&parser_language).is_err() {
155 return;
156 }
157 let Some(tree) = parser.parse(source.as_str(), None) else {
158 return;
159 };
160 owned_source = Some(Arc::new(source));
161 owned_tree = Some(tree);
162 (
163 owned_source.as_deref().unwrap().as_str(),
164 owned_tree.as_ref().unwrap(),
165 )
166 };
167 if cancellation.is_some_and(CancellationToken::is_cancelled) {
168 return;
169 }
170
171 let edges = {
172 let _scope = brokk_bifrost_core::profiling::scope("python_graph::matching_edges");
173 usage_matching_edges(python, file, seeds)
174 };
175 if !file_may_reference_target(
180 tree_ref.root_node(),
181 source_str,
182 target,
183 target_short.as_str(),
184 target_member.as_deref(),
185 &edges,
186 ) {
187 return;
188 }
189 let module_bindings = {
190 let _scope =
191 brokk_bifrost_core::profiling::scope("python_graph::module_binding_timeline");
192 let raw_module_bindings = usage_module_binding_timeline(python, file, || {
193 collect_module_binding_timeline(tree_ref.root_node(), source_str)
194 });
195 classify_module_binding_timeline(
196 python,
197 file,
198 raw_module_bindings.as_ref(),
199 seeds,
200 &edges,
201 )
202 };
203 let target_self_file = *file == target.source();
204 let scope_facts = {
205 let _scope = brokk_bifrost_core::profiling::scope("python_graph::scope_facts");
206 usage_scope_facts(python, file, || {
207 collect_scope_facts_from_parsed_source(
208 graph,
209 python,
210 file,
211 source_str,
212 tree_ref.root_node(),
213 )
214 })
215 };
216 let scope_range_index = build_scope_range_index(graph, scope_facts.as_ref());
217
218 let mut local_hits = BTreeSet::new();
219 let mut local_unproven_hits = BTreeSet::new();
220 let line_starts = compute_line_starts(source_str);
221
222 let mut scan_ctx = ScanCtx {
223 python,
224 file,
225 source: source_str,
226 line_starts: &line_starts,
227 graph,
228 target,
229 target_short: &target_short,
230 target_member: target_member.as_deref(),
231 target_owner: target_owner.clone(),
232 target_is_module: target.is_module(),
233 target_source: target.source(),
234 seeds,
235 edges: &edges,
236 target_self_file,
237 member_best_effort_unique: target_self_file && member_unique_in_target_file,
238 module_bindings: &module_bindings,
239 scope_facts: scope_facts.as_ref(),
240 scope_range_index: &scope_range_index,
241 hits: &mut local_hits,
242 unproven_hits: &mut local_unproven_hits,
243 };
244
245 {
246 let _scope = brokk_bifrost_core::profiling::scope("python_graph::scan_tree");
247 scan_node(tree_ref.root_node(), &mut scan_ctx);
248 }
249
250 if !local_hits.is_empty() {
251 let mut sink = collected
252 .lock()
253 .expect("usage hit collector mutex poisoned");
254 sink.extend(local_hits);
255 }
256 if !local_unproven_hits.is_empty() {
257 let mut sink = unproven_collected
258 .lock()
259 .expect("usage unproven hit collector mutex poisoned");
260 sink.extend(local_unproven_hits);
261 }
262 });
263
264 ScanResult {
265 hits: collected
266 .into_inner()
267 .expect("usage hit collector mutex poisoned"),
268 unproven_hits: unproven_collected
269 .into_inner()
270 .expect("usage unproven hit collector mutex poisoned"),
271 }
272}
273
274fn file_may_reference_target(
275 root: Node<'_>,
276 source: &str,
277 target: &CodeUnit,
278 target_short: &str,
279 target_member: Option<&str>,
280 edges: &[ImportEdge],
281) -> bool {
282 if target.is_module() {
283 return true;
284 }
285
286 let mut stack = vec![root];
287 while let Some(node) = stack.pop() {
288 if node.kind() == "string" {
289 return true;
293 }
294 if node.kind() == "identifier" {
295 let name = slice(node, source);
296 if name == target_short
297 || target_member.is_some_and(|member| name == member)
298 || edges.iter().any(|edge| edge.local_name == name)
299 {
300 return true;
301 }
302 }
303
304 let mut cursor = node.walk();
305 stack.extend(node.named_children(&mut cursor));
306 }
307 false
308}
309
310pub struct ScanResult {
311 pub hits: BTreeSet<UsageHit>,
312 pub unproven_hits: BTreeSet<UsageHit>,
313}
314
315pub struct ScanCtx<'a> {
316 python: &'a dyn PythonUsageSource,
317 pub file: &'a ProjectFile,
318 pub source: &'a str,
319 pub line_starts: &'a [usize],
320 pub graph: &'a PythonGraphSource<'a>,
321 target: &'a CodeUnit,
322 target_short: &'a str,
323 target_member: Option<&'a str>,
324 target_owner: Option<CodeUnit>,
325 target_is_module: bool,
326 target_source: &'a ProjectFile,
327 seeds: &'a BTreeSet<(ProjectFile, String)>,
328 edges: &'a [ImportEdge],
329 target_self_file: bool,
330 member_best_effort_unique: bool,
336 module_bindings: &'a HashMap<String, Vec<ClassifiedModuleBindingEvent>>,
337 scope_facts: &'a HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
338 scope_range_index: &'a [ScopeRangeEntry],
339 pub hits: &'a mut BTreeSet<UsageHit>,
340 pub unproven_hits: &'a mut BTreeSet<UsageHit>,
341}
342
343struct ScopeRangeEntry {
344 range: Range,
345 scope: CodeUnit,
346 prefix_max_end: usize,
347}
348
349fn build_scope_range_index(
350 graph: &PythonGraphSource<'_>,
351 scope_facts: &HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
352) -> Vec<ScopeRangeEntry> {
353 let mut entries = scope_facts
354 .keys()
355 .flat_map(|scope| {
356 graph
357 .index
358 .ranges(scope)
359 .into_iter()
360 .map(|range| ScopeRangeEntry {
361 range,
362 scope: scope.clone(),
363 prefix_max_end: 0,
364 })
365 })
366 .collect::<Vec<_>>();
367 entries.sort_by(|left, right| {
368 left.range
369 .start_byte
370 .cmp(&right.range.start_byte)
371 .then_with(|| right.range.end_byte.cmp(&left.range.end_byte))
372 .then_with(|| left.scope.cmp(&right.scope))
373 });
374 let mut max_end = 0;
375 for entry in &mut entries {
376 max_end = max_end.max(entry.range.end_byte);
377 entry.prefix_max_end = max_end;
378 }
379 entries
380}
381
382fn indexed_scope_entry<'entry, 'facts>(
383 scope_range_index: &'entry [ScopeRangeEntry],
384 scope_facts: &'facts HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
385 node: Node<'_>,
386) -> Option<(&'entry CodeUnit, &'facts LocalBindingsSnapshot<String>)> {
387 let mut cursor =
388 scope_range_index.partition_point(|entry| entry.range.start_byte <= node.start_byte());
389 while cursor > 0 {
390 cursor -= 1;
391 let entry = &scope_range_index[cursor];
392 if entry.prefix_max_end < node.end_byte() {
393 return None;
394 }
395 if entry.range.end_byte >= node.end_byte() {
396 return scope_facts
397 .get(&entry.scope)
398 .map(|facts| (&entry.scope, facts));
399 }
400 }
401 None
402}
403
404pub fn enclosing_scope_facts<'a>(
408 index: &dyn CodeUnitIndex,
409 file: &ProjectFile,
410 scope_facts: &'a HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
411 node: Node<'_>,
412) -> Option<&'a LocalBindingsSnapshot<String>> {
413 let range = Range {
414 start_byte: node.start_byte(),
415 end_byte: node.end_byte(),
416 start_line: 0,
417 end_line: 0,
418 };
419 let enclosing = index.enclosing_code_unit(file, &range)?;
420 scope_facts.get(&enclosing)
421}
422
423impl ScanCtx<'_> {
424 fn scope_entry_for_node(
425 &self,
426 node: Node<'_>,
427 ) -> Option<(&CodeUnit, &LocalBindingsSnapshot<String>)> {
428 indexed_scope_entry(self.scope_range_index, self.scope_facts, node)
429 }
430
431 fn scope_facts_for_node(&self, node: Node<'_>) -> Option<&LocalBindingsSnapshot<String>> {
432 self.scope_entry_for_node(node).map(|(_, facts)| facts)
433 }
434
435 fn binds_target(&self, ident: &str, node: Node<'_>) -> bool {
436 let scope_entry = self.scope_entry_for_node(node);
437 if self.target_self_file
438 && ident == self.target_short
439 && scope_entry
440 .is_none_or(|(scope, facts)| scope.is_module() || !facts.is_shadowed(ident))
441 {
442 return true;
443 }
444 if scope_entry.is_some_and(|(scope, facts)| !scope.is_module() && facts.is_shadowed(ident))
445 {
446 return false;
447 }
448 self.module_binding_targets_query(ident, node)
449 }
450
451 fn receiver_binds_target(&self, expr: &str, node: Node<'_>) -> bool {
452 if self.binds_target(expr, node) {
453 return true;
454 }
455
456 if self.target_member.is_some() && self.import_edge_visible_for(expr, node) {
457 return true;
458 }
459
460 if matches!(expr, "self" | "cls") && self.self_receiver_matches_target(node) {
464 return true;
465 }
466
467 match enclosing_runtime_parameter_type(expr, node, self.source) {
468 EnclosingParameterType::Typed(raw_type) => {
469 return self.receiver_type_matches_target(&raw_type);
470 }
471 EnclosingParameterType::Untyped => return false,
472 EnclosingParameterType::NotDeclared => {}
473 }
474
475 let Some(scope_facts) = self.scope_facts_for_node(node) else {
476 return false;
477 };
478 let resolution = scope_facts.resolution_for(expr);
479 let Some(raw_type) = resolution
480 .as_precise()
481 .and_then(|targets| targets.iter().next())
482 else {
483 return false;
484 };
485 self.receiver_type_matches_target(raw_type)
486 }
487
488 fn node_directly_in_owner_class_body(&self, node: Node<'_>) -> bool {
494 let Some(target_owner) = self.target_owner.as_ref() else {
495 return false;
496 };
497 let range = Range {
498 start_byte: node.start_byte(),
499 end_byte: node.end_byte(),
500 start_line: 0,
501 end_line: 0,
502 };
503 let Some(enclosing) = self.graph.index.enclosing_code_unit(self.file, &range) else {
504 return false;
505 };
506 if &enclosing == target_owner {
507 return true;
508 }
509 if enclosing.is_function() {
510 return target_owner_code_unit(self.graph.index, &enclosing).as_ref()
511 == Some(target_owner)
512 && function_declaration_expression_is_class_scoped(node);
513 }
514 target_owner_code_unit(self.graph.index, &enclosing).as_ref() == Some(target_owner)
515 }
516
517 fn receiver_type_is_unknown(&self, expr: &str, node: Node<'_>) -> bool {
521 match enclosing_runtime_parameter_type(expr, node, self.source) {
522 EnclosingParameterType::Typed(_) => return false,
523 EnclosingParameterType::Untyped => return true,
524 EnclosingParameterType::NotDeclared => {}
525 }
526 match self.scope_facts_for_node(node) {
527 Some(facts) => facts.resolution_for(expr).is_unknown(),
528 None => true,
529 }
530 }
531
532 fn import_edge_visible_for(&self, ident: &str, node: Node<'_>) -> bool {
533 if let Some(scope_facts) = self.scope_facts_for_node(node)
534 && scope_facts.is_shadowed(ident)
535 {
536 return false;
537 }
538 self.module_binding_targets_query(ident, node)
539 }
540
541 fn module_binding_targets_query(&self, ident: &str, node: Node<'_>) -> bool {
542 self.module_binding_matches_query(ident, node, true, |kind| {
543 kind != ModuleBindingKind::Other
544 })
545 }
546
547 fn module_binding_targets_symbol(&self, ident: &str, node: Node<'_>) -> bool {
548 let unclassified_named_import = self.edges.iter().any(|edge| {
549 edge.local_name == ident && !matches!(edge.kind, ImportEdgeKind::Namespace)
550 });
551 self.module_binding_matches_query(ident, node, unclassified_named_import, |kind| {
552 kind == ModuleBindingKind::TargetSymbolImport
553 })
554 }
555
556 fn module_binding_matches_query(
557 &self,
558 ident: &str,
559 node: Node<'_>,
560 unclassified: bool,
561 matches: impl Fn(ModuleBindingKind) -> bool,
562 ) -> bool {
563 if !self.edges.iter().any(|edge| edge.local_name == ident) {
564 return false;
565 }
566 let Some(events) = self.module_bindings.get(ident) else {
567 return unclassified;
568 };
569 let cutoff = if reference_is_deferred_function_body(node) {
570 usize::MAX
571 } else {
572 node.start_byte()
573 };
574 let visible: Vec<_> = events
575 .iter()
576 .filter(|event| event.visible_from <= cutoff)
577 .collect();
578 let start = visible
579 .iter()
580 .rposition(|event| !event.conditional)
581 .unwrap_or(0);
582 visible[start..].iter().any(|event| matches(event.kind))
583 }
584
585 fn self_receiver_matches_target(&self, node: Node<'_>) -> bool {
589 let Some(target_owner) = self.target_owner.as_ref() else {
590 return false;
591 };
592 let range = Range {
593 start_byte: node.start_byte(),
594 end_byte: node.end_byte(),
595 start_line: 0,
596 end_line: 0,
597 };
598 let Some(enclosing) = self.graph.index.enclosing_code_unit(self.file, &range) else {
599 return false;
600 };
601 let enclosing_class = if enclosing.is_class() {
602 enclosing
603 } else {
604 match target_owner_code_unit(self.graph.index, &enclosing) {
605 Some(class) => class,
606 None => return false,
607 }
608 };
609 if &enclosing_class == target_owner {
610 return true;
611 }
612 self.graph
613 .hierarchy
614 .map(|provider| provider.get_ancestors(&enclosing_class))
615 .unwrap_or_default()
616 .into_iter()
617 .any(|ancestor| ancestor == *target_owner)
618 }
619
620 fn receiver_type_matches_target(&self, raw_type: &str) -> bool {
621 if receiver_annotation_matches_target(
622 raw_type,
623 self.edges,
624 self.target_short,
625 self.target_self_file,
626 ) {
627 return true;
628 }
629
630 let Some(target_owner) = self.target_owner.as_ref() else {
631 return false;
632 };
633 let Some(receiver_type) = resolve_receiver_type(
634 self.graph,
635 self.python,
636 self.file,
637 raw_type,
638 self.target_self_file,
639 ) else {
640 return false;
641 };
642 if &receiver_type == target_owner {
643 return true;
644 }
645 self.graph
646 .hierarchy
647 .map(|provider| provider.get_ancestors(&receiver_type))
648 .unwrap_or_default()
649 .into_iter()
650 .any(|ancestor| ancestor == *target_owner)
651 }
652}
653
654fn function_declaration_expression_is_class_scoped(node: Node<'_>) -> bool {
655 let site_start = node.start_byte();
656 let site_end = node.end_byte();
657 let mut current = node;
658 while let Some(parent) = current.parent() {
659 if parent.kind() == "function_definition" {
660 return parent.child_by_field_name("body").is_none_or(|body| {
661 !(body.start_byte() <= site_start && site_end <= body.end_byte())
662 });
663 }
664 if parent.kind() == "decorated_definition" {
665 return current.kind() == "decorator";
666 }
667 if parent.kind() == "class_definition" {
668 break;
669 }
670 current = parent;
671 }
672 false
673}
674
675fn scan_node(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
676 let mut stack = vec![node];
677 while let Some(node) = stack.pop() {
678 match node.kind() {
679 "import_statement" | "import_from_statement" => {
680 handle_import_candidate(node, ctx);
681 continue;
682 }
683 "identifier" => {
684 if handle_annotation_reference_candidate(node, ctx) {
685 continue;
686 }
687 handle_identifier_candidate(node, ctx);
688 }
689 "attribute" => {
690 if handle_annotation_reference_candidate(node, ctx) {
691 continue;
692 }
693 handle_attribute_candidate(node, ctx);
694 }
695 "string_content" => {
696 handle_annotation_reference_candidate(node, ctx);
697 }
698 "keyword_argument" => {
699 handle_keyword_argument_candidate(node, ctx);
700 if let Some(value) = node.child_by_field_name("value") {
701 stack.push(value);
702 }
703 continue;
704 }
705 _ => {}
706 }
707
708 let mut cursor = node.walk();
709 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
710 children.reverse();
711 stack.extend(children);
712 }
713}
714
715fn handle_annotation_reference_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) -> bool {
716 let Some(candidates) = annotation_reference_candidates(
717 ctx.graph,
718 ctx.python,
719 ctx.file,
720 ctx.source,
721 node,
722 ctx.target_self_file,
723 ) else {
724 return false;
725 };
726
727 if (ctx.target.is_class() || ctx.target.is_field() || ctx.target_member.is_none())
728 && candidates.iter().all(|candidate| *candidate == *ctx.target)
729 && candidates.iter().any(|candidate| *candidate == *ctx.target)
730 {
731 let site = if node.kind() == "attribute" {
732 node.child_by_field_name("attribute").unwrap_or(node)
733 } else {
734 node
735 };
736 record_hit(site, ctx);
737 }
738
739 if ctx.target_member.is_some() && node.kind() == "attribute" && candidates.is_empty() {
740 return false;
741 }
742
743 true
744}
745
746fn handle_keyword_argument_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
747 let (Some(target_member), Some(name), Some(arguments)) = (
748 ctx.target_member,
749 node.child_by_field_name("name"),
750 node.parent(),
751 ) else {
752 return;
753 };
754 if name.kind() != "identifier"
755 || slice(name, ctx.source) != target_member
756 || arguments.kind() != "argument_list"
757 {
758 return;
759 }
760 let Some(call) = arguments.parent().filter(|parent| parent.kind() == "call") else {
761 return;
762 };
763 let Some(function) = call.child_by_field_name("function") else {
764 return;
765 };
766 if function.kind() == "identifier" && slice(function, ctx.source) == "cls" {
767 if ctx.self_receiver_matches_target(function) {
768 record_hit(name, ctx);
769 }
770 return;
771 }
772 let Some(target_owner) = ctx.target_owner.as_ref() else {
773 return;
774 };
775 if let Some(root) = leftmost_identifier(function)
776 && ctx
777 .scope_facts_for_node(function)
778 .is_some_and(|facts| facts.is_shadowed(slice(root, ctx.source)))
779 {
780 return;
781 }
782 let matches = resolve_constructor_types(ctx.graph, ctx.python, ctx.file, ctx.source, function)
783 .into_iter()
784 .any(|class| {
785 &class == target_owner
786 || ctx
787 .graph
788 .hierarchy
789 .map(|provider| provider.get_ancestors(&class))
790 .unwrap_or_default()
791 .into_iter()
792 .any(|ancestor| &ancestor == target_owner)
793 });
794 if matches {
795 record_hit(name, ctx);
796 }
797}
798
799fn leftmost_identifier(mut node: Node<'_>) -> Option<Node<'_>> {
800 loop {
801 match node.kind() {
802 "identifier" => return Some(node),
803 "attribute" => node = node.child_by_field_name("object")?,
804 _ => return None,
805 }
806 }
807}
808
809fn handle_import_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
815 if ctx.target_member.is_some() {
816 return;
817 }
818 if !ctx
819 .edges
820 .iter()
821 .any(|edge| edge.local_name == ctx.target_short)
822 {
823 return;
824 }
825 let mut stack = vec![node];
826 while let Some(node) = stack.pop() {
827 if node.kind() == "identifier" && slice(node, ctx.source) == ctx.target_short {
828 record_import_hit(node, ctx);
829 return;
830 }
831 let mut cursor = node.walk();
832 for child in node.named_children(&mut cursor) {
833 stack.push(child);
834 }
835 }
836}
837
838fn handle_identifier_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
839 if node
840 .parent()
841 .is_some_and(|parent| parent.kind() == "attribute")
842 {
843 return;
844 }
845 let text = slice(node, ctx.source);
846 if text.is_empty() || is_declaration_identifier(node) {
847 return;
848 }
849 if let Some(member) = ctx.target_member {
850 if member == "__init__" && is_call_callee(node) && ctx.binds_target(text, node) {
853 record_hit(node, ctx);
854 return;
855 }
856 if text == member && ctx.node_directly_in_owner_class_body(node) {
859 record_hit(node, ctx);
860 }
861 return;
862 }
863 if !ctx.binds_target(text, node) {
864 return;
865 }
866 if !ctx.target_is_module
867 && ctx.edges.iter().any(|edge| edge.local_name == text)
868 && !ctx.module_binding_targets_symbol(text, node)
869 {
870 return;
871 }
872 record_hit(node, ctx);
873}
874
875fn handle_attribute_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
876 let Some(object) = node.child_by_field_name("object") else {
877 return;
878 };
879 let Some(attribute) = node.child_by_field_name("attribute") else {
880 return;
881 };
882 let object_text = slice(object, ctx.source);
883 let attribute_text = slice(attribute, ctx.source);
884 if let Some(member) = ctx.target_member
885 && attribute_text == member
886 {
887 let is_same_owner_receiver =
892 matches!(object_text, "self" | "cls") && ctx.self_receiver_matches_target(node);
893 if is_same_owner_receiver {
894 record_self_receiver_hit(attribute, ctx);
895 } else if ctx.receiver_binds_target(object_text, node)
896 || (object.kind() == "call" && call_result_matches_target(object, ctx))
897 {
898 record_hit(attribute, ctx);
899 } else if member_receiver_match_is_unproven(object, object_text, node, ctx) {
900 record_unproven_hit(attribute, ctx);
901 }
902 }
903
904 let object_binds_target = if ctx.target_is_module {
905 imported_root_targets_module(ctx, object, node)
906 } else {
907 ctx.binds_target(object_text, node)
908 };
909 if object.kind() == "identifier"
910 && object_binds_target
911 && (ctx.target_is_module
912 || (ctx.target_member.is_none()
913 && !ctx.edges.iter().any(|edge| {
914 matches!(edge.kind, ImportEdgeKind::Namespace) && edge.local_name == object_text
915 })))
916 {
917 record_hit(object, ctx);
918 }
919
920 if ctx.target_is_module
921 && let Some(module_qualifier) = module_attribute_target_hit(node, ctx)
922 {
923 record_hit(module_qualifier, ctx);
924 }
925
926 if let Some(member) = ctx.target_member
930 && object.kind() == "identifier"
931 && object_text == member
932 && ctx.node_directly_in_owner_class_body(object)
933 {
934 record_hit(object, ctx);
935 }
936
937 if ctx.member_best_effort_unique
943 && let Some(member) = ctx.target_member
944 && attribute_text == member
945 && object.kind() == "identifier"
946 && !matches!(object_text, "self" | "cls")
947 && !ctx.receiver_binds_target(object_text, node)
948 && ctx.receiver_type_is_unknown(object_text, node)
949 {
950 record_hit(attribute, ctx);
951 }
952
953 if let Some(namespace_target) = namespace_attribute_target_hit(node, ctx) {
954 record_hit(namespace_target, ctx);
955 }
956}
957
958fn namespace_attribute_target_hit<'a>(node: Node<'a>, ctx: &ScanCtx<'_>) -> Option<Node<'a>> {
966 if ctx.target_member.is_some() {
967 return None;
968 }
969 let (root, attributes) = attribute_chain(node)?;
970 let terminal = *attributes.last()?;
971 let terminal_name = slice(terminal, ctx.source);
972 if terminal_name.is_empty()
973 || !ctx
974 .seeds
975 .iter()
976 .any(|(_, seed_name)| seed_name == terminal_name)
977 {
978 return None;
979 }
980
981 let root_name = slice(root, ctx.source);
982 if root_name.is_empty() || import_root_shadowed(ctx, root_name, root, node) {
983 return None;
984 }
985 let binder = ctx.python.import_binder_of(ctx.file);
986 let binding = binder.bindings.get(root_name)?;
987 if binding.kind != ImportKind::Namespace {
988 return None;
989 }
990 let mut written_module = binding.module_specifier.clone();
991 for attribute in &attributes[..attributes.len() - 1] {
992 let segment = slice(*attribute, ctx.source);
993 if segment.is_empty() {
994 return None;
995 }
996 written_module.push('.');
997 written_module.push_str(segment);
998 }
999 if usage_resolve_module_files(ctx.python, ctx.file, &written_module)
1000 .iter()
1001 .any(|resolved| {
1002 ctx.seeds
1003 .contains(&(resolved.clone(), terminal_name.to_string()))
1004 })
1005 {
1006 return Some(terminal);
1007 }
1008
1009 let mut written_fqn = binding.module_specifier.clone();
1010 for attribute in attributes {
1011 let segment = slice(attribute, ctx.source);
1012 if segment.is_empty() {
1013 return None;
1014 }
1015 written_fqn.push('.');
1016 written_fqn.push_str(segment);
1017 }
1018 resolve_fqn_candidates(ctx.python, &written_fqn, |name| {
1019 ctx.graph.index.definitions(name).collect()
1020 })
1021 .into_iter()
1022 .any(|candidate| &candidate == ctx.target)
1023 .then_some(terminal)
1024}
1025
1026fn imported_root_targets_module(ctx: &ScanCtx<'_>, root: Node<'_>, reference: Node<'_>) -> bool {
1027 let Some(module_fqn) = imported_module_binding_fqn(ctx, root, reference) else {
1028 return false;
1029 };
1030 usage_resolve_module_files(ctx.python, ctx.file, &module_fqn)
1031 .into_iter()
1032 .any(|resolved_file| &resolved_file == ctx.target_source)
1033}
1034
1035fn module_attribute_target_hit<'a>(node: Node<'a>, ctx: &ScanCtx<'_>) -> Option<Node<'a>> {
1036 let (root, attributes) = attribute_chain(node)?;
1037 if attributes.is_empty() {
1038 return None;
1039 }
1040 let mut module_fqn = imported_module_binding_fqn(ctx, root, node)?;
1041 for attribute in attributes {
1042 let segment = slice(attribute, ctx.source);
1043 if segment.is_empty() {
1044 return None;
1045 }
1046 if module_fqn.ends_with('.') {
1047 module_fqn.push_str(segment);
1048 } else {
1049 module_fqn.push('.');
1050 module_fqn.push_str(segment);
1051 }
1052 let resolved = usage_resolve_module_files(ctx.python, ctx.file, &module_fqn);
1053 if resolved.is_empty() {
1054 return None;
1055 }
1056 if resolved
1057 .iter()
1058 .any(|resolved_file| resolved_file == ctx.target_source)
1059 {
1060 return Some(attribute);
1061 }
1062 }
1063 None
1064}
1065
1066fn call_result_matches_target(call: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
1067 let Some(target_owner) = ctx.target_owner.as_ref() else {
1068 return false;
1069 };
1070 let scope_facts = ctx.scope_facts_for_node(call);
1071 call_result_types(
1072 ctx.graph,
1073 ctx.python,
1074 ctx.file,
1075 ctx.source,
1076 call,
1077 scope_facts,
1078 )
1079 .into_iter()
1080 .any(|class| {
1081 &class == target_owner
1082 || ctx
1083 .graph
1084 .hierarchy
1085 .map(|provider| provider.get_ancestors(&class))
1086 .unwrap_or_default()
1087 .into_iter()
1088 .any(|ancestor| &ancestor == target_owner)
1089 })
1090}
1091
1092pub fn call_result_types(
1093 graph: &PythonGraphSource<'_>,
1094 python: &dyn PythonUsageSource,
1095 file: &ProjectFile,
1096 source: &str,
1097 call: Node<'_>,
1098 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1099) -> Vec<CodeUnit> {
1100 let Some(function) = call.child_by_field_name("function") else {
1101 return Vec::new();
1102 };
1103 let constructed = resolve_constructor_types(graph, python, file, source, function);
1104 if !constructed.is_empty() {
1105 return constructed;
1106 }
1107 let callable_fqns = resolve_callable_fqns(graph, python, file, source, function, scope_facts);
1108 if callable_fqns.is_empty() {
1109 return Vec::new();
1110 }
1111 let callables = callable_fqns
1112 .into_iter()
1113 .flat_map(|callable_fqn| {
1114 resolve_fqn_candidates(python, &callable_fqn, |name| {
1115 graph.index.definitions(name).collect()
1116 })
1117 })
1118 .collect::<Vec<_>>();
1119 let mut classes = Vec::new();
1120 for callable in callables.into_iter().filter(CodeUnit::is_function) {
1121 let Some(raw_type) = callable_return_type_name(graph, python, &callable) else {
1122 continue;
1123 };
1124 if let Some(class) =
1125 resolve_receiver_type(graph, python, callable.source(), &raw_type, true)
1126 {
1127 classes.push(class);
1128 }
1129 }
1130 classes.sort();
1131 classes.dedup();
1132 classes
1133}
1134
1135fn resolve_callable_fqns(
1136 graph: &PythonGraphSource<'_>,
1137 python: &dyn PythonUsageSource,
1138 file: &ProjectFile,
1139 source: &str,
1140 function: Node<'_>,
1141 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1142) -> Vec<String> {
1143 match function.kind() {
1144 "identifier" => {
1145 resolve_identifier_callable_fqns(graph, python, file, source, function, scope_facts)
1146 }
1147 "attribute" => {
1148 resolve_attribute_callable_fqns(graph, python, file, source, function, scope_facts)
1149 }
1150 _ => Vec::new(),
1151 }
1152}
1153
1154fn resolve_identifier_callable_fqns(
1155 graph: &PythonGraphSource<'_>,
1156 python: &dyn PythonUsageSource,
1157 file: &ProjectFile,
1158 source: &str,
1159 function: Node<'_>,
1160 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1161) -> Vec<String> {
1162 let local = slice(function, source);
1163 if local.is_empty() || scope_facts.is_some_and(|facts| facts.is_shadowed(local)) {
1164 return Vec::new();
1165 }
1166 let binder = python.import_binder_of(file);
1167 match binder.bindings.get(local) {
1168 Some(binding) if binding.kind == ImportKind::Named => binding
1169 .imported_name
1170 .as_ref()
1171 .map(|imported| vec![format!("{}.{}", binding.module_specifier, imported)])
1172 .unwrap_or_default(),
1173 _ => graph
1174 .index
1175 .declarations(file)
1176 .into_iter()
1177 .find(|unit| unit.is_function() && unit.identifier() == local)
1178 .map(|unit| vec![unit.fq_name()])
1179 .unwrap_or_default(),
1180 }
1181}
1182
1183fn resolve_attribute_callable_fqns(
1184 graph: &PythonGraphSource<'_>,
1185 python: &dyn PythonUsageSource,
1186 file: &ProjectFile,
1187 source: &str,
1188 function: Node<'_>,
1189 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1190) -> Vec<String> {
1191 let Some(receiver) = function.child_by_field_name("object") else {
1192 return Vec::new();
1193 };
1194 let Some(method) = function.child_by_field_name("attribute") else {
1195 return Vec::new();
1196 };
1197 let method = slice(method, source);
1198 if method.is_empty() {
1199 return Vec::new();
1200 }
1201 let mut fqns = attribute_receiver_classes(graph, python, file, source, receiver, scope_facts)
1202 .into_iter()
1203 .map(|class| format!("{}.{}", class.fq_name(), method))
1204 .collect::<Vec<_>>();
1205 fqns.sort();
1206 fqns.dedup();
1207 fqns
1208}
1209
1210fn attribute_receiver_classes(
1211 graph: &PythonGraphSource<'_>,
1212 python: &dyn PythonUsageSource,
1213 file: &ProjectFile,
1214 source: &str,
1215 receiver: Node<'_>,
1216 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1217) -> Vec<CodeUnit> {
1218 let mut classes = match receiver.kind() {
1219 "identifier" => {
1220 identifier_receiver_classes(graph, python, file, source, receiver, scope_facts)
1221 }
1222 "attribute" => {
1223 if let Some(root) = leftmost_identifier(receiver)
1224 && scope_facts.is_some_and(|facts| facts.is_shadowed(slice(root, source)))
1225 {
1226 Vec::new()
1227 } else {
1228 resolve_constructor_types(graph, python, file, source, receiver)
1229 }
1230 }
1231 _ => Vec::new(),
1232 };
1233 classes.sort();
1234 classes.dedup();
1235 classes
1236}
1237
1238fn identifier_receiver_classes(
1239 graph: &PythonGraphSource<'_>,
1240 python: &dyn PythonUsageSource,
1241 file: &ProjectFile,
1242 source: &str,
1243 receiver: Node<'_>,
1244 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1245) -> Vec<CodeUnit> {
1246 let ident = slice(receiver, source);
1247 if ident.is_empty() {
1248 return Vec::new();
1249 }
1250 if matches!(ident, "self" | "cls")
1251 && let Some(class) = enclosing_class_for_node(graph, file, receiver)
1252 {
1253 return vec![class];
1254 }
1255 if let Some(facts) = scope_facts {
1256 if let Some(raw_type) = facts
1257 .resolution_for(ident)
1258 .as_precise()
1259 .and_then(|targets| targets.iter().next())
1260 && let Some(class) = resolve_receiver_type(graph, python, file, raw_type, false)
1261 {
1262 return vec![class];
1263 }
1264 if facts.is_shadowed(ident) {
1265 return Vec::new();
1266 }
1267 }
1268 resolve_receiver_type(graph, python, file, ident, false)
1269 .into_iter()
1270 .collect()
1271}
1272
1273fn enclosing_class_for_node(
1274 graph: &PythonGraphSource<'_>,
1275 file: &ProjectFile,
1276 node: Node<'_>,
1277) -> Option<CodeUnit> {
1278 let range = Range {
1279 start_byte: node.start_byte(),
1280 end_byte: node.end_byte(),
1281 start_line: 0,
1282 end_line: 0,
1283 };
1284 let enclosing = graph.index.enclosing_code_unit(file, &range)?;
1285 brokk_bifrost_core::analyzer::usages::common::enclosing_owner_chain(enclosing, |unit| {
1291 graph.index.parent_of(unit)
1292 })
1293 .take(2)
1294 .find(|unit| unit.is_class() && unit.source() == file)
1295}
1296
1297fn attribute_chain<'a>(node: Node<'a>) -> Option<(Node<'a>, Vec<Node<'a>>)> {
1298 let mut attributes = Vec::new();
1299 let mut current = node;
1300 loop {
1301 if current.kind() != "attribute" {
1302 return None;
1303 }
1304 attributes.push(current.child_by_field_name("attribute")?);
1305 current = current.child_by_field_name("object")?;
1306 if current.kind() == "identifier" {
1307 attributes.reverse();
1308 return Some((current, attributes));
1309 }
1310 }
1311}
1312
1313fn imported_module_binding_fqn(
1314 ctx: &ScanCtx<'_>,
1315 root: Node<'_>,
1316 reference: Node<'_>,
1317) -> Option<String> {
1318 let root_text = slice(root, ctx.source);
1319 if root_text.is_empty() {
1320 return None;
1321 }
1322 if import_root_shadowed(ctx, root_text, root, reference) {
1323 return None;
1324 }
1325 let binder = ctx.python.import_binder_of(ctx.file);
1326 let binding = binder.bindings.get(root_text)?;
1327 match binding.kind {
1328 ImportKind::Namespace => Some(binding.module_specifier.clone()),
1329 ImportKind::Named => {
1330 let imported = binding.imported_name.as_ref()?;
1331 let candidate = if binding.module_specifier.ends_with('.') {
1332 format!("{}{}", binding.module_specifier, imported)
1333 } else {
1334 format!("{}.{}", binding.module_specifier, imported)
1335 };
1336 (!usage_resolve_module_files(ctx.python, ctx.file, &candidate).is_empty())
1337 .then_some(candidate)
1338 }
1339 _ => None,
1340 }
1341}
1342
1343fn import_root_shadowed(
1344 ctx: &ScanCtx<'_>,
1345 root_text: &str,
1346 root: Node<'_>,
1347 reference: Node<'_>,
1348) -> bool {
1349 ctx.scope_facts_for_node(root)
1350 .or_else(|| ctx.scope_facts_for_node(reference))
1351 .is_some_and(|facts| facts.is_shadowed(root_text))
1352 || enclosing_parameters_shadow(root_text, reference, ctx.source)
1353}
1354
1355fn enclosing_parameters_shadow(root_text: &str, reference: Node<'_>, source: &str) -> bool {
1356 let mut current = reference;
1357 while let Some(parent) = current.parent() {
1358 if matches!(parent.kind(), "function_definition" | "lambda") {
1359 let Some(parameters) = parent.child_by_field_name("parameters") else {
1360 return false;
1361 };
1362 let mut cursor = parameters.walk();
1363 return parameters.named_children(&mut cursor).any(|parameter| {
1364 parameter_symbol(parameter, source).as_deref() == Some(root_text)
1365 });
1366 }
1367 current = parent;
1368 }
1369 false
1370}
1371
1372enum EnclosingParameterType {
1373 NotDeclared,
1374 Untyped,
1375 Typed(String),
1376}
1377
1378fn enclosing_runtime_parameter_type(
1379 name: &str,
1380 reference: Node<'_>,
1381 source: &str,
1382) -> EnclosingParameterType {
1383 let site_start = reference.start_byte();
1384 let site_end = reference.end_byte();
1385 let mut current = reference;
1386 while let Some(parent) = current.parent() {
1387 if matches!(parent.kind(), "function_definition" | "lambda")
1388 && parent
1389 .child_by_field_name("body")
1390 .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
1391 && let Some(parameters) = parent.child_by_field_name("parameters")
1392 {
1393 let mut cursor = parameters.walk();
1394 for parameter in parameters.named_children(&mut cursor) {
1395 if parameter_symbol(parameter, source).as_deref() != Some(name) {
1396 continue;
1397 }
1398 return parameter
1399 .child_by_field_name("type")
1400 .and_then(|annotation| normalized_receiver_type(slice(annotation, source)))
1401 .map_or(EnclosingParameterType::Untyped, |raw_type| {
1402 EnclosingParameterType::Typed(raw_type)
1403 });
1404 }
1405 }
1406 current = parent;
1407 }
1408 EnclosingParameterType::NotDeclared
1409}
1410
1411fn member_receiver_match_is_unproven(
1412 object: Node<'_>,
1413 object_text: &str,
1414 node: Node<'_>,
1415 ctx: &ScanCtx<'_>,
1416) -> bool {
1417 if matches!(object_text, "self" | "cls") {
1418 return false;
1419 }
1420 match object.kind() {
1421 "identifier" => {
1422 ctx.receiver_type_is_unknown(object_text, node) && !ctx.member_best_effort_unique
1423 }
1424 "attribute" => true,
1425 _ => false,
1426 }
1427}
1428
1429pub fn slice<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1430 brokk_bifrost_core::analyzer::common::node_source_text(node, source)
1431}
1432
1433fn is_call_callee(node: Node<'_>) -> bool {
1435 node.parent().is_some_and(|parent| {
1436 parent.kind() == "call"
1437 && parent
1438 .child_by_field_name("function")
1439 .is_some_and(|function| function.id() == node.id())
1440 })
1441}
1442
1443pub fn is_declaration_identifier(node: Node<'_>) -> bool {
1444 let Some(parent) = node.parent() else {
1445 return false;
1446 };
1447 let parent_kind = parent.kind();
1448 if matches!(
1449 parent_kind,
1450 "class_definition" | "function_definition" | "parameters"
1451 ) && parent
1452 .child_by_field_name("name")
1453 .map(|name| name.id() == node.id())
1454 .unwrap_or(false)
1455 {
1456 return true;
1457 }
1458
1459 if matches!(
1460 parent_kind,
1461 "aliased_import" | "import_from_statement" | "import_statement"
1462 ) {
1463 return true;
1464 }
1465
1466 parent_kind == "assignment"
1467 && parent
1468 .child_by_field_name("left")
1469 .map(|left| {
1470 left.start_byte() <= node.start_byte() && node.end_byte() <= left.end_byte()
1471 })
1472 .unwrap_or(false)
1473}
1474
1475#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1476enum ModuleBindingKind {
1477 TargetSymbolImport,
1478 TargetModuleImport,
1479 Other,
1480}
1481
1482#[derive(Clone, Copy, Debug)]
1483struct ClassifiedModuleBindingEvent {
1484 visible_from: usize,
1485 conditional: bool,
1486 kind: ModuleBindingKind,
1487}
1488
1489pub fn collect_module_binding_timeline(root: Node<'_>, source: &str) -> ModuleBindingTimeline {
1490 let mut timeline = ModuleBindingTimeline::default();
1491 let mut stack = vec![root];
1492 while let Some(node) = stack.pop() {
1493 match node.kind() {
1494 "function_definition" | "class_definition" => {
1495 if let Some(name) = node.child_by_field_name("name") {
1496 record_module_binding(
1497 &mut timeline,
1498 slice(name, source),
1499 node.end_byte(),
1500 binding_is_conditional(node),
1501 ModuleBindingEventKind::Other,
1502 );
1503 }
1504 continue;
1505 }
1506 "import_statement" | "import_from_statement" => {
1507 collect_import_binding_events(node, source, &mut timeline);
1508 continue;
1509 }
1510 "assignment" | "augmented_assignment" | "named_expression" => {
1511 if let Some(left) = node.child_by_field_name("left") {
1512 record_local_binding_targets(
1513 left,
1514 source,
1515 node.end_byte(),
1516 binding_is_conditional(node),
1517 &mut timeline,
1518 );
1519 }
1520 continue;
1521 }
1522 "for_statement" => {
1523 if let Some(left) = node.child_by_field_name("left") {
1524 record_local_binding_targets(
1525 left,
1526 source,
1527 left.end_byte(),
1528 true,
1529 &mut timeline,
1530 );
1531 }
1532 }
1533 _ => {}
1534 }
1535
1536 let mut cursor = node.walk();
1537 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
1538 children.reverse();
1539 stack.extend(children);
1540 }
1541 for events in timeline.values_mut() {
1542 events.sort_by_key(|event| event.visible_from);
1543 }
1544 timeline
1545}
1546
1547fn collect_import_binding_events(
1548 node: Node<'_>,
1549 source: &str,
1550 timeline: &mut ModuleBindingTimeline,
1551) {
1552 if node.kind() == "import_statement" {
1553 let mut cursor = node.walk();
1554 for imported in node.children_by_field_name("name", &mut cursor) {
1555 let name = imported.child_by_field_name("name").unwrap_or(imported);
1556 let Some(local) = imported
1557 .child_by_field_name("alias")
1558 .or_else(|| first_identifier(name))
1559 else {
1560 continue;
1561 };
1562 let module = slice(name, source).trim();
1563 record_module_binding(
1564 timeline,
1565 slice(local, source),
1566 node.end_byte(),
1567 binding_is_conditional(node),
1568 ModuleBindingEventKind::ImportModule(module.to_string()),
1569 );
1570 }
1571 return;
1572 }
1573
1574 let Some(module_node) = node.child_by_field_name("module_name") else {
1575 return;
1576 };
1577 let module = slice(module_node, source).trim();
1578 let mut cursor = node.walk();
1579 for imported in node.children_by_field_name("name", &mut cursor) {
1580 if imported.kind() == "wildcard_import" {
1581 continue;
1582 }
1583 let name = imported.child_by_field_name("name").unwrap_or(imported);
1584 let Some(imported_identifier) = last_identifier(name) else {
1585 continue;
1586 };
1587 let imported_name = slice(imported_identifier, source).trim();
1588 let Some(local) = imported
1589 .child_by_field_name("alias")
1590 .or_else(|| last_identifier(name))
1591 else {
1592 continue;
1593 };
1594 record_module_binding(
1595 timeline,
1596 slice(local, source),
1597 node.end_byte(),
1598 binding_is_conditional(node),
1599 ModuleBindingEventKind::FromImport {
1600 module: module.to_string(),
1601 imported_name: imported_name.to_string(),
1602 },
1603 );
1604 }
1605}
1606
1607fn classify_module_binding_timeline(
1608 python: &dyn PythonUsageSource,
1609 file: &ProjectFile,
1610 timeline: &ModuleBindingTimeline,
1611 seeds: &BTreeSet<(ProjectFile, String)>,
1612 edges: &[ImportEdge],
1613) -> HashMap<String, Vec<ClassifiedModuleBindingEvent>> {
1614 let mut classified = HashMap::default();
1615 let mut module_targets: HashMap<String, bool> = HashMap::default();
1616 let relevant_locals: HashSet<&str> =
1617 edges.iter().map(|edge| edge.local_name.as_str()).collect();
1618 for (local, events) in timeline {
1619 if !relevant_locals.contains(local.as_str()) {
1620 continue;
1621 }
1622 let classified_events = events
1623 .iter()
1624 .map(|event| {
1625 let kind = match &event.kind {
1626 ModuleBindingEventKind::ImportModule(module) => {
1627 if *module_targets
1628 .entry(module.clone())
1629 .or_insert_with(|| module_contains_seed(python, file, module, seeds))
1630 {
1631 ModuleBindingKind::TargetModuleImport
1632 } else {
1633 ModuleBindingKind::Other
1634 }
1635 }
1636 ModuleBindingEventKind::FromImport {
1637 module,
1638 imported_name,
1639 } => {
1640 let direct = usage_resolve_module_files(python, file, module).iter().any(
1641 |resolved| seeds.contains(&(resolved.clone(), imported_name.clone())),
1642 );
1643 let submodule = if module.ends_with('.') {
1644 format!("{module}{imported_name}")
1645 } else {
1646 format!("{module}.{imported_name}")
1647 };
1648 let imports_target_module =
1649 *module_targets.entry(submodule.clone()).or_insert_with(|| {
1650 module_contains_seed(python, file, &submodule, seeds)
1651 });
1652 if direct {
1653 ModuleBindingKind::TargetSymbolImport
1654 } else if imports_target_module {
1655 ModuleBindingKind::TargetModuleImport
1656 } else {
1657 ModuleBindingKind::Other
1658 }
1659 }
1660 ModuleBindingEventKind::Other => ModuleBindingKind::Other,
1661 };
1662 ClassifiedModuleBindingEvent {
1663 visible_from: event.visible_from,
1664 conditional: event.conditional,
1665 kind,
1666 }
1667 })
1668 .collect();
1669 classified.insert(local.clone(), classified_events);
1670 }
1671 classified
1672}
1673
1674fn module_contains_seed(
1675 python: &dyn PythonUsageSource,
1676 file: &ProjectFile,
1677 module: &str,
1678 seeds: &BTreeSet<(ProjectFile, String)>,
1679) -> bool {
1680 usage_resolve_module_files(python, file, module)
1681 .iter()
1682 .any(|resolved| seeds.iter().any(|(seed_file, _)| seed_file == resolved))
1683}
1684
1685fn record_module_binding(
1686 timeline: &mut ModuleBindingTimeline,
1687 name: &str,
1688 visible_from: usize,
1689 conditional: bool,
1690 kind: ModuleBindingEventKind,
1691) {
1692 let name = name.trim();
1693 if name.is_empty() {
1694 return;
1695 }
1696 timeline
1697 .entry(name.to_string())
1698 .or_default()
1699 .push(ModuleBindingEvent {
1700 visible_from,
1701 conditional,
1702 kind,
1703 });
1704}
1705
1706fn record_local_binding_targets(
1707 target: Node<'_>,
1708 source: &str,
1709 visible_from: usize,
1710 conditional: bool,
1711 timeline: &mut ModuleBindingTimeline,
1712) {
1713 let mut stack = vec![target];
1714 while let Some(node) = stack.pop() {
1715 if node.kind() == "identifier" {
1716 record_module_binding(
1717 timeline,
1718 slice(node, source),
1719 visible_from,
1720 conditional,
1721 ModuleBindingEventKind::Other,
1722 );
1723 continue;
1724 }
1725 if matches!(node.kind(), "attribute" | "subscript") {
1726 continue;
1727 }
1728 let mut cursor = node.walk();
1729 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
1730 children.reverse();
1731 stack.extend(children);
1732 }
1733}
1734
1735fn binding_is_conditional(mut node: Node<'_>) -> bool {
1736 while let Some(parent) = node.parent() {
1737 if matches!(
1738 parent.kind(),
1739 "if_statement"
1740 | "try_statement"
1741 | "except_clause"
1742 | "match_statement"
1743 | "case_clause"
1744 | "for_statement"
1745 | "while_statement"
1746 ) {
1747 return true;
1748 }
1749 if matches!(
1750 parent.kind(),
1751 "module" | "function_definition" | "class_definition"
1752 ) {
1753 return false;
1754 }
1755 node = parent;
1756 }
1757 false
1758}
1759
1760fn first_identifier(node: Node<'_>) -> Option<Node<'_>> {
1761 identifier_extreme(node, false)
1762}
1763
1764fn last_identifier(node: Node<'_>) -> Option<Node<'_>> {
1765 identifier_extreme(node, true)
1766}
1767
1768fn identifier_extreme(node: Node<'_>, last: bool) -> Option<Node<'_>> {
1769 let mut best = None;
1770 let mut stack = vec![node];
1771 while let Some(node) = stack.pop() {
1772 if node.kind() == "identifier" {
1773 if best.is_none_or(|current: Node<'_>| {
1774 if last {
1775 node.start_byte() > current.start_byte()
1776 } else {
1777 node.start_byte() < current.start_byte()
1778 }
1779 }) {
1780 best = Some(node);
1781 }
1782 continue;
1783 }
1784 let mut cursor = node.walk();
1785 stack.extend(node.named_children(&mut cursor));
1786 }
1787 best
1788}
1789
1790fn reference_is_deferred_function_body(node: Node<'_>) -> bool {
1791 let site_start = node.start_byte();
1792 let site_end = node.end_byte();
1793 let mut current = node;
1794 while let Some(parent) = current.parent() {
1795 if matches!(parent.kind(), "function_definition" | "lambda")
1796 && parent
1797 .child_by_field_name("body")
1798 .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
1799 {
1800 return true;
1801 }
1802 current = parent;
1803 }
1804 false
1805}
1806
1807pub fn collect_assigned_identifiers(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
1808 let mut stack = vec![node];
1809 while let Some(node) = stack.pop() {
1810 if node.kind() == "identifier" {
1811 let text = slice(node, source).trim();
1812 if !text.is_empty() {
1813 out.insert(text.to_string());
1814 }
1815 continue;
1816 }
1817
1818 let mut cursor = node.walk();
1819 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
1820 children.reverse();
1821 stack.extend(children);
1822 }
1823}
1824
1825pub fn collect_scope_facts_from_parsed_source(
1826 graph: &PythonGraphSource<'_>,
1827 python: &dyn PythonUsageSource,
1828 file: &ProjectFile,
1829 source: &str,
1830 root: Node<'_>,
1831) -> PythonScopeFacts {
1832 let mut factory_return_types = collect_factory_return_types_from_root(root, source);
1833 collect_imported_factory_return_types(graph, python, file, &mut factory_return_types);
1834 collect_scope_facts_with_factory_returns(graph, file, source, &factory_return_types)
1835}
1836
1837fn collect_imported_factory_return_types(
1838 graph: &PythonGraphSource<'_>,
1839 python: &dyn PythonUsageSource,
1840 file: &ProjectFile,
1841 factory_return_types: &mut HashMap<String, String>,
1842) {
1843 let binder = python.import_binder_of(file);
1844 for (local, binding) in &binder.bindings {
1845 if !matches!(binding.kind, ImportKind::Named) {
1846 continue;
1847 }
1848 let Some(imported) = binding.imported_name.as_deref() else {
1849 continue;
1850 };
1851 let fqn = format!("{}.{}", binding.module_specifier, imported);
1852 let units =
1853 resolve_fqn_candidates(python, &fqn, |name| graph.index.definitions(name).collect());
1854 for unit in units {
1855 if unit.is_function() {
1856 if let Some(return_type) = callable_return_type_name(graph, python, &unit) {
1857 factory_return_types
1858 .entry(local.clone())
1859 .or_insert(return_type);
1860 }
1861 continue;
1862 }
1863 if !unit.is_class() {
1864 continue;
1865 }
1866 factory_return_types
1867 .entry(local.clone())
1868 .or_insert_with(|| unit.identifier().to_string());
1869 collect_imported_class_method_return_types(
1870 graph,
1871 python,
1872 local,
1873 &unit,
1874 factory_return_types,
1875 );
1876 }
1877 }
1878}
1879
1880fn collect_imported_class_method_return_types(
1881 graph: &PythonGraphSource<'_>,
1882 python: &dyn PythonSource,
1883 local_class_name: &str,
1884 class_unit: &CodeUnit,
1885 factory_return_types: &mut HashMap<String, String>,
1886) {
1887 for member in graph.index.direct_children(class_unit) {
1888 if !member.is_function() {
1889 continue;
1890 }
1891 let Some(return_type) = callable_return_type_name(graph, python, &member) else {
1892 continue;
1893 };
1894 factory_return_types
1895 .entry(format!("{}.{}", local_class_name, member.identifier()))
1896 .or_insert(return_type);
1897 }
1898}
1899
1900fn callable_return_type_name(
1901 graph: &PythonGraphSource<'_>,
1902 python: &dyn PythonSource,
1903 callable: &CodeUnit,
1904) -> Option<String> {
1905 if let Some(prepared) = python.prepared_syntax(callable.source()) {
1910 #[cfg(any(test, feature = "test-support"))]
1911 note_callable_return_type_lookup_for_test(true);
1912 return callable_return_type_name_in_tree(
1913 graph,
1914 callable,
1915 prepared.source(),
1916 prepared.tree().root_node(),
1917 );
1918 }
1919 #[cfg(any(test, feature = "test-support"))]
1920 note_callable_return_type_lookup_for_test(false);
1921 let source = graph.index.indexed_source(callable.source())?;
1922 declaration_source_slices(graph, callable, &source)
1923 .into_iter()
1924 .find_map(|declaration_source| {
1925 let mut parser = Parser::new();
1926 parser
1927 .set_language(&tree_sitter_python::LANGUAGE.into())
1928 .ok()?;
1929 let tree = parser.parse(declaration_source, None)?;
1930 let function = first_function_definition(tree.root_node())?;
1931 factory_return_type(function, declaration_source)
1932 })
1933}
1934
1935#[cfg(any(test, feature = "test-support"))]
1941#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
1942pub struct CallableReturnTypeLookupCounts {
1943 pub prepared: usize,
1945 pub reparsed: usize,
1947}
1948
1949#[cfg(any(test, feature = "test-support"))]
1950thread_local! {
1951 static CALLABLE_RETURN_TYPE_LOOKUPS_FOR_TEST: std::cell::Cell<CallableReturnTypeLookupCounts> =
1952 const { std::cell::Cell::new(CallableReturnTypeLookupCounts { prepared: 0, reparsed: 0 }) };
1953}
1954
1955#[cfg(any(test, feature = "test-support"))]
1956fn note_callable_return_type_lookup_for_test(from_prepared_syntax: bool) {
1957 CALLABLE_RETURN_TYPE_LOOKUPS_FOR_TEST.with(|counts| {
1958 let mut observed = counts.get();
1959 if from_prepared_syntax {
1960 observed.prepared += 1;
1961 } else {
1962 observed.reparsed += 1;
1963 }
1964 counts.set(observed);
1965 });
1966}
1967
1968#[cfg(any(test, feature = "test-support"))]
1971pub fn with_callable_return_type_lookup_counter_for_test<T>(
1972 body: impl FnOnce() -> T,
1973) -> (T, CallableReturnTypeLookupCounts) {
1974 CALLABLE_RETURN_TYPE_LOOKUPS_FOR_TEST.with(|counts| {
1975 counts.set(CallableReturnTypeLookupCounts::default());
1976 let result = body();
1977 let observed = counts.get();
1978 counts.set(CallableReturnTypeLookupCounts::default());
1979 (result, observed)
1980 })
1981}
1982
1983fn callable_return_type_name_in_tree(
1989 graph: &PythonGraphSource<'_>,
1990 callable: &CodeUnit,
1991 source: &str,
1992 root: Node<'_>,
1993) -> Option<String> {
1994 let mut ranges = graph.index.ranges(callable);
1995 ranges.sort_by_key(|range| range.start_byte);
1996 ranges.into_iter().find_map(|range| {
1997 let declaration = root.descendant_for_byte_range(range.start_byte, range.end_byte)?;
1998 let function = first_function_definition(declaration)?;
1999 factory_return_type(function, source)
2000 })
2001}
2002
2003fn first_function_definition(root: Node<'_>) -> Option<Node<'_>> {
2004 let mut stack = vec![root];
2005 while let Some(node) = stack.pop() {
2006 if node.kind() == "function_definition" {
2007 return Some(node);
2008 }
2009 let mut cursor = node.walk();
2010 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2011 children.reverse();
2012 stack.extend(children);
2013 }
2014 None
2015}
2016
2017fn collect_scope_facts_with_factory_returns(
2018 graph: &PythonGraphSource<'_>,
2019 file: &ProjectFile,
2020 source: &str,
2021 factory_return_types: &HashMap<String, String>,
2022) -> PythonScopeFacts {
2023 let declarations = graph.index.declarations(file);
2024 let mut class_facts_by_name: HashMap<String, LocalBindingsSnapshot<String>> =
2025 HashMap::default();
2026 for declaration in declarations
2027 .iter()
2028 .filter(|declaration| declaration.is_class())
2029 {
2030 let Some(declaration_source) = declaration_source(graph, declaration, source) else {
2031 continue;
2032 };
2033 let facts = collect_scope_facts_from_source(
2034 &declaration_source,
2035 ScopeFactTraversal::Class,
2036 true,
2037 Some(declaration.short_name()),
2038 factory_return_types,
2039 );
2040 class_facts_by_name.insert(
2041 declaration.short_name().to_string(),
2042 facts.filtered_visible_bindings(|symbol, _| symbol.starts_with("self.")),
2043 );
2044 }
2045
2046 let mut scope_facts = HashMap::default();
2047 for declaration in declarations
2048 .iter()
2049 .filter(|declaration| declaration.is_function())
2050 {
2051 let Some(declaration_source) = declaration_source(graph, declaration, source) else {
2052 continue;
2053 };
2054 let owner = declaration
2059 .short_name()
2060 .rsplit_once('.')
2061 .map(|(owner, _)| owner);
2062 let mut facts = collect_scope_facts_from_source(
2063 &declaration_source,
2064 ScopeFactTraversal::Function,
2065 false,
2066 owner,
2067 factory_return_types,
2068 );
2069 if let Some(owner) = owner
2070 && let Some(class_facts) = class_facts_by_name.get(owner)
2071 {
2072 facts = facts.merged_with_visible(class_facts);
2073 }
2074 scope_facts.insert(declaration.clone(), facts);
2075 }
2076
2077 for declaration in declarations.iter().filter(|d| d.is_module()) {
2082 let Some(declaration_source) = declaration_source(graph, declaration, source) else {
2083 continue;
2084 };
2085 let facts = collect_scope_facts_from_source(
2086 &declaration_source,
2087 ScopeFactTraversal::Module,
2088 false,
2089 None,
2090 factory_return_types,
2091 );
2092 scope_facts.insert(declaration.clone(), facts);
2093 }
2094 scope_facts
2095}
2096
2097fn declaration_source(
2098 graph: &PythonGraphSource<'_>,
2099 declaration: &CodeUnit,
2100 file_source: &str,
2101) -> Option<String> {
2102 let slices = declaration_source_slices(graph, declaration, file_source);
2103 (!slices.is_empty()).then(|| slices.join("\n\n"))
2104}
2105
2106fn declaration_source_slices<'a>(
2107 graph: &PythonGraphSource<'_>,
2108 declaration: &CodeUnit,
2109 file_source: &'a str,
2110) -> Vec<&'a str> {
2111 let mut ranges = graph.index.ranges(declaration);
2112 ranges.sort_by_key(|range| range.start_byte);
2113 ranges
2114 .into_iter()
2115 .filter_map(|range| file_source.get(range.start_byte..range.end_byte))
2116 .collect()
2117}
2118
2119fn collect_scope_facts_from_source(
2120 source: &str,
2121 traversal: ScopeFactTraversal,
2122 allow_self_receivers: bool,
2123 current_class: Option<&str>,
2124 factory_return_types: &HashMap<String, String>,
2125) -> LocalBindingsSnapshot<String> {
2126 let events = collect_scope_fact_events(source, traversal);
2127 collect_scope_facts_from_events(
2128 &events,
2129 allow_self_receivers,
2130 current_class,
2131 factory_return_types,
2132 )
2133}
2134
2135pub fn collect_function_scope_facts_from_node(
2136 function: Node<'_>,
2137 source: &str,
2138) -> LocalBindingsSnapshot<String> {
2139 let mut events = Vec::new();
2140 if function.kind() == "lambda" {
2141 if let Some(parameters) = function.child_by_field_name("parameters") {
2142 collect_parameter_events(parameters, source, &mut events);
2143 }
2144 } else {
2145 collect_scope_fact_events_from_node(
2146 function,
2147 source,
2148 ScopeFactTraversal::Function,
2149 &mut events,
2150 );
2151 }
2152 collect_scope_facts_from_events(&events, false, None, &HashMap::default())
2153}
2154
2155fn collect_scope_facts_from_events(
2156 events: &[ScopeFactEvent],
2157 allow_self_receivers: bool,
2158 current_class: Option<&str>,
2159 factory_return_types: &HashMap<String, String>,
2160) -> LocalBindingsSnapshot<String> {
2161 let mut engine = LocalInferenceEngine::new(LocalInferenceConfig::default());
2162 for event in events {
2163 if let ScopeFactEvent::Parameter { symbol, .. } = event
2164 && !engine.is_shadowed(symbol)
2165 {
2166 engine.declare_shadow(symbol.clone());
2167 }
2168 }
2169
2170 let mut changed = true;
2171 while changed {
2172 changed = false;
2173 let mut aliases = Vec::new();
2174 for event in events {
2175 match event {
2176 ScopeFactEvent::Parameter {
2177 symbol,
2178 annotation: Some(annotation),
2179 }
2180 | ScopeFactEvent::Annotation { symbol, annotation } => {
2181 apply_annotation_event(
2182 symbol,
2183 annotation,
2184 allow_self_receivers,
2185 &mut engine,
2186 &mut changed,
2187 );
2188 }
2189 ScopeFactEvent::Parameter {
2190 annotation: None, ..
2191 } => {}
2192 ScopeFactEvent::Assignment { lhs, rhs } => {
2193 if !engine.is_shadowed(lhs) {
2194 engine.declare_shadow(lhs.clone());
2195 }
2196 if lhs.starts_with("self.") && !allow_self_receivers {
2197 continue;
2198 }
2199
2200 match rhs {
2201 AssignmentRhs::Call(callee) => {
2202 if !engine.is_shadowed(callee) {
2203 if let Some(receiver_type) = factory_return_type_for_callee(
2204 callee,
2205 current_class,
2206 factory_return_types,
2207 ) && engine.resolve_symbol(lhs).is_unknown()
2208 {
2209 engine.seed_symbol(lhs.clone(), receiver_type.clone());
2210 changed = true;
2211 continue;
2212 }
2213
2214 if let Some(receiver_type) = normalized_receiver_type(callee)
2215 && engine.resolve_symbol(lhs).is_unknown()
2216 {
2217 engine.seed_symbol(lhs.clone(), receiver_type);
2218 changed = true;
2219 continue;
2220 }
2221 }
2222 }
2223 AssignmentRhs::Symbol(rhs_symbol) => {
2224 if !engine.is_shadowed(rhs_symbol)
2225 && let Some(receiver_type) = normalized_receiver_type(rhs_symbol)
2226 && engine.resolve_symbol(lhs).is_unknown()
2227 {
2228 engine.seed_symbol(lhs.clone(), receiver_type);
2229 changed = true;
2230 continue;
2231 }
2232
2233 if let SymbolResolution::Precise(targets) =
2234 engine.resolve_symbol(rhs_symbol)
2235 && !targets.is_empty()
2236 {
2237 aliases.push((lhs.clone(), rhs_symbol.clone()));
2238 }
2239 }
2240 AssignmentRhs::Unknown => {}
2241 }
2242 }
2243 }
2244 }
2245 let before = engine.snapshot();
2246 engine.apply_aliases_until_stable(aliases);
2247 if engine.snapshot() != before {
2248 changed = true;
2249 }
2250 }
2251
2252 engine.snapshot()
2253}
2254
2255fn factory_return_type_for_callee<'a>(
2256 callee: &str,
2257 current_class: Option<&str>,
2258 factory_return_types: &'a HashMap<String, String>,
2259) -> Option<&'a String> {
2260 if let Some(receiver_type) = factory_return_types.get(callee) {
2261 return Some(receiver_type);
2262 }
2263 let class_name = current_class?;
2264 let method = callee
2265 .strip_prefix("self.")
2266 .or_else(|| callee.strip_prefix("cls."))?;
2267 factory_return_types.get(&format!("{class_name}.{method}"))
2268}
2269
2270fn apply_annotation_event(
2271 symbol: &str,
2272 annotation: &str,
2273 allow_self_receivers: bool,
2274 engine: &mut LocalInferenceEngine<String>,
2275 changed: &mut bool,
2276) {
2277 if symbol.starts_with("self.") && !allow_self_receivers {
2278 return;
2279 }
2280 if let Some(receiver_type) = normalized_receiver_type(annotation)
2281 && engine.resolve_symbol(symbol).is_unknown()
2282 {
2283 engine.seed_symbol(symbol.to_string(), receiver_type);
2284 *changed = true;
2285 }
2286}
2287
2288enum ScopeFactEvent {
2289 Parameter {
2290 symbol: String,
2291 annotation: Option<String>,
2292 },
2293 Annotation {
2294 symbol: String,
2295 annotation: String,
2296 },
2297 Assignment {
2298 lhs: String,
2299 rhs: AssignmentRhs,
2300 },
2301}
2302
2303enum AssignmentRhs {
2304 Symbol(String),
2305 Call(String),
2306 Unknown,
2307}
2308
2309#[derive(Clone, Copy)]
2310enum ScopeFactTraversal {
2311 Module,
2312 Function,
2313 Class,
2314}
2315
2316fn collect_scope_fact_events(source: &str, traversal: ScopeFactTraversal) -> Vec<ScopeFactEvent> {
2317 if source.trim().is_empty() {
2318 return Vec::new();
2319 }
2320
2321 let mut parser = Parser::new();
2322 if parser
2323 .set_language(&tree_sitter_python::LANGUAGE.into())
2324 .is_err()
2325 {
2326 return Vec::new();
2327 }
2328 let Some(tree) = parser.parse(source, None) else {
2329 return Vec::new();
2330 };
2331
2332 let mut events = Vec::new();
2333 collect_scope_fact_events_from_node(tree.root_node(), source, traversal, &mut events);
2334 events
2335}
2336
2337fn collect_scope_fact_events_from_node(
2338 root: Node<'_>,
2339 source: &str,
2340 traversal: ScopeFactTraversal,
2341 events: &mut Vec<ScopeFactEvent>,
2342) {
2343 let mut stack = vec![(root, false)];
2344 while let Some((node, inside_function)) = stack.pop() {
2345 let next_inside_function = match traversal {
2346 ScopeFactTraversal::Module => {
2347 if matches!(
2348 node.kind(),
2349 "function_definition" | "class_definition" | "lambda"
2350 ) {
2351 continue;
2352 }
2353 false
2354 }
2355 ScopeFactTraversal::Function => match node.kind() {
2356 "function_definition" if inside_function => continue,
2357 "function_definition" => true,
2358 "class_definition" | "lambda" => continue,
2359 _ => inside_function,
2360 },
2361 ScopeFactTraversal::Class => inside_function,
2362 };
2363 if matches!(traversal, ScopeFactTraversal::Function) && !next_inside_function {
2364 let mut cursor = node.walk();
2365 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2366 children.reverse();
2367 stack.extend(children.into_iter().map(|child| (child, false)));
2368 continue;
2369 }
2370 match node.kind() {
2371 "parameters" | "lambda_parameters" => collect_parameter_events(node, source, events),
2372 "assignment" => collect_assignment_events(node, source, events),
2373 _ => {}
2374 }
2375
2376 let mut cursor = node.walk();
2377 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2378 children.reverse();
2379 stack.extend(
2380 children
2381 .into_iter()
2382 .map(|child| (child, next_inside_function)),
2383 );
2384 }
2385}
2386
2387fn collect_parameter_events(node: Node<'_>, source: &str, events: &mut Vec<ScopeFactEvent>) {
2388 let mut cursor = node.walk();
2389 for child in node.named_children(&mut cursor) {
2390 if child.kind() == "type_parameter" {
2391 continue;
2392 }
2393 let Some(symbol) = parameter_symbol(child, source) else {
2394 continue;
2395 };
2396 if matches!(symbol.as_str(), "self" | "cls" | "/") {
2397 continue;
2398 }
2399 let annotation = child
2400 .child_by_field_name("type")
2401 .map(|annotation| slice(annotation, source).trim().to_string())
2402 .filter(|annotation| !annotation.is_empty());
2403 events.push(ScopeFactEvent::Parameter { symbol, annotation });
2404 }
2405}
2406
2407fn parameter_symbol(node: Node<'_>, source: &str) -> Option<String> {
2408 if node.kind() == "identifier" {
2409 return non_empty_node_text(node, source);
2410 }
2411 if let Some(name) = node.child_by_field_name("name") {
2412 return non_empty_node_text(name, source);
2413 }
2414 let mut cursor = node.walk();
2415 node.named_children(&mut cursor)
2416 .find(|child| child.kind() == "identifier")
2417 .and_then(|identifier| non_empty_node_text(identifier, source))
2418}
2419
2420fn collect_assignment_events(node: Node<'_>, source: &str, events: &mut Vec<ScopeFactEvent>) {
2421 let Some(left) = node.child_by_field_name("left") else {
2422 return;
2423 };
2424 let Some(lhs) = receiver_symbol(left, source) else {
2425 return;
2426 };
2427
2428 if let Some(annotation) = node
2429 .child_by_field_name("type")
2430 .map(|annotation| slice(annotation, source).trim().to_string())
2431 .filter(|annotation| !annotation.is_empty())
2432 {
2433 events.push(ScopeFactEvent::Annotation {
2434 symbol: lhs,
2435 annotation,
2436 });
2437 return;
2438 }
2439
2440 let rhs = node
2441 .child_by_field_name("right")
2442 .and_then(|right| rhs_symbol(right, source))
2443 .unwrap_or(AssignmentRhs::Unknown);
2444 events.push(ScopeFactEvent::Assignment { lhs, rhs });
2445}
2446
2447fn receiver_symbol(node: Node<'_>, source: &str) -> Option<String> {
2448 match node.kind() {
2449 "identifier" | "attribute" => non_empty_node_text(node, source),
2450 _ => None,
2451 }
2452}
2453
2454fn rhs_symbol(node: Node<'_>, source: &str) -> Option<AssignmentRhs> {
2455 match node.kind() {
2456 "identifier" | "attribute" => non_empty_node_text(node, source).map(AssignmentRhs::Symbol),
2457 "call" => node
2458 .child_by_field_name("function")
2459 .or_else(|| node.named_child(0))
2460 .and_then(|callee| receiver_symbol(callee, source))
2461 .map(AssignmentRhs::Call),
2462 _ => None,
2463 }
2464}
2465
2466fn non_empty_node_text(node: Node<'_>, source: &str) -> Option<String> {
2467 let text = slice(node, source).trim();
2468 (!text.is_empty()).then(|| text.to_string())
2469}
2470
2471fn collect_factory_return_types_from_root(root: Node<'_>, source: &str) -> HashMap<String, String> {
2472 let mut returns = HashMap::default();
2473 let mut stack = vec![(root, None::<String>)];
2474 while let Some((node, class_name)) = stack.pop() {
2475 match node.kind() {
2476 "class_definition" => {
2477 let next_class = node
2478 .child_by_field_name("name")
2479 .and_then(|name| non_empty_node_text(name, source))
2480 .or(class_name);
2481 push_factory_index_children(node, next_class, &mut stack);
2482 }
2483 "function_definition" => {
2484 if let Some(name) = node
2485 .child_by_field_name("name")
2486 .and_then(|name| non_empty_node_text(name, source))
2487 && let Some(return_type) = factory_return_type(node, source)
2488 {
2489 let key = class_name
2490 .as_ref()
2491 .map(|class| format!("{class}.{name}"))
2492 .unwrap_or(name);
2493 returns.insert(key, return_type);
2494 }
2495 }
2496 _ => push_factory_index_children(node, class_name, &mut stack),
2497 }
2498 }
2499 returns
2500}
2501
2502fn push_factory_index_children<'tree>(
2503 node: Node<'tree>,
2504 class_name: Option<String>,
2505 stack: &mut Vec<(Node<'tree>, Option<String>)>,
2506) {
2507 let mut cursor = node.walk();
2508 let mut children: Vec<Node<'tree>> = node.named_children(&mut cursor).collect();
2509 children.reverse();
2510 stack.extend(
2511 children
2512 .into_iter()
2513 .map(|child| (child, class_name.clone())),
2514 );
2515}
2516
2517fn factory_return_type(function: Node<'_>, source: &str) -> Option<String> {
2518 if let Some(return_type) = function.child_by_field_name("return_type") {
2519 let raw = slice(return_type, source).trim();
2520 return normalized_receiver_type(raw);
2521 }
2522
2523 let body = function.child_by_field_name("body")?;
2524 let mut candidates = HashSet::default();
2525 let mut saw_return = false;
2526 let mut saw_unknown_return = false;
2527 let mut stack = vec![body];
2528 while let Some(node) = stack.pop() {
2529 if node != body && matches!(node.kind(), "function_definition" | "class_definition") {
2530 continue;
2531 }
2532 if node.kind() == "return_statement" {
2533 saw_return = true;
2534 match node
2535 .named_child(0)
2536 .and_then(|value| returned_receiver_type(value, source))
2537 {
2538 Some(returned_type) => {
2539 candidates.insert(returned_type);
2540 }
2541 None => saw_unknown_return = true,
2542 }
2543 }
2544 let mut cursor = node.walk();
2545 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2546 children.reverse();
2547 stack.extend(children);
2548 }
2549 if !saw_return || saw_unknown_return {
2550 return None;
2551 }
2552 (candidates.len() == 1)
2553 .then(|| candidates.into_iter().next())
2554 .flatten()
2555}
2556
2557fn returned_receiver_type(node: Node<'_>, source: &str) -> Option<String> {
2558 let raw = match node.kind() {
2559 "identifier" => non_empty_node_text(node, source),
2560 "call" => node
2561 .child_by_field_name("function")
2562 .or_else(|| node.named_child(0))
2563 .filter(|callee| callee.kind() == "identifier")
2564 .and_then(|callee| non_empty_node_text(callee, source)),
2565 _ => None,
2566 }?;
2567 normalized_receiver_type(&raw)
2568}
2569
2570#[cfg(test)]
2571mod tests {
2572 use super::*;
2573 use std::path::PathBuf;
2574
2575 #[test]
2576 fn lambda_scope_facts_preserve_untyped_parameter_shadowing() {
2577 let source = "shadowed = lambda method: method.signature\n";
2578 let mut parser = Parser::new();
2579 parser
2580 .set_language(&tree_sitter_python::LANGUAGE.into())
2581 .unwrap();
2582 let tree = parser.parse(source, None).unwrap();
2583 let mut nodes = vec![tree.root_node()];
2584 let lambda = loop {
2585 let node = nodes.pop().unwrap();
2586 if node.kind() == "lambda" {
2587 break node;
2588 }
2589 let mut cursor = node.walk();
2590 nodes.extend(node.named_children(&mut cursor));
2591 };
2592
2593 let facts = collect_function_scope_facts_from_node(lambda, source);
2594
2595 assert!(facts.is_shadowed("method"));
2596 assert!(facts.resolution_for("method").is_unknown());
2597 }
2598
2599 #[test]
2600 fn pre_cancelled_graph_build_skips_python_file_parsing() {
2601 let temp = tempfile::tempdir().unwrap();
2602 let root = temp.path().canonicalize().unwrap();
2603 std::fs::write(root.join("target.py"), "def target():\n pass\n").unwrap();
2604 let file = ProjectFile::new(root.clone(), PathBuf::from("target.py"));
2605 let files = [file.clone()].into_iter().collect();
2606 let cancellation = CancellationToken::default();
2607 cancellation.cancel();
2608
2609 let graph = build_python_graph(&files, &file, Some(&cancellation));
2610
2611 assert!(graph.parsed.is_empty());
2612 }
2613
2614 #[test]
2615 fn graph_build_parses_only_candidates_and_target_not_transitive_imports() {
2616 let temp = tempfile::tempdir().unwrap();
2617 let root = temp.path().canonicalize().unwrap();
2618 std::fs::write(root.join("target.py"), "from dependency import value\n").unwrap();
2619 std::fs::write(
2620 root.join("candidate.py"),
2621 "from transitively_imported import value\n",
2622 )
2623 .unwrap();
2624 std::fs::write(root.join("dependency.py"), "value = 1\n").unwrap();
2625 std::fs::write(root.join("transitively_imported.py"), "value = 2\n").unwrap();
2626 let target = ProjectFile::new(root.clone(), PathBuf::from("target.py"));
2627 let candidate = ProjectFile::new(root.clone(), PathBuf::from("candidate.py"));
2628 let dependency = ProjectFile::new(root.clone(), PathBuf::from("dependency.py"));
2629 let transitive = ProjectFile::new(root.clone(), PathBuf::from("transitively_imported.py"));
2630 let candidates = [candidate.clone()].into_iter().collect();
2631
2632 let graph = build_python_graph(&candidates, &target, None);
2633
2634 assert_eq!(graph.parsed.len(), 2);
2635 assert!(graph.parsed.contains_key(&target));
2636 assert!(graph.parsed.contains_key(&candidate));
2637 assert!(!graph.parsed.contains_key(&dependency));
2638 assert!(!graph.parsed.contains_key(&transitive));
2639 }
2640}