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::structural::resolution::DeclaredVisibility;
13use brokk_bifrost_core::analyzer::tree_walk::{WalkControl, walk_named_tree_preorder};
14use brokk_bifrost_core::analyzer::{CodeUnit, ProjectFile};
15use brokk_bifrost_core::hash::{HashMap, HashSet};
16use brokk_bifrost_core::path_normalization::NormalizePath;
17use brokk_bifrost_core::text_utils::{compute_line_starts, find_line_index_for_offset};
18use std::path::{Path, PathBuf};
19use tree_sitter::{Node, Parser, Tree};
20
21fn py_segment(text: &str, kind: SegmentKind) -> SegmentId {
23 segment_interner().intern(text, kind)
24}
25
26pub fn python_module_fq(file: &ProjectFile) -> FqName {
39 python_module_fq_from_components(&python_module_components(file))
40}
41
42fn python_module_fq_from_components(components: &[String]) -> FqName {
43 let mut fq = FqName::new();
44 for component in components {
45 fq.push(py_segment(component, SegmentKind::Package));
46 }
47 fq
48}
49
50fn python_module_components(file: &ProjectFile) -> Vec<String> {
51 python_module_components_from_root(file, &python_import_root(file))
52}
53
54pub fn python_project_module_components(file: &ProjectFile) -> Vec<String> {
66 python_module_components_from_root(file, Path::new(""))
67}
68
69fn python_module_components_from_root(file: &ProjectFile, import_root_rel: &Path) -> Vec<String> {
72 let mut components = python_package_components_for_file(file, import_root_rel);
73 let module_name = file
74 .rel_path()
75 .file_stem()
76 .and_then(|stem| stem.to_str())
77 .unwrap_or_default();
78 if module_name != "__init__" || components.is_empty() {
79 components.push(module_name.to_string());
80 }
81 components
82}
83
84pub fn python_import_root(file: &ProjectFile) -> PathBuf {
96 let Some(parent_rel) = file.rel_path().parent() else {
97 return PathBuf::new();
98 };
99 if parent_rel.as_os_str().is_empty() {
100 return PathBuf::new();
101 }
102
103 if let Some(import_root_rel) = python_configured_import_root(file, parent_rel) {
104 return import_root_rel;
105 }
106
107 let mut effective_package_root_rel: Option<&Path> = None;
108 let mut current_rel = Some(parent_rel);
109 while let Some(path) = current_rel {
110 if file.root().join(path).join("__init__.py").exists() {
111 effective_package_root_rel = Some(path);
112 }
113 current_rel = path.parent();
114 }
115
116 effective_package_root_rel
119 .and_then(Path::parent)
120 .unwrap_or_else(|| Path::new(""))
121 .to_path_buf()
122}
123
124fn python_package_components_for_file(file: &ProjectFile, import_root_rel: &Path) -> Vec<String> {
125 let Some(parent_rel) = file.rel_path().parent() else {
126 return Vec::new();
127 };
128 let relative_package = parent_rel
129 .strip_prefix(import_root_rel)
130 .expect("a Python import root is a prefix of the paths it names modules for");
131 path_components(relative_package)
132}
133
134fn python_configured_import_root(file: &ProjectFile, parent_rel: &Path) -> Option<PathBuf> {
140 let mut manifest_dir_rel = Some(parent_rel);
141 while let Some(directory) = manifest_dir_rel {
142 let manifest_dir = file.root().join(directory);
143 let mut roots = setuptools_where_entries(&manifest_dir.join("pyproject.toml"))
144 .iter()
145 .map(|entry| manifest_dir.join(entry).normalize())
146 .filter_map(|root| root.strip_prefix(file.root()).ok().map(Path::to_path_buf))
147 .filter(|root| parent_rel.starts_with(root))
148 .collect::<Vec<_>>();
149 roots.sort_by_key(|root| root.components().count());
150 if let Some(root) = roots.pop() {
151 return Some(root);
152 }
153 if let Some(package_dir) = setuptools_setup_py_import_root(&manifest_dir.join("setup.py")) {
154 let root = manifest_dir.join(package_dir).normalize();
155 if let Ok(root) = root.strip_prefix(file.root())
156 && parent_rel.starts_with(root)
157 {
158 return Some(root.to_path_buf());
159 }
160 }
161 manifest_dir_rel = directory.parent();
162 }
163 None
164}
165
166pub fn python_declared_import_root(file: &ProjectFile) -> Option<PathBuf> {
178 let parent_rel = file.rel_path().parent()?;
179 if parent_rel.as_os_str().is_empty() {
180 return None;
181 }
182 python_configured_import_root(file, parent_rel)
183}
184
185pub(crate) fn python_repeated_root_extension_dir(
210 file: &ProjectFile,
211 source_root_rel: &Path,
212 declared_root_rel: &Path,
213) -> Option<PathBuf> {
214 let implied_rel = source_root_rel.strip_prefix(declared_root_rel).ok()?;
215 let implied = path_components(implied_rel);
216 if implied.is_empty()
217 || !python_module_components_from_root(file, source_root_rel).starts_with(&implied)
218 {
219 return None;
220 }
221 Some(source_root_rel.join(implied_rel))
222}
223
224#[derive(Clone, Copy, PartialEq, Eq)]
225struct FileStamp {
226 len: u64,
227 modified: Option<std::time::SystemTime>,
228}
229
230fn file_stamp(path: &Path) -> Option<FileStamp> {
231 let metadata = std::fs::metadata(path).ok()?;
232 Some(FileStamp {
233 len: metadata.len(),
234 modified: metadata.modified().ok(),
235 })
236}
237
238struct ManifestWhereEntries {
243 stamp: FileStamp,
244 entries: Vec<String>,
245}
246
247fn setuptools_where_entries(manifest: &Path) -> Vec<String> {
258 static MEMO: std::sync::OnceLock<
259 std::sync::RwLock<std::collections::HashMap<PathBuf, ManifestWhereEntries>>,
260 > = std::sync::OnceLock::new();
261 let memo = MEMO.get_or_init(Default::default);
262
263 let Some(stamp) = file_stamp(manifest) else {
264 return Vec::new();
265 };
266 if let Some(cached) = memo.read().expect("manifest memo").get(manifest)
267 && cached.stamp == stamp
268 {
269 return cached.entries.clone();
270 }
271
272 let entries = parse_setuptools_where_entries(manifest);
273 memo.write().expect("manifest memo").insert(
274 manifest.to_path_buf(),
275 ManifestWhereEntries {
276 stamp,
277 entries: entries.clone(),
278 },
279 );
280 entries
281}
282
283struct SetupPyImportRoot {
287 stamp: FileStamp,
288 package_dir: Option<PathBuf>,
289}
290
291fn setuptools_setup_py_import_root(setup_py: &Path) -> Option<PathBuf> {
301 static MEMO: std::sync::OnceLock<
302 std::sync::RwLock<std::collections::HashMap<PathBuf, SetupPyImportRoot>>,
303 > = std::sync::OnceLock::new();
304 let memo = MEMO.get_or_init(Default::default);
305
306 let stamp = file_stamp(setup_py)?;
307 if let Some(cached) = memo.read().expect("setup.py memo").get(setup_py)
308 && cached.stamp == stamp
309 {
310 return cached.package_dir.clone();
311 }
312
313 let package_dir = parse_setuptools_setup_py_import_root(setup_py);
314 memo.write().expect("setup.py memo").insert(
315 setup_py.to_path_buf(),
316 SetupPyImportRoot {
317 stamp,
318 package_dir: package_dir.clone(),
319 },
320 );
321 package_dir
322}
323
324fn parse_setuptools_setup_py_import_root(setup_py: &Path) -> Option<PathBuf> {
325 let source = std::fs::read_to_string(setup_py).ok()?;
326 let tree = parse_python_tree(&source)?;
327 let root = tree.root_node();
328 if root.has_error() {
329 return None;
330 }
331 let mut import_root = None;
332 for (call, setup_bindings) in setup_py_setup_calls(&source, root) {
333 let candidate = setup_py_import_root_from_call(call, &source, &setup_bindings)?;
334 if import_root
335 .replace(candidate.clone())
336 .is_some_and(|root| root != candidate)
337 {
338 return None;
339 }
340 }
341 import_root
342}
343
344pub fn setuptools_setup_py_python_requires(setup_py: &Path) -> Option<String> {
352 let source = std::fs::read_to_string(setup_py).ok()?;
353 let tree = parse_python_tree(&source)?;
354 let root = tree.root_node();
355 if root.has_error() {
356 return None;
357 }
358 let mut requirement: Option<String> = None;
359 for (call, _) in setup_py_setup_calls(&source, root) {
360 let Some(arguments) = call.child_by_field_name("arguments") else {
361 continue;
362 };
363 if arguments.kind() != "argument_list" {
364 continue;
365 }
366 let mut cursor = arguments.walk();
367 for argument in arguments.named_children(&mut cursor) {
368 if argument.kind() != "keyword_argument" {
369 continue;
370 }
371 let Some(name) = argument.child_by_field_name("name") else {
372 continue;
373 };
374 if py_node_text(name, &source).trim() != "python_requires" {
375 continue;
376 }
377 let value = argument.child_by_field_name("value")?;
378 let declared = python_plain_string_literal(value, &source)?.to_owned();
379 if requirement
380 .replace(declared.clone())
381 .is_some_and(|previous| previous != declared)
382 {
383 return None;
384 }
385 }
386 }
387 requirement
388}
389
390fn setup_py_setup_calls<'tree>(
398 source: &str,
399 root: Node<'tree>,
400) -> Vec<(Node<'tree>, HashMap<Vec<String>, String>)> {
401 let mut setup_bindings: HashMap<Vec<String>, String> = HashMap::default();
402 let mut calls = Vec::new();
403 let mut cursor = root.walk();
404
405 for statement in root.named_children(&mut cursor) {
406 if matches!(
407 statement.kind(),
408 "import_statement" | "import_from_statement"
409 ) {
410 for binding in setup_py_bound_names(statement, source) {
411 setup_bindings.retain(|path, _| path.first() != Some(&binding));
412 }
413 for import in python_import_infos_from_node(statement, source) {
414 if import.is_wildcard {
415 setup_bindings.clear();
416 continue;
417 }
418 let Some(path) = import.path else { continue };
419 let segments = path.segments.iter().map(String::as_str).collect::<Vec<_>>();
420 match path.kind {
421 Some(StructuredImportPathKind::ImportFrom) => {
422 let function_name = match segments.as_slice() {
423 ["setuptools", "setup"] | ["distutils", "core", "setup"] => "setup",
424 ["setuptools", "find_packages"] => "find_packages",
425 ["setuptools", "find_namespace_packages"] => "find_namespace_packages",
426 _ => continue,
427 };
428 setup_bindings.insert(
429 vec![import.identifier.expect("imported function binds a name")],
430 function_name.to_string(),
431 );
432 }
433 Some(StructuredImportPathKind::Namespace) => {
434 let function_names = match segments.as_slice() {
435 ["setuptools"] => {
436 ["setup", "find_packages", "find_namespace_packages"].as_slice()
437 }
438 ["distutils", "core"] => ["setup"].as_slice(),
439 _ => continue,
440 };
441 let binding_prefix = import
442 .alias
443 .map(|alias| vec![alias])
444 .unwrap_or_else(|| path.segments.clone());
445 for function_name in function_names {
446 let mut callable = binding_prefix.clone();
447 callable.push((*function_name).to_string());
448 setup_bindings.insert(callable, (*function_name).to_string());
449 }
450 }
451 _ => continue,
452 }
453 }
454 continue;
455 }
456
457 if statement.kind() == "expression_statement"
458 && statement.named_child_count() == 1
459 && let Some(call) = statement.named_child(0)
460 && call.kind() == "call"
461 && setup_py_call_imported_function(call, source, &setup_bindings)
462 .is_some_and(|function| function == "setup")
463 {
464 calls.push((call, setup_bindings.clone()));
465 }
466
467 for binding in setup_py_bound_names(statement, source) {
468 setup_bindings.retain(|path, _| path.first() != Some(&binding));
469 }
470 }
471 calls
472}
473
474fn setup_py_bound_names(statement: Node<'_>, source: &str) -> Vec<String> {
481 let mut names = Vec::new();
482 let mut pending = vec![statement];
483 while let Some(node) = pending.pop() {
484 for binding in python_direct_scope_bindings_bounded(node, source, || true)
485 .expect("unbounded setup.py binding walk")
486 {
487 let name = py_node_text(binding.declaration, source).trim();
488 if !name.is_empty() {
489 names.push(name.to_string());
490 }
491 }
492 let excluded_body = matches!(
493 node.kind(),
494 "function_definition" | "class_definition" | "lambda"
495 )
496 .then(|| node.child_by_field_name("body").map(|body| body.id()))
497 .flatten();
498 let mut cursor = node.walk();
499 pending.extend(
500 node.named_children(&mut cursor)
501 .filter(|child| Some(child.id()) != excluded_body),
502 );
503 }
504 names
505}
506
507fn setup_py_call_imported_function<'a>(
508 call: Node<'_>,
509 source: &str,
510 setup_bindings: &'a HashMap<Vec<String>, String>,
511) -> Option<&'a str> {
512 let mut function = call.child_by_field_name("function")?;
513 let mut path = Vec::new();
514 while function.kind() == "attribute" {
515 let attribute = function.child_by_field_name("attribute")?;
516 path.push(py_node_text(attribute, source).to_string());
517 let object = function.child_by_field_name("object")?;
518 function = object;
519 }
520 if function.kind() != "identifier" {
521 return None;
522 }
523 path.push(py_node_text(function, source).to_string());
524 path.reverse();
525 setup_bindings.get(&path).map(String::as_str)
526}
527
528fn setup_py_import_root_from_call(
529 call: Node<'_>,
530 source: &str,
531 setup_bindings: &HashMap<Vec<String>, String>,
532) -> Option<PathBuf> {
533 let arguments = call.child_by_field_name("arguments")?;
534 if arguments.kind() != "argument_list" {
535 return None;
536 }
537
538 let mut packages = None;
539 let mut package_dir = None;
540 let mut cursor = arguments.walk();
541 for argument in arguments.named_children(&mut cursor) {
542 if argument.kind() == "comment" {
543 continue;
544 }
545 if argument.kind() != "keyword_argument" {
547 return None;
548 }
549 let name = argument.child_by_field_name("name")?;
550 let value = argument.child_by_field_name("value")?;
551 match py_node_text(name, source).trim() {
552 "packages" if packages.is_none() => packages = Some(value),
553 "packages" => return None,
554 "package_dir" if package_dir.is_none() => package_dir = Some(value),
555 "package_dir" => return None,
556 _ => {}
557 }
558 }
559
560 let packages = packages?;
561 if let Some(package_dir) = package_dir {
562 return setup_py_static_package_dir(package_dir, source);
563 }
564 if setup_py_nonempty_literal_packages(packages, source) {
565 return Some(PathBuf::new());
566 }
567 setup_py_discovery_root_from_call(packages, source, setup_bindings)
568}
569
570fn setup_py_discovery_root_from_call(
571 call: Node<'_>,
572 source: &str,
573 setup_bindings: &HashMap<Vec<String>, String>,
574) -> Option<PathBuf> {
575 let function = setup_py_call_imported_function(call, source, setup_bindings)?;
576 if !matches!(function, "find_packages" | "find_namespace_packages") {
577 return None;
578 }
579 let arguments = call.child_by_field_name("arguments")?;
580 if arguments.kind() != "argument_list" {
581 return None;
582 }
583
584 let mut where_value = None;
585 let mut seen_exclude = false;
586 let mut seen_include = false;
587 let mut positional_index = 0;
588 let mut cursor = arguments.walk();
589 for argument in arguments.named_children(&mut cursor) {
590 if argument.kind() == "comment" {
591 continue;
592 }
593 if matches!(argument.kind(), "list_splat" | "dictionary_splat") {
594 return None;
595 }
596 if argument.kind() == "keyword_argument" {
597 let name = py_node_text(argument.child_by_field_name("name")?, source).trim();
598 let value = argument.child_by_field_name("value")?;
599 match name {
600 "where" if where_value.is_none() => where_value = Some(value),
601 "where" => return None,
602 "exclude" if !seen_exclude => seen_exclude = true,
603 "exclude" => return None,
604 "include" if !seen_include => seen_include = true,
605 "include" => return None,
606 _ => return None,
607 }
608 continue;
609 }
610
611 let slot = positional_index;
612 positional_index += 1;
613 match slot {
614 0 if where_value.is_none() => where_value = Some(argument),
615 0 => return None,
616 1 if !seen_exclude => seen_exclude = true,
617 1 => return None,
618 2 if !seen_include => seen_include = true,
619 2 => return None,
620 _ => return None,
621 }
622 }
623
624 where_value
625 .map(|value| python_plain_string_literal(value, source))
626 .unwrap_or(Some(""))
627 .map(PathBuf::from)
628}
629
630fn setup_py_nonempty_literal_packages(value: Node<'_>, source: &str) -> bool {
631 let value = setup_py_unwrap_parenthesized(value);
632 if !matches!(value.kind(), "list" | "set" | "tuple") {
633 return false;
634 }
635 let mut cursor = value.walk();
636 let mut nonempty = false;
637 for element in value.named_children(&mut cursor) {
638 if element.kind() == "comment" {
639 continue;
640 }
641 let Some(package) = python_plain_string_literal(element, source) else {
642 return false;
643 };
644 if package.is_empty() {
645 return false;
646 }
647 nonempty = true;
648 }
649 nonempty
650}
651
652fn setup_py_static_package_dir(value: Node<'_>, source: &str) -> Option<PathBuf> {
653 let value = setup_py_unwrap_parenthesized(value);
654 if value.kind() != "dictionary" {
655 return None;
656 }
657 let mut cursor = value.walk();
658 let mut pairs = value
659 .named_children(&mut cursor)
660 .filter(|node| node.kind() != "comment");
661 let Some(pair) = pairs.next() else {
662 return Some(PathBuf::new());
663 };
664 if pairs.next().is_some() || pair.kind() != "pair" {
665 return None;
666 }
667 let key = python_plain_string_literal(pair.child_by_field_name("key")?, source)?;
668 if !key.is_empty() {
669 return None;
670 }
671 let root = python_plain_string_literal(pair.child_by_field_name("value")?, source)?;
672 let root = PathBuf::from(root);
673 (!root.is_absolute()).then_some(root)
674}
675
676fn setup_py_unwrap_parenthesized(mut node: Node<'_>) -> Node<'_> {
677 while node.kind() == "parenthesized_expression" && node.named_child_count() == 1 {
678 node = node.named_child(0).expect("parenthesized expression child");
679 }
680 node
681}
682
683fn parse_setuptools_where_entries(manifest: &Path) -> Vec<String> {
684 let Ok(source) = std::fs::read_to_string(manifest) else {
685 return Vec::new();
686 };
687 let Ok(document) = source.parse::<toml::Value>() else {
688 return Vec::new();
689 };
690 document
691 .get("tool")
692 .and_then(|tool| tool.get("setuptools"))
693 .and_then(|setuptools| setuptools.get("packages"))
694 .and_then(|packages| packages.get("find"))
695 .and_then(|find| find.get("where"))
696 .and_then(toml::Value::as_array)
697 .map(|entries| {
698 entries
699 .iter()
700 .filter_map(toml::Value::as_str)
701 .map(str::to_string)
702 .collect()
703 })
704 .unwrap_or_default()
705}
706
707fn path_components(path: &Path) -> Vec<String> {
708 path.components()
709 .map(|component| component.as_os_str().to_string_lossy().to_string())
710 .filter(|component| !component.is_empty())
711 .collect()
712}
713
714pub fn python_is_decorated_function_boundary(node: Node<'_>) -> bool {
715 if node.kind() != "decorated_definition" {
716 return false;
717 }
718 let mut cursor = node.walk();
719 node.named_children(&mut cursor)
720 .any(|child| child.kind() == "function_definition")
721}
722
723#[derive(Clone)]
724pub struct Scope {
725 kind: ScopeKind,
726 path: String,
727 fq: FqName,
734 code_unit: Option<CodeUnit>,
735 method_receiver: Option<String>,
736}
737
738#[derive(Clone, Copy, PartialEq, Eq)]
739enum ScopeKind {
740 Class,
741 Function,
742}
743
744pub struct PythonVisitor<'a> {
745 pub file: &'a ProjectFile,
746 pub source: &'a str,
747 pub package_name: &'a str,
748 module_fq: &'a FqName,
749 pub parsed: &'a mut ParsedFile,
750 pub module: Option<CodeUnit>,
751 pub overload_decorators: &'a PythonOverloadDecoratorBindings,
752}
753
754struct PythonContainer<'tree> {
755 node: Node<'tree>,
756 scope: Vec<Scope>,
757 module_control_depth: usize,
758}
759
760enum PythonWork<'tree> {
761 Container(PythonContainer<'tree>),
762 Statement {
763 node: Node<'tree>,
764 scope: Vec<Scope>,
765 module_control_depth: usize,
766 },
767}
768
769impl<'a> PythonVisitor<'a> {
770 pub fn visit_container(
771 &mut self,
772 node: Node<'_>,
773 scope: &[Scope],
774 module_control_depth: usize,
775 ) {
776 let mut stack = vec![PythonWork::Container(PythonContainer {
777 node,
778 scope: scope.to_vec(),
779 module_control_depth,
780 })];
781 while let Some(work) = stack.pop() {
782 match work {
783 PythonWork::Container(container) => {
784 let mut cursor = container.node.walk();
785 let children = container
786 .node
787 .named_children(&mut cursor)
788 .collect::<Vec<_>>();
789 for child in children.into_iter().rev() {
790 stack.push(PythonWork::Statement {
791 node: child,
792 scope: container.scope.clone(),
793 module_control_depth: container.module_control_depth,
794 });
795 }
796 }
797 PythonWork::Statement {
798 node,
799 scope,
800 module_control_depth,
801 } => self.visit_statement(node, &scope, module_control_depth, &mut stack),
802 }
803 }
804 }
805
806 fn visit_statement<'tree>(
807 &mut self,
808 node: Node<'tree>,
809 scope: &[Scope],
810 module_control_depth: usize,
811 stack: &mut Vec<PythonWork<'tree>>,
812 ) {
813 match node.kind() {
814 "decorated_definition" => {
815 if let Some(definition) = node.child_by_field_name("definition") {
816 self.visit_definition(
817 definition,
818 Some(node),
819 scope,
820 module_control_depth,
821 stack,
822 );
823 }
824 }
825 "class_definition" | "function_definition" => {
826 self.visit_definition(node, None, scope, module_control_depth, stack)
827 }
828 "expression_statement" => {
829 self.visit_expression_statement(node, scope, module_control_depth)
830 }
831 "import_statement" | "import_from_statement" => self.visit_import_statement(node),
832 "if_statement" | "try_statement" | "with_statement" | "for_statement"
833 | "while_statement" => {
834 let next_depth = if scope.is_empty() {
835 module_control_depth + 1
836 } else {
837 module_control_depth
838 };
839 stack.push(PythonWork::Container(PythonContainer {
840 node,
841 scope: scope.to_vec(),
842 module_control_depth: next_depth,
843 }));
844 }
845 "elif_clause" | "else_clause" | "except_clause" | "finally_clause" => {
846 stack.push(PythonWork::Container(PythonContainer {
847 node,
848 scope: scope.to_vec(),
849 module_control_depth,
850 }));
851 }
852 "block" | "module" => stack.push(PythonWork::Container(PythonContainer {
853 node,
854 scope: scope.to_vec(),
855 module_control_depth,
856 })),
857 _ => {}
858 }
859 }
860
861 fn visit_definition<'tree>(
862 &mut self,
863 definition: Node<'tree>,
864 wrapper: Option<Node<'tree>>,
865 scope: &[Scope],
866 module_control_depth: usize,
867 stack: &mut Vec<PythonWork<'tree>>,
868 ) {
869 match definition.kind() {
870 "class_definition" => self.visit_class_definition(
871 definition,
872 wrapper.unwrap_or(definition),
873 scope,
874 module_control_depth,
875 stack,
876 ),
877 "function_definition" => self.visit_function_definition(
878 definition,
879 wrapper.unwrap_or(definition),
880 scope,
881 module_control_depth,
882 stack,
883 ),
884 _ => {}
885 }
886 }
887
888 fn visit_class_definition<'tree>(
889 &mut self,
890 node: Node<'tree>,
891 range_node: Node<'tree>,
892 scope: &[Scope],
893 module_control_depth: usize,
894 stack: &mut Vec<PythonWork<'tree>>,
895 ) {
896 let Some(name_node) = node.child_by_field_name("name") else {
897 return;
898 };
899 let name = py_node_text(name_node, self.source).trim();
900 if name.is_empty() {
901 return;
902 }
903
904 let capture = !scope.is_empty() || module_control_depth <= 1;
905
906 let short_name = scope
907 .last()
908 .map(|parent| format!("{}${name}", parent.path))
909 .unwrap_or_else(|| name.to_string());
910 let fq = match scope.last() {
916 Some(parent) => parent
917 .fq
918 .clone()
919 .with_pushed(py_segment(name, SegmentKind::Nested)),
920 None => self
921 .module_fq
922 .clone()
923 .with_pushed(py_segment(name, SegmentKind::Type)),
924 };
925 let code_unit = CodeUnit::new_fq(
926 self.file.clone(),
927 CodeUnitType::Class,
928 self.package_name.to_string(),
929 short_name.clone(),
930 fq.clone(),
931 );
932 if capture {
933 self.parsed
934 .replace_code_unit(code_unit.clone(), range_node, self.source, None, None);
935 self.parsed.add_signature(
936 code_unit.clone(),
937 python_class_signature(range_node, self.source),
938 );
939 if let Some(module) = &self.module
940 && scope.is_empty()
941 {
942 self.parsed.add_child(module.clone(), code_unit.clone());
943 }
944 if let Some(parent) = scope.last()
945 && let Some(parent_cu) = &parent.code_unit
946 {
947 self.parsed.add_child(parent_cu.clone(), code_unit.clone());
948 }
949 self.parsed.set_raw_supertypes(
950 code_unit.clone(),
951 extract_python_supertypes(node, self.source),
952 );
953 }
954
955 let mut next_scope = scope.to_vec();
956 if capture {
957 next_scope.push(Scope {
958 kind: ScopeKind::Class,
959 path: short_name,
960 fq,
961 code_unit: Some(code_unit),
962 method_receiver: None,
963 });
964 }
965 if let Some(body) = node.child_by_field_name("body") {
966 stack.push(PythonWork::Container(PythonContainer {
967 node: body,
968 scope: next_scope,
969 module_control_depth,
970 }));
971 }
972 }
973
974 fn visit_function_definition<'tree>(
975 &mut self,
976 node: Node<'tree>,
977 range_node: Node<'tree>,
978 scope: &[Scope],
979 module_control_depth: usize,
980 stack: &mut Vec<PythonWork<'tree>>,
981 ) {
982 let Some(name_node) = node.child_by_field_name("name") else {
983 return;
984 };
985 let name = py_node_text(name_node, self.source).trim();
986 if name.is_empty() {
987 return;
988 }
989
990 let capture = !python_is_property_mutator(range_node, self.source)
998 && ((scope.is_empty() && module_control_depth <= 1)
999 || scope
1000 .last()
1001 .is_some_and(|parent| parent.kind == ScopeKind::Class));
1002 let short_name = if let Some(parent) = scope.last() {
1003 match parent.kind {
1004 ScopeKind::Class => format!("{}.{}", parent.path, name),
1005 ScopeKind::Function => format!("{}${name}", parent.path),
1006 }
1007 } else {
1008 name.to_string()
1009 };
1010 let fq = if let Some(parent) = scope.last() {
1015 match parent.kind {
1016 ScopeKind::Class => parent
1017 .fq
1018 .clone()
1019 .with_pushed(py_segment(name, SegmentKind::Member)),
1020 ScopeKind::Function => parent
1021 .fq
1022 .clone()
1023 .with_pushed(py_segment(name, SegmentKind::Nested)),
1024 }
1025 } else {
1026 self.module_fq
1027 .clone()
1028 .with_pushed(py_segment(name, SegmentKind::Member))
1029 };
1030
1031 if capture {
1032 let code_unit_type = if python_function_has_decorator(node, self.source, "property") {
1033 CodeUnitType::Field
1034 } else {
1035 CodeUnitType::Function
1036 };
1037 let signature = node
1038 .child_by_field_name("parameters")
1039 .map(|parameters| py_node_text(parameters, self.source).trim().to_string());
1040 let code_unit = CodeUnit::with_signature_and_fq(
1041 self.file.clone(),
1042 code_unit_type,
1043 self.package_name.to_string(),
1044 short_name.clone(),
1045 signature,
1046 false,
1047 fq.clone(),
1048 );
1049 self.parsed
1050 .replace_code_unit(code_unit.clone(), range_node, self.source, None, None);
1051 let signature = python_function_signature(range_node, self.source);
1052 self.parsed.add_signature_with_metadata(
1053 code_unit.clone(),
1054 python_signature_metadata(signature, node, self.source).with_declaration_only(
1055 self.overload_decorators
1056 .decorates_as_overload(node, self.source),
1057 ),
1058 );
1059 if let Some(module) = &self.module
1060 && scope.is_empty()
1061 {
1062 self.parsed.add_child(module.clone(), code_unit.clone());
1063 }
1064 if let Some(parent) = scope.last()
1065 && parent.kind == ScopeKind::Class
1066 && let Some(parent_cu) = &parent.code_unit
1067 {
1068 self.parsed.add_child(parent_cu.clone(), code_unit.clone());
1069 }
1070 let scope_code_unit = Some(code_unit);
1071 let mut next_scope = scope.to_vec();
1072 next_scope.push(Scope {
1073 kind: ScopeKind::Function,
1074 path: short_name,
1075 fq,
1076 code_unit: scope_code_unit,
1077 method_receiver: scope
1078 .last()
1079 .is_some_and(|parent| parent.kind == ScopeKind::Class)
1080 .then(|| python_instance_method_receiver_name(node, self.source))
1081 .flatten(),
1082 });
1083 if let Some(body) = node.child_by_field_name("body") {
1084 stack.push(PythonWork::Container(PythonContainer {
1085 node: body,
1086 scope: next_scope,
1087 module_control_depth,
1088 }));
1089 }
1090 return;
1091 }
1092
1093 let mut next_scope = scope.to_vec();
1094 next_scope.push(Scope {
1095 kind: ScopeKind::Function,
1096 path: short_name,
1097 fq,
1098 code_unit: None,
1099 method_receiver: None,
1100 });
1101 if let Some(body) = node.child_by_field_name("body") {
1102 stack.push(PythonWork::Container(PythonContainer {
1103 node: body,
1104 scope: next_scope,
1105 module_control_depth,
1106 }));
1107 }
1108 }
1109
1110 fn visit_expression_statement(
1111 &mut self,
1112 node: Node<'_>,
1113 scope: &[Scope],
1114 module_control_depth: usize,
1115 ) {
1116 let Some(assignment) = node.named_child(0) else {
1117 return;
1118 };
1119 if assignment.kind() != "assignment" {
1120 return;
1121 }
1122 let targets = python_chained_assignment_targets(assignment);
1123 if targets.is_empty() {
1124 return;
1125 }
1126 for left in &targets {
1127 self.visit_instance_attribute_assignment(*left, scope);
1128 }
1129 let names = targets
1130 .iter()
1131 .flat_map(|left| collect_assigned_names(*left, self.source))
1132 .collect::<Vec<_>>();
1133 for name in names {
1134 let (short_name, fq) = if let Some(parent) = scope.last() {
1135 if parent.kind != ScopeKind::Class {
1136 continue;
1137 }
1138 (
1139 format!("{}.{}", parent.path, name),
1140 parent
1141 .fq
1142 .clone()
1143 .with_pushed(py_segment(&name, SegmentKind::Member)),
1144 )
1145 } else if module_control_depth <= 1 {
1146 (
1147 name.clone(),
1148 self.module_fq
1149 .clone()
1150 .with_pushed(py_segment(&name, SegmentKind::Member)),
1151 )
1152 } else {
1153 continue;
1154 };
1155 let code_unit = CodeUnit::new_fq(
1156 self.file.clone(),
1157 CodeUnitType::Field,
1158 self.package_name.to_string(),
1159 short_name,
1160 fq,
1161 );
1162 if scope
1163 .last()
1164 .is_some_and(|parent| parent.kind == ScopeKind::Class)
1165 {
1166 self.parsed
1170 .add_code_unit(code_unit.clone(), node, self.source, None, None);
1171 } else {
1172 self.parsed
1173 .replace_code_unit(code_unit.clone(), node, self.source, None, None);
1174 }
1175 self.parsed.add_signature(
1176 code_unit.clone(),
1177 py_node_text(node, self.source).trim().to_string(),
1178 );
1179 if let Some(module) = &self.module
1180 && scope.is_empty()
1181 {
1182 self.parsed.add_child(module.clone(), code_unit.clone());
1183 }
1184 if let Some(parent) = scope.last()
1185 && parent.kind == ScopeKind::Class
1186 && let Some(parent_cu) = &parent.code_unit
1187 {
1188 self.parsed.add_child(parent_cu.clone(), code_unit);
1189 }
1190 }
1191 }
1192
1193 fn visit_instance_attribute_assignment(&mut self, left: Node<'_>, scope: &[Scope]) {
1194 let Some(function) = scope
1195 .last()
1196 .filter(|scope| scope.kind == ScopeKind::Function)
1197 else {
1198 return;
1199 };
1200 let Some(receiver) = function.method_receiver.as_deref() else {
1201 return;
1202 };
1203 let Some(parent) = scope
1204 .get(scope.len().saturating_sub(2))
1205 .filter(|scope| scope.kind == ScopeKind::Class)
1206 else {
1207 return;
1208 };
1209 let Some(parent_cu) = parent.code_unit.clone() else {
1210 return;
1211 };
1212 for (name, node) in collect_self_assigned_attributes(left, self.source, receiver) {
1213 let code_unit = CodeUnit::new_fq(
1214 self.file.clone(),
1215 CodeUnitType::Field,
1216 self.package_name.to_string(),
1217 format!("{}.{}", parent.path, name),
1218 parent
1219 .fq
1220 .clone()
1221 .with_pushed(py_segment(&name, SegmentKind::Member)),
1222 );
1223 if !self.parsed.contains_declaration(&code_unit) {
1224 self.parsed.replace_code_unit(
1225 code_unit.clone(),
1226 node,
1227 self.source,
1228 Some(parent_cu.clone()),
1229 Some(parent_cu.clone()),
1230 );
1231 }
1232 self.parsed.add_signature(
1233 code_unit.clone(),
1234 py_node_text(left, self.source).trim().to_string(),
1235 );
1236 }
1237 }
1238
1239 fn visit_import_statement(&mut self, node: Node<'_>) {
1240 for info in python_import_infos_from_node(node, self.source) {
1241 self.parsed.imports.push(info);
1242 }
1243 }
1244}
1245
1246pub fn parse_python_file(file: &ProjectFile, source: &str, tree: &Tree) -> ParsedFile {
1250 let module_components = python_module_components(file);
1251 let module_name = module_components.join(".");
1252 let module_fq = python_module_fq_from_components(&module_components);
1253 let mut parsed = ParsedFile::new(module_name.clone());
1254 let root = tree.root_node();
1255
1256 collect_python_identifiers(root, source, &mut parsed.type_identifiers);
1257
1258 let module_code_unit = module_code_unit_from_fq(file, &module_components, module_fq.clone());
1259 if let Some(module) = module_code_unit.clone() {
1260 parsed.add_code_unit(module, root, source, None, None);
1261 }
1262
1263 let overload_decorators = PythonOverloadDecoratorBindings::collect(root, source);
1264 let mut visitor = PythonVisitor {
1265 file,
1266 source,
1267 package_name: &module_name,
1268 module_fq: &module_fq,
1269 parsed: &mut parsed,
1270 module: module_code_unit,
1271 overload_decorators: &overload_decorators,
1272 };
1273 visitor.visit_container(root, &[], 0);
1274
1275 parsed
1276}
1277
1278pub fn py_node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1279 brokk_bifrost_core::analyzer::common::node_source_text(node, source)
1280}
1281
1282pub fn python_module_name(file: &ProjectFile) -> String {
1283 python_module_components(file).join(".")
1284}
1285
1286pub fn module_code_unit(file: &ProjectFile, module_fq: &str) -> Option<CodeUnit> {
1287 if module_fq.is_empty() {
1288 return None;
1289 }
1290 let components = python_module_components(file);
1291 debug_assert_eq!(
1292 module_fq,
1293 components.join("."),
1294 "module_code_unit must be built from the file's path-derived Python module name"
1295 );
1296 let structured_fq = python_module_fq_from_components(&components);
1297 module_code_unit_from_fq(file, &components, structured_fq)
1298}
1299
1300fn module_code_unit_from_fq(
1301 file: &ProjectFile,
1302 components: &[String],
1303 structured_fq: FqName,
1304) -> Option<CodeUnit> {
1305 let (short_name, package_components) = components.split_last()?;
1306 let package_name = package_components.join(".");
1307 Some(CodeUnit::new_fq(
1308 file.clone(),
1309 CodeUnitType::Module,
1310 package_name,
1311 short_name.clone(),
1312 structured_fq,
1313 ))
1314}
1315
1316fn python_class_signature(node: Node<'_>, source: &str) -> String {
1317 python_header_with_decorators(node, source)
1318}
1319
1320fn python_function_signature(node: Node<'_>, source: &str) -> String {
1321 let header = python_header_with_decorators(node, source);
1322 if let Some((head, tail)) = header.rsplit_once('\n') {
1323 format!("{head}\n{tail} ...")
1324 } else {
1325 format!("{header} ...")
1326 }
1327}
1328
1329fn python_signature_metadata(signature: String, node: Node<'_>, source: &str) -> SignatureMetadata {
1330 let with_modifiers = |metadata: SignatureMetadata| {
1331 metadata.with_callable_modifiers(
1332 python_callable_is_static(node, source),
1333 false,
1334 DeclaredVisibility::Unknown,
1335 )
1336 };
1337 let Some(parameters_node) = node.child_by_field_name("parameters") else {
1338 return with_modifiers(
1339 SignatureMetadata::new(signature, Vec::new())
1340 .with_dispatch_extensibility(DispatchExtensibility::Open),
1341 );
1342 };
1343 let parameter_text = py_node_text(parameters_node, source).trim();
1344 let Some(parameters_start) = signature.find(parameter_text) else {
1345 return with_modifiers(
1346 SignatureMetadata::new(signature, Vec::new())
1347 .with_dispatch_extensibility(DispatchExtensibility::Open),
1348 );
1349 };
1350 let parameters_end = parameters_start + parameter_text.len();
1351 let mut search_start = parameters_start;
1352 let parameters = python_parameter_label_nodes(parameters_node)
1353 .into_iter()
1354 .filter_map(|label_node| {
1355 let label = py_node_text(label_node, source).trim();
1356 if label.is_empty() || search_start > parameters_end {
1357 return None;
1358 }
1359 let haystack = signature.get(search_start..parameters_end)?;
1360 let relative_start = haystack.find(label)?;
1361 let start_byte = search_start + relative_start;
1362 let end_byte = start_byte + label.len();
1363 search_start = end_byte;
1364 Some(ParameterMetadata::new(label, start_byte, end_byte))
1365 })
1366 .collect();
1367 with_modifiers(
1368 SignatureMetadata::new(signature, parameters)
1369 .with_dispatch_extensibility(DispatchExtensibility::Open),
1370 )
1371}
1372
1373fn python_callable_is_static(node: Node<'_>, source: &str) -> bool {
1398 debug_assert!(
1399 node.kind() == "function_definition",
1400 "Python callable signature metadata is built for function_definition, not {}",
1401 node.kind()
1402 );
1403 python_function_has_decorator(node, source, "staticmethod")
1404 || python_function_has_decorator(node, source, "classmethod")
1405}
1406
1407fn python_parameter_label_nodes(parameters_node: Node<'_>) -> Vec<Node<'_>> {
1408 let mut labels = Vec::new();
1409 let mut cursor = parameters_node.walk();
1410 for child in parameters_node.named_children(&mut cursor) {
1411 if let Some(label_node) = python_parameter_label_node(child) {
1412 labels.push(label_node);
1413 }
1414 }
1415 labels
1416}
1417
1418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1435pub enum PythonParameterSplat {
1436 Positional,
1438 Keyword,
1440}
1441
1442pub fn python_parameter_splat(parameter: Node<'_>) -> Option<PythonParameterSplat> {
1443 let splat = match parameter.kind() {
1444 kind @ ("list_splat_pattern" | "dictionary_splat_pattern") => kind,
1445 _ => {
1446 let mut cursor = parameter.walk();
1447 parameter
1448 .named_children(&mut cursor)
1449 .map(|child| child.kind())
1450 .find(|kind| matches!(*kind, "list_splat_pattern" | "dictionary_splat_pattern"))?
1451 }
1452 };
1453 match splat {
1454 "list_splat_pattern" => Some(PythonParameterSplat::Positional),
1455 "dictionary_splat_pattern" => Some(PythonParameterSplat::Keyword),
1456 _ => unreachable!("the splat kind was matched above"),
1457 }
1458}
1459
1460pub fn python_parameter_label_node(node: Node<'_>) -> Option<Node<'_>> {
1461 match node.kind() {
1462 "identifier" => Some(node),
1463 "typed_parameter"
1464 | "typed_default_parameter"
1465 | "default_parameter"
1466 | "list_splat_pattern"
1467 | "dictionary_splat_pattern"
1468 | "keyword_separator" => node.child_by_field_name("name").or_else(|| {
1469 let mut cursor = node.walk();
1470 node.named_children(&mut cursor)
1471 .find_map(python_parameter_label_node)
1472 }),
1473 _ => None,
1474 }
1475}
1476
1477fn python_is_property_mutator(node: Node<'_>, source: &str) -> bool {
1478 python_header_with_decorators(node, source)
1479 .lines()
1480 .map(str::trim)
1481 .filter(|line| line.starts_with('@'))
1482 .any(|decorator| decorator.ends_with(".setter") || decorator.ends_with(".deleter"))
1483}
1484
1485pub fn python_expanded_comment_start(source: &str, start_byte: usize) -> usize {
1486 let line_starts = compute_line_starts(source);
1487 let line_index = find_line_index_for_offset(&line_starts, start_byte);
1488
1489 let mut comment_start = start_byte;
1490 for line_idx in (0..line_index).rev() {
1491 let line_start = line_starts[line_idx];
1492 let line_end = line_starts
1493 .get(line_idx + 1)
1494 .copied()
1495 .unwrap_or(source.len());
1496 let line = &source[line_start..line_end];
1497 let trimmed = line.trim_start();
1498
1499 if trimmed.trim().is_empty() {
1500 continue;
1501 }
1502
1503 if trimmed.starts_with('#') {
1504 comment_start = line_start;
1505 continue;
1506 }
1507
1508 break;
1509 }
1510
1511 comment_start
1512}
1513
1514fn python_header_with_decorators(node: Node<'_>, source: &str) -> String {
1515 let raw = py_node_text(node, source);
1516 let lines: Vec<_> = raw
1517 .lines()
1518 .map(str::trim_end)
1519 .filter(|line| !line.trim().is_empty())
1520 .collect();
1521 let mut relevant = Vec::new();
1522 for line in lines {
1523 let trimmed = line.trim_start();
1524 if trimmed.starts_with('@')
1525 || trimmed.starts_with("def ")
1526 || trimmed.starts_with("async def ")
1527 || trimmed.starts_with("class ")
1528 {
1529 relevant.push(trimmed.to_string());
1530 if trimmed.starts_with("def ")
1531 || trimmed.starts_with("async def ")
1532 || trimmed.starts_with("class ")
1533 {
1534 break;
1535 }
1536 }
1537 }
1538 relevant.join("\n")
1539}
1540
1541fn extract_python_supertypes(node: Node<'_>, source: &str) -> Vec<String> {
1560 let Some(superclasses) = node.child_by_field_name("superclasses") else {
1561 return Vec::new();
1562 };
1563 let mut result = Vec::new();
1564 let mut cursor = superclasses.walk();
1565 for child in superclasses.named_children(&mut cursor) {
1566 if child.kind() == "keyword_argument" {
1567 continue;
1568 }
1569 let named = python_base_origin_node(child);
1570 let text = py_node_text(named, source).trim();
1571 if !text.is_empty() {
1572 result.push(text.to_string());
1573 }
1574 }
1575 result
1576}
1577
1578pub fn python_base_origin_node<'tree>(base: Node<'tree>) -> Node<'tree> {
1585 if base.kind() != "subscript" {
1586 return base;
1587 }
1588 let Some(value) = base.child_by_field_name("value") else {
1589 return base;
1590 };
1591 if matches!(value.kind(), "identifier" | "attribute") {
1592 value
1593 } else {
1594 base
1595 }
1596}
1597
1598fn collect_assigned_names(node: Node<'_>, source: &str) -> Vec<String> {
1599 let mut names = Vec::new();
1600 walk_named_tree_preorder(node, true, |node| {
1601 match node.kind() {
1602 "attribute" | "subscript" => WalkControl::SkipChildren,
1606 "identifier" => {
1607 let text = py_node_text(node, source).trim();
1608 if !text.is_empty() {
1609 names.push(text.to_string());
1610 }
1611 WalkControl::Continue
1612 }
1613 _ => WalkControl::Continue,
1614 }
1615 });
1616 names
1617}
1618
1619fn collect_self_assigned_attributes<'tree>(
1620 node: Node<'tree>,
1621 source: &str,
1622 receiver_name: &str,
1623) -> Vec<(String, Node<'tree>)> {
1624 let mut attributes = Vec::new();
1625 collect_direct_self_assigned_attributes(node, source, receiver_name, &mut attributes);
1626 attributes
1627}
1628
1629fn collect_direct_self_assigned_attributes<'tree>(
1630 node: Node<'tree>,
1631 source: &str,
1632 receiver_name: &str,
1633 attributes: &mut Vec<(String, Node<'tree>)>,
1634) {
1635 match node.kind() {
1636 "attribute" => {
1637 let Some(object) = node.child_by_field_name("object") else {
1638 return;
1639 };
1640 if object.kind() != "identifier" || py_node_text(object, source).trim() != receiver_name
1641 {
1642 return;
1643 }
1644 let Some(attribute) = node.child_by_field_name("attribute") else {
1645 return;
1646 };
1647 let name = py_node_text(attribute, source).trim();
1648 if !name.is_empty() {
1649 attributes.push((name.to_string(), attribute));
1650 }
1651 }
1652 "pattern_list"
1657 | "tuple_pattern"
1658 | "list_pattern"
1659 | "tuple"
1660 | "list"
1661 | "parenthesized_expression" => {
1662 let mut cursor = node.walk();
1663 for child in node.named_children(&mut cursor) {
1664 collect_direct_self_assigned_attributes(child, source, receiver_name, attributes);
1665 }
1666 }
1667 _ => {}
1668 }
1669}
1670
1671fn python_instance_method_receiver_name(node: Node<'_>, source: &str) -> Option<String> {
1672 if python_function_has_decorator(node, source, "staticmethod")
1673 || python_function_has_decorator(node, source, "classmethod")
1674 {
1675 return None;
1676 }
1677 python_first_parameter_name(node, source)
1678}
1679
1680fn python_function_has_decorator(node: Node<'_>, source: &str, decorator_name: &str) -> bool {
1681 let Some(parent) = node.parent() else {
1682 return false;
1683 };
1684 if parent.kind() != "decorated_definition" {
1685 return false;
1686 }
1687 let mut cursor = parent.walk();
1688 parent
1689 .named_children(&mut cursor)
1690 .filter(|child| child.kind() == "decorator")
1691 .filter_map(|decorator| decorator.named_child(0))
1692 .filter_map(expression_name_node)
1693 .any(|name| py_node_text(name, source).trim() == decorator_name)
1694}
1695
1696pub fn python_first_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
1699 let parameters = node.child_by_field_name("parameters")?;
1700 let mut cursor = parameters.walk();
1701 parameters
1702 .named_children(&mut cursor)
1703 .find_map(|child| python_parameter_name(child, source))
1704}
1705
1706fn python_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
1707 match node.kind() {
1708 "identifier" => Some(py_node_text(node, source).trim().to_string()),
1709 "typed_parameter"
1710 | "default_parameter"
1711 | "list_splat_pattern"
1712 | "dictionary_splat_pattern" => node
1713 .child_by_field_name("name")
1714 .or_else(|| {
1715 let mut cursor = node.walk();
1716 node.named_children(&mut cursor)
1717 .find(|child| child.kind() == "identifier")
1718 })
1719 .and_then(|name| python_parameter_name(name, source)),
1720 _ => None,
1721 }
1722 .filter(|name| !name.is_empty())
1723}
1724
1725pub fn collect_python_identifiers(node: Node<'_>, source: &str, identifiers: &mut HashSet<String>) {
1726 walk_named_tree_preorder(node, true, |node| {
1727 if node.kind() == "identifier" {
1728 let text = py_node_text(node, source).trim();
1729 if !text.is_empty() {
1730 identifiers.insert(text.to_string());
1731 }
1732 }
1733 WalkControl::Continue
1734 });
1735}
1736
1737pub fn parse_python_tree(source: &str) -> Option<Tree> {
1738 let mut parser = Parser::new();
1739 parser
1740 .set_language(&tree_sitter_python::LANGUAGE.into())
1741 .expect("failed to load python parser");
1742 parser.parse(source, None)
1743}
1744
1745fn python_chained_assignment_targets<'tree>(assignment: Node<'tree>) -> Vec<Node<'tree>> {
1752 let mut targets = Vec::new();
1753 let mut node = assignment;
1754 while let Some(left) = node.child_by_field_name("left") {
1755 targets.push(left);
1756 match node.child_by_field_name("right") {
1757 Some(right) if right.kind() == "assignment" => node = right,
1758 _ => break,
1759 }
1760 }
1761 targets
1762}
1763
1764#[cfg(test)]
1765mod supertype_tests {
1766 use super::extract_python_supertypes;
1767 use tree_sitter::{Node, Parser};
1768
1769 fn class_node<'tree>(tree: &'tree tree_sitter::Tree, source: &str) -> Node<'tree> {
1770 let mut cursor = tree.root_node().walk();
1771 tree.root_node()
1772 .named_children(&mut cursor)
1773 .find(|node| node.kind() == "class_definition")
1774 .unwrap_or_else(|| panic!("source declares a class: {source}"))
1775 }
1776
1777 fn parse(source: &str) -> tree_sitter::Tree {
1778 let mut parser = Parser::new();
1779 parser
1780 .set_language(&tree_sitter_python::LANGUAGE.into())
1781 .expect("the Python grammar loads");
1782 parser.parse(source, None).expect("the source parses")
1783 }
1784
1785 #[test]
1786 fn every_positional_base_contributes_a_spelling() {
1787 for (source, expected) in [
1788 ("class A(Base): pass\n", vec!["Base"]),
1789 ("class A(pkg.Base): pass\n", vec!["pkg.Base"]),
1790 ("class A(Base[int]): pass\n", vec!["Base"]),
1793 ("class A(pkg.Base[str, int]): pass\n", vec!["pkg.Base"]),
1794 (
1795 "class A(Mapping[str, Any], Base): pass\n",
1796 vec!["Mapping", "Base"],
1797 ),
1798 ("class A(Base, metaclass=Meta): pass\n", vec!["Base"]),
1800 ("class A(metaclass=Meta): pass\n", Vec::new()),
1801 (
1804 "class A(namedtuple(\"P\", \"x\")): pass\n",
1805 vec!["namedtuple(\"P\", \"x\")"],
1806 ),
1807 ("class A(*bases): pass\n", vec!["*bases"]),
1808 ("class A: pass\n", Vec::new()),
1809 ] {
1810 let tree = parse(source);
1811 let node = class_node(&tree, source);
1812 assert_eq!(
1813 extract_python_supertypes(node, source),
1814 expected,
1815 "supertypes of {source}"
1816 );
1817 }
1818 }
1819}
1820
1821#[cfg(test)]
1822mod callable_modifier_tests {
1823 use super::*;
1824
1825 #[test]
1848 fn callable_metadata_records_python_receiver_contracts_structurally() {
1849 let source = "def free(value):\n return value\n\nclass Widget:\n def __init__(self, spec):\n self.spec = spec\n\n def render(self, target):\n return target\n\n @staticmethod\n def build(spec):\n return spec\n\n @classmethod\n def measure(cls, target):\n return target\n\n @property\n def label(self):\n return \"widget\"\n\nasync def fetch_all(url):\n return url\n\ndef outer(seed):\n def inner(value):\n return value\n return inner(seed)\n\nassigned = lambda value: value\n";
1850 let file = ProjectFile::new(std::env::temp_dir(), "widget.py");
1851 let tree = parse_python_tree(source).expect("parse the Python fixture");
1852 let parsed = parse_python_file(&file, source, &tree);
1853
1854 let entry = |fq_name: &str| {
1855 parsed
1856 .signature_metadata
1857 .iter()
1858 .find(|(unit, _)| unit.fq_name() == fq_name)
1859 .unwrap_or_else(|| {
1860 panic!(
1861 "missing Python declaration {fq_name}; recorded {:?}",
1862 parsed
1863 .signature_metadata
1864 .keys()
1865 .map(CodeUnit::fq_name)
1866 .collect::<Vec<_>>()
1867 )
1868 })
1869 };
1870 let modifiers = |fq_name: &str| {
1871 let (_, entries) = entry(fq_name);
1872 let metadata = entries
1873 .first()
1874 .unwrap_or_else(|| panic!("{fq_name} carries no signature metadata"));
1875 assert!(
1876 metadata.callable_modifiers_recorded(),
1877 "{fq_name} must record that the walk read its declaration shape"
1878 );
1879 (
1880 metadata.callable_is_static(),
1881 metadata.callable_is_constructor(),
1882 metadata.parameters().len(),
1883 )
1884 };
1885
1886 assert_eq!(modifiers("widget.free"), (false, false, 1));
1887 assert_eq!(
1888 modifiers("widget.Widget.__init__"),
1889 (false, false, 2),
1890 "`__init__` is an ordinary instance method, exactly as Ruby's `initialize`"
1891 );
1892 assert_eq!(modifiers("widget.Widget.render"), (false, false, 2));
1893 assert_eq!(
1894 modifiers("widget.Widget.build"),
1895 (true, false, 1),
1896 "`@staticmethod` binds no instance receiver"
1897 );
1898 assert_eq!(
1899 modifiers("widget.Widget.measure"),
1900 (true, false, 2),
1901 "`@classmethod` receives the class, not an instance"
1902 );
1903 assert_eq!(
1904 modifiers("widget.fetch_all"),
1905 (false, false, 1),
1906 "an async def is the same function_definition node"
1907 );
1908 assert_eq!(modifiers("widget.outer"), (false, false, 1));
1909
1910 let (property_unit, property_entries) = entry("widget.Widget.label");
1911 assert_eq!(
1912 property_unit.kind(),
1913 CodeUnitType::Field,
1914 "`@property` is read as an attribute, so it mints a Field and no key"
1915 );
1916 assert!(!property_unit.is_callable());
1917 assert!(
1918 property_entries
1919 .first()
1920 .expect("the property still carries signature metadata")
1921 .callable_modifiers_recorded(),
1922 "the shared metadata path records modifiers for every declaration it mints"
1923 );
1924
1925 let lambda_unit = parsed
1930 .declarations()
1931 .iter()
1932 .find(|unit| unit.fq_name() == "widget.assigned")
1933 .expect("the module walk still records the name-bound lambda's assignment");
1934 assert_eq!(
1935 lambda_unit.kind(),
1936 CodeUnitType::Field,
1937 "a name-bound lambda is recorded as a field, not as a callable declaration"
1938 );
1939 assert!(
1940 !parsed.signature_metadata.contains_key(lambda_unit),
1941 "a field assignment carries signature text but no callable signature metadata"
1942 );
1943 assert!(
1944 parsed
1945 .declarations()
1946 .iter()
1947 .all(|unit| unit.terminal_name() != "inner"),
1948 "a function nested in another function is a local binding, not a declaration"
1949 );
1950 }
1951}