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, push_named_children_reversed_where_as, 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;
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::missing_dependency_discovery(
106 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(py.project(), 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" => brokk_bifrost_core::analyzer::tree_walk::push_named_children_reversed_as(
370 node,
371 stack,
372 ScanFrame::Node,
373 ),
374 "function_definition" | "lambda" => {
375 self.seed_named_declaration(node, scopes);
376 scopes.enter();
377 self.seed_parameters(node, scopes);
378 stack.push(ScanFrame::ExitScope);
379 push_named_children_except(stack, node, node.child_by_field_name("name"));
380 }
381 "class_definition" => {
382 self.seed_named_declaration(node, scopes);
383 self.push_field_if_present(stack, node, "superclasses");
384 scopes.enter();
385 stack.push(ScanFrame::ExitScope);
386 if let Some(body) = node.child_by_field_name("body") {
387 stack.push(ScanFrame::Node(body));
388 }
389 }
390 "list_comprehension"
391 | "set_comprehension"
392 | "dictionary_comprehension"
393 | "generator_expression" => {
394 scopes.enter();
395 self.seed_comprehension_targets(node, scopes);
396 stack.push(ScanFrame::ExitScope);
397 brokk_bifrost_core::analyzer::tree_walk::push_named_children_reversed_as(
398 node,
399 stack,
400 ScanFrame::Node,
401 );
402 }
403 "match_statement" => {
404 self.report.push_incomplete(
408 Some(node_range(node, self.line_starts)),
409 vec![SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
410 detail: "match statement pattern bindings are not modeled".to_string(),
411 }],
412 );
413 }
414 "import_statement" | "import_from_statement" => {}
415 "assignment" | "augmented_assignment" | "named_expression" => {
416 stack.push(ScanFrame::SeedTargets(node));
417 self.push_field_if_present(stack, node, "right");
418 self.push_field_if_present(stack, node, "value");
419 }
420 "for_statement" | "for_in_clause" => {
421 if let Some(body) = node.child_by_field_name("body") {
422 stack.push(ScanFrame::Node(body));
423 }
424 stack.push(ScanFrame::SeedTargets(node));
425 self.push_field_if_present(stack, node, "right");
426 }
427 "with_statement" | "with_item" => {
428 stack.push(ScanFrame::SeedTargets(node));
429 brokk_bifrost_core::analyzer::tree_walk::push_named_children_reversed_as(
430 node,
431 stack,
432 ScanFrame::Node,
433 );
434 }
435 "except_clause" => {
436 self.seed_except_alias(node, scopes);
437 brokk_bifrost_core::analyzer::tree_walk::push_named_children_reversed_as(
438 node,
439 stack,
440 ScanFrame::Node,
441 );
442 }
443 "identifier" => self.check_identifier(node, scopes),
444 "attribute" => {
445 self.check_attribute(node);
446 if let Some(object) = node.child_by_field_name("object") {
447 stack.push(ScanFrame::Node(object));
448 }
449 }
450 "string" | "string_content" | "comment" => {}
451 _ => brokk_bifrost_core::analyzer::tree_walk::push_named_children_reversed_as(
452 node,
453 stack,
454 ScanFrame::Node,
455 ),
456 }
457 }
458
459 fn seed_module_scope(
460 &self,
461 scopes: &mut ScopeStack,
462 imports: &[ImportInfo],
463 resolved: &[Vec<(String, CodeUnit)>],
464 ) {
465 for import in imports {
466 if let Some(local_name) = import.alias.as_ref().or(import.identifier.as_ref()) {
467 scopes.declare(local_name.clone());
468 }
469 }
470 for binding in resolved.iter().flatten() {
471 scopes.declare(binding.0.clone());
472 }
473 for unit in self.py.declarations(self.file) {
474 if !unit.identifier().is_empty() {
475 scopes.declare(unit.identifier().to_string());
476 }
477 }
478 }
479
480 fn seed_named_declaration(&self, node: Node<'_>, scopes: &mut ScopeStack) {
481 if let Some(name) = node.child_by_field_name("name") {
482 let text = node_text(name, self.source).trim();
483 if !text.is_empty() {
484 scopes.declare(text.to_string());
485 }
486 }
487 }
488
489 fn seed_parameters(&self, node: Node<'_>, scopes: &mut ScopeStack) {
490 if let Some(parameters) = node.child_by_field_name("parameters") {
491 collect_parameter_names(parameters, self.source, scopes);
492 }
493 }
494
495 fn seed_assignment_targets(&self, node: Node<'_>, scopes: &mut ScopeStack) {
496 for field in ["left", "name", "alias"] {
497 if let Some(target) = node.child_by_field_name(field) {
498 collect_bound_identifiers(target, self.source, scopes);
499 }
500 }
501 if node.kind() == "with_item" || node.kind() == "with_statement" {
502 collect_alias_children(node, self.source, scopes);
503 }
504 }
505
506 fn seed_except_alias(&self, node: Node<'_>, scopes: &mut ScopeStack) {
507 if let Some(alias) = node.child_by_field_name("alias") {
508 collect_bound_identifiers(alias, self.source, scopes);
509 return;
510 }
511 let mut identifiers = Vec::new();
512 let mut stack = vec![node];
513 while let Some(current) = stack.pop() {
514 if current.kind() == "identifier" {
515 let text = node_text(current, self.source).trim();
516 if !text.is_empty() {
517 identifiers.push(text.to_string());
518 }
519 continue;
520 }
521 let mut cursor = current.walk();
522 for child in current.named_children(&mut cursor) {
523 stack.push(child);
524 }
525 }
526 if identifiers.len() >= 2
527 && let Some(alias) = identifiers.into_iter().next()
528 {
529 scopes.declare(alias);
530 }
531 }
532
533 fn seed_comprehension_targets(&self, node: Node<'_>, scopes: &mut ScopeStack) {
534 let mut stack = vec![node];
535 while let Some(current) = stack.pop() {
536 if matches!(current.kind(), "for_statement" | "for_in_clause")
537 && let Some(left) = current.child_by_field_name("left")
538 {
539 collect_bound_identifiers(left, self.source, scopes);
540 }
541 let mut cursor = current.walk();
542 for child in current.named_children(&mut cursor) {
543 stack.push(child);
544 }
545 }
546 }
547
548 fn check_identifier(&mut self, node: Node<'_>, scopes: &ScopeStack) {
549 if !self.is_reference_identifier(node) {
550 return;
551 }
552 let name = node_text(node, self.source);
553 if name.is_empty() || name == "_" {
556 return;
557 }
558 let range = node_range(node, self.line_starts);
559 if is_python_builtin_or_constant(name) {
560 self.report
563 .push_resolved(range, BoundaryStatus::ExternalIndexed);
564 return;
565 }
566 if scopes.contains(name) || self.name_resolves_project_locally(name) {
567 self.report
568 .push_resolved(range, BoundaryStatus::WorkspaceLocal);
569 return;
570 }
571 self.report.push_absent(
575 SemanticAbsenceProof {
576 range,
577 domain: SemanticDiagnosticDomain::LexicalScope {
578 file: self.file.rel_path().to_path_buf(),
579 range,
580 },
581 boundary: BoundaryStatus::WorkspaceLocal,
582 },
583 SemanticDiagnostic {
584 range,
585 source: PYTHON_SEMANTIC_DIAGNOSTIC_SOURCE,
586 kind: PYTHON_UNRECOGNIZED_SYMBOL,
587 message: format!("Unrecognized Python symbol `{name}`"),
588 },
589 );
590 }
591
592 fn check_attribute(&mut self, node: Node<'_>) {
594 let Some((owner, member)) = self.module_attribute_target(node) else {
595 return;
596 };
597 let Some(attribute) = node.child_by_field_name("attribute") else {
598 return;
599 };
600 let range = node_range(attribute, self.line_starts);
601 let boundary = self.environment.attribute_boundary(&owner, &member);
602 self.record_boundary(
603 range,
604 SemanticDiagnosticDomain::MemberSurface {
605 owner,
606 member: member.clone(),
607 },
608 &member,
609 boundary,
610 );
611 }
612
613 fn module_attribute_target(&self, node: Node<'_>) -> Option<(String, String)> {
623 let member = node_text(node.child_by_field_name("attribute")?, self.source);
624 if member.is_empty() {
625 return None;
626 }
627 let mut intermediate = Vec::new();
628 let mut current = node.child_by_field_name("object")?;
629 while current.kind() == "attribute" {
630 intermediate.push(node_text(
631 current.child_by_field_name("attribute")?,
632 self.source,
633 ));
634 current = current.child_by_field_name("object")?;
635 }
636 if current.kind() != "identifier" {
637 return None;
638 }
639 let mut owner = self
640 .module_binders
641 .get(node_text(current, self.source))?
642 .clone();
643 for segment in intermediate.iter().rev() {
644 owner.push('.');
645 owner.push_str(segment);
646 }
647 Some((owner, member.to_string()))
648 }
649
650 fn is_reference_identifier(&self, node: Node<'_>) -> bool {
651 if is_declaration_identifier(node)
652 || is_import_identifier(node)
653 || is_attribute_identifier(node)
654 || is_pattern_identifier(node)
655 {
656 return false;
657 }
658 let mut current = node;
659 while let Some(parent) = current.parent() {
660 if matches!(parent.kind(), "string" | "string_content" | "comment") {
661 return false;
662 }
663 current = parent;
664 }
665 true
666 }
667
668 fn name_resolves_project_locally(&self, name: &str) -> bool {
669 if !self.support.file_identifier(self.file, name).is_empty() {
670 return true;
671 }
672 if !self
673 .support
674 .fqn(&format!("{}.{}", self.module_name, name))
675 .is_empty()
676 {
677 return true;
678 }
679 false
680 }
681
682 fn push_field_if_present<'tree>(
683 &self,
684 stack: &mut Vec<ScanFrame<'tree>>,
685 node: Node<'tree>,
686 field_name: &str,
687 ) {
688 if let Some(child) = node.child_by_field_name(field_name) {
689 stack.push(ScanFrame::Node(child));
690 }
691 }
692}
693
694fn span_range(span: Span, line_starts: &[usize]) -> Range {
695 brokk_bifrost_core::analyzer::semantic_diagnostics::range_for_byte_span(
696 line_starts,
697 span.start_byte,
698 span.end_byte,
699 )
700}
701
702fn dynamic_surface_reasons(
706 imports: &[ImportInfo],
707 resolved: &[Vec<(String, CodeUnit)>],
708 source: &str,
709 root: Node<'_>,
710) -> Vec<SemanticDiagnosticIncompleteReason> {
711 debug_assert_eq!(imports.len(), resolved.len());
712 let mut reasons = Vec::new();
713 for (import, resolved) in imports.iter().zip(resolved) {
714 if import.is_wildcard && resolved.is_empty() {
715 reasons.push(SemanticDiagnosticIncompleteReason::DynamicBehavior {
716 detail: format!(
717 "`{}` binds an unknown set of names",
718 import.raw_snippet.trim()
719 ),
720 });
721 }
722 }
723 if has_module_getattr(source, root) {
724 reasons.push(SemanticDiagnosticIncompleteReason::DynamicBehavior {
725 detail: "the module defines `__getattr__`".to_string(),
726 });
727 }
728 for call in dynamic_namespace_calls(source, root) {
729 reasons.push(SemanticDiagnosticIncompleteReason::DynamicBehavior {
730 detail: format!("the module calls `{call}`"),
731 });
732 }
733 reasons
734}
735
736fn has_module_getattr(source: &str, root: Node<'_>) -> bool {
737 let mut cursor = root.walk();
738 root.named_children(&mut cursor).any(|child| {
739 child.kind() == "function_definition"
740 && child
741 .child_by_field_name("name")
742 .is_some_and(|name| node_text(name, source) == "__getattr__")
743 })
744}
745
746fn dynamic_namespace_calls(source: &str, root: Node<'_>) -> Vec<String> {
748 let mut calls: Vec<String> = Vec::new();
749 walk_tree_preorder(root, true, |node| {
750 if node.kind() == "call"
751 && let Some(function) = node.child_by_field_name("function")
752 && let Some(name) = dynamic_function_name(function, source)
753 && !calls.iter().any(|seen| seen == name)
754 {
755 calls.push(name.to_string());
756 }
757 WalkControl::Continue
758 });
759 calls
760}
761
762fn dynamic_function_name<'a>(node: Node<'_>, source: &'a str) -> Option<&'a str> {
763 let text = node_text(node, source);
764 match node.kind() {
765 "identifier" => matches!(text, "globals" | "locals" | "__import__").then_some(text),
766 "attribute" => (text == "importlib.import_module").then_some(text),
767 _ => None,
768 }
769}
770
771fn collect_bound_identifiers(node: Node<'_>, source: &str, scopes: &mut ScopeStack) {
772 let mut stack = vec![node];
773 while let Some(current) = stack.pop() {
774 match current.kind() {
775 "identifier" => {
776 let text = node_text(current, source).trim();
777 if !text.is_empty() {
778 scopes.declare(text.to_string());
779 }
780 }
781 "attribute" | "call" => {}
782 _ => {
783 let mut cursor = current.walk();
784 for child in current.named_children(&mut cursor) {
785 stack.push(child);
786 }
787 }
788 }
789 }
790}
791
792fn collect_parameter_names(node: Node<'_>, source: &str, scopes: &mut ScopeStack) {
793 let mut cursor = node.walk();
794 for child in node.named_children(&mut cursor) {
795 if let Some(name) = python_parameter_name(child, source) {
796 scopes.declare(name);
797 }
798 }
799}
800
801use crate::syntax::python_parameter_name;
802
803fn collect_alias_children(node: Node<'_>, source: &str, scopes: &mut ScopeStack) {
804 let mut cursor = node.walk();
805 for alias in node.children_by_field_name("alias", &mut cursor) {
806 collect_bound_identifiers(alias, source, scopes);
807 }
808 let mut cursor = node.walk();
809 for item in node.named_children(&mut cursor) {
810 let mut item_cursor = item.walk();
811 for alias in item.children_by_field_name("alias", &mut item_cursor) {
812 collect_bound_identifiers(alias, source, scopes);
813 }
814 }
815}
816
817fn push_named_children_except<'tree>(
818 stack: &mut Vec<ScanFrame<'tree>>,
819 node: Node<'tree>,
820 excluded: Option<Node<'tree>>,
821) {
822 push_named_children_reversed_where_as(
823 node,
824 stack,
825 |child| excluded.is_none_or(|excluded| !same_node(child, excluded)),
826 ScanFrame::Node,
827 );
828}
829
830fn is_declaration_identifier(node: Node<'_>) -> bool {
831 let Some(parent) = node.parent() else {
832 return false;
833 };
834 match parent.kind() {
835 "function_definition" | "class_definition" => parent
836 .child_by_field_name("name")
837 .is_some_and(|name| same_node(name, node)),
838 "parameters" | "list_splat_pattern" | "dictionary_splat_pattern" => true,
839 "default_parameter" | "typed_parameter" | "typed_default_parameter" => parent
840 .child_by_field_name("name")
841 .is_some_and(|name| contains_node(name, node)),
842 "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => parent
843 .child_by_field_name("left")
844 .is_some_and(|left| contains_node(left, node)),
845 "named_expression" => parent
846 .child_by_field_name("name")
847 .is_some_and(|name| contains_node(name, node)),
848 _ => false,
849 }
850}
851
852fn is_import_identifier(node: Node<'_>) -> bool {
853 let mut current = node;
854 while let Some(parent) = current.parent() {
855 if matches!(parent.kind(), "import_statement" | "import_from_statement") {
856 return true;
857 }
858 current = parent;
859 }
860 false
861}
862
863fn is_attribute_identifier(node: Node<'_>) -> bool {
864 let Some(parent) = node.parent() else {
865 return false;
866 };
867 parent.kind() == "attribute"
868 && parent
869 .child_by_field_name("attribute")
870 .is_some_and(|attribute| same_node(attribute, node))
871}
872
873fn is_pattern_identifier(node: Node<'_>) -> bool {
874 let mut current = node;
875 while let Some(parent) = current.parent() {
876 if parent.kind().contains("pattern") {
877 return true;
878 }
879 current = parent;
880 }
881 false
882}
883
884pub fn is_python_builtin_or_constant(name: &str) -> bool {
885 matches!(
886 name,
887 "None"
888 | "True"
889 | "False"
890 | "NotImplemented"
891 | "Ellipsis"
892 | "__annotations__"
893 | "__builtins__"
894 | "__debug__"
895 | "__doc__"
896 | "__file__"
897 | "__loader__"
898 | "__name__"
899 | "__package__"
900 | "__spec__"
901 | "ArithmeticError"
902 | "AssertionError"
903 | "AttributeError"
904 | "BaseException"
905 | "BaseExceptionGroup"
906 | "BlockingIOError"
907 | "BrokenPipeError"
908 | "BufferError"
909 | "BytesWarning"
910 | "ChildProcessError"
911 | "ConnectionAbortedError"
912 | "ConnectionError"
913 | "ConnectionRefusedError"
914 | "ConnectionResetError"
915 | "DeprecationWarning"
916 | "EOFError"
917 | "EncodingWarning"
918 | "EnvironmentError"
919 | "Exception"
920 | "ExceptionGroup"
921 | "FileExistsError"
922 | "FileNotFoundError"
923 | "FloatingPointError"
924 | "FutureWarning"
925 | "GeneratorExit"
926 | "IOError"
927 | "ImportError"
928 | "ImportWarning"
929 | "IndentationError"
930 | "IndexError"
931 | "InterruptedError"
932 | "IsADirectoryError"
933 | "KeyError"
934 | "KeyboardInterrupt"
935 | "LookupError"
936 | "MemoryError"
937 | "ModuleNotFoundError"
938 | "NameError"
939 | "NotADirectoryError"
940 | "NotImplementedError"
941 | "OSError"
942 | "OverflowError"
943 | "PendingDeprecationWarning"
944 | "PermissionError"
945 | "ProcessLookupError"
946 | "RecursionError"
947 | "ReferenceError"
948 | "ResourceWarning"
949 | "RuntimeError"
950 | "RuntimeWarning"
951 | "StopAsyncIteration"
952 | "StopIteration"
953 | "SyntaxError"
954 | "SyntaxWarning"
955 | "SystemError"
956 | "SystemExit"
957 | "TabError"
958 | "TimeoutError"
959 | "TypeError"
960 | "UnboundLocalError"
961 | "UnicodeDecodeError"
962 | "UnicodeEncodeError"
963 | "UnicodeError"
964 | "UnicodeTranslateError"
965 | "UnicodeWarning"
966 | "UserWarning"
967 | "ValueError"
968 | "Warning"
969 | "ZeroDivisionError"
970 | "abs"
971 | "aiter"
972 | "all"
973 | "anext"
974 | "any"
975 | "ascii"
976 | "bin"
977 | "bool"
978 | "breakpoint"
979 | "bytearray"
980 | "bytes"
981 | "callable"
982 | "chr"
983 | "classmethod"
984 | "compile"
985 | "complex"
986 | "copyright"
987 | "credits"
988 | "delattr"
989 | "dict"
990 | "dir"
991 | "divmod"
992 | "enumerate"
993 | "eval"
994 | "exec"
995 | "exit"
996 | "filter"
997 | "float"
998 | "format"
999 | "frozenset"
1000 | "getattr"
1001 | "hasattr"
1002 | "hash"
1003 | "help"
1004 | "hex"
1005 | "id"
1006 | "input"
1007 | "int"
1008 | "isinstance"
1009 | "issubclass"
1010 | "iter"
1011 | "len"
1012 | "license"
1013 | "list"
1014 | "locals"
1015 | "map"
1016 | "max"
1017 | "memoryview"
1018 | "min"
1019 | "next"
1020 | "object"
1021 | "oct"
1022 | "open"
1023 | "ord"
1024 | "pow"
1025 | "print"
1026 | "property"
1027 | "quit"
1028 | "range"
1029 | "repr"
1030 | "reversed"
1031 | "round"
1032 | "set"
1033 | "setattr"
1034 | "slice"
1035 | "sorted"
1036 | "staticmethod"
1037 | "str"
1038 | "sum"
1039 | "super"
1040 | "tuple"
1041 | "type"
1042 | "vars"
1043 | "zip"
1044 | "__import__"
1045 | "apply"
1049 | "basestring"
1050 | "buffer"
1051 | "cmp"
1052 | "coerce"
1053 | "execfile"
1054 | "intern"
1055 | "long"
1056 | "raw_input"
1057 | "reduce"
1058 | "reload"
1059 | "unichr"
1060 | "unicode"
1061 | "xrange"
1062 )
1063}