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