1use super::extractor::{
20 call_result_types, collect_assigned_identifiers, collect_function_scope_facts_from_node,
21 collect_scope_facts_from_parsed_source, enclosing_scope_facts, is_declaration_identifier,
22 slice,
23};
24use super::resolver::{
25 annotation_reference_candidates, resolve_callable_parameter_default_types,
26 resolve_constructor_types, resolve_receiver_type, resolved_member_declarations,
27};
28use crate::graph::PythonGraphSource;
29use crate::graph_support::PythonUsageSource;
30use crate::imports::{imported_module_assignment_at, resolve_fqn_candidates};
31use crate::usage_index::{usage_resolve_module_files, usage_scope_facts};
32use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path;
33use brokk_bifrost_core::analyzer::usages::inverted_edges::{
34 FileEdgeScanInput, PerFileEdges, classify_reference_node,
35};
36use brokk_bifrost_core::analyzer::usages::local_inference::LocalBindingsSnapshot;
37use brokk_bifrost_core::analyzer::usages::model::ImportKind;
38use brokk_bifrost_core::analyzer::{CodeUnit, Language, ProjectFile, Range};
39use brokk_bifrost_core::hash::{HashMap, HashSet};
40use std::sync::{Arc, Mutex};
41use tree_sitter::Node;
42
43pub struct PythonEdgeScan<'a> {
50 targets: Option<&'a HashSet<String>>,
51 targets_by_terminal: HashMap<String, Vec<String>>,
52 canonical_namespace_candidates: Mutex<HashMap<String, Arc<Vec<String>>>>,
53}
54
55impl<'a> PythonEdgeScan<'a> {
56 pub fn new(nodes: &HashSet<String>, targets: &'a HashSet<String>) -> Self {
59 debug_assert!(targets.is_subset(nodes));
60 let mut targets_by_terminal: HashMap<String, Vec<String>> = HashMap::default();
61 for target in targets {
62 let terminal = parse_symbol_path(Language::Python, target)
67 .pop()
68 .unwrap_or_else(|| target.clone());
69 targets_by_terminal
70 .entry(terminal)
71 .or_default()
72 .push(target.clone());
73 }
74 Self {
75 targets: Some(targets),
76 targets_by_terminal,
77 canonical_namespace_candidates: Mutex::new(HashMap::default()),
78 }
79 }
80
81 pub fn new_rooted() -> Self {
85 Self {
86 targets: None,
87 targets_by_terminal: HashMap::default(),
88 canonical_namespace_candidates: Mutex::new(HashMap::default()),
89 }
90 }
91
92 pub fn scan_file(
98 &self,
99 graph: &PythonGraphSource<'_>,
100 python: &dyn PythonUsageSource,
101 file: &ProjectFile,
102 input: &FileEdgeScanInput<'_>,
103 ) -> PerFileEdges {
104 let source = input.source;
105
106 let binder = python.import_binder_of(file);
111 let mut named: HashMap<String, String> = HashMap::default();
112 let mut namespace: HashMap<String, NamespaceBinding> = HashMap::default();
113 for (local, binding) in &binder.bindings {
114 match binding.kind {
115 ImportKind::Named => {
116 if let Some(imported) = &binding.imported_name {
117 let module = canonical_import_module_fqn(
118 graph,
119 python,
120 file,
121 &binding.module_specifier,
122 )
123 .unwrap_or_else(|| binding.module_specifier.clone());
124 let imported_fqn = if module.ends_with('.') {
125 format!("{module}{imported}")
126 } else {
127 format!("{module}.{imported}")
128 };
129 if let Some(imported_module) =
130 canonical_import_module_fqn(graph, python, file, &imported_fqn)
131 {
132 namespace.insert(
133 local.clone(),
134 NamespaceBinding {
135 module: imported_module,
136 workspace_module: true,
137 consumed_attributes: 0,
138 },
139 );
140 } else {
141 named.insert(local.clone(), imported_fqn);
142 }
143 }
144 }
145 ImportKind::Namespace => {
146 let direct_module = binding.module_specifier.clone();
147 let imported_module = binding
148 .namespace_imported_module
149 .as_deref()
150 .unwrap_or(&direct_module);
151 let module = canonical_import_module_fqn(graph, python, file, imported_module);
152 let workspace_module = module.is_some();
153 let consumed_attributes = module.as_ref().map_or(0, |_| {
154 let imported_segments =
155 parse_symbol_path(Language::Python, imported_module);
156 let bound_segments = parse_symbol_path(Language::Python, &direct_module);
157 imported_segments.len().saturating_sub(bound_segments.len())
158 });
159 namespace.insert(
160 local.clone(),
161 NamespaceBinding {
162 module: module.unwrap_or(direct_module),
163 workspace_module,
164 consumed_attributes,
165 },
166 );
167 }
168 ImportKind::Default | ImportKind::CommonJsRequire | ImportKind::Glob => {}
169 }
170 }
171 let same_file: HashMap<String, String> = graph
172 .index
173 .declarations(file)
174 .into_iter()
175 .map(|unit| (unit.identifier().to_string(), unit.fq_name()))
176 .collect();
177
178 let scope_facts = usage_scope_facts(python, file, || {
182 collect_scope_facts_from_parsed_source(graph, python, file, source, input.root())
183 });
184
185 let mut ctx = PyScan {
186 graph,
187 python,
188 targets: self.targets,
189 targets_by_terminal: &self.targets_by_terminal,
190 file,
191 source,
192 named,
193 namespace,
194 same_file,
195 scope_facts: scope_facts.as_ref(),
196 canonical_namespace_candidates: &self.canonical_namespace_candidates,
197 input,
198 edges: PerFileEdges::default(),
199 };
200 scan_tree(input.root(), &mut ctx);
201 ctx.edges
202 }
203}
204
205fn canonical_import_module_fqn(
206 graph: &PythonGraphSource<'_>,
207 python: &dyn PythonUsageSource,
208 importing_file: &ProjectFile,
209 module_specifier: &str,
210) -> Option<String> {
211 let resolved = usage_resolve_module_files(python, importing_file, module_specifier);
212 let [module_file] = resolved.as_slice() else {
213 return None;
214 };
215 graph
216 .index
217 .declarations(module_file)
218 .into_iter()
219 .find(CodeUnit::is_module)
220 .map(|module| module.fq_name())
221}
222
223struct PyScan<'a> {
224 graph: &'a PythonGraphSource<'a>,
225 python: &'a dyn PythonUsageSource,
226 targets: Option<&'a HashSet<String>>,
227 targets_by_terminal: &'a HashMap<String, Vec<String>>,
228 file: &'a ProjectFile,
229 source: &'a str,
230 named: HashMap<String, String>,
231 namespace: HashMap<String, NamespaceBinding>,
232 same_file: HashMap<String, String>,
233 scope_facts: &'a HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
234 canonical_namespace_candidates: &'a Mutex<HashMap<String, Arc<Vec<String>>>>,
235 input: &'a FileEdgeScanInput<'a>,
236 edges: PerFileEdges,
237}
238
239struct NamespaceBinding {
240 module: String,
241 workspace_module: bool,
242 consumed_attributes: usize,
243}
244
245impl PyScan<'_> {
246 fn bare_callee(&self, text: &str) -> Option<String> {
249 if let Some(fqn) = self.named.get(text) {
250 return Some(fqn.clone());
251 }
252 if let Some(fqn) = self.namespace.get(text) {
253 return Some(fqn.module.clone());
254 }
255 if let Some(fqn) = self.same_file.get(text) {
256 return Some(fqn.clone());
257 }
258 None
259 }
260
261 fn receiver_type_fqn(
265 &self,
266 facts: &LocalBindingsSnapshot<String>,
267 receiver: &str,
268 ) -> Option<String> {
269 let resolution = facts.resolution_for(receiver);
270 let type_name = resolution
271 .as_precise()
272 .and_then(|targets| targets.iter().next())?;
273 resolve_receiver_type(self.graph, self.python, self.file, type_name, false)
279 .map(|unit| unit.fq_name())
280 }
281
282 fn record(&mut self, callee: String, node: Node<'_>) {
283 if !self.accepts_target(&callee) {
284 return;
285 }
286 self.edges.record_kind(
287 self.input,
288 callee,
289 classify_reference_node(node),
290 node.start_byte(),
291 node.end_byte(),
292 );
293 }
294
295 fn record_unproven_name(&mut self, name: &str, node: Node<'_>) {
296 let Some(targets) = self.targets_by_terminal.get(name) else {
297 return;
298 };
299 for target in targets {
300 self.edges.record_unproven(
301 self.input,
302 target.clone(),
303 node.start_byte(),
304 node.end_byte(),
305 );
306 }
307 }
308
309 fn accepts_target(&self, fqn: &str) -> bool {
310 self.targets.map_or_else(
311 || self.graph.index.definitions(fqn).next().is_some(),
312 |targets| targets.contains(fqn),
313 )
314 }
315
316 fn may_have_target_terminal(&self, terminal: &str) -> bool {
317 self.targets.is_none() || self.targets_by_terminal.contains_key(terminal)
318 }
319
320 fn canonical_namespace_candidates(&self, direct: &str) -> Arc<Vec<String>> {
321 if let Some(cached) = self
322 .canonical_namespace_candidates
323 .lock()
324 .expect("Python namespace candidate cache mutex poisoned")
325 .get(direct)
326 .cloned()
327 {
328 return cached;
329 }
330
331 let resolved: Arc<Vec<String>> = Arc::new(
332 resolve_fqn_candidates(self.python, direct, |name| {
333 self.graph.index.definitions(name).collect()
334 })
335 .into_iter()
336 .map(|unit| unit.fq_name())
337 .collect(),
338 );
339 self.canonical_namespace_candidates
340 .lock()
341 .expect("Python namespace candidate cache mutex poisoned")
342 .entry(direct.to_string())
343 .or_insert_with(|| resolved.clone())
344 .clone()
345 }
346}
347
348fn scan_tree(root: Node<'_>, ctx: &mut PyScan<'_>) {
349 let mut scopes: Vec<FunctionScope> = Vec::new();
352 walk(root, ctx, &mut scopes, None);
353}
354
355fn walk(
356 node: Node<'_>,
357 ctx: &mut PyScan<'_>,
358 scopes: &mut Vec<FunctionScope>,
359 facts: Option<usize>,
360) {
361 let mut merged_facts = Vec::new();
362 let mut stack = vec![WalkFrame::Enter { node, facts }];
363 while let Some(frame) = stack.pop() {
364 match frame {
365 WalkFrame::Enter { node, facts } => match node.kind() {
366 "import_statement" | "import_from_statement" => {}
367 "function_definition" | "lambda" => {
371 let function_scope = collect_function_scope(node, ctx.source);
372 let scope_facts = merged_enclosing_scope_facts(
373 ctx.graph,
374 ctx.file,
375 ctx.scope_facts,
376 &mut merged_facts,
377 node,
378 ctx.source,
379 facts,
380 );
381 push_function_children(node, facts, scope_facts, function_scope, &mut stack);
382 }
383 "class_definition" => push_children(node, None, &mut stack),
386 "identifier" => {
387 if !handle_annotation_reference(node, ctx) {
388 handle_identifier(node, ctx, scopes);
389 }
390 push_children(node, facts, &mut stack);
391 }
392 "attribute" => {
393 if handle_annotation_reference(node, ctx) {
394 continue;
395 }
396 let scope_facts = facts.and_then(|id| merged_facts.get(id));
397 handle_attribute(node, ctx, scopes, scope_facts);
398 push_children(node, facts, &mut stack);
399 }
400 "string_content" => {
401 handle_annotation_reference(node, ctx);
402 }
403 "keyword_argument" => {
404 handle_keyword_argument(node, ctx, scopes);
405 if let Some(value) = node.child_by_field_name("value") {
406 stack.push(WalkFrame::Enter { node: value, facts });
407 }
408 }
409 _ => push_children(node, facts, &mut stack),
410 },
411 WalkFrame::ExitScope => {
412 scopes.pop();
413 }
414 WalkFrame::EnterScope(scope) => scopes.push(scope),
415 }
416 }
417}
418
419enum WalkFrame<'tree> {
420 Enter {
421 node: Node<'tree>,
422 facts: Option<usize>,
423 },
424 EnterScope(FunctionScope),
425 ExitScope,
426}
427
428fn push_children<'tree>(
429 node: Node<'tree>,
430 facts: Option<usize>,
431 stack: &mut Vec<WalkFrame<'tree>>,
432) {
433 for index in (0..node.named_child_count()).rev() {
434 if let Some(child) = node.named_child(index) {
435 stack.push(WalkFrame::Enter { node: child, facts });
436 }
437 }
438}
439
440fn push_function_children<'tree>(
441 function: Node<'tree>,
442 enclosing_facts: Option<usize>,
443 body_facts: Option<usize>,
444 function_scope: FunctionScope,
445 stack: &mut Vec<WalkFrame<'tree>>,
446) {
447 let body = function.child_by_field_name("body");
448 let mut function_scope = Some(function_scope);
449 for index in (0..function.named_child_count()).rev() {
450 if let Some(child) = function.named_child(index) {
451 let facts = if body == Some(child) {
455 body_facts
456 } else {
457 enclosing_facts
458 };
459 if body == Some(child) {
460 stack.push(WalkFrame::ExitScope);
461 stack.push(WalkFrame::Enter { node: child, facts });
462 stack.push(WalkFrame::EnterScope(
463 function_scope
464 .take()
465 .expect("a function has exactly one body scope"),
466 ));
467 } else {
468 stack.push(WalkFrame::Enter { node: child, facts });
469 }
470 }
471 }
472}
473
474fn merged_enclosing_scope_facts(
475 graph: &PythonGraphSource<'_>,
476 file: &ProjectFile,
477 scope_facts: &HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
478 merged_facts: &mut Vec<LocalBindingsSnapshot<String>>,
479 node: Node<'_>,
480 source: &str,
481 inherited: Option<usize>,
482) -> Option<usize> {
483 let structural_local = collect_function_scope_facts_from_node(node, source);
484 let local = if inherited.is_none() && node.kind() == "function_definition" {
489 enclosing_scope_facts(graph.index, file, scope_facts, node)
490 .cloned()
491 .unwrap_or(structural_local)
492 } else {
493 structural_local
494 };
495 match (local, inherited) {
496 (local, Some(inherited_id)) => {
497 let inherited = merged_facts.get(inherited_id)?;
498 let merged = inherited.merged_with_shadowing(&local);
499 let next_id = merged_facts.len();
500 merged_facts.push(merged);
501 Some(next_id)
502 }
503 (local, None) => {
504 let next_id = merged_facts.len();
505 merged_facts.push(local);
506 Some(next_id)
507 }
508 }
509}
510
511#[derive(Default)]
512struct FunctionScope {
513 locals: HashSet<String>,
514 parameters: HashSet<String>,
515}
516
517fn is_shadowed(scopes: &[FunctionScope], name: &str) -> bool {
518 scopes.iter().any(|scope| scope.locals.contains(name))
519}
520
521fn is_receiver_parameter(scopes: &[FunctionScope], name: &str) -> bool {
522 scopes
523 .iter()
524 .rev()
525 .any(|scope| scope.parameters.contains(name))
526}
527
528fn handle_identifier(node: Node<'_>, ctx: &mut PyScan<'_>, scopes: &[FunctionScope]) {
529 if node
531 .parent()
532 .is_some_and(|parent| parent.kind() == "attribute")
533 {
534 return;
535 }
536 if is_declaration_identifier(node) {
537 return;
538 }
539 let text = slice(node, ctx.source);
540 if text.is_empty() || is_shadowed(scopes, text) {
541 return;
542 }
543 if let Some(callee) = ctx.bare_callee(text) {
544 ctx.record(callee, node);
545 }
546}
547
548fn handle_annotation_reference(node: Node<'_>, ctx: &mut PyScan<'_>) -> bool {
549 let Some(candidates) =
550 annotation_reference_candidates(ctx.graph, ctx.python, ctx.file, ctx.source, node, false)
551 else {
552 return false;
553 };
554 let [candidate] = candidates.as_slice() else {
555 return !(node.kind() == "attribute" && candidates.is_empty());
556 };
557
558 let site = if node.kind() == "attribute" {
559 node.child_by_field_name("attribute").unwrap_or(node)
560 } else {
561 node
562 };
563 ctx.record(candidate.fq_name(), site);
564 true
565}
566
567fn handle_attribute(
568 node: Node<'_>,
569 ctx: &mut PyScan<'_>,
570 scopes: &[FunctionScope],
571 facts: Option<&LocalBindingsSnapshot<String>>,
572) {
573 let (Some(object), Some(attribute)) = (
574 node.child_by_field_name("object"),
575 node.child_by_field_name("attribute"),
576 ) else {
577 return;
578 };
579 let object_text = slice(object, ctx.source);
580 let attribute_text = slice(attribute, ctx.source);
581 if object_text.is_empty() || attribute_text.is_empty() {
582 return;
583 }
584 if object.kind() == "identifier"
585 && ctx.may_have_target_terminal(attribute_text)
586 && let Some(module) =
587 imported_module_assignment_at(node, object_text, ctx.source, |local| {
588 !is_shadowed(scopes, local)
589 && ctx.namespace.get(local).is_some_and(|binding| {
590 binding.module == "importlib" && binding.consumed_attributes == 0
591 })
592 })
593 {
594 let direct = format!("{module}.{attribute_text}");
595 if ctx.accepts_target(&direct) {
596 ctx.record(direct, attribute);
597 } else {
598 for resolved in ctx.canonical_namespace_candidates(&direct).iter() {
599 ctx.record(resolved.clone(), attribute);
600 }
601 }
602 }
603 if object.kind() == "call" && ctx.may_have_target_terminal(attribute_text) {
604 for class in call_result_types(ctx.graph, ctx.python, ctx.file, ctx.source, object, facts) {
605 let direct = format!("{}.{attribute_text}", class.fq_name());
606 if ctx.accepts_target(&direct) {
607 ctx.record(direct, attribute);
608 continue;
609 }
610 if let Some(provider) = ctx.graph.hierarchy {
611 for ancestor in provider.get_ancestors(&class) {
612 let inherited = format!("{}.{attribute_text}", ancestor.fq_name());
613 if ctx.accepts_target(&inherited) {
614 ctx.record(inherited, attribute);
615 }
616 }
617 }
618 }
619 }
620 if let Some((root, attributes)) = attribute_chain(node) {
624 let root_text = slice(root, ctx.source);
625 if !root_text.is_empty()
626 && !is_shadowed(scopes, root_text)
627 && let Some(binding) = ctx.namespace.get(root_text)
628 {
629 let mut direct = binding.module.clone();
630 let workspace_module = binding.workspace_module;
631 let consumed_attributes = binding.consumed_attributes;
632 if object.kind() == "identifier" && ctx.accepts_target(&direct) {
633 ctx.record(direct.clone(), object);
634 }
635 for member in attributes.into_iter().skip(consumed_attributes) {
636 let member_text = slice(member, ctx.source);
637 if member_text.is_empty() {
638 return;
639 }
640 direct.push('.');
641 direct.push_str(member_text);
642 }
643 if ctx.accepts_target(&direct) {
644 ctx.record(direct, attribute);
645 return;
646 }
647 if workspace_module {
653 for resolved in ctx.canonical_namespace_candidates(&direct).iter() {
654 ctx.record(resolved.clone(), attribute);
655 }
656 }
657 return;
658 }
659 }
660
661 if let Some(facts) = facts
667 && ctx.may_have_target_terminal(attribute_text)
668 {
669 if matches!(object_text, "self" | "cls") {
670 ctx.record_unproven_name(attribute_text, attribute);
676 } else if let Some(type_fqn) = ctx.receiver_type_fqn(facts, object_text) {
677 ctx.record(format!("{type_fqn}.{attribute_text}"), attribute);
678 } else if object.kind() == "identifier" && !ctx.named.contains_key(object_text) {
679 let resolution = facts.resolution_for(object_text);
680 if resolution.is_ambiguous()
681 || (resolution.is_unknown() && is_receiver_parameter(scopes, object_text))
682 {
683 ctx.record_unproven_name(attribute_text, attribute);
684 }
685 }
686 }
687}
688
689fn handle_keyword_argument(node: Node<'_>, ctx: &mut PyScan<'_>, scopes: &[FunctionScope]) {
690 let (Some(name), Some(arguments)) = (node.child_by_field_name("name"), node.parent()) else {
691 return;
692 };
693 if name.kind() != "identifier" || arguments.kind() != "argument_list" {
694 return;
695 }
696 let Some(call) = arguments.parent().filter(|parent| parent.kind() == "call") else {
697 return;
698 };
699 let Some(function) = call.child_by_field_name("function") else {
700 return;
701 };
702 let member = slice(name, ctx.source);
703 if member.is_empty() || !ctx.may_have_target_terminal(member) {
704 return;
705 }
706 let scoped_class_fqn = if function.kind() == "identifier" {
707 enclosing_scope_facts(ctx.graph.index, ctx.file, ctx.scope_facts, function)
708 .and_then(|facts| ctx.receiver_type_fqn(facts, slice(function, ctx.source)))
709 } else {
710 None
711 };
712 let function_name = (function.kind() == "identifier").then(|| slice(function, ctx.source));
713 let mut default_classes = function_name.map_or_else(Vec::new, |local_name| {
714 resolve_callable_parameter_default_types(
715 ctx.graph, ctx.python, ctx.file, ctx.source, function, local_name,
716 )
717 });
718 let root_shadowed = leftmost_identifier(function)
719 .is_some_and(|root| is_shadowed(scopes, slice(root, ctx.source)));
720 let mut classes = if function_name == Some("cls") {
721 lexical_class(ctx, function).into_iter().collect()
722 } else {
723 if root_shadowed && scoped_class_fqn.is_none() && default_classes.is_empty() {
724 return;
725 }
726 if !root_shadowed {
727 default_classes.extend(resolve_constructor_types(
728 ctx.graph, ctx.python, ctx.file, ctx.source, function,
729 ));
730 }
731 default_classes
732 };
733 if let Some(fqn) = scoped_class_fqn {
734 classes.extend(ctx.graph.index.definitions(&fqn).filter(CodeUnit::is_class));
735 classes.sort();
736 classes.dedup();
737 }
738 for class in classes {
739 for declaration in resolved_member_declarations(ctx.graph, &class, member) {
740 let fqn = declaration.fq_name();
741 if ctx.accepts_target(&fqn) {
742 ctx.record(fqn, name);
743 }
744 }
745 }
746}
747
748fn lexical_class(ctx: &PyScan<'_>, node: Node<'_>) -> Option<CodeUnit> {
749 let range = Range {
750 start_byte: node.start_byte(),
751 end_byte: node.end_byte(),
752 start_line: 0,
753 end_line: 0,
754 };
755 let enclosing = ctx.graph.index.enclosing_code_unit(ctx.file, &range)?;
756 if enclosing.is_class() {
757 Some(enclosing)
758 } else {
759 ctx.graph
760 .index
761 .parent_of(&enclosing)
762 .filter(CodeUnit::is_class)
763 }
764}
765
766fn leftmost_identifier(mut node: Node<'_>) -> Option<Node<'_>> {
767 loop {
768 match node.kind() {
769 "identifier" => return Some(node),
770 "attribute" => node = node.child_by_field_name("object")?,
771 _ => return None,
772 }
773 }
774}
775
776fn attribute_chain<'a>(node: Node<'a>) -> Option<(Node<'a>, Vec<Node<'a>>)> {
777 let mut attributes = Vec::new();
778 let mut current = node;
779 loop {
780 if current.kind() != "attribute" {
781 return None;
782 }
783 attributes.push(current.child_by_field_name("attribute")?);
784 current = current.child_by_field_name("object")?;
785 if current.kind() == "identifier" {
786 attributes.reverse();
787 return Some((current, attributes));
788 }
789 }
790}
791
792fn collect_function_scope(func: Node<'_>, source: &str) -> FunctionScope {
797 let mut scope = FunctionScope::default();
798 if let Some(params) = func.child_by_field_name("parameters") {
799 collect_parameter_names(params, source, &mut scope.parameters);
800 scope.locals.extend(scope.parameters.iter().cloned());
801 }
802 if let Some(body) = func.child_by_field_name("body") {
803 collect_bound_targets(body, source, &mut scope.locals);
804 }
805 scope
806}
807
808fn collect_parameter_names(params: Node<'_>, source: &str, out: &mut HashSet<String>) {
809 let mut cursor = params.walk();
810 for child in params.named_children(&mut cursor) {
811 let name = match child.kind() {
812 "identifier" => Some(child),
813 _ => child
816 .child_by_field_name("name")
817 .or_else(|| child.named_child(0).filter(|n| n.kind() == "identifier")),
818 };
819 if let Some(name) = name {
820 let text = slice(name, source).trim();
821 if !text.is_empty() {
822 out.insert(text.to_string());
823 }
824 }
825 }
826}
827
828fn collect_bound_targets(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
832 let mut stack = vec![node];
833 while let Some(node) = stack.pop() {
834 match node.kind() {
835 "function_definition" | "class_definition" => {
836 if let Some(name) = node.child_by_field_name("name") {
837 let text = slice(name, source).trim();
838 if !text.is_empty() {
839 out.insert(text.to_string());
840 }
841 }
842 continue;
843 }
844 "lambda" => continue,
845 "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => {
846 if let Some(left) = node.child_by_field_name("left") {
847 collect_assigned_identifiers(left, source, out);
848 }
849 }
850 "named_expression" => {
851 if let Some(name) = node.child_by_field_name("name") {
852 collect_assigned_identifiers(name, source, out);
853 }
854 }
855 _ => {}
856 }
857 let mut cursor = node.walk();
858 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
859 children.reverse();
860 stack.extend(children);
861 }
862}