1use crate::graph_support::PythonSource;
29use crate::imports::resolve_imports_batched;
30use brokk_bifrost_core::analyzer::model::{
31 ImportInfo, SemanticAbsenceProof, SemanticDiagnostic, SemanticDiagnosticDomain,
32 SemanticDiagnosticIncompleteReason, SemanticDiagnosticReport, StructuredImportPathKind,
33};
34use brokk_bifrost_core::analyzer::semantic_diagnostics::{
35 ScopeStack, contains_node, node_range, node_text, same_node,
36};
37use brokk_bifrost_core::analyzer::structural::facts::Span;
38use brokk_bifrost_core::analyzer::structural::resolution::BoundaryStatus;
39use brokk_bifrost_core::analyzer::tree_walk::{
40 WalkControl, collect_parse_errors, walk_tree_preorder,
41};
42use brokk_bifrost_core::analyzer::{BoundedDefinitionLookup, CodeUnit, ProjectFile, Range};
43use brokk_bifrost_core::hash::HashMap;
44use brokk_bifrost_core::text_utils::{compute_line_starts, find_line_index_for_offset};
45use tree_sitter::{Node, Parser};
46
47pub const PYTHON_UNRECOGNIZED_SYMBOL: &str = "python_unrecognized_symbol";
48pub const PYTHON_SEMANTIC_DIAGNOSTIC_SOURCE: &str = "bifrost-python";
49const MAX_PYTHON_SEMANTIC_DIAGNOSTIC_BYTES: usize = 512 * 1024;
50pub const MAX_PYTHON_SEMANTIC_DIAGNOSTICS: usize = 200;
51
52#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum PythonEnvironmentBoundary {
56 Indexed,
58 Absent,
61 Incomplete(SemanticDiagnosticIncompleteReason),
63}
64
65pub trait PythonEnvironmentSurface {
72 fn module_boundary(&self, module_path: &str) -> PythonEnvironmentBoundary;
74
75 fn attribute_boundary(&self, owner_path: &str, member: &str) -> PythonEnvironmentBoundary;
85}
86
87#[derive(Debug, Clone, Copy, Default)]
90pub struct UnacquiredPythonEnvironment;
91
92impl PythonEnvironmentSurface for UnacquiredPythonEnvironment {
93 fn module_boundary(&self, _module_path: &str) -> PythonEnvironmentBoundary {
94 unknown_boundary()
95 }
96
97 fn attribute_boundary(&self, _owner_path: &str, _member: &str) -> PythonEnvironmentBoundary {
98 unknown_boundary()
99 }
100}
101
102fn unknown_boundary() -> PythonEnvironmentBoundary {
103 PythonEnvironmentBoundary::Incomplete(
104 SemanticDiagnosticIncompleteReason::MissingDependencyDiscovery {
105 boundary: BoundaryStatus::ExternalUnknown,
106 },
107 )
108}
109
110pub fn collect_python_semantic_diagnostics(
112 py: &dyn PythonSource,
113 support: &dyn BoundedDefinitionLookup,
114 environment: &dyn PythonEnvironmentSurface,
115 file: &ProjectFile,
116 source: &str,
117) -> SemanticDiagnosticReport {
118 let mut report = SemanticDiagnosticReport::new();
119 if source.len() > MAX_PYTHON_SEMANTIC_DIAGNOSTIC_BYTES {
120 report.push_incomplete(None, vec![SemanticDiagnosticIncompleteReason::Truncated]);
121 return report;
122 }
123 let mut parser = Parser::new();
124 if parser
125 .set_language(&tree_sitter_python::LANGUAGE.into())
126 .is_err()
127 {
128 report.push_incomplete(
129 None,
130 vec![SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
131 detail: "Python parser is unavailable".to_string(),
132 }],
133 );
134 return report;
135 }
136 let Some(tree) = parser.parse(source, None) else {
137 report.push_incomplete(
138 None,
139 vec![SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
140 detail: "Python source did not parse".to_string(),
141 }],
142 );
143 return report;
144 };
145 let mut parse_errors = Vec::new();
146 collect_parse_errors(tree.root_node(), &mut parse_errors);
147 if !parse_errors.is_empty() {
148 report.push_incomplete(
153 None,
154 vec![SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
155 detail: "Python source has parse errors".to_string(),
156 }],
157 );
158 return report;
159 }
160
161 let line_starts = compute_line_starts(source);
162 let imports = py.import_info_of(file);
163 let resolved_imports = resolve_imports_batched(py, file, &imports);
164 let dynamic = dynamic_surface_reasons(&imports, &resolved_imports, source, tree.root_node());
165 if !dynamic.is_empty() {
166 report.push_incomplete(None, dynamic);
170 return report;
171 }
172
173 let mut collector = PythonDiagnosticCollector {
174 py,
175 support,
176 environment,
177 file,
178 source,
179 line_starts: &line_starts,
180 module_name: crate::declarations::python_module_name(file),
181 module_binders: HashMap::default(),
182 report,
183 };
184 collector.classify_imports(&imports, &resolved_imports);
185 collector.scan_tree(tree.root_node(), &imports, &resolved_imports);
186 collector.report
187}
188
189struct PythonDiagnosticCollector<'a> {
190 py: &'a dyn PythonSource,
191 support: &'a dyn BoundedDefinitionLookup,
192 environment: &'a dyn PythonEnvironmentSurface,
193 file: &'a ProjectFile,
194 source: &'a str,
195 line_starts: &'a [usize],
196 module_name: String,
197 module_binders: HashMap<String, String>,
201 report: SemanticDiagnosticReport,
202}
203
204enum ScanFrame<'tree> {
205 Node(Node<'tree>),
206 ExitScope,
207 SeedTargets(Node<'tree>),
208}
209
210impl PythonDiagnosticCollector<'_> {
211 fn classify_imports(&mut self, imports: &[ImportInfo], resolved: &[Vec<(String, CodeUnit)>]) {
216 debug_assert_eq!(imports.len(), resolved.len());
217 for (import, resolved) in imports.iter().zip(resolved) {
218 let range = import
219 .binder_span
220 .map(|span| span_range(span, self.line_starts));
221 let Some(path) = import.path.as_ref() else {
222 self.report.push_incomplete(
223 range,
224 vec![SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
225 detail: format!(
226 "import `{}` records no parser-derived path",
227 import.raw_snippet
228 ),
229 }],
230 );
231 continue;
232 };
233 let namespace_import = path.kind != Some(StructuredImportPathKind::ImportFrom);
234 if namespace_import && let Some(name) = import.local_name() {
235 let bound = if import.alias.is_some() {
239 path.render_segments(".")
240 } else {
241 path.segments.first().cloned().unwrap_or_default()
242 };
243 if !bound.is_empty() {
244 self.module_binders.insert(name.to_string(), bound);
245 }
246 }
247 if !resolved.is_empty() {
248 if let Some(range) = range {
252 self.report
253 .push_resolved(range, BoundaryStatus::WorkspaceLocal);
254 }
255 continue;
256 }
257 let (owner, boundary) = if namespace_import {
258 let module = path.render_segments(".");
259 let boundary = self.environment.module_boundary(&module);
260 (SemanticDiagnosticDomain::Module { name: module }, boundary)
261 } else if let Some((member, module)) = path.segments.split_last() {
262 let module = module.join(".");
265 let boundary = self.environment.attribute_boundary(&module, member);
266 (SemanticDiagnosticDomain::Module { name: module }, boundary)
267 } else {
268 self.report.push_incomplete(
269 range,
270 vec![SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
271 detail: format!("import `{}` records no path segments", import.raw_snippet),
272 }],
273 );
274 continue;
275 };
276 let Some(range) = range else {
277 self.report.push_incomplete(
280 None,
281 vec![SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
282 detail: format!(
283 "import `{}` binds no single name token",
284 import.raw_snippet
285 ),
286 }],
287 );
288 continue;
289 };
290 self.record_boundary(range, owner, import.raw_snippet.as_str(), boundary);
291 }
292 }
293
294 fn record_boundary(
296 &mut self,
297 range: Range,
298 domain: SemanticDiagnosticDomain,
299 subject: &str,
300 boundary: PythonEnvironmentBoundary,
301 ) {
302 match boundary {
303 PythonEnvironmentBoundary::Indexed => self
304 .report
305 .push_resolved(range, BoundaryStatus::ExternalIndexed),
306 PythonEnvironmentBoundary::Absent => {
307 let message = match &domain {
308 SemanticDiagnosticDomain::MemberSurface { owner, member } => {
312 format!("Unrecognized Python attribute `{member}` on `{owner}`")
313 }
314 _ => format!("Unrecognized Python import `{subject}`"),
315 };
316 self.report.push_absent(
317 SemanticAbsenceProof {
318 range,
319 domain,
320 boundary: BoundaryStatus::ExternalIndexed,
321 },
322 SemanticDiagnostic {
323 range,
324 source: PYTHON_SEMANTIC_DIAGNOSTIC_SOURCE,
325 kind: PYTHON_UNRECOGNIZED_SYMBOL,
326 message,
327 },
328 );
329 }
330 PythonEnvironmentBoundary::Incomplete(reason) => {
331 self.report.push_incomplete(Some(range), vec![reason])
332 }
333 }
334 }
335
336 fn scan_tree(
337 &mut self,
338 root: Node<'_>,
339 imports: &[ImportInfo],
340 resolved: &[Vec<(String, CodeUnit)>],
341 ) {
342 let mut scopes = ScopeStack::default();
343 scopes.enter();
344 self.seed_module_scope(&mut scopes, imports, resolved);
345 let mut stack = vec![ScanFrame::Node(root)];
346 while let Some(frame) = stack.pop() {
347 if self.report.diagnostics().len() >= MAX_PYTHON_SEMANTIC_DIAGNOSTICS {
348 self.report
349 .push_incomplete(None, vec![SemanticDiagnosticIncompleteReason::Truncated]);
350 return;
351 }
352 match frame {
353 ScanFrame::Node(node) => self.scan_node(node, &mut scopes, &mut stack),
354 ScanFrame::ExitScope => scopes.exit(),
355 ScanFrame::SeedTargets(node) => self.seed_assignment_targets(node, &mut scopes),
356 }
357 }
358 }
359
360 fn scan_node<'tree>(
361 &mut self,
362 node: Node<'tree>,
363 scopes: &mut ScopeStack,
364 stack: &mut Vec<ScanFrame<'tree>>,
365 ) {
366 match node.kind() {
367 "module" => push_named_children(stack, node),
368 "function_definition" | "lambda" => {
369 self.seed_named_declaration(node, scopes);
370 scopes.enter();
371 self.seed_parameters(node, scopes);
372 stack.push(ScanFrame::ExitScope);
373 push_named_children_except(stack, node, node.child_by_field_name("name"));
374 }
375 "class_definition" => {
376 self.seed_named_declaration(node, scopes);
377 self.push_field_if_present(stack, node, "superclasses");
378 scopes.enter();
379 stack.push(ScanFrame::ExitScope);
380 if let Some(body) = node.child_by_field_name("body") {
381 stack.push(ScanFrame::Node(body));
382 }
383 }
384 "list_comprehension"
385 | "set_comprehension"
386 | "dictionary_comprehension"
387 | "generator_expression" => {
388 scopes.enter();
389 self.seed_comprehension_targets(node, scopes);
390 stack.push(ScanFrame::ExitScope);
391 push_named_children(stack, node);
392 }
393 "match_statement" => {
394 self.report.push_incomplete(
398 Some(node_range(node, self.line_starts)),
399 vec![SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
400 detail: "match statement pattern bindings are not modeled".to_string(),
401 }],
402 );
403 }
404 "import_statement" | "import_from_statement" => {}
405 "assignment" | "augmented_assignment" | "named_expression" => {
406 stack.push(ScanFrame::SeedTargets(node));
407 self.push_field_if_present(stack, node, "right");
408 self.push_field_if_present(stack, node, "value");
409 }
410 "for_statement" | "for_in_clause" => {
411 if let Some(body) = node.child_by_field_name("body") {
412 stack.push(ScanFrame::Node(body));
413 }
414 stack.push(ScanFrame::SeedTargets(node));
415 self.push_field_if_present(stack, node, "right");
416 }
417 "with_statement" | "with_item" => {
418 stack.push(ScanFrame::SeedTargets(node));
419 push_named_children(stack, node);
420 }
421 "except_clause" => {
422 self.seed_except_alias(node, scopes);
423 push_named_children(stack, node);
424 }
425 "identifier" => self.check_identifier(node, scopes),
426 "attribute" => {
427 self.check_attribute(node);
428 if let Some(object) = node.child_by_field_name("object") {
429 stack.push(ScanFrame::Node(object));
430 }
431 }
432 "string" | "string_content" | "comment" => {}
433 _ => push_named_children(stack, node),
434 }
435 }
436
437 fn seed_module_scope(
438 &self,
439 scopes: &mut ScopeStack,
440 imports: &[ImportInfo],
441 resolved: &[Vec<(String, CodeUnit)>],
442 ) {
443 for import in imports {
444 if let Some(local_name) = import.alias.as_ref().or(import.identifier.as_ref()) {
445 scopes.declare(local_name.clone());
446 }
447 }
448 for binding in resolved.iter().flatten() {
449 scopes.declare(binding.0.clone());
450 }
451 for unit in self.py.declarations(self.file) {
452 if !unit.identifier().is_empty() {
453 scopes.declare(unit.identifier().to_string());
454 }
455 }
456 }
457
458 fn seed_named_declaration(&self, node: Node<'_>, scopes: &mut ScopeStack) {
459 if let Some(name) = node.child_by_field_name("name") {
460 let text = node_text(name, self.source).trim();
461 if !text.is_empty() {
462 scopes.declare(text.to_string());
463 }
464 }
465 }
466
467 fn seed_parameters(&self, node: Node<'_>, scopes: &mut ScopeStack) {
468 if let Some(parameters) = node.child_by_field_name("parameters") {
469 collect_parameter_names(parameters, self.source, scopes);
470 }
471 }
472
473 fn seed_assignment_targets(&self, node: Node<'_>, scopes: &mut ScopeStack) {
474 for field in ["left", "name", "alias"] {
475 if let Some(target) = node.child_by_field_name(field) {
476 collect_bound_identifiers(target, self.source, scopes);
477 }
478 }
479 if node.kind() == "with_item" || node.kind() == "with_statement" {
480 collect_alias_children(node, self.source, scopes);
481 }
482 }
483
484 fn seed_except_alias(&self, node: Node<'_>, scopes: &mut ScopeStack) {
485 if let Some(alias) = node.child_by_field_name("alias") {
486 collect_bound_identifiers(alias, self.source, scopes);
487 return;
488 }
489 let mut identifiers = Vec::new();
490 let mut stack = vec![node];
491 while let Some(current) = stack.pop() {
492 if current.kind() == "identifier" {
493 let text = node_text(current, self.source).trim();
494 if !text.is_empty() {
495 identifiers.push(text.to_string());
496 }
497 continue;
498 }
499 let mut cursor = current.walk();
500 for child in current.named_children(&mut cursor) {
501 stack.push(child);
502 }
503 }
504 if identifiers.len() >= 2
505 && let Some(alias) = identifiers.into_iter().next()
506 {
507 scopes.declare(alias);
508 }
509 }
510
511 fn seed_comprehension_targets(&self, node: Node<'_>, scopes: &mut ScopeStack) {
512 let mut stack = vec![node];
513 while let Some(current) = stack.pop() {
514 if matches!(current.kind(), "for_statement" | "for_in_clause")
515 && let Some(left) = current.child_by_field_name("left")
516 {
517 collect_bound_identifiers(left, self.source, scopes);
518 }
519 let mut cursor = current.walk();
520 for child in current.named_children(&mut cursor) {
521 stack.push(child);
522 }
523 }
524 }
525
526 fn check_identifier(&mut self, node: Node<'_>, scopes: &ScopeStack) {
527 if !self.is_reference_identifier(node) {
528 return;
529 }
530 let name = node_text(node, self.source);
531 if name.is_empty() || name == "_" {
534 return;
535 }
536 let range = node_range(node, self.line_starts);
537 if is_python_builtin_or_constant(name) {
538 self.report
541 .push_resolved(range, BoundaryStatus::ExternalIndexed);
542 return;
543 }
544 if scopes.contains(name) || self.name_resolves_project_locally(name) {
545 self.report
546 .push_resolved(range, BoundaryStatus::WorkspaceLocal);
547 return;
548 }
549 self.report.push_absent(
553 SemanticAbsenceProof {
554 range,
555 domain: SemanticDiagnosticDomain::LexicalScope {
556 file: self.file.rel_path().to_path_buf(),
557 range,
558 },
559 boundary: BoundaryStatus::WorkspaceLocal,
560 },
561 SemanticDiagnostic {
562 range,
563 source: PYTHON_SEMANTIC_DIAGNOSTIC_SOURCE,
564 kind: PYTHON_UNRECOGNIZED_SYMBOL,
565 message: format!("Unrecognized Python symbol `{name}`"),
566 },
567 );
568 }
569
570 fn check_attribute(&mut self, node: Node<'_>) {
572 let Some((owner, member)) = self.module_attribute_target(node) else {
573 return;
574 };
575 let Some(attribute) = node.child_by_field_name("attribute") else {
576 return;
577 };
578 let range = node_range(attribute, self.line_starts);
579 let boundary = self.environment.attribute_boundary(&owner, &member);
580 self.record_boundary(
581 range,
582 SemanticDiagnosticDomain::MemberSurface {
583 owner,
584 member: member.clone(),
585 },
586 &member,
587 boundary,
588 );
589 }
590
591 fn module_attribute_target(&self, node: Node<'_>) -> Option<(String, String)> {
601 let member = node_text(node.child_by_field_name("attribute")?, self.source);
602 if member.is_empty() {
603 return None;
604 }
605 let mut intermediate = Vec::new();
606 let mut current = node.child_by_field_name("object")?;
607 while current.kind() == "attribute" {
608 intermediate.push(node_text(
609 current.child_by_field_name("attribute")?,
610 self.source,
611 ));
612 current = current.child_by_field_name("object")?;
613 }
614 if current.kind() != "identifier" {
615 return None;
616 }
617 let mut owner = self
618 .module_binders
619 .get(node_text(current, self.source))?
620 .clone();
621 for segment in intermediate.iter().rev() {
622 owner.push('.');
623 owner.push_str(segment);
624 }
625 Some((owner, member.to_string()))
626 }
627
628 fn is_reference_identifier(&self, node: Node<'_>) -> bool {
629 if is_declaration_identifier(node)
630 || is_import_identifier(node)
631 || is_attribute_identifier(node)
632 || is_pattern_identifier(node)
633 {
634 return false;
635 }
636 let mut current = node;
637 while let Some(parent) = current.parent() {
638 if matches!(parent.kind(), "string" | "string_content" | "comment") {
639 return false;
640 }
641 current = parent;
642 }
643 true
644 }
645
646 fn name_resolves_project_locally(&self, name: &str) -> bool {
647 if !self.support.file_identifier(self.file, name).is_empty() {
648 return true;
649 }
650 if !self
651 .support
652 .fqn(&format!("{}.{}", self.module_name, name))
653 .is_empty()
654 {
655 return true;
656 }
657 false
658 }
659
660 fn push_field_if_present<'tree>(
661 &self,
662 stack: &mut Vec<ScanFrame<'tree>>,
663 node: Node<'tree>,
664 field_name: &str,
665 ) {
666 if let Some(child) = node.child_by_field_name(field_name) {
667 stack.push(ScanFrame::Node(child));
668 }
669 }
670}
671
672fn span_range(span: Span, line_starts: &[usize]) -> Range {
673 Range {
674 start_byte: span.start_byte,
675 end_byte: span.end_byte,
676 start_line: find_line_index_for_offset(line_starts, span.start_byte) + 1,
677 end_line: find_line_index_for_offset(line_starts, span.end_byte.saturating_sub(1)) + 1,
678 }
679}
680
681fn dynamic_surface_reasons(
685 imports: &[ImportInfo],
686 resolved: &[Vec<(String, CodeUnit)>],
687 source: &str,
688 root: Node<'_>,
689) -> Vec<SemanticDiagnosticIncompleteReason> {
690 debug_assert_eq!(imports.len(), resolved.len());
691 let mut reasons = Vec::new();
692 for (import, resolved) in imports.iter().zip(resolved) {
693 if import.is_wildcard && resolved.is_empty() {
694 reasons.push(SemanticDiagnosticIncompleteReason::DynamicBehavior {
695 detail: format!(
696 "`{}` binds an unknown set of names",
697 import.raw_snippet.trim()
698 ),
699 });
700 }
701 }
702 if has_module_getattr(source, root) {
703 reasons.push(SemanticDiagnosticIncompleteReason::DynamicBehavior {
704 detail: "the module defines `__getattr__`".to_string(),
705 });
706 }
707 for call in dynamic_namespace_calls(source, root) {
708 reasons.push(SemanticDiagnosticIncompleteReason::DynamicBehavior {
709 detail: format!("the module calls `{call}`"),
710 });
711 }
712 reasons
713}
714
715fn has_module_getattr(source: &str, root: Node<'_>) -> bool {
716 let mut cursor = root.walk();
717 root.named_children(&mut cursor).any(|child| {
718 child.kind() == "function_definition"
719 && child
720 .child_by_field_name("name")
721 .is_some_and(|name| node_text(name, source) == "__getattr__")
722 })
723}
724
725fn dynamic_namespace_calls(source: &str, root: Node<'_>) -> Vec<String> {
727 let mut calls: Vec<String> = Vec::new();
728 walk_tree_preorder(root, true, |node| {
729 if node.kind() == "call"
730 && let Some(function) = node.child_by_field_name("function")
731 && let Some(name) = dynamic_function_name(function, source)
732 && !calls.iter().any(|seen| seen == name)
733 {
734 calls.push(name.to_string());
735 }
736 WalkControl::Continue
737 });
738 calls
739}
740
741fn dynamic_function_name<'a>(node: Node<'_>, source: &'a str) -> Option<&'a str> {
742 let text = node_text(node, source);
743 match node.kind() {
744 "identifier" => matches!(text, "globals" | "locals" | "__import__").then_some(text),
745 "attribute" => (text == "importlib.import_module").then_some(text),
746 _ => None,
747 }
748}
749
750fn collect_bound_identifiers(node: Node<'_>, source: &str, scopes: &mut ScopeStack) {
751 let mut stack = vec![node];
752 while let Some(current) = stack.pop() {
753 match current.kind() {
754 "identifier" => {
755 let text = node_text(current, source).trim();
756 if !text.is_empty() {
757 scopes.declare(text.to_string());
758 }
759 }
760 "attribute" | "call" => {}
761 _ => {
762 let mut cursor = current.walk();
763 for child in current.named_children(&mut cursor) {
764 stack.push(child);
765 }
766 }
767 }
768 }
769}
770
771fn collect_parameter_names(node: Node<'_>, source: &str, scopes: &mut ScopeStack) {
772 let mut cursor = node.walk();
773 for child in node.named_children(&mut cursor) {
774 if let Some(name) = python_parameter_name(child, source) {
775 scopes.declare(name);
776 }
777 }
778}
779
780fn python_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
781 match node.kind() {
782 "identifier" => Some(node_text(node, source).trim().to_string()),
783 "typed_parameter"
784 | "typed_default_parameter"
785 | "default_parameter"
786 | "list_splat_pattern"
787 | "dictionary_splat_pattern" => node
788 .child_by_field_name("name")
789 .or_else(|| {
790 let mut cursor = node.walk();
791 node.named_children(&mut cursor)
792 .find(|child| child.kind() == "identifier")
793 })
794 .and_then(|name| python_parameter_name(name, source)),
795 _ => None,
796 }
797 .filter(|name| !name.is_empty())
798}
799
800fn collect_alias_children(node: Node<'_>, source: &str, scopes: &mut ScopeStack) {
801 let mut cursor = node.walk();
802 for alias in node.children_by_field_name("alias", &mut cursor) {
803 collect_bound_identifiers(alias, source, scopes);
804 }
805 let mut cursor = node.walk();
806 for item in node.named_children(&mut cursor) {
807 let mut item_cursor = item.walk();
808 for alias in item.children_by_field_name("alias", &mut item_cursor) {
809 collect_bound_identifiers(alias, source, scopes);
810 }
811 }
812}
813
814fn push_named_children<'tree>(stack: &mut Vec<ScanFrame<'tree>>, node: Node<'tree>) {
815 let mut cursor = node.walk();
816 let children: Vec<_> = node.named_children(&mut cursor).collect();
817 for child in children.into_iter().rev() {
818 stack.push(ScanFrame::Node(child));
819 }
820}
821
822fn push_named_children_except<'tree>(
823 stack: &mut Vec<ScanFrame<'tree>>,
824 node: Node<'tree>,
825 excluded: Option<Node<'tree>>,
826) {
827 let mut cursor = node.walk();
828 let children: Vec<_> = node
829 .named_children(&mut cursor)
830 .filter(|child| excluded.is_none_or(|excluded| !same_node(*child, excluded)))
831 .collect();
832 for child in children.into_iter().rev() {
833 stack.push(ScanFrame::Node(child));
834 }
835}
836
837fn is_declaration_identifier(node: Node<'_>) -> bool {
838 let Some(parent) = node.parent() else {
839 return false;
840 };
841 match parent.kind() {
842 "function_definition" | "class_definition" => parent
843 .child_by_field_name("name")
844 .is_some_and(|name| same_node(name, node)),
845 "parameters" | "list_splat_pattern" | "dictionary_splat_pattern" => true,
846 "default_parameter" | "typed_parameter" | "typed_default_parameter" => parent
847 .child_by_field_name("name")
848 .is_some_and(|name| contains_node(name, node)),
849 "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => parent
850 .child_by_field_name("left")
851 .is_some_and(|left| contains_node(left, node)),
852 "named_expression" => parent
853 .child_by_field_name("name")
854 .is_some_and(|name| contains_node(name, node)),
855 _ => false,
856 }
857}
858
859fn is_import_identifier(node: Node<'_>) -> bool {
860 let mut current = node;
861 while let Some(parent) = current.parent() {
862 if matches!(parent.kind(), "import_statement" | "import_from_statement") {
863 return true;
864 }
865 current = parent;
866 }
867 false
868}
869
870fn is_attribute_identifier(node: Node<'_>) -> bool {
871 let Some(parent) = node.parent() else {
872 return false;
873 };
874 parent.kind() == "attribute"
875 && parent
876 .child_by_field_name("attribute")
877 .is_some_and(|attribute| same_node(attribute, node))
878}
879
880fn is_pattern_identifier(node: Node<'_>) -> bool {
881 let mut current = node;
882 while let Some(parent) = current.parent() {
883 if parent.kind().contains("pattern") {
884 return true;
885 }
886 current = parent;
887 }
888 false
889}
890
891fn is_python_builtin_or_constant(name: &str) -> bool {
892 matches!(
893 name,
894 "None"
895 | "True"
896 | "False"
897 | "NotImplemented"
898 | "Ellipsis"
899 | "__annotations__"
900 | "__builtins__"
901 | "__debug__"
902 | "__doc__"
903 | "__file__"
904 | "__loader__"
905 | "__name__"
906 | "__package__"
907 | "__spec__"
908 | "ArithmeticError"
909 | "AssertionError"
910 | "AttributeError"
911 | "BaseException"
912 | "BaseExceptionGroup"
913 | "BlockingIOError"
914 | "BrokenPipeError"
915 | "BufferError"
916 | "BytesWarning"
917 | "ChildProcessError"
918 | "ConnectionAbortedError"
919 | "ConnectionError"
920 | "ConnectionRefusedError"
921 | "ConnectionResetError"
922 | "DeprecationWarning"
923 | "EOFError"
924 | "EncodingWarning"
925 | "EnvironmentError"
926 | "Exception"
927 | "ExceptionGroup"
928 | "FileExistsError"
929 | "FileNotFoundError"
930 | "FloatingPointError"
931 | "FutureWarning"
932 | "GeneratorExit"
933 | "IOError"
934 | "ImportError"
935 | "ImportWarning"
936 | "IndentationError"
937 | "IndexError"
938 | "InterruptedError"
939 | "IsADirectoryError"
940 | "KeyError"
941 | "KeyboardInterrupt"
942 | "LookupError"
943 | "MemoryError"
944 | "ModuleNotFoundError"
945 | "NameError"
946 | "NotADirectoryError"
947 | "NotImplementedError"
948 | "OSError"
949 | "OverflowError"
950 | "PendingDeprecationWarning"
951 | "PermissionError"
952 | "ProcessLookupError"
953 | "RecursionError"
954 | "ReferenceError"
955 | "ResourceWarning"
956 | "RuntimeError"
957 | "RuntimeWarning"
958 | "StopAsyncIteration"
959 | "StopIteration"
960 | "SyntaxError"
961 | "SyntaxWarning"
962 | "SystemError"
963 | "SystemExit"
964 | "TabError"
965 | "TimeoutError"
966 | "TypeError"
967 | "UnboundLocalError"
968 | "UnicodeDecodeError"
969 | "UnicodeEncodeError"
970 | "UnicodeError"
971 | "UnicodeTranslateError"
972 | "UnicodeWarning"
973 | "UserWarning"
974 | "ValueError"
975 | "Warning"
976 | "ZeroDivisionError"
977 | "abs"
978 | "aiter"
979 | "all"
980 | "anext"
981 | "any"
982 | "ascii"
983 | "bin"
984 | "bool"
985 | "breakpoint"
986 | "bytearray"
987 | "bytes"
988 | "callable"
989 | "chr"
990 | "classmethod"
991 | "compile"
992 | "complex"
993 | "copyright"
994 | "credits"
995 | "delattr"
996 | "dict"
997 | "dir"
998 | "divmod"
999 | "enumerate"
1000 | "eval"
1001 | "exec"
1002 | "exit"
1003 | "filter"
1004 | "float"
1005 | "format"
1006 | "frozenset"
1007 | "getattr"
1008 | "hasattr"
1009 | "hash"
1010 | "help"
1011 | "hex"
1012 | "id"
1013 | "input"
1014 | "int"
1015 | "isinstance"
1016 | "issubclass"
1017 | "iter"
1018 | "len"
1019 | "license"
1020 | "list"
1021 | "locals"
1022 | "map"
1023 | "max"
1024 | "memoryview"
1025 | "min"
1026 | "next"
1027 | "object"
1028 | "oct"
1029 | "open"
1030 | "ord"
1031 | "pow"
1032 | "print"
1033 | "property"
1034 | "quit"
1035 | "range"
1036 | "repr"
1037 | "reversed"
1038 | "round"
1039 | "set"
1040 | "setattr"
1041 | "slice"
1042 | "sorted"
1043 | "staticmethod"
1044 | "str"
1045 | "sum"
1046 | "super"
1047 | "tuple"
1048 | "type"
1049 | "vars"
1050 | "zip"
1051 | "__import__"
1052 )
1053}