1use crate::bindings::python_direct_scope_bindings_bounded;
2use crate::imports::python_import_infos_from_node;
3use crate::syntax::{
4 PythonOverloadDecoratorBindings, expression_name_node, python_plain_string_literal,
5};
6use brokk_bifrost_core::analyzer::fq_name::{FqName, SegmentId, SegmentKind, segment_interner};
7use brokk_bifrost_core::analyzer::model::{
8 CodeUnitType, DispatchExtensibility, ParameterMetadata, SignatureMetadata,
9 StructuredImportPathKind,
10};
11use brokk_bifrost_core::analyzer::parsed_file::ParsedFile;
12use brokk_bifrost_core::analyzer::tree_walk::{WalkControl, walk_named_tree_preorder};
13use brokk_bifrost_core::analyzer::{CodeUnit, ProjectFile};
14use brokk_bifrost_core::hash::{HashMap, HashSet};
15use brokk_bifrost_core::path_normalization::NormalizePath;
16use brokk_bifrost_core::text_utils::{compute_line_starts, find_line_index_for_offset};
17use std::path::{Path, PathBuf};
18use tree_sitter::{Node, Parser, Tree};
19
20fn py_segment(text: &str, kind: SegmentKind) -> SegmentId {
22 segment_interner().intern(text, kind)
23}
24
25pub fn python_module_fq(file: &ProjectFile) -> FqName {
38 python_module_fq_from_components(&python_module_components(file))
39}
40
41fn python_module_fq_from_components(components: &[String]) -> FqName {
42 let mut fq = FqName::new();
43 for component in components {
44 fq.push(py_segment(component, SegmentKind::Package));
45 }
46 fq
47}
48
49fn python_module_components(file: &ProjectFile) -> Vec<String> {
50 let mut components = python_package_components_for_file(file);
51 let module_name = file
52 .rel_path()
53 .file_stem()
54 .and_then(|stem| stem.to_str())
55 .unwrap_or_default();
56 if module_name != "__init__" || components.is_empty() {
57 components.push(module_name.to_string());
58 }
59 components
60}
61
62fn python_package_components_for_file(file: &ProjectFile) -> Vec<String> {
63 let Some(parent_rel) = file.rel_path().parent() else {
64 return Vec::new();
65 };
66 if parent_rel.as_os_str().is_empty() {
67 return Vec::new();
68 }
69
70 if let Some(import_root_rel) = python_configured_import_root(file, parent_rel)
71 && let Ok(relative_package) = parent_rel.strip_prefix(import_root_rel)
72 {
73 return path_components(relative_package);
74 }
75
76 let mut effective_package_root_rel: Option<&Path> = None;
77 let mut current_rel = Some(parent_rel);
78 while let Some(path) = current_rel {
79 if file.root().join(path).join("__init__.py").exists() {
80 effective_package_root_rel = Some(path);
81 }
82 current_rel = path.parent();
83 }
84
85 let relative_package = match effective_package_root_rel {
86 Some(package_root_rel) => package_root_rel
87 .parent()
88 .and_then(|import_root_rel| parent_rel.strip_prefix(import_root_rel).ok())
89 .unwrap_or(parent_rel),
90 None => parent_rel,
91 };
92 path_components(relative_package)
93}
94
95fn python_configured_import_root(file: &ProjectFile, parent_rel: &Path) -> Option<PathBuf> {
101 let mut manifest_dir_rel = Some(parent_rel);
102 while let Some(directory) = manifest_dir_rel {
103 let manifest_dir = file.root().join(directory);
104 let mut roots = setuptools_where_entries(&manifest_dir.join("pyproject.toml"))
105 .iter()
106 .map(|entry| manifest_dir.join(entry).normalize())
107 .filter_map(|root| root.strip_prefix(file.root()).ok().map(Path::to_path_buf))
108 .filter(|root| parent_rel.starts_with(root))
109 .collect::<Vec<_>>();
110 roots.sort_by_key(|root| root.components().count());
111 if let Some(root) = roots.pop() {
112 return Some(root);
113 }
114 if let Some(package_dir) = setuptools_setup_py_import_root(&manifest_dir.join("setup.py")) {
115 let root = manifest_dir.join(package_dir).normalize();
116 if let Ok(root) = root.strip_prefix(file.root())
117 && parent_rel.starts_with(root)
118 {
119 return Some(root.to_path_buf());
120 }
121 }
122 manifest_dir_rel = directory.parent();
123 }
124 None
125}
126
127#[derive(Clone, Copy, PartialEq, Eq)]
128struct FileStamp {
129 len: u64,
130 modified: Option<std::time::SystemTime>,
131}
132
133fn file_stamp(path: &Path) -> Option<FileStamp> {
134 let metadata = std::fs::metadata(path).ok()?;
135 Some(FileStamp {
136 len: metadata.len(),
137 modified: metadata.modified().ok(),
138 })
139}
140
141struct ManifestWhereEntries {
146 stamp: FileStamp,
147 entries: Vec<String>,
148}
149
150fn setuptools_where_entries(manifest: &Path) -> Vec<String> {
161 static MEMO: std::sync::OnceLock<
162 std::sync::RwLock<std::collections::HashMap<PathBuf, ManifestWhereEntries>>,
163 > = std::sync::OnceLock::new();
164 let memo = MEMO.get_or_init(Default::default);
165
166 let Some(stamp) = file_stamp(manifest) else {
167 return Vec::new();
168 };
169 if let Some(cached) = memo.read().expect("manifest memo").get(manifest)
170 && cached.stamp == stamp
171 {
172 return cached.entries.clone();
173 }
174
175 let entries = parse_setuptools_where_entries(manifest);
176 memo.write().expect("manifest memo").insert(
177 manifest.to_path_buf(),
178 ManifestWhereEntries {
179 stamp,
180 entries: entries.clone(),
181 },
182 );
183 entries
184}
185
186struct SetupPyImportRoot {
190 stamp: FileStamp,
191 package_dir: Option<PathBuf>,
192}
193
194fn setuptools_setup_py_import_root(setup_py: &Path) -> Option<PathBuf> {
204 static MEMO: std::sync::OnceLock<
205 std::sync::RwLock<std::collections::HashMap<PathBuf, SetupPyImportRoot>>,
206 > = std::sync::OnceLock::new();
207 let memo = MEMO.get_or_init(Default::default);
208
209 let stamp = file_stamp(setup_py)?;
210 if let Some(cached) = memo.read().expect("setup.py memo").get(setup_py)
211 && cached.stamp == stamp
212 {
213 return cached.package_dir.clone();
214 }
215
216 let package_dir = parse_setuptools_setup_py_import_root(setup_py);
217 memo.write().expect("setup.py memo").insert(
218 setup_py.to_path_buf(),
219 SetupPyImportRoot {
220 stamp,
221 package_dir: package_dir.clone(),
222 },
223 );
224 package_dir
225}
226
227fn parse_setuptools_setup_py_import_root(setup_py: &Path) -> Option<PathBuf> {
228 let source = std::fs::read_to_string(setup_py).ok()?;
229 let tree = parse_python_tree(&source)?;
230 let root = tree.root_node();
231 if root.has_error() {
232 return None;
233 }
234 let mut import_root = None;
235 for (call, setup_bindings) in setup_py_setup_calls(&source, root) {
236 let candidate = setup_py_import_root_from_call(call, &source, &setup_bindings)?;
237 if import_root
238 .replace(candidate.clone())
239 .is_some_and(|root| root != candidate)
240 {
241 return None;
242 }
243 }
244 import_root
245}
246
247pub fn setuptools_setup_py_python_requires(setup_py: &Path) -> Option<String> {
255 let source = std::fs::read_to_string(setup_py).ok()?;
256 let tree = parse_python_tree(&source)?;
257 let root = tree.root_node();
258 if root.has_error() {
259 return None;
260 }
261 let mut requirement: Option<String> = None;
262 for (call, _) in setup_py_setup_calls(&source, root) {
263 let Some(arguments) = call.child_by_field_name("arguments") else {
264 continue;
265 };
266 if arguments.kind() != "argument_list" {
267 continue;
268 }
269 let mut cursor = arguments.walk();
270 for argument in arguments.named_children(&mut cursor) {
271 if argument.kind() != "keyword_argument" {
272 continue;
273 }
274 let Some(name) = argument.child_by_field_name("name") else {
275 continue;
276 };
277 if py_node_text(name, &source).trim() != "python_requires" {
278 continue;
279 }
280 let value = argument.child_by_field_name("value")?;
281 let declared = python_plain_string_literal(value, &source)?.to_owned();
282 if requirement
283 .replace(declared.clone())
284 .is_some_and(|previous| previous != declared)
285 {
286 return None;
287 }
288 }
289 }
290 requirement
291}
292
293fn setup_py_setup_calls<'tree>(
301 source: &str,
302 root: Node<'tree>,
303) -> Vec<(Node<'tree>, HashMap<Vec<String>, String>)> {
304 let mut setup_bindings: HashMap<Vec<String>, String> = HashMap::default();
305 let mut calls = Vec::new();
306 let mut cursor = root.walk();
307
308 for statement in root.named_children(&mut cursor) {
309 if matches!(
310 statement.kind(),
311 "import_statement" | "import_from_statement"
312 ) {
313 for binding in setup_py_bound_names(statement, source) {
314 setup_bindings.retain(|path, _| path.first() != Some(&binding));
315 }
316 for import in python_import_infos_from_node(statement, source) {
317 if import.is_wildcard {
318 setup_bindings.clear();
319 continue;
320 }
321 let Some(path) = import.path else { continue };
322 let segments = path.segments.iter().map(String::as_str).collect::<Vec<_>>();
323 match path.kind {
324 Some(StructuredImportPathKind::ImportFrom) => {
325 let function_name = match segments.as_slice() {
326 ["setuptools", "setup"] | ["distutils", "core", "setup"] => "setup",
327 ["setuptools", "find_packages"] => "find_packages",
328 ["setuptools", "find_namespace_packages"] => "find_namespace_packages",
329 _ => continue,
330 };
331 setup_bindings.insert(
332 vec![import.identifier.expect("imported function binds a name")],
333 function_name.to_string(),
334 );
335 }
336 Some(StructuredImportPathKind::Namespace) => {
337 let function_names = match segments.as_slice() {
338 ["setuptools"] => {
339 ["setup", "find_packages", "find_namespace_packages"].as_slice()
340 }
341 ["distutils", "core"] => ["setup"].as_slice(),
342 _ => continue,
343 };
344 let binding_prefix = import
345 .alias
346 .map(|alias| vec![alias])
347 .unwrap_or_else(|| path.segments.clone());
348 for function_name in function_names {
349 let mut callable = binding_prefix.clone();
350 callable.push((*function_name).to_string());
351 setup_bindings.insert(callable, (*function_name).to_string());
352 }
353 }
354 _ => continue,
355 }
356 }
357 continue;
358 }
359
360 if statement.kind() == "expression_statement"
361 && statement.named_child_count() == 1
362 && let Some(call) = statement.named_child(0)
363 && call.kind() == "call"
364 && setup_py_call_imported_function(call, source, &setup_bindings)
365 .is_some_and(|function| function == "setup")
366 {
367 calls.push((call, setup_bindings.clone()));
368 }
369
370 for binding in setup_py_bound_names(statement, source) {
371 setup_bindings.retain(|path, _| path.first() != Some(&binding));
372 }
373 }
374 calls
375}
376
377fn setup_py_bound_names(statement: Node<'_>, source: &str) -> Vec<String> {
384 let mut names = Vec::new();
385 let mut pending = vec![statement];
386 while let Some(node) = pending.pop() {
387 for binding in python_direct_scope_bindings_bounded(node, source, || true)
388 .expect("unbounded setup.py binding walk")
389 {
390 let name = py_node_text(binding.declaration, source).trim();
391 if !name.is_empty() {
392 names.push(name.to_string());
393 }
394 }
395 let excluded_body = matches!(
396 node.kind(),
397 "function_definition" | "class_definition" | "lambda"
398 )
399 .then(|| node.child_by_field_name("body").map(|body| body.id()))
400 .flatten();
401 let mut cursor = node.walk();
402 pending.extend(
403 node.named_children(&mut cursor)
404 .filter(|child| Some(child.id()) != excluded_body),
405 );
406 }
407 names
408}
409
410fn setup_py_call_imported_function<'a>(
411 call: Node<'_>,
412 source: &str,
413 setup_bindings: &'a HashMap<Vec<String>, String>,
414) -> Option<&'a str> {
415 let mut function = call.child_by_field_name("function")?;
416 let mut path = Vec::new();
417 while function.kind() == "attribute" {
418 let attribute = function.child_by_field_name("attribute")?;
419 path.push(py_node_text(attribute, source).to_string());
420 let object = function.child_by_field_name("object")?;
421 function = object;
422 }
423 if function.kind() != "identifier" {
424 return None;
425 }
426 path.push(py_node_text(function, source).to_string());
427 path.reverse();
428 setup_bindings.get(&path).map(String::as_str)
429}
430
431fn setup_py_import_root_from_call(
432 call: Node<'_>,
433 source: &str,
434 setup_bindings: &HashMap<Vec<String>, String>,
435) -> Option<PathBuf> {
436 let arguments = call.child_by_field_name("arguments")?;
437 if arguments.kind() != "argument_list" {
438 return None;
439 }
440
441 let mut packages = None;
442 let mut package_dir = None;
443 let mut cursor = arguments.walk();
444 for argument in arguments.named_children(&mut cursor) {
445 if argument.kind() == "comment" {
446 continue;
447 }
448 if argument.kind() != "keyword_argument" {
450 return None;
451 }
452 let name = argument.child_by_field_name("name")?;
453 let value = argument.child_by_field_name("value")?;
454 match py_node_text(name, source).trim() {
455 "packages" if packages.is_none() => packages = Some(value),
456 "packages" => return None,
457 "package_dir" if package_dir.is_none() => package_dir = Some(value),
458 "package_dir" => return None,
459 _ => {}
460 }
461 }
462
463 let packages = packages?;
464 if let Some(package_dir) = package_dir {
465 return setup_py_static_package_dir(package_dir, source);
466 }
467 if setup_py_nonempty_literal_packages(packages, source) {
468 return Some(PathBuf::new());
469 }
470 setup_py_discovery_root_from_call(packages, source, setup_bindings)
471}
472
473fn setup_py_discovery_root_from_call(
474 call: Node<'_>,
475 source: &str,
476 setup_bindings: &HashMap<Vec<String>, String>,
477) -> Option<PathBuf> {
478 let function = setup_py_call_imported_function(call, source, setup_bindings)?;
479 if !matches!(function, "find_packages" | "find_namespace_packages") {
480 return None;
481 }
482 let arguments = call.child_by_field_name("arguments")?;
483 if arguments.kind() != "argument_list" {
484 return None;
485 }
486
487 let mut where_value = None;
488 let mut seen_exclude = false;
489 let mut seen_include = false;
490 let mut positional_index = 0;
491 let mut cursor = arguments.walk();
492 for argument in arguments.named_children(&mut cursor) {
493 if argument.kind() == "comment" {
494 continue;
495 }
496 if matches!(argument.kind(), "list_splat" | "dictionary_splat") {
497 return None;
498 }
499 if argument.kind() == "keyword_argument" {
500 let name = py_node_text(argument.child_by_field_name("name")?, source).trim();
501 let value = argument.child_by_field_name("value")?;
502 match name {
503 "where" if where_value.is_none() => where_value = Some(value),
504 "where" => return None,
505 "exclude" if !seen_exclude => seen_exclude = true,
506 "exclude" => return None,
507 "include" if !seen_include => seen_include = true,
508 "include" => return None,
509 _ => return None,
510 }
511 continue;
512 }
513
514 let slot = positional_index;
515 positional_index += 1;
516 match slot {
517 0 if where_value.is_none() => where_value = Some(argument),
518 0 => return None,
519 1 if !seen_exclude => seen_exclude = true,
520 1 => return None,
521 2 if !seen_include => seen_include = true,
522 2 => return None,
523 _ => return None,
524 }
525 }
526
527 where_value
528 .map(|value| python_plain_string_literal(value, source))
529 .unwrap_or(Some(""))
530 .map(PathBuf::from)
531}
532
533fn setup_py_nonempty_literal_packages(value: Node<'_>, source: &str) -> bool {
534 let value = setup_py_unwrap_parenthesized(value);
535 if !matches!(value.kind(), "list" | "set" | "tuple") {
536 return false;
537 }
538 let mut cursor = value.walk();
539 let mut nonempty = false;
540 for element in value.named_children(&mut cursor) {
541 if element.kind() == "comment" {
542 continue;
543 }
544 let Some(package) = python_plain_string_literal(element, source) else {
545 return false;
546 };
547 if package.is_empty() {
548 return false;
549 }
550 nonempty = true;
551 }
552 nonempty
553}
554
555fn setup_py_static_package_dir(value: Node<'_>, source: &str) -> Option<PathBuf> {
556 let value = setup_py_unwrap_parenthesized(value);
557 if value.kind() != "dictionary" {
558 return None;
559 }
560 let mut cursor = value.walk();
561 let mut pairs = value
562 .named_children(&mut cursor)
563 .filter(|node| node.kind() != "comment");
564 let Some(pair) = pairs.next() else {
565 return Some(PathBuf::new());
566 };
567 if pairs.next().is_some() || pair.kind() != "pair" {
568 return None;
569 }
570 let key = python_plain_string_literal(pair.child_by_field_name("key")?, source)?;
571 if !key.is_empty() {
572 return None;
573 }
574 let root = python_plain_string_literal(pair.child_by_field_name("value")?, source)?;
575 let root = PathBuf::from(root);
576 (!root.is_absolute()).then_some(root)
577}
578
579fn setup_py_unwrap_parenthesized(mut node: Node<'_>) -> Node<'_> {
580 while node.kind() == "parenthesized_expression" && node.named_child_count() == 1 {
581 node = node.named_child(0).expect("parenthesized expression child");
582 }
583 node
584}
585
586fn parse_setuptools_where_entries(manifest: &Path) -> Vec<String> {
587 let Ok(source) = std::fs::read_to_string(manifest) else {
588 return Vec::new();
589 };
590 let Ok(document) = source.parse::<toml::Value>() else {
591 return Vec::new();
592 };
593 document
594 .get("tool")
595 .and_then(|tool| tool.get("setuptools"))
596 .and_then(|setuptools| setuptools.get("packages"))
597 .and_then(|packages| packages.get("find"))
598 .and_then(|find| find.get("where"))
599 .and_then(toml::Value::as_array)
600 .map(|entries| {
601 entries
602 .iter()
603 .filter_map(toml::Value::as_str)
604 .map(str::to_string)
605 .collect()
606 })
607 .unwrap_or_default()
608}
609
610fn path_components(path: &Path) -> Vec<String> {
611 path.components()
612 .map(|component| component.as_os_str().to_string_lossy().to_string())
613 .filter(|component| !component.is_empty())
614 .collect()
615}
616
617pub fn python_is_decorated_function_boundary(node: Node<'_>) -> bool {
618 if node.kind() != "decorated_definition" {
619 return false;
620 }
621 let mut cursor = node.walk();
622 node.named_children(&mut cursor)
623 .any(|child| child.kind() == "function_definition")
624}
625
626#[derive(Clone)]
627pub struct Scope {
628 kind: ScopeKind,
629 path: String,
630 fq: FqName,
637 code_unit: Option<CodeUnit>,
638 method_receiver: Option<String>,
639}
640
641#[derive(Clone, Copy, PartialEq, Eq)]
642enum ScopeKind {
643 Class,
644 Function,
645}
646
647pub struct PythonVisitor<'a> {
648 pub file: &'a ProjectFile,
649 pub source: &'a str,
650 pub package_name: &'a str,
651 module_fq: &'a FqName,
652 pub parsed: &'a mut ParsedFile,
653 pub module: Option<CodeUnit>,
654 pub overload_decorators: &'a PythonOverloadDecoratorBindings,
655}
656
657struct PythonContainer<'tree> {
658 node: Node<'tree>,
659 scope: Vec<Scope>,
660 module_control_depth: usize,
661}
662
663enum PythonWork<'tree> {
664 Container(PythonContainer<'tree>),
665 Statement {
666 node: Node<'tree>,
667 scope: Vec<Scope>,
668 module_control_depth: usize,
669 },
670}
671
672impl<'a> PythonVisitor<'a> {
673 pub fn visit_container(
674 &mut self,
675 node: Node<'_>,
676 scope: &[Scope],
677 module_control_depth: usize,
678 ) {
679 let mut stack = vec![PythonWork::Container(PythonContainer {
680 node,
681 scope: scope.to_vec(),
682 module_control_depth,
683 })];
684 while let Some(work) = stack.pop() {
685 match work {
686 PythonWork::Container(container) => {
687 let mut cursor = container.node.walk();
688 let children = container
689 .node
690 .named_children(&mut cursor)
691 .collect::<Vec<_>>();
692 for child in children.into_iter().rev() {
693 stack.push(PythonWork::Statement {
694 node: child,
695 scope: container.scope.clone(),
696 module_control_depth: container.module_control_depth,
697 });
698 }
699 }
700 PythonWork::Statement {
701 node,
702 scope,
703 module_control_depth,
704 } => self.visit_statement(node, &scope, module_control_depth, &mut stack),
705 }
706 }
707 }
708
709 fn visit_statement<'tree>(
710 &mut self,
711 node: Node<'tree>,
712 scope: &[Scope],
713 module_control_depth: usize,
714 stack: &mut Vec<PythonWork<'tree>>,
715 ) {
716 match node.kind() {
717 "decorated_definition" => {
718 if let Some(definition) = node.child_by_field_name("definition") {
719 self.visit_definition(
720 definition,
721 Some(node),
722 scope,
723 module_control_depth,
724 stack,
725 );
726 }
727 }
728 "class_definition" | "function_definition" => {
729 self.visit_definition(node, None, scope, module_control_depth, stack)
730 }
731 "expression_statement" => {
732 self.visit_expression_statement(node, scope, module_control_depth)
733 }
734 "import_statement" | "import_from_statement" => self.visit_import_statement(node),
735 "if_statement" | "try_statement" | "with_statement" | "for_statement"
736 | "while_statement" => {
737 let next_depth = if scope.is_empty() {
738 module_control_depth + 1
739 } else {
740 module_control_depth
741 };
742 stack.push(PythonWork::Container(PythonContainer {
743 node,
744 scope: scope.to_vec(),
745 module_control_depth: next_depth,
746 }));
747 }
748 "elif_clause" | "else_clause" | "except_clause" | "finally_clause" => {
749 stack.push(PythonWork::Container(PythonContainer {
750 node,
751 scope: scope.to_vec(),
752 module_control_depth,
753 }));
754 }
755 "block" | "module" => stack.push(PythonWork::Container(PythonContainer {
756 node,
757 scope: scope.to_vec(),
758 module_control_depth,
759 })),
760 _ => {}
761 }
762 }
763
764 fn visit_definition<'tree>(
765 &mut self,
766 definition: Node<'tree>,
767 wrapper: Option<Node<'tree>>,
768 scope: &[Scope],
769 module_control_depth: usize,
770 stack: &mut Vec<PythonWork<'tree>>,
771 ) {
772 match definition.kind() {
773 "class_definition" => self.visit_class_definition(
774 definition,
775 wrapper.unwrap_or(definition),
776 scope,
777 module_control_depth,
778 stack,
779 ),
780 "function_definition" => self.visit_function_definition(
781 definition,
782 wrapper.unwrap_or(definition),
783 scope,
784 module_control_depth,
785 stack,
786 ),
787 _ => {}
788 }
789 }
790
791 fn visit_class_definition<'tree>(
792 &mut self,
793 node: Node<'tree>,
794 range_node: Node<'tree>,
795 scope: &[Scope],
796 module_control_depth: usize,
797 stack: &mut Vec<PythonWork<'tree>>,
798 ) {
799 let Some(name_node) = node.child_by_field_name("name") else {
800 return;
801 };
802 let name = py_node_text(name_node, self.source).trim();
803 if name.is_empty() {
804 return;
805 }
806
807 let capture = !scope.is_empty() || module_control_depth <= 1;
808
809 let short_name = scope
810 .last()
811 .map(|parent| format!("{}${name}", parent.path))
812 .unwrap_or_else(|| name.to_string());
813 let fq = match scope.last() {
819 Some(parent) => parent
820 .fq
821 .clone()
822 .with_pushed(py_segment(name, SegmentKind::Nested)),
823 None => self
824 .module_fq
825 .clone()
826 .with_pushed(py_segment(name, SegmentKind::Type)),
827 };
828 let code_unit = CodeUnit::new_fq(
829 self.file.clone(),
830 CodeUnitType::Class,
831 self.package_name.to_string(),
832 short_name.clone(),
833 fq.clone(),
834 );
835 if capture {
836 self.parsed
837 .replace_code_unit(code_unit.clone(), range_node, self.source, None, None);
838 self.parsed.add_signature(
839 code_unit.clone(),
840 python_class_signature(range_node, self.source),
841 );
842 if let Some(module) = &self.module
843 && scope.is_empty()
844 {
845 self.parsed.add_child(module.clone(), code_unit.clone());
846 }
847 if let Some(parent) = scope.last()
848 && let Some(parent_cu) = &parent.code_unit
849 {
850 self.parsed.add_child(parent_cu.clone(), code_unit.clone());
851 }
852 self.parsed.set_raw_supertypes(
853 code_unit.clone(),
854 extract_python_supertypes(node, self.source),
855 );
856 }
857
858 let mut next_scope = scope.to_vec();
859 if capture {
860 next_scope.push(Scope {
861 kind: ScopeKind::Class,
862 path: short_name,
863 fq,
864 code_unit: Some(code_unit),
865 method_receiver: None,
866 });
867 }
868 if let Some(body) = node.child_by_field_name("body") {
869 stack.push(PythonWork::Container(PythonContainer {
870 node: body,
871 scope: next_scope,
872 module_control_depth,
873 }));
874 }
875 }
876
877 fn visit_function_definition<'tree>(
878 &mut self,
879 node: Node<'tree>,
880 range_node: Node<'tree>,
881 scope: &[Scope],
882 module_control_depth: usize,
883 stack: &mut Vec<PythonWork<'tree>>,
884 ) {
885 let Some(name_node) = node.child_by_field_name("name") else {
886 return;
887 };
888 let name = py_node_text(name_node, self.source).trim();
889 if name.is_empty() {
890 return;
891 }
892
893 let capture = !python_is_property_mutator(range_node, self.source)
894 && ((scope.is_empty() && module_control_depth <= 1)
895 || scope
896 .last()
897 .is_some_and(|parent| parent.kind == ScopeKind::Class));
898 let short_name = if let Some(parent) = scope.last() {
899 match parent.kind {
900 ScopeKind::Class => format!("{}.{}", parent.path, name),
901 ScopeKind::Function => format!("{}${name}", parent.path),
902 }
903 } else {
904 name.to_string()
905 };
906 let fq = if let Some(parent) = scope.last() {
911 match parent.kind {
912 ScopeKind::Class => parent
913 .fq
914 .clone()
915 .with_pushed(py_segment(name, SegmentKind::Member)),
916 ScopeKind::Function => parent
917 .fq
918 .clone()
919 .with_pushed(py_segment(name, SegmentKind::Nested)),
920 }
921 } else {
922 self.module_fq
923 .clone()
924 .with_pushed(py_segment(name, SegmentKind::Member))
925 };
926
927 if capture {
928 let code_unit_type = if python_function_has_decorator(node, self.source, "property") {
929 CodeUnitType::Field
930 } else {
931 CodeUnitType::Function
932 };
933 let signature = node
934 .child_by_field_name("parameters")
935 .map(|parameters| py_node_text(parameters, self.source).trim().to_string());
936 let code_unit = CodeUnit::with_signature_and_fq(
937 self.file.clone(),
938 code_unit_type,
939 self.package_name.to_string(),
940 short_name.clone(),
941 signature,
942 false,
943 fq.clone(),
944 );
945 self.parsed
946 .replace_code_unit(code_unit.clone(), range_node, self.source, None, None);
947 let signature = python_function_signature(range_node, self.source);
948 self.parsed.add_signature_with_metadata(
949 code_unit.clone(),
950 python_signature_metadata(signature, node, self.source).with_declaration_only(
951 self.overload_decorators
952 .decorates_as_overload(node, self.source),
953 ),
954 );
955 if let Some(module) = &self.module
956 && scope.is_empty()
957 {
958 self.parsed.add_child(module.clone(), code_unit.clone());
959 }
960 if let Some(parent) = scope.last()
961 && parent.kind == ScopeKind::Class
962 && let Some(parent_cu) = &parent.code_unit
963 {
964 self.parsed.add_child(parent_cu.clone(), code_unit.clone());
965 }
966 let scope_code_unit = Some(code_unit);
967 let mut next_scope = scope.to_vec();
968 next_scope.push(Scope {
969 kind: ScopeKind::Function,
970 path: short_name,
971 fq,
972 code_unit: scope_code_unit,
973 method_receiver: scope
974 .last()
975 .is_some_and(|parent| parent.kind == ScopeKind::Class)
976 .then(|| python_instance_method_receiver_name(node, self.source))
977 .flatten(),
978 });
979 if let Some(body) = node.child_by_field_name("body") {
980 stack.push(PythonWork::Container(PythonContainer {
981 node: body,
982 scope: next_scope,
983 module_control_depth,
984 }));
985 }
986 return;
987 }
988
989 let mut next_scope = scope.to_vec();
990 next_scope.push(Scope {
991 kind: ScopeKind::Function,
992 path: short_name,
993 fq,
994 code_unit: None,
995 method_receiver: None,
996 });
997 if let Some(body) = node.child_by_field_name("body") {
998 stack.push(PythonWork::Container(PythonContainer {
999 node: body,
1000 scope: next_scope,
1001 module_control_depth,
1002 }));
1003 }
1004 }
1005
1006 fn visit_expression_statement(
1007 &mut self,
1008 node: Node<'_>,
1009 scope: &[Scope],
1010 module_control_depth: usize,
1011 ) {
1012 let Some(assignment) = node.named_child(0) else {
1013 return;
1014 };
1015 if assignment.kind() != "assignment" {
1016 return;
1017 }
1018 let targets = python_chained_assignment_targets(assignment);
1019 if targets.is_empty() {
1020 return;
1021 }
1022 for left in &targets {
1023 self.visit_instance_attribute_assignment(*left, scope);
1024 }
1025 let names = targets
1026 .iter()
1027 .flat_map(|left| collect_assigned_names(*left, self.source))
1028 .collect::<Vec<_>>();
1029 for name in names {
1030 let (short_name, fq) = if let Some(parent) = scope.last() {
1031 if parent.kind != ScopeKind::Class {
1032 continue;
1033 }
1034 (
1035 format!("{}.{}", parent.path, name),
1036 parent
1037 .fq
1038 .clone()
1039 .with_pushed(py_segment(&name, SegmentKind::Member)),
1040 )
1041 } else if module_control_depth <= 1 {
1042 (
1043 name.clone(),
1044 self.module_fq
1045 .clone()
1046 .with_pushed(py_segment(&name, SegmentKind::Member)),
1047 )
1048 } else {
1049 continue;
1050 };
1051 let code_unit = CodeUnit::new_fq(
1052 self.file.clone(),
1053 CodeUnitType::Field,
1054 self.package_name.to_string(),
1055 short_name,
1056 fq,
1057 );
1058 if scope
1059 .last()
1060 .is_some_and(|parent| parent.kind == ScopeKind::Class)
1061 {
1062 self.parsed
1066 .add_code_unit(code_unit.clone(), node, self.source, None, None);
1067 } else {
1068 self.parsed
1069 .replace_code_unit(code_unit.clone(), node, self.source, None, None);
1070 }
1071 self.parsed.add_signature(
1072 code_unit.clone(),
1073 py_node_text(node, self.source).trim().to_string(),
1074 );
1075 if let Some(module) = &self.module
1076 && scope.is_empty()
1077 {
1078 self.parsed.add_child(module.clone(), code_unit.clone());
1079 }
1080 if let Some(parent) = scope.last()
1081 && parent.kind == ScopeKind::Class
1082 && let Some(parent_cu) = &parent.code_unit
1083 {
1084 self.parsed.add_child(parent_cu.clone(), code_unit);
1085 }
1086 }
1087 }
1088
1089 fn visit_instance_attribute_assignment(&mut self, left: Node<'_>, scope: &[Scope]) {
1090 let Some(function) = scope
1091 .last()
1092 .filter(|scope| scope.kind == ScopeKind::Function)
1093 else {
1094 return;
1095 };
1096 let Some(receiver) = function.method_receiver.as_deref() else {
1097 return;
1098 };
1099 let Some(parent) = scope
1100 .get(scope.len().saturating_sub(2))
1101 .filter(|scope| scope.kind == ScopeKind::Class)
1102 else {
1103 return;
1104 };
1105 let Some(parent_cu) = parent.code_unit.clone() else {
1106 return;
1107 };
1108 for (name, node) in collect_self_assigned_attributes(left, self.source, receiver) {
1109 let code_unit = CodeUnit::new_fq(
1110 self.file.clone(),
1111 CodeUnitType::Field,
1112 self.package_name.to_string(),
1113 format!("{}.{}", parent.path, name),
1114 parent
1115 .fq
1116 .clone()
1117 .with_pushed(py_segment(&name, SegmentKind::Member)),
1118 );
1119 if !self.parsed.contains_declaration(&code_unit) {
1120 self.parsed.replace_code_unit(
1121 code_unit.clone(),
1122 node,
1123 self.source,
1124 Some(parent_cu.clone()),
1125 Some(parent_cu.clone()),
1126 );
1127 }
1128 self.parsed.add_signature(
1129 code_unit.clone(),
1130 py_node_text(left, self.source).trim().to_string(),
1131 );
1132 }
1133 }
1134
1135 fn visit_import_statement(&mut self, node: Node<'_>) {
1136 for info in python_import_infos_from_node(node, self.source) {
1137 self.parsed.imports.push(info);
1138 }
1139 }
1140}
1141
1142pub fn parse_python_file(file: &ProjectFile, source: &str, tree: &Tree) -> ParsedFile {
1146 let module_components = python_module_components(file);
1147 let module_name = module_components.join(".");
1148 let module_fq = python_module_fq_from_components(&module_components);
1149 let mut parsed = ParsedFile::new(module_name.clone());
1150 let root = tree.root_node();
1151
1152 collect_python_identifiers(root, source, &mut parsed.type_identifiers);
1153
1154 let module_code_unit = module_code_unit_from_fq(file, &module_components, module_fq.clone());
1155 if let Some(module) = module_code_unit.clone() {
1156 parsed.add_code_unit(module, root, source, None, None);
1157 }
1158
1159 let overload_decorators = PythonOverloadDecoratorBindings::collect(root, source);
1160 let mut visitor = PythonVisitor {
1161 file,
1162 source,
1163 package_name: &module_name,
1164 module_fq: &module_fq,
1165 parsed: &mut parsed,
1166 module: module_code_unit,
1167 overload_decorators: &overload_decorators,
1168 };
1169 visitor.visit_container(root, &[], 0);
1170
1171 parsed
1172}
1173
1174pub fn py_node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1175 brokk_bifrost_core::analyzer::common::node_source_text(node, source)
1176}
1177
1178pub fn python_module_name(file: &ProjectFile) -> String {
1179 python_module_components(file).join(".")
1180}
1181
1182pub fn module_code_unit(file: &ProjectFile, module_fq: &str) -> Option<CodeUnit> {
1183 if module_fq.is_empty() {
1184 return None;
1185 }
1186 let components = python_module_components(file);
1187 debug_assert_eq!(
1188 module_fq,
1189 components.join("."),
1190 "module_code_unit must be built from the file's path-derived Python module name"
1191 );
1192 let structured_fq = python_module_fq_from_components(&components);
1193 module_code_unit_from_fq(file, &components, structured_fq)
1194}
1195
1196fn module_code_unit_from_fq(
1197 file: &ProjectFile,
1198 components: &[String],
1199 structured_fq: FqName,
1200) -> Option<CodeUnit> {
1201 let (short_name, package_components) = components.split_last()?;
1202 let package_name = package_components.join(".");
1203 Some(CodeUnit::new_fq(
1204 file.clone(),
1205 CodeUnitType::Module,
1206 package_name,
1207 short_name.clone(),
1208 structured_fq,
1209 ))
1210}
1211
1212fn python_class_signature(node: Node<'_>, source: &str) -> String {
1213 python_header_with_decorators(node, source)
1214}
1215
1216fn python_function_signature(node: Node<'_>, source: &str) -> String {
1217 let header = python_header_with_decorators(node, source);
1218 if let Some((head, tail)) = header.rsplit_once('\n') {
1219 format!("{head}\n{tail} ...")
1220 } else {
1221 format!("{header} ...")
1222 }
1223}
1224
1225fn python_signature_metadata(signature: String, node: Node<'_>, source: &str) -> SignatureMetadata {
1226 let Some(parameters_node) = node.child_by_field_name("parameters") else {
1227 return SignatureMetadata::new(signature, Vec::new())
1228 .with_dispatch_extensibility(DispatchExtensibility::Open);
1229 };
1230 let parameter_text = py_node_text(parameters_node, source).trim();
1231 let Some(parameters_start) = signature.find(parameter_text) else {
1232 return SignatureMetadata::new(signature, Vec::new())
1233 .with_dispatch_extensibility(DispatchExtensibility::Open);
1234 };
1235 let parameters_end = parameters_start + parameter_text.len();
1236 let mut search_start = parameters_start;
1237 let parameters = python_parameter_label_nodes(parameters_node)
1238 .into_iter()
1239 .filter_map(|label_node| {
1240 let label = py_node_text(label_node, source).trim();
1241 if label.is_empty() || search_start > parameters_end {
1242 return None;
1243 }
1244 let haystack = signature.get(search_start..parameters_end)?;
1245 let relative_start = haystack.find(label)?;
1246 let start_byte = search_start + relative_start;
1247 let end_byte = start_byte + label.len();
1248 search_start = end_byte;
1249 Some(ParameterMetadata::new(label, start_byte, end_byte))
1250 })
1251 .collect();
1252 SignatureMetadata::new(signature, parameters)
1253 .with_dispatch_extensibility(DispatchExtensibility::Open)
1254}
1255
1256fn python_parameter_label_nodes(parameters_node: Node<'_>) -> Vec<Node<'_>> {
1257 let mut labels = Vec::new();
1258 let mut cursor = parameters_node.walk();
1259 for child in parameters_node.named_children(&mut cursor) {
1260 if let Some(label_node) = python_parameter_label_node(child) {
1261 labels.push(label_node);
1262 }
1263 }
1264 labels
1265}
1266
1267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1284pub enum PythonParameterSplat {
1285 Positional,
1287 Keyword,
1289}
1290
1291pub fn python_parameter_splat(parameter: Node<'_>) -> Option<PythonParameterSplat> {
1292 let splat = match parameter.kind() {
1293 kind @ ("list_splat_pattern" | "dictionary_splat_pattern") => kind,
1294 _ => {
1295 let mut cursor = parameter.walk();
1296 parameter
1297 .named_children(&mut cursor)
1298 .map(|child| child.kind())
1299 .find(|kind| matches!(*kind, "list_splat_pattern" | "dictionary_splat_pattern"))?
1300 }
1301 };
1302 match splat {
1303 "list_splat_pattern" => Some(PythonParameterSplat::Positional),
1304 "dictionary_splat_pattern" => Some(PythonParameterSplat::Keyword),
1305 _ => unreachable!("the splat kind was matched above"),
1306 }
1307}
1308
1309pub fn python_parameter_label_node(node: Node<'_>) -> Option<Node<'_>> {
1310 match node.kind() {
1311 "identifier" => Some(node),
1312 "typed_parameter"
1313 | "typed_default_parameter"
1314 | "default_parameter"
1315 | "list_splat_pattern"
1316 | "dictionary_splat_pattern"
1317 | "keyword_separator" => node.child_by_field_name("name").or_else(|| {
1318 let mut cursor = node.walk();
1319 node.named_children(&mut cursor)
1320 .find_map(python_parameter_label_node)
1321 }),
1322 _ => None,
1323 }
1324}
1325
1326fn python_is_property_mutator(node: Node<'_>, source: &str) -> bool {
1327 python_header_with_decorators(node, source)
1328 .lines()
1329 .map(str::trim)
1330 .filter(|line| line.starts_with('@'))
1331 .any(|decorator| decorator.ends_with(".setter") || decorator.ends_with(".deleter"))
1332}
1333
1334pub fn python_expanded_comment_start(source: &str, start_byte: usize) -> usize {
1335 let line_starts = compute_line_starts(source);
1336 let line_index = find_line_index_for_offset(&line_starts, start_byte);
1337
1338 let mut comment_start = start_byte;
1339 for line_idx in (0..line_index).rev() {
1340 let line_start = line_starts[line_idx];
1341 let line_end = line_starts
1342 .get(line_idx + 1)
1343 .copied()
1344 .unwrap_or(source.len());
1345 let line = &source[line_start..line_end];
1346 let trimmed = line.trim_start();
1347
1348 if trimmed.trim().is_empty() {
1349 continue;
1350 }
1351
1352 if trimmed.starts_with('#') {
1353 comment_start = line_start;
1354 continue;
1355 }
1356
1357 break;
1358 }
1359
1360 comment_start
1361}
1362
1363fn python_header_with_decorators(node: Node<'_>, source: &str) -> String {
1364 let raw = py_node_text(node, source);
1365 let lines: Vec<_> = raw
1366 .lines()
1367 .map(str::trim_end)
1368 .filter(|line| !line.trim().is_empty())
1369 .collect();
1370 let mut relevant = Vec::new();
1371 for line in lines {
1372 let trimmed = line.trim_start();
1373 if trimmed.starts_with('@')
1374 || trimmed.starts_with("def ")
1375 || trimmed.starts_with("async def ")
1376 || trimmed.starts_with("class ")
1377 {
1378 relevant.push(trimmed.to_string());
1379 if trimmed.starts_with("def ")
1380 || trimmed.starts_with("async def ")
1381 || trimmed.starts_with("class ")
1382 {
1383 break;
1384 }
1385 }
1386 }
1387 relevant.join("\n")
1388}
1389
1390fn extract_python_supertypes(node: Node<'_>, source: &str) -> Vec<String> {
1409 let Some(superclasses) = node.child_by_field_name("superclasses") else {
1410 return Vec::new();
1411 };
1412 let mut result = Vec::new();
1413 let mut cursor = superclasses.walk();
1414 for child in superclasses.named_children(&mut cursor) {
1415 if child.kind() == "keyword_argument" {
1416 continue;
1417 }
1418 let named = python_base_origin_node(child);
1419 let text = py_node_text(named, source).trim();
1420 if !text.is_empty() {
1421 result.push(text.to_string());
1422 }
1423 }
1424 result
1425}
1426
1427pub fn python_base_origin_node<'tree>(base: Node<'tree>) -> Node<'tree> {
1434 if base.kind() != "subscript" {
1435 return base;
1436 }
1437 let Some(value) = base.child_by_field_name("value") else {
1438 return base;
1439 };
1440 if matches!(value.kind(), "identifier" | "attribute") {
1441 value
1442 } else {
1443 base
1444 }
1445}
1446
1447fn collect_assigned_names(node: Node<'_>, source: &str) -> Vec<String> {
1448 let mut names = Vec::new();
1449 walk_named_tree_preorder(node, true, |node| {
1450 match node.kind() {
1451 "attribute" | "subscript" => WalkControl::SkipChildren,
1455 "identifier" => {
1456 let text = py_node_text(node, source).trim();
1457 if !text.is_empty() {
1458 names.push(text.to_string());
1459 }
1460 WalkControl::Continue
1461 }
1462 _ => WalkControl::Continue,
1463 }
1464 });
1465 names
1466}
1467
1468fn collect_self_assigned_attributes<'tree>(
1469 node: Node<'tree>,
1470 source: &str,
1471 receiver_name: &str,
1472) -> Vec<(String, Node<'tree>)> {
1473 let mut attributes = Vec::new();
1474 collect_direct_self_assigned_attributes(node, source, receiver_name, &mut attributes);
1475 attributes
1476}
1477
1478fn collect_direct_self_assigned_attributes<'tree>(
1479 node: Node<'tree>,
1480 source: &str,
1481 receiver_name: &str,
1482 attributes: &mut Vec<(String, Node<'tree>)>,
1483) {
1484 match node.kind() {
1485 "attribute" => {
1486 let Some(object) = node.child_by_field_name("object") else {
1487 return;
1488 };
1489 if object.kind() != "identifier" || py_node_text(object, source).trim() != receiver_name
1490 {
1491 return;
1492 }
1493 let Some(attribute) = node.child_by_field_name("attribute") else {
1494 return;
1495 };
1496 let name = py_node_text(attribute, source).trim();
1497 if !name.is_empty() {
1498 attributes.push((name.to_string(), attribute));
1499 }
1500 }
1501 "pattern_list"
1506 | "tuple_pattern"
1507 | "list_pattern"
1508 | "tuple"
1509 | "list"
1510 | "parenthesized_expression" => {
1511 let mut cursor = node.walk();
1512 for child in node.named_children(&mut cursor) {
1513 collect_direct_self_assigned_attributes(child, source, receiver_name, attributes);
1514 }
1515 }
1516 _ => {}
1517 }
1518}
1519
1520fn python_instance_method_receiver_name(node: Node<'_>, source: &str) -> Option<String> {
1521 if python_function_has_decorator(node, source, "staticmethod")
1522 || python_function_has_decorator(node, source, "classmethod")
1523 {
1524 return None;
1525 }
1526 python_first_parameter_name(node, source)
1527}
1528
1529fn python_function_has_decorator(node: Node<'_>, source: &str, decorator_name: &str) -> bool {
1530 let Some(parent) = node.parent() else {
1531 return false;
1532 };
1533 if parent.kind() != "decorated_definition" {
1534 return false;
1535 }
1536 let mut cursor = parent.walk();
1537 parent
1538 .named_children(&mut cursor)
1539 .filter(|child| child.kind() == "decorator")
1540 .filter_map(|decorator| decorator.named_child(0))
1541 .filter_map(expression_name_node)
1542 .any(|name| py_node_text(name, source).trim() == decorator_name)
1543}
1544
1545pub fn python_first_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
1548 let parameters = node.child_by_field_name("parameters")?;
1549 let mut cursor = parameters.walk();
1550 parameters
1551 .named_children(&mut cursor)
1552 .find_map(|child| python_parameter_name(child, source))
1553}
1554
1555fn python_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
1556 match node.kind() {
1557 "identifier" => Some(py_node_text(node, source).trim().to_string()),
1558 "typed_parameter"
1559 | "default_parameter"
1560 | "list_splat_pattern"
1561 | "dictionary_splat_pattern" => node
1562 .child_by_field_name("name")
1563 .or_else(|| {
1564 let mut cursor = node.walk();
1565 node.named_children(&mut cursor)
1566 .find(|child| child.kind() == "identifier")
1567 })
1568 .and_then(|name| python_parameter_name(name, source)),
1569 _ => None,
1570 }
1571 .filter(|name| !name.is_empty())
1572}
1573
1574pub fn collect_python_identifiers(node: Node<'_>, source: &str, identifiers: &mut HashSet<String>) {
1575 walk_named_tree_preorder(node, true, |node| {
1576 if node.kind() == "identifier" {
1577 let text = py_node_text(node, source).trim();
1578 if !text.is_empty() {
1579 identifiers.insert(text.to_string());
1580 }
1581 }
1582 WalkControl::Continue
1583 });
1584}
1585
1586pub fn parse_python_tree(source: &str) -> Option<Tree> {
1587 let mut parser = Parser::new();
1588 parser
1589 .set_language(&tree_sitter_python::LANGUAGE.into())
1590 .expect("failed to load python parser");
1591 parser.parse(source, None)
1592}
1593
1594fn python_chained_assignment_targets<'tree>(assignment: Node<'tree>) -> Vec<Node<'tree>> {
1601 let mut targets = Vec::new();
1602 let mut node = assignment;
1603 while let Some(left) = node.child_by_field_name("left") {
1604 targets.push(left);
1605 match node.child_by_field_name("right") {
1606 Some(right) if right.kind() == "assignment" => node = right,
1607 _ => break,
1608 }
1609 }
1610 targets
1611}
1612
1613#[cfg(test)]
1614mod supertype_tests {
1615 use super::extract_python_supertypes;
1616 use tree_sitter::{Node, Parser};
1617
1618 fn class_node<'tree>(tree: &'tree tree_sitter::Tree, source: &str) -> Node<'tree> {
1619 let mut cursor = tree.root_node().walk();
1620 tree.root_node()
1621 .named_children(&mut cursor)
1622 .find(|node| node.kind() == "class_definition")
1623 .unwrap_or_else(|| panic!("source declares a class: {source}"))
1624 }
1625
1626 fn parse(source: &str) -> tree_sitter::Tree {
1627 let mut parser = Parser::new();
1628 parser
1629 .set_language(&tree_sitter_python::LANGUAGE.into())
1630 .expect("the Python grammar loads");
1631 parser.parse(source, None).expect("the source parses")
1632 }
1633
1634 #[test]
1635 fn every_positional_base_contributes_a_spelling() {
1636 for (source, expected) in [
1637 ("class A(Base): pass\n", vec!["Base"]),
1638 ("class A(pkg.Base): pass\n", vec!["pkg.Base"]),
1639 ("class A(Base[int]): pass\n", vec!["Base"]),
1642 ("class A(pkg.Base[str, int]): pass\n", vec!["pkg.Base"]),
1643 (
1644 "class A(Mapping[str, Any], Base): pass\n",
1645 vec!["Mapping", "Base"],
1646 ),
1647 ("class A(Base, metaclass=Meta): pass\n", vec!["Base"]),
1649 ("class A(metaclass=Meta): pass\n", Vec::new()),
1650 (
1653 "class A(namedtuple(\"P\", \"x\")): pass\n",
1654 vec!["namedtuple(\"P\", \"x\")"],
1655 ),
1656 ("class A(*bases): pass\n", vec!["*bases"]),
1657 ("class A: pass\n", Vec::new()),
1658 ] {
1659 let tree = parse(source);
1660 let node = class_node(&tree, source);
1661 assert_eq!(
1662 extract_python_supertypes(node, source),
1663 expected,
1664 "supertypes of {source}"
1665 );
1666 }
1667 }
1668}