1use crate::bindings::python_direct_scope_bindings_bounded;
2use crate::imports::{python_import_infos_from_node, python_import_syntaxes_from_node};
3use crate::syntax::{
4 PythonOverloadDecoratorBindings, PythonOverloadDecoratorName, expression_name_node,
5 python_plain_string_literal,
6};
7use brokk_bifrost_core::analyzer::fq_name::{FqName, SegmentId, SegmentKind, segment_interner};
8use brokk_bifrost_core::analyzer::model::{
9 CodeUnitType, DispatchExtensibility, ParameterMetadata, SignatureMetadata,
10 StructuredImportPathKind, StructuredTypeIdentity,
11};
12use brokk_bifrost_core::analyzer::parsed_file::{
13 ParsedFile, ParsedSourceFacts, SourceDeclarationMetadataLink, SourceImportFact,
14};
15use brokk_bifrost_core::analyzer::python_facts::{PythonCallableReturnFact, PythonSourceFacts};
16use brokk_bifrost_core::analyzer::rust_facts::RustItemSourceFacts;
17use brokk_bifrost_core::analyzer::source_facts::{
18 PrimarySourceFactCollector, SourceDeclarationId, SourceImportId,
19};
20use brokk_bifrost_core::analyzer::structural::callable::CallSiteContext;
21use brokk_bifrost_core::analyzer::structural::collector::StructuralFactCollector;
22use brokk_bifrost_core::analyzer::structural::resolution::DeclaredVisibility;
23use brokk_bifrost_core::analyzer::structural::spec::{CompiledKinds, StructuralSpec};
24use brokk_bifrost_core::analyzer::tree_walk::{ParentIndex, WalkControl, walk_named_tree_preorder};
25use brokk_bifrost_core::analyzer::{CodeUnit, ProjectFile};
26use brokk_bifrost_core::hash::{HashMap, HashSet};
27use brokk_bifrost_core::path_normalization::NormalizePath;
28use brokk_bifrost_core::text_utils::{compute_line_starts, find_line_index_for_offset};
29use std::path::{Path, PathBuf};
30use tree_sitter::{Node, Parser, Tree};
31
32use crate::source_properties::{python_annotation_references, python_return_type_identity};
33use crate::structural::PYTHON_STRUCTURAL_SPEC;
34
35fn py_segment(text: &str, kind: SegmentKind) -> SegmentId {
37 segment_interner().intern(text, kind)
38}
39
40pub fn python_module_fq(file: &ProjectFile) -> FqName {
53 python_module_fq_from_components(&python_module_components(file))
54}
55
56fn python_module_fq_from_components(components: &[String]) -> FqName {
57 let mut fq = FqName::new();
58 for component in components {
59 fq.push(py_segment(component, SegmentKind::Package));
60 }
61 fq
62}
63
64fn python_module_components(file: &ProjectFile) -> Vec<String> {
65 python_module_components_from_root(file, &python_import_root(file))
66}
67
68pub fn python_project_module_components(file: &ProjectFile) -> Vec<String> {
80 python_module_components_from_root(file, Path::new(""))
81}
82
83fn python_module_components_from_root(file: &ProjectFile, import_root_rel: &Path) -> Vec<String> {
86 let mut components = python_package_components_for_file(file, import_root_rel);
87 let module_name = file
88 .rel_path()
89 .file_stem()
90 .and_then(|stem| stem.to_str())
91 .unwrap_or_default();
92 if module_name != "__init__" || components.is_empty() {
93 components.push(module_name.to_string());
94 }
95 components
96}
97
98pub fn python_import_root(file: &ProjectFile) -> PathBuf {
110 let Some(parent_rel) = file.rel_path().parent() else {
111 return PathBuf::new();
112 };
113 if parent_rel.as_os_str().is_empty() {
114 return PathBuf::new();
115 }
116
117 if let Some(import_root_rel) = python_configured_import_root(file, parent_rel) {
118 return import_root_rel;
119 }
120
121 let mut effective_package_root_rel: Option<&Path> = None;
122 let mut current_rel = Some(parent_rel);
123 while let Some(path) = current_rel {
124 if file.root().join(path).join("__init__.py").exists() {
125 effective_package_root_rel = Some(path);
126 }
127 current_rel = path.parent();
128 }
129
130 effective_package_root_rel
133 .and_then(Path::parent)
134 .unwrap_or_else(|| Path::new(""))
135 .to_path_buf()
136}
137
138fn python_package_components_for_file(file: &ProjectFile, import_root_rel: &Path) -> Vec<String> {
139 let Some(parent_rel) = file.rel_path().parent() else {
140 return Vec::new();
141 };
142 let relative_package = parent_rel
143 .strip_prefix(import_root_rel)
144 .expect("a Python import root is a prefix of the paths it names modules for");
145 path_components(relative_package)
146}
147
148fn python_configured_import_root(file: &ProjectFile, parent_rel: &Path) -> Option<PathBuf> {
154 let mut manifest_dir_rel = Some(parent_rel);
155 while let Some(directory) = manifest_dir_rel {
156 let manifest_dir = file.root().join(directory);
157 let mut roots = setuptools_where_entries(&manifest_dir.join("pyproject.toml"))
158 .iter()
159 .map(|entry| manifest_dir.join(entry).normalize())
160 .filter_map(|root| root.strip_prefix(file.root()).ok().map(Path::to_path_buf))
161 .filter(|root| parent_rel.starts_with(root))
162 .collect::<Vec<_>>();
163 roots.sort_by_key(|root| root.components().count());
164 if let Some(root) = roots.pop() {
165 return Some(root);
166 }
167 if let Some(package_dir) = setuptools_setup_py_import_root(&manifest_dir.join("setup.py")) {
168 let root = manifest_dir.join(package_dir).normalize();
169 if let Ok(root) = root.strip_prefix(file.root())
170 && parent_rel.starts_with(root)
171 {
172 return Some(root.to_path_buf());
173 }
174 }
175 manifest_dir_rel = directory.parent();
176 }
177 None
178}
179
180pub fn python_declared_import_root(file: &ProjectFile) -> Option<PathBuf> {
192 let parent_rel = file.rel_path().parent()?;
193 if parent_rel.as_os_str().is_empty() {
194 return None;
195 }
196 python_configured_import_root(file, parent_rel)
197}
198
199pub(crate) fn python_repeated_root_extension_dir(
224 file: &ProjectFile,
225 source_root_rel: &Path,
226 declared_root_rel: &Path,
227) -> Option<PathBuf> {
228 let implied_rel = source_root_rel.strip_prefix(declared_root_rel).ok()?;
229 let implied = path_components(implied_rel);
230 if implied.is_empty()
231 || !python_module_components_from_root(file, source_root_rel).starts_with(&implied)
232 {
233 return None;
234 }
235 Some(source_root_rel.join(implied_rel))
236}
237
238#[derive(Clone, Copy, PartialEq, Eq)]
239struct FileStamp {
240 len: u64,
241 modified: Option<std::time::SystemTime>,
242}
243
244fn file_stamp(path: &Path) -> Option<FileStamp> {
245 let metadata = std::fs::metadata(path).ok()?;
246 Some(FileStamp {
247 len: metadata.len(),
248 modified: metadata.modified().ok(),
249 })
250}
251
252struct ManifestWhereEntries {
257 stamp: FileStamp,
258 entries: Vec<String>,
259}
260
261fn setuptools_where_entries(manifest: &Path) -> Vec<String> {
272 static MEMO: std::sync::OnceLock<
273 std::sync::RwLock<std::collections::HashMap<PathBuf, ManifestWhereEntries>>,
274 > = std::sync::OnceLock::new();
275 let memo = MEMO.get_or_init(Default::default);
276
277 let Some(stamp) = file_stamp(manifest) else {
278 return Vec::new();
279 };
280 if let Some(cached) = memo.read().expect("manifest memo").get(manifest)
281 && cached.stamp == stamp
282 {
283 return cached.entries.clone();
284 }
285
286 let entries = parse_setuptools_where_entries(manifest);
287 memo.write().expect("manifest memo").insert(
288 manifest.to_path_buf(),
289 ManifestWhereEntries {
290 stamp,
291 entries: entries.clone(),
292 },
293 );
294 entries
295}
296
297struct SetupPyImportRoot {
301 stamp: FileStamp,
302 package_dir: Option<PathBuf>,
303}
304
305fn setuptools_setup_py_import_root(setup_py: &Path) -> Option<PathBuf> {
315 static MEMO: std::sync::OnceLock<
316 std::sync::RwLock<std::collections::HashMap<PathBuf, SetupPyImportRoot>>,
317 > = std::sync::OnceLock::new();
318 let memo = MEMO.get_or_init(Default::default);
319
320 let stamp = file_stamp(setup_py)?;
321 if let Some(cached) = memo.read().expect("setup.py memo").get(setup_py)
322 && cached.stamp == stamp
323 {
324 return cached.package_dir.clone();
325 }
326
327 let package_dir = parse_setuptools_setup_py_import_root(setup_py);
328 memo.write().expect("setup.py memo").insert(
329 setup_py.to_path_buf(),
330 SetupPyImportRoot {
331 stamp,
332 package_dir: package_dir.clone(),
333 },
334 );
335 package_dir
336}
337
338fn parse_setuptools_setup_py_import_root(setup_py: &Path) -> Option<PathBuf> {
339 let source = std::fs::read_to_string(setup_py).ok()?;
340 let tree = parse_python_tree(&source)?;
341 let root = tree.root_node();
342 if root.has_error() {
343 return None;
344 }
345 let mut import_root = None;
346 for (call, setup_bindings) in setup_py_setup_calls(&source, root) {
347 let candidate = setup_py_import_root_from_call(call, &source, &setup_bindings)?;
348 if import_root
349 .replace(candidate.clone())
350 .is_some_and(|root| root != candidate)
351 {
352 return None;
353 }
354 }
355 import_root
356}
357
358pub fn setuptools_setup_py_python_requires(setup_py: &Path) -> Option<String> {
366 let source = std::fs::read_to_string(setup_py).ok()?;
367 let tree = parse_python_tree(&source)?;
368 let root = tree.root_node();
369 if root.has_error() {
370 return None;
371 }
372 let mut requirement: Option<String> = None;
373 for (call, _) in setup_py_setup_calls(&source, root) {
374 let Some(arguments) = call.child_by_field_name("arguments") else {
375 continue;
376 };
377 if arguments.kind() != "argument_list" {
378 continue;
379 }
380 let mut cursor = arguments.walk();
381 for argument in arguments.named_children(&mut cursor) {
382 if argument.kind() != "keyword_argument" {
383 continue;
384 }
385 let Some(name) = argument.child_by_field_name("name") else {
386 continue;
387 };
388 if py_node_text(name, &source).trim() != "python_requires" {
389 continue;
390 }
391 let value = argument.child_by_field_name("value")?;
392 let declared = python_plain_string_literal(value, &source)?.to_owned();
393 if requirement
394 .replace(declared.clone())
395 .is_some_and(|previous| previous != declared)
396 {
397 return None;
398 }
399 }
400 }
401 requirement
402}
403
404fn setup_py_setup_calls<'tree>(
412 source: &str,
413 root: Node<'tree>,
414) -> Vec<(Node<'tree>, HashMap<Vec<String>, String>)> {
415 let mut setup_bindings: HashMap<Vec<String>, String> = HashMap::default();
416 let mut calls = Vec::new();
417 let mut cursor = root.walk();
418
419 for statement in root.named_children(&mut cursor) {
420 if matches!(
421 statement.kind(),
422 "import_statement" | "import_from_statement"
423 ) {
424 for binding in setup_py_bound_names(statement, source) {
425 setup_bindings.retain(|path, _| path.first() != Some(&binding));
426 }
427 for import in python_import_infos_from_node(statement, source) {
428 if import.is_wildcard {
429 setup_bindings.clear();
430 continue;
431 }
432 let Some(path) = import.path else { continue };
433 let segments = path.segments.iter().map(String::as_str).collect::<Vec<_>>();
434 match path.kind {
435 Some(StructuredImportPathKind::ImportFrom) => {
436 let function_name = match segments.as_slice() {
437 ["setuptools", "setup"] | ["distutils", "core", "setup"] => "setup",
438 ["setuptools", "find_packages"] => "find_packages",
439 ["setuptools", "find_namespace_packages"] => "find_namespace_packages",
440 _ => continue,
441 };
442 setup_bindings.insert(
443 vec![import.identifier.expect("imported function binds a name")],
444 function_name.to_string(),
445 );
446 }
447 Some(StructuredImportPathKind::Namespace) => {
448 let function_names = match segments.as_slice() {
449 ["setuptools"] => {
450 ["setup", "find_packages", "find_namespace_packages"].as_slice()
451 }
452 ["distutils", "core"] => ["setup"].as_slice(),
453 _ => continue,
454 };
455 let binding_prefix = import
456 .alias
457 .map(|alias| vec![alias])
458 .unwrap_or_else(|| path.segments.clone());
459 for function_name in function_names {
460 let mut callable = binding_prefix.clone();
461 callable.push((*function_name).to_string());
462 setup_bindings.insert(callable, (*function_name).to_string());
463 }
464 }
465 _ => continue,
466 }
467 }
468 continue;
469 }
470
471 if statement.kind() == "expression_statement"
472 && statement.named_child_count() == 1
473 && let Some(call) = statement.named_child(0)
474 && call.kind() == "call"
475 && setup_py_call_imported_function(call, source, &setup_bindings)
476 .is_some_and(|function| function == "setup")
477 {
478 calls.push((call, setup_bindings.clone()));
479 }
480
481 for binding in setup_py_bound_names(statement, source) {
482 setup_bindings.retain(|path, _| path.first() != Some(&binding));
483 }
484 }
485 calls
486}
487
488fn setup_py_bound_names(statement: Node<'_>, source: &str) -> Vec<String> {
495 let mut names = Vec::new();
496 let mut pending = vec![statement];
497 while let Some(node) = pending.pop() {
498 for binding in python_direct_scope_bindings_bounded(node, source, || true)
499 .expect("unbounded setup.py binding walk")
500 {
501 let name = py_node_text(binding.declaration, source).trim();
502 if !name.is_empty() {
503 names.push(name.to_string());
504 }
505 }
506 let excluded_body = matches!(
507 node.kind(),
508 "function_definition" | "class_definition" | "lambda"
509 )
510 .then(|| node.child_by_field_name("body").map(|body| body.id()))
511 .flatten();
512 let mut cursor = node.walk();
513 pending.extend(
514 node.named_children(&mut cursor)
515 .filter(|child| Some(child.id()) != excluded_body),
516 );
517 }
518 names
519}
520
521fn setup_py_call_imported_function<'a>(
522 call: Node<'_>,
523 source: &str,
524 setup_bindings: &'a HashMap<Vec<String>, String>,
525) -> Option<&'a str> {
526 let mut function = call.child_by_field_name("function")?;
527 let mut path = Vec::new();
528 while function.kind() == "attribute" {
529 let attribute = function.child_by_field_name("attribute")?;
530 path.push(py_node_text(attribute, source).to_string());
531 let object = function.child_by_field_name("object")?;
532 function = object;
533 }
534 if function.kind() != "identifier" {
535 return None;
536 }
537 path.push(py_node_text(function, source).to_string());
538 path.reverse();
539 setup_bindings.get(&path).map(String::as_str)
540}
541
542fn setup_py_import_root_from_call(
543 call: Node<'_>,
544 source: &str,
545 setup_bindings: &HashMap<Vec<String>, String>,
546) -> Option<PathBuf> {
547 let arguments = call.child_by_field_name("arguments")?;
548 if arguments.kind() != "argument_list" {
549 return None;
550 }
551
552 let mut packages = None;
553 let mut package_dir = None;
554 let mut cursor = arguments.walk();
555 for argument in arguments.named_children(&mut cursor) {
556 if argument.kind() == "comment" {
557 continue;
558 }
559 if argument.kind() != "keyword_argument" {
561 return None;
562 }
563 let name = argument.child_by_field_name("name")?;
564 let value = argument.child_by_field_name("value")?;
565 match py_node_text(name, source).trim() {
566 "packages" if packages.is_none() => packages = Some(value),
567 "packages" => return None,
568 "package_dir" if package_dir.is_none() => package_dir = Some(value),
569 "package_dir" => return None,
570 _ => {}
571 }
572 }
573
574 let packages = packages?;
575 if let Some(package_dir) = package_dir {
576 return setup_py_static_package_dir(package_dir, source);
577 }
578 if setup_py_nonempty_literal_packages(packages, source) {
579 return Some(PathBuf::new());
580 }
581 setup_py_discovery_root_from_call(packages, source, setup_bindings)
582}
583
584fn setup_py_discovery_root_from_call(
585 call: Node<'_>,
586 source: &str,
587 setup_bindings: &HashMap<Vec<String>, String>,
588) -> Option<PathBuf> {
589 let function = setup_py_call_imported_function(call, source, setup_bindings)?;
590 if !matches!(function, "find_packages" | "find_namespace_packages") {
591 return None;
592 }
593 let arguments = call.child_by_field_name("arguments")?;
594 if arguments.kind() != "argument_list" {
595 return None;
596 }
597
598 let mut where_value = None;
599 let mut seen_exclude = false;
600 let mut seen_include = false;
601 let mut positional_index = 0;
602 let mut cursor = arguments.walk();
603 for argument in arguments.named_children(&mut cursor) {
604 if argument.kind() == "comment" {
605 continue;
606 }
607 if matches!(argument.kind(), "list_splat" | "dictionary_splat") {
608 return None;
609 }
610 if argument.kind() == "keyword_argument" {
611 let name = py_node_text(argument.child_by_field_name("name")?, source).trim();
612 let value = argument.child_by_field_name("value")?;
613 match name {
614 "where" if where_value.is_none() => where_value = Some(value),
615 "where" => return None,
616 "exclude" if !seen_exclude => seen_exclude = true,
617 "exclude" => return None,
618 "include" if !seen_include => seen_include = true,
619 "include" => return None,
620 _ => return None,
621 }
622 continue;
623 }
624
625 let slot = positional_index;
626 positional_index += 1;
627 match slot {
628 0 if where_value.is_none() => where_value = Some(argument),
629 0 => return None,
630 1 if !seen_exclude => seen_exclude = true,
631 1 => return None,
632 2 if !seen_include => seen_include = true,
633 2 => return None,
634 _ => return None,
635 }
636 }
637
638 where_value
639 .map(|value| python_plain_string_literal(value, source))
640 .unwrap_or(Some(""))
641 .map(PathBuf::from)
642}
643
644fn setup_py_nonempty_literal_packages(value: Node<'_>, source: &str) -> bool {
645 let value = setup_py_unwrap_parenthesized(value);
646 if !matches!(value.kind(), "list" | "set" | "tuple") {
647 return false;
648 }
649 let mut cursor = value.walk();
650 let mut nonempty = false;
651 for element in value.named_children(&mut cursor) {
652 if element.kind() == "comment" {
653 continue;
654 }
655 let Some(package) = python_plain_string_literal(element, source) else {
656 return false;
657 };
658 if package.is_empty() {
659 return false;
660 }
661 nonempty = true;
662 }
663 nonempty
664}
665
666fn setup_py_static_package_dir(value: Node<'_>, source: &str) -> Option<PathBuf> {
667 let value = setup_py_unwrap_parenthesized(value);
668 if value.kind() != "dictionary" {
669 return None;
670 }
671 let mut cursor = value.walk();
672 let mut pairs = value
673 .named_children(&mut cursor)
674 .filter(|node| node.kind() != "comment");
675 let Some(pair) = pairs.next() else {
676 return Some(PathBuf::new());
677 };
678 if pairs.next().is_some() || pair.kind() != "pair" {
679 return None;
680 }
681 let key = python_plain_string_literal(pair.child_by_field_name("key")?, source)?;
682 if !key.is_empty() {
683 return None;
684 }
685 let root = python_plain_string_literal(pair.child_by_field_name("value")?, source)?;
686 let root = PathBuf::from(root);
687 (!root.is_absolute()).then_some(root)
688}
689
690fn setup_py_unwrap_parenthesized(mut node: Node<'_>) -> Node<'_> {
691 while node.kind() == "parenthesized_expression" && node.named_child_count() == 1 {
692 node = node.named_child(0).expect("parenthesized expression child");
693 }
694 node
695}
696
697fn parse_setuptools_where_entries(manifest: &Path) -> Vec<String> {
698 let Ok(source) = std::fs::read_to_string(manifest) else {
699 return Vec::new();
700 };
701 let Ok(document) = source.parse::<toml::Value>() else {
702 return Vec::new();
703 };
704 document
705 .get("tool")
706 .and_then(|tool| tool.get("setuptools"))
707 .and_then(|setuptools| setuptools.get("packages"))
708 .and_then(|packages| packages.get("find"))
709 .and_then(|find| find.get("where"))
710 .and_then(toml::Value::as_array)
711 .map(|entries| {
712 entries
713 .iter()
714 .filter_map(toml::Value::as_str)
715 .map(str::to_string)
716 .collect()
717 })
718 .unwrap_or_default()
719}
720
721fn path_components(path: &Path) -> Vec<String> {
722 path.components()
723 .map(|component| component.as_os_str().to_string_lossy().to_string())
724 .filter(|component| !component.is_empty())
725 .collect()
726}
727
728pub fn python_is_decorated_function_boundary(node: Node<'_>) -> bool {
729 if node.kind() != "decorated_definition" {
730 return false;
731 }
732 let mut cursor = node.walk();
733 node.named_children(&mut cursor)
734 .any(|child| child.kind() == "function_definition")
735}
736
737#[derive(Clone)]
738pub struct Scope {
739 kind: ScopeKind,
740 path: String,
741 fq: FqName,
748 code_unit: Option<CodeUnit>,
749 method_receiver: Option<String>,
750}
751
752#[derive(Clone, Copy, PartialEq, Eq)]
753enum ScopeKind {
754 Class,
755 Function,
756}
757
758pub struct PythonVisitor<'source, 'parsed, 'structural>
759where
760 'source: 'structural,
761{
762 pub file: &'parsed ProjectFile,
763 pub source: &'source str,
764 pub package_name: &'parsed str,
765 module_fq: &'parsed FqName,
766 pub parsed: &'parsed mut ParsedFile,
767 pub module: Option<CodeUnit>,
768 overload_decorators: PythonOverloadDecoratorBindings,
769 source_facts: PrimarySourceFactCollector<'source>,
770 structural: StructuralFactCollector<'structural, 'structural>,
771 structural_kinds: &'structural CompiledKinds,
772 call_site_context: &'structural CallSiteContext,
773 structural_parents: Vec<Option<u32>>,
774 source_imports: Vec<SourceImportFact>,
775 generic_imports: Vec<SourceImportId>,
776 source_declaration_units: Vec<(SourceDeclarationId, CodeUnit)>,
777 python_source_facts: PythonSourceFacts,
778 python_callable_return_by_node: HashMap<usize, usize>,
779 overload_metadata: Vec<(CodeUnit, usize, Vec<PythonOverloadDecoratorName>)>,
780}
781
782struct PythonFrame<'tree> {
783 node: Node<'tree>,
784 scope: Vec<Scope>,
785 module_control_depth: usize,
786 declaration: bool,
787 wrapper: Option<Node<'tree>>,
788 lexical_scopes: Vec<Node<'tree>>,
789}
790
791enum PythonWork<'tree> {
792 Enter(PythonFrame<'tree>),
793 Exit,
794}
795
796struct DeclarationChildren<'tree> {
797 body: Option<(Node<'tree>, Vec<Scope>)>,
798 container_scope: Option<Vec<Scope>>,
799 container_depth: usize,
800}
801
802impl<'source, 'parsed, 'structural> PythonVisitor<'source, 'parsed, 'structural>
803where
804 'source: 'structural,
805{
806 pub fn visit_container(
807 &mut self,
808 node: Node<'_>,
809 scope: &[Scope],
810 module_control_depth: usize,
811 ) {
812 let mut stack = vec![PythonWork::Enter(PythonFrame {
813 node,
814 scope: scope.to_vec(),
815 module_control_depth,
816 declaration: true,
817 wrapper: None,
818 lexical_scopes: Vec::new(),
819 })];
820 while let Some(work) = stack.pop() {
821 let PythonWork::Enter(frame) = work else {
822 self.structural_parents
823 .pop()
824 .expect("Python structural parent stack must balance");
825 continue;
826 };
827 let structural_parent = self.structural_parents.last().copied().flatten();
828 let structural_id = self.admit_structural(frame.node, structural_parent);
829 self.structural_parents
830 .push(structural_id.or(structural_parent));
831
832 if frame.node.kind() == "identifier" {
833 let text = py_node_text(frame.node, self.source).trim();
834 if !text.is_empty() {
835 self.parsed.type_identifiers.insert(text.to_string());
836 }
837 }
838
839 if frame.node.kind() == "function_definition" {
840 self.capture_function_return_fact(frame.node, frame.wrapper);
841 }
842
843 if matches!(
844 frame.node.kind(),
845 "import_statement" | "import_from_statement"
846 ) {
847 self.visit_import_statement(frame.node, &frame.lexical_scopes);
848 }
849
850 let declaration_children = if frame.declaration {
851 self.visit_statement(
852 frame.node,
853 &frame.scope,
854 frame.module_control_depth,
855 frame.wrapper,
856 )
857 } else {
858 DeclarationChildren {
859 body: None,
860 container_scope: None,
861 container_depth: frame.module_control_depth,
862 }
863 };
864
865 let mut children = Vec::new();
866 let mut cursor = frame.node.walk();
867 for child in frame.node.named_children(&mut cursor) {
868 let mut child_scope = frame.scope.clone();
869 let mut declaration = false;
870 let mut wrapper = None;
871 let mut child_depth = frame.module_control_depth;
872
873 if let Some((body, scope)) = &declaration_children.body
874 && child.id() == body.id()
875 {
876 declaration = true;
877 child_scope = scope.clone();
878 } else if let Some(scope) = &declaration_children.container_scope {
879 declaration = true;
880 child_scope = scope.clone();
881 child_depth = declaration_children.container_depth;
882 } else if frame.declaration
883 && frame.node.kind() == "decorated_definition"
884 && matches!(child.kind(), "class_definition" | "function_definition")
885 {
886 declaration = true;
889 wrapper = Some(frame.node);
890 }
891
892 let mut lexical_scopes = frame.lexical_scopes.clone();
893 if matches!(
894 frame.node.kind(),
895 "class_definition" | "function_definition" | "lambda"
896 ) {
897 lexical_scopes.push(frame.node);
898 }
899 children.push(PythonFrame {
900 node: child,
901 scope: child_scope,
902 module_control_depth: child_depth,
903 declaration,
904 wrapper,
905 lexical_scopes,
906 });
907 }
908
909 stack.push(PythonWork::Exit);
910 stack.extend(children.into_iter().rev().map(PythonWork::Enter));
911 }
912 }
913
914 fn admit_structural(&mut self, node: Node<'_>, parent: Option<u32>) -> Option<u32> {
915 if !node.is_named() {
916 return parent;
917 }
918 let Some(raw_kind) = self.structural_kinds.kind_of(&node) else {
919 return parent;
920 };
921 if !PYTHON_STRUCTURAL_SPEC.should_extract(node, raw_kind) {
922 return parent;
923 }
924 let kind = PYTHON_STRUCTURAL_SPEC.refine_kind(
925 node,
926 raw_kind,
927 parent.map(|id| self.structural.normalized_kind(id)),
928 self.source,
929 self.call_site_context,
930 );
931 let fact_id = self
932 .structural
933 .enter(node, kind, parent, &mut self.source_facts)
934 .expect("Python structural fact collection must remain unbounded");
935 let mut sink = self.structural.role_sink(&mut self.source_facts);
936 PYTHON_STRUCTURAL_SPEC.extract(node, kind, &mut sink);
937 self.structural
938 .accept_roles(fact_id, sink.into_parts())
939 .expect("Python structural role collection must remain unbounded");
940 Some(fact_id)
941 }
942
943 fn replace_source_declaration(
944 &mut self,
945 declaration_node: Node<'_>,
946 name_node: Option<Node<'_>>,
947 unit: CodeUnit,
948 metadata_ordinal: Option<usize>,
949 ) -> SourceDeclarationId {
950 self.source_declaration_units
951 .retain(|(_, existing)| existing != &unit);
952 self.parsed
953 .source_declaration_metadata
954 .retain(|link| link.unit != unit);
955 self.record_source_declaration(declaration_node, name_node, unit, metadata_ordinal)
956 }
957
958 fn record_source_declaration(
959 &mut self,
960 declaration_node: Node<'_>,
961 name_node: Option<Node<'_>>,
962 unit: CodeUnit,
963 metadata_ordinal: Option<usize>,
964 ) -> SourceDeclarationId {
965 let occurrence = self.source_facts.intern_node(declaration_node);
966 let name = name_node.map(|node| self.source_facts.intern_node(node));
967 let declaration = self.source_facts.declare(occurrence, name);
968 self.source_declaration_units
969 .push((declaration, unit.clone()));
970 if let Some(metadata_ordinal) = metadata_ordinal {
971 self.parsed
972 .source_declaration_metadata
973 .push(SourceDeclarationMetadataLink {
974 declaration,
975 unit,
976 metadata_ordinal,
977 });
978 }
979 declaration
980 }
981
982 fn capture_function_return_fact<'tree>(
983 &mut self,
984 node: Node<'tree>,
985 wrapper: Option<Node<'tree>>,
986 ) {
987 let declaration_node = wrapper
988 .or_else(|| {
989 node.parent()
990 .filter(|parent| parent.kind() == "decorated_definition")
991 })
992 .unwrap_or(node);
993 let occurrence = self.source_facts.intern_node(declaration_node);
994 let name = node
995 .child_by_field_name("name")
996 .map(|name| self.source_facts.intern_node(name));
997 let declaration = self.source_facts.declare(occurrence, name);
998 let return_annotation = node
999 .child_by_field_name("return_type")
1000 .map(|annotation| self.source_facts.intern_node(annotation));
1001 let annotation_references = node
1002 .child_by_field_name("return_type")
1003 .map(|annotation| {
1004 python_annotation_references(annotation, self.source, &mut self.source_facts)
1005 })
1006 .unwrap_or_default();
1007 let runtime_type = node
1008 .child_by_field_name("return_type")
1009 .and_then(|annotation| python_return_type_identity(annotation, self.source));
1010 let fact_index = self.python_source_facts.callable_returns.len();
1011 self.python_source_facts
1012 .callable_returns
1013 .push(PythonCallableReturnFact {
1014 declaration,
1015 return_annotation,
1016 annotation_references,
1017 runtime_type,
1018 });
1019 assert!(
1020 self.python_callable_return_by_node
1021 .insert(node.id(), fact_index)
1022 .is_none(),
1023 "each Python function definition must have one callable return fact"
1024 );
1025 }
1026
1027 fn link_source_declaration(
1028 &mut self,
1029 declaration: SourceDeclarationId,
1030 unit: CodeUnit,
1031 metadata_ordinal: Option<usize>,
1032 ) {
1033 self.source_declaration_units
1034 .push((declaration, unit.clone()));
1035 if let Some(metadata_ordinal) = metadata_ordinal {
1036 self.parsed
1037 .source_declaration_metadata
1038 .push(SourceDeclarationMetadataLink {
1039 declaration,
1040 unit,
1041 metadata_ordinal,
1042 });
1043 }
1044 }
1045
1046 fn visit_statement<'tree>(
1047 &mut self,
1048 node: Node<'tree>,
1049 scope: &[Scope],
1050 module_control_depth: usize,
1051 wrapper: Option<Node<'tree>>,
1052 ) -> DeclarationChildren<'tree> {
1053 match node.kind() {
1054 "decorated_definition" => DeclarationChildren {
1055 body: None,
1056 container_scope: None,
1057 container_depth: module_control_depth,
1058 },
1059 "class_definition" | "function_definition" => {
1060 self.visit_definition(node, wrapper, scope, module_control_depth)
1061 }
1062 "expression_statement" => {
1063 self.visit_expression_statement(node, scope, module_control_depth);
1064 DeclarationChildren {
1065 body: None,
1066 container_scope: None,
1067 container_depth: module_control_depth,
1068 }
1069 }
1070 "import_statement" | "import_from_statement" => DeclarationChildren {
1071 body: None,
1072 container_scope: None,
1073 container_depth: module_control_depth,
1074 },
1075 "if_statement" | "try_statement" | "with_statement" | "for_statement"
1076 | "while_statement" => {
1077 let next_depth = if scope.is_empty() {
1078 module_control_depth + 1
1079 } else {
1080 module_control_depth
1081 };
1082 DeclarationChildren {
1083 body: None,
1084 container_scope: Some(scope.to_vec()),
1085 container_depth: next_depth,
1086 }
1087 }
1088 "elif_clause" | "else_clause" | "except_clause" | "finally_clause" => {
1089 DeclarationChildren {
1090 body: None,
1091 container_scope: Some(scope.to_vec()),
1092 container_depth: module_control_depth,
1093 }
1094 }
1095 "block" | "module" => DeclarationChildren {
1096 body: None,
1097 container_scope: Some(scope.to_vec()),
1098 container_depth: module_control_depth,
1099 },
1100 _ => DeclarationChildren {
1101 body: None,
1102 container_scope: None,
1103 container_depth: module_control_depth,
1104 },
1105 }
1106 }
1107
1108 fn visit_definition<'tree>(
1109 &mut self,
1110 definition: Node<'tree>,
1111 wrapper: Option<Node<'tree>>,
1112 scope: &[Scope],
1113 module_control_depth: usize,
1114 ) -> DeclarationChildren<'tree> {
1115 match definition.kind() {
1116 "class_definition" => self.visit_class_definition(
1117 definition,
1118 wrapper.unwrap_or(definition),
1119 scope,
1120 module_control_depth,
1121 ),
1122 "function_definition" => self.visit_function_definition(
1123 definition,
1124 wrapper.unwrap_or(definition),
1125 scope,
1126 module_control_depth,
1127 ),
1128 _ => DeclarationChildren {
1129 body: None,
1130 container_scope: None,
1131 container_depth: module_control_depth,
1132 },
1133 }
1134 }
1135
1136 fn visit_class_definition<'tree>(
1137 &mut self,
1138 node: Node<'tree>,
1139 range_node: Node<'tree>,
1140 scope: &[Scope],
1141 module_control_depth: usize,
1142 ) -> DeclarationChildren<'tree> {
1143 let Some(name_node) = node.child_by_field_name("name") else {
1144 return DeclarationChildren {
1145 body: None,
1146 container_scope: None,
1147 container_depth: module_control_depth,
1148 };
1149 };
1150 let name = py_node_text(name_node, self.source).trim();
1151 if name.is_empty() {
1152 return DeclarationChildren {
1153 body: None,
1154 container_scope: None,
1155 container_depth: module_control_depth,
1156 };
1157 }
1158
1159 let capture = !scope.is_empty() || module_control_depth <= 1;
1160
1161 let short_name = scope
1162 .last()
1163 .map(|parent| format!("{}${name}", parent.path))
1164 .unwrap_or_else(|| name.to_string());
1165 let fq = match scope.last() {
1171 Some(parent) => parent
1172 .fq
1173 .clone()
1174 .with_pushed(py_segment(name, SegmentKind::Nested)),
1175 None => self
1176 .module_fq
1177 .clone()
1178 .with_pushed(py_segment(name, SegmentKind::Type)),
1179 };
1180 let code_unit = CodeUnit::new_fq(
1181 self.file.clone(),
1182 CodeUnitType::Class,
1183 self.package_name.to_string(),
1184 short_name.clone(),
1185 fq.clone(),
1186 );
1187 if capture {
1188 self.parsed
1189 .replace_code_unit(code_unit.clone(), range_node, self.source, None, None);
1190 self.parsed.add_signature(
1191 code_unit.clone(),
1192 python_class_signature(range_node, self.source),
1193 );
1194 if let Some(module) = &self.module
1195 && scope.is_empty()
1196 {
1197 self.parsed.add_child(module.clone(), code_unit.clone());
1198 }
1199 if let Some(parent) = scope.last()
1200 && let Some(parent_cu) = &parent.code_unit
1201 {
1202 self.parsed.add_child(parent_cu.clone(), code_unit.clone());
1203 }
1204 self.parsed.set_raw_supertypes(
1205 code_unit.clone(),
1206 extract_python_supertypes(node, self.source),
1207 );
1208 self.replace_source_declaration(range_node, Some(name_node), code_unit.clone(), None);
1209 }
1210
1211 let mut next_scope = scope.to_vec();
1212 if capture {
1213 next_scope.push(Scope {
1214 kind: ScopeKind::Class,
1215 path: short_name,
1216 fq,
1217 code_unit: Some(code_unit),
1218 method_receiver: None,
1219 });
1220 }
1221 DeclarationChildren {
1222 body: node
1223 .child_by_field_name("body")
1224 .map(|body| (body, next_scope)),
1225 container_scope: None,
1226 container_depth: module_control_depth,
1227 }
1228 }
1229
1230 fn visit_function_definition<'tree>(
1231 &mut self,
1232 node: Node<'tree>,
1233 range_node: Node<'tree>,
1234 scope: &[Scope],
1235 module_control_depth: usize,
1236 ) -> DeclarationChildren<'tree> {
1237 let Some(name_node) = node.child_by_field_name("name") else {
1238 return DeclarationChildren {
1239 body: None,
1240 container_scope: None,
1241 container_depth: module_control_depth,
1242 };
1243 };
1244 let name = py_node_text(name_node, self.source).trim();
1245 if name.is_empty() {
1246 return DeclarationChildren {
1247 body: None,
1248 container_scope: None,
1249 container_depth: module_control_depth,
1250 };
1251 }
1252
1253 let capture = !python_is_property_mutator(range_node, self.source)
1261 && ((scope.is_empty() && module_control_depth <= 1)
1262 || scope
1263 .last()
1264 .is_some_and(|parent| parent.kind == ScopeKind::Class));
1265 let short_name = if let Some(parent) = scope.last() {
1266 match parent.kind {
1267 ScopeKind::Class => format!("{}.{}", parent.path, name),
1268 ScopeKind::Function => format!("{}${name}", parent.path),
1269 }
1270 } else {
1271 name.to_string()
1272 };
1273 let fq = if let Some(parent) = scope.last() {
1278 match parent.kind {
1279 ScopeKind::Class => parent
1280 .fq
1281 .clone()
1282 .with_pushed(py_segment(name, SegmentKind::Member)),
1283 ScopeKind::Function => parent
1284 .fq
1285 .clone()
1286 .with_pushed(py_segment(name, SegmentKind::Nested)),
1287 }
1288 } else {
1289 self.module_fq
1290 .clone()
1291 .with_pushed(py_segment(name, SegmentKind::Member))
1292 };
1293
1294 if capture {
1295 let code_unit_type = if python_function_has_decorator(node, self.source, "property") {
1296 CodeUnitType::Field
1297 } else {
1298 CodeUnitType::Function
1299 };
1300 let signature = node
1301 .child_by_field_name("parameters")
1302 .map(|parameters| py_node_text(parameters, self.source).trim().to_string());
1303 let code_unit = CodeUnit::with_signature_and_fq(
1304 self.file.clone(),
1305 code_unit_type,
1306 self.package_name.to_string(),
1307 short_name.clone(),
1308 signature,
1309 false,
1310 fq.clone(),
1311 );
1312 self.parsed
1313 .replace_code_unit(code_unit.clone(), range_node, self.source, None, None);
1314 let signature = python_function_signature(range_node, self.source);
1315 let fact_index = *self
1316 .python_callable_return_by_node
1317 .get(&node.id())
1318 .expect("function return fact must precede declaration admission");
1319 let return_fact = &self.python_source_facts.callable_returns[fact_index];
1320 let return_declaration = return_fact.declaration;
1321 let return_type_identity = return_fact.runtime_type.clone();
1322 let return_type_text = return_fact.return_annotation.map(|annotation| {
1323 let range = self.source_facts.occurrence(annotation).range;
1324 self.source[range.start_byte..range.end_byte].to_string()
1325 });
1326 let metadata_ordinal = self.parsed.add_signature_with_metadata(
1327 code_unit.clone(),
1328 python_signature_metadata(
1329 signature,
1330 node,
1331 self.source,
1332 return_type_text.as_deref(),
1333 return_type_identity,
1334 ),
1335 );
1336 self.overload_metadata
1337 .retain(|(existing, _, _)| existing != &code_unit);
1338 self.overload_metadata.push((
1339 code_unit.clone(),
1340 metadata_ordinal,
1341 PythonOverloadDecoratorBindings::overload_decorator_names(node, self.source),
1342 ));
1343 self.source_declaration_units
1344 .retain(|(_, existing)| existing != &code_unit);
1345 self.parsed
1346 .source_declaration_metadata
1347 .retain(|link| link.unit != code_unit);
1348 self.link_source_declaration(
1349 return_declaration,
1350 code_unit.clone(),
1351 Some(metadata_ordinal),
1352 );
1353 if let Some(module) = &self.module
1354 && scope.is_empty()
1355 {
1356 self.parsed.add_child(module.clone(), code_unit.clone());
1357 }
1358 if let Some(parent) = scope.last()
1359 && parent.kind == ScopeKind::Class
1360 && let Some(parent_cu) = &parent.code_unit
1361 {
1362 self.parsed.add_child(parent_cu.clone(), code_unit.clone());
1363 }
1364 let scope_code_unit = Some(code_unit);
1365 let mut next_scope = scope.to_vec();
1366 next_scope.push(Scope {
1367 kind: ScopeKind::Function,
1368 path: short_name,
1369 fq,
1370 code_unit: scope_code_unit,
1371 method_receiver: scope
1372 .last()
1373 .is_some_and(|parent| parent.kind == ScopeKind::Class)
1374 .then(|| python_instance_method_receiver_name(node, self.source))
1375 .flatten(),
1376 });
1377 return DeclarationChildren {
1378 body: node
1379 .child_by_field_name("body")
1380 .map(|body| (body, next_scope)),
1381 container_scope: None,
1382 container_depth: module_control_depth,
1383 };
1384 }
1385
1386 let mut next_scope = scope.to_vec();
1387 next_scope.push(Scope {
1388 kind: ScopeKind::Function,
1389 path: short_name,
1390 fq,
1391 code_unit: None,
1392 method_receiver: None,
1393 });
1394 DeclarationChildren {
1395 body: node
1396 .child_by_field_name("body")
1397 .map(|body| (body, next_scope)),
1398 container_scope: None,
1399 container_depth: module_control_depth,
1400 }
1401 }
1402
1403 fn visit_expression_statement(
1404 &mut self,
1405 node: Node<'_>,
1406 scope: &[Scope],
1407 module_control_depth: usize,
1408 ) {
1409 let Some(assignment) = node.named_child(0) else {
1410 return;
1411 };
1412 if assignment.kind() != "assignment" {
1413 return;
1414 }
1415 let targets = python_chained_assignment_targets(assignment);
1416 if targets.is_empty() {
1417 return;
1418 }
1419 for left in &targets {
1420 self.visit_instance_attribute_assignment(*left, scope);
1421 }
1422 let names = targets
1423 .iter()
1424 .flat_map(|left| collect_assigned_name_nodes(*left, self.source));
1425 for name_node in names {
1426 let name = py_node_text(name_node, self.source).trim().to_string();
1427 if name.is_empty() {
1428 continue;
1429 }
1430 let (short_name, fq) = if let Some(parent) = scope.last() {
1431 if parent.kind != ScopeKind::Class {
1432 continue;
1433 }
1434 (
1435 format!("{}.{}", parent.path, name),
1436 parent
1437 .fq
1438 .clone()
1439 .with_pushed(py_segment(&name, SegmentKind::Member)),
1440 )
1441 } else if module_control_depth <= 1 {
1442 (
1443 name.clone(),
1444 self.module_fq
1445 .clone()
1446 .with_pushed(py_segment(&name, SegmentKind::Member)),
1447 )
1448 } else {
1449 continue;
1450 };
1451 let code_unit = CodeUnit::new_fq(
1452 self.file.clone(),
1453 CodeUnitType::Field,
1454 self.package_name.to_string(),
1455 short_name,
1456 fq,
1457 );
1458 if scope
1459 .last()
1460 .is_some_and(|parent| parent.kind == ScopeKind::Class)
1461 {
1462 self.parsed
1466 .add_code_unit(code_unit.clone(), node, self.source, None, None);
1467 } else {
1468 self.parsed
1469 .replace_code_unit(code_unit.clone(), node, self.source, None, None);
1470 }
1471 self.parsed.add_signature(
1472 code_unit.clone(),
1473 py_node_text(node, self.source).trim().to_string(),
1474 );
1475 if scope
1476 .last()
1477 .is_some_and(|parent| parent.kind == ScopeKind::Class)
1478 {
1479 self.record_source_declaration(node, Some(name_node), code_unit.clone(), None);
1480 } else {
1481 self.replace_source_declaration(node, Some(name_node), code_unit.clone(), None);
1482 }
1483 if let Some(module) = &self.module
1484 && scope.is_empty()
1485 {
1486 self.parsed.add_child(module.clone(), code_unit.clone());
1487 }
1488 if let Some(parent) = scope.last()
1489 && parent.kind == ScopeKind::Class
1490 && let Some(parent_cu) = &parent.code_unit
1491 {
1492 self.parsed.add_child(parent_cu.clone(), code_unit);
1493 }
1494 }
1495 }
1496
1497 fn visit_instance_attribute_assignment(&mut self, left: Node<'_>, scope: &[Scope]) {
1498 let Some(function) = scope
1499 .last()
1500 .filter(|scope| scope.kind == ScopeKind::Function)
1501 else {
1502 return;
1503 };
1504 let Some(receiver) = function.method_receiver.as_deref() else {
1505 return;
1506 };
1507 let Some(parent) = scope
1508 .get(scope.len().saturating_sub(2))
1509 .filter(|scope| scope.kind == ScopeKind::Class)
1510 else {
1511 return;
1512 };
1513 let Some(parent_cu) = parent.code_unit.clone() else {
1514 return;
1515 };
1516 for (name, node) in collect_self_assigned_attributes(left, self.source, receiver) {
1517 let code_unit = CodeUnit::new_fq(
1518 self.file.clone(),
1519 CodeUnitType::Field,
1520 self.package_name.to_string(),
1521 format!("{}.{}", parent.path, name),
1522 parent
1523 .fq
1524 .clone()
1525 .with_pushed(py_segment(&name, SegmentKind::Member)),
1526 );
1527 if !self.parsed.contains_declaration(&code_unit) {
1528 self.parsed.replace_code_unit(
1529 code_unit.clone(),
1530 node,
1531 self.source,
1532 Some(parent_cu.clone()),
1533 Some(parent_cu.clone()),
1534 );
1535 self.record_source_declaration(node, Some(node), code_unit.clone(), None);
1536 }
1537 self.parsed.add_signature(
1538 code_unit.clone(),
1539 py_node_text(node, self.source).trim().to_string(),
1540 );
1541 }
1542 }
1543
1544 fn visit_import_statement(&mut self, node: Node<'_>, lexical_scopes: &[Node<'_>]) {
1545 for syntax in python_import_syntaxes_from_node(node, self.source) {
1546 if lexical_scopes.is_empty() {
1547 self.overload_decorators.collect_import(&syntax.info);
1548 }
1549 let declaration = self.source_facts.intern_node(syntax.declaration);
1550 let target = syntax
1551 .target
1552 .map(|node| self.source_facts.intern_node(node));
1553 let alias_occurrence = syntax.alias.map(|node| self.source_facts.intern_node(node));
1554 let lexical_scopes = lexical_scopes
1555 .iter()
1556 .map(|node| self.source_facts.intern_node(*node))
1557 .collect();
1558 let source_import = SourceImportFact::from_import(
1559 syntax.info,
1560 declaration,
1561 target,
1562 alias_occurrence,
1563 lexical_scopes,
1564 );
1565 let id = SourceImportId::try_from_index(self.source_imports.len())
1566 .expect("Python source import ids must fit in a u32");
1567 self.source_imports.push(source_import);
1568 self.generic_imports.push(id);
1569 }
1570 }
1571
1572 fn finish(self) {
1573 let PythonVisitor {
1574 parsed,
1575 source,
1576 source_facts,
1577 structural,
1578 structural_parents,
1579 source_imports,
1580 generic_imports,
1581 source_declaration_units,
1582 python_source_facts,
1583 python_callable_return_by_node: _,
1584 overload_decorators,
1585 overload_metadata,
1586 ..
1587 } = self;
1588 assert_eq!(structural_parents, vec![None]);
1589 let structural = structural
1590 .finish()
1591 .expect("Python structural fact collection must finish");
1592 let occurrences = source_facts.finish();
1593 assert!(
1594 python_source_facts.valid_links(&occurrences),
1595 "Python callable return facts must link to exact source identities"
1596 );
1597 for (unit, metadata_ordinal, decorators) in overload_metadata {
1598 let declaration_only = decorators
1599 .iter()
1600 .any(|name| overload_decorators.matches_overload_binding(name));
1601 if declaration_only
1602 && let Some(metadata) = parsed
1603 .signature_metadata
1604 .get_mut(&unit)
1605 .and_then(|entries| entries.get_mut(metadata_ordinal))
1606 {
1607 *metadata = metadata.clone().with_declaration_only(true);
1608 }
1609 }
1610 parsed.imports = generic_imports
1611 .iter()
1612 .map(|id| source_imports[id.index()].import_info(&occurrences))
1613 .collect();
1614 parsed.source_declaration_units = source_declaration_units;
1615 parsed.source_facts = Some(ParsedSourceFacts {
1616 cpp: None,
1617 go: None,
1618 java: None,
1619 js_ts: None,
1620 php: None,
1621 scala: None,
1622 ruby: None,
1623 source_bytes: source.len(),
1624 occurrences,
1625 structural,
1626 python: Some(python_source_facts),
1627 native_site_occurrences: Vec::new(),
1628 native_declaration_sources: Vec::new(),
1629 declaration_visibilities: None,
1630 rust_declaration_properties: Vec::new(),
1631 rust_modules: None,
1632 rust_types: Vec::new(),
1633 rust_items: RustItemSourceFacts::default(),
1634 imports: source_imports,
1635 generic_imports,
1636 rust_import_contexts: Vec::new(),
1637 });
1638 }
1639}
1640
1641pub fn parse_python_file(file: &ProjectFile, source: &str, tree: &Tree) -> ParsedFile {
1645 let module_components = python_module_components(file);
1646 let module_name = module_components.join(".");
1647 let module_fq = python_module_fq_from_components(&module_components);
1648 let mut parsed = ParsedFile::new(module_name.clone());
1649 let root = tree.root_node();
1650
1651 let module_code_unit = module_code_unit_from_fq(file, &module_components, module_fq.clone());
1652 if let Some(module) = module_code_unit.clone() {
1653 parsed.add_code_unit(module, root, source, None, None);
1654 }
1655
1656 let overload_decorators = PythonOverloadDecoratorBindings::default();
1657 let structural_kinds = CompiledKinds::compile(
1658 &tree_sitter_python::LANGUAGE.into(),
1659 PYTHON_STRUCTURAL_SPEC.kind_table(),
1660 );
1661 let call_site_context = PYTHON_STRUCTURAL_SPEC.call_site_context(root, source);
1662 let mut visitor = PythonVisitor {
1663 file,
1664 source,
1665 package_name: &module_name,
1666 module_fq: &module_fq,
1667 parsed: &mut parsed,
1668 module: module_code_unit,
1669 overload_decorators,
1670 source_facts: PrimarySourceFactCollector::new(source),
1671 structural: StructuralFactCollector::new(
1672 &PYTHON_STRUCTURAL_SPEC,
1673 source,
1674 &call_site_context,
1675 ParentIndex::new(root),
1676 usize::MAX,
1677 None,
1678 ),
1679 structural_kinds: &structural_kinds,
1680 call_site_context: &call_site_context,
1681 structural_parents: vec![None],
1682 source_imports: Vec::new(),
1683 generic_imports: Vec::new(),
1684 source_declaration_units: Vec::new(),
1685 python_source_facts: PythonSourceFacts::default(),
1686 python_callable_return_by_node: HashMap::default(),
1687 overload_metadata: Vec::new(),
1688 };
1689 visitor.visit_container(root, &[], 0);
1690 visitor.finish();
1691
1692 parsed
1693}
1694
1695pub fn py_node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1696 brokk_bifrost_core::analyzer::common::node_source_text(node, source)
1697}
1698
1699pub fn python_module_name(file: &ProjectFile) -> String {
1700 python_module_components(file).join(".")
1701}
1702
1703pub fn module_code_unit(file: &ProjectFile, module_fq: &str) -> Option<CodeUnit> {
1704 if module_fq.is_empty() {
1705 return None;
1706 }
1707 let components = python_module_components(file);
1708 debug_assert_eq!(
1709 module_fq,
1710 components.join("."),
1711 "module_code_unit must be built from the file's path-derived Python module name"
1712 );
1713 let structured_fq = python_module_fq_from_components(&components);
1714 module_code_unit_from_fq(file, &components, structured_fq)
1715}
1716
1717fn module_code_unit_from_fq(
1718 file: &ProjectFile,
1719 components: &[String],
1720 structured_fq: FqName,
1721) -> Option<CodeUnit> {
1722 let (short_name, package_components) = components.split_last()?;
1723 let package_name = package_components.join(".");
1724 Some(CodeUnit::new_fq(
1725 file.clone(),
1726 CodeUnitType::Module,
1727 package_name,
1728 short_name.clone(),
1729 structured_fq,
1730 ))
1731}
1732
1733fn python_class_signature(node: Node<'_>, source: &str) -> String {
1734 python_header_with_decorators(node, source)
1735}
1736
1737fn python_function_signature(node: Node<'_>, source: &str) -> String {
1738 let header = python_header_with_decorators(node, source);
1739 if let Some((head, tail)) = header.rsplit_once('\n') {
1740 format!("{head}\n{tail} ...")
1741 } else {
1742 format!("{header} ...")
1743 }
1744}
1745
1746fn python_signature_metadata(
1747 signature: String,
1748 node: Node<'_>,
1749 source: &str,
1750 return_type_text: Option<&str>,
1751 return_type_identity: Option<StructuredTypeIdentity>,
1752) -> SignatureMetadata {
1753 let Some(parameters_node) = node
1754 .child_by_field_name("parameters")
1755 .filter(|parameters| parameters.start_position().row == parameters.end_position().row)
1756 else {
1757 return SignatureMetadata::new(signature, Vec::new())
1758 .with_return_type_text(return_type_text)
1759 .with_return_type_identity(return_type_identity)
1760 .with_dispatch_extensibility(DispatchExtensibility::Open)
1761 .with_callable_modifiers(
1762 python_callable_is_static(node, source),
1763 false,
1764 DeclaredVisibility::Unknown,
1765 );
1766 };
1767 let header_start = signature.rfind('\n').map_or(0, |offset| offset + 1);
1773 let parameters = python_parameter_label_nodes(parameters_node)
1774 .into_iter()
1775 .filter_map(|label_node| {
1776 let label = py_node_text(label_node, source);
1777 let start_byte = header_start + label_node.start_byte() - node.start_byte();
1778 let end_byte = start_byte + label.len();
1779 (signature.get(start_byte..end_byte) == Some(label))
1780 .then(|| ParameterMetadata::new(label, start_byte, end_byte))
1781 })
1782 .collect();
1783 SignatureMetadata::new(signature, parameters)
1784 .with_return_type_text(return_type_text)
1785 .with_return_type_identity(return_type_identity)
1786 .with_dispatch_extensibility(DispatchExtensibility::Open)
1787 .with_callable_modifiers(
1788 python_callable_is_static(node, source),
1789 false,
1790 DeclaredVisibility::Unknown,
1791 )
1792}
1793
1794fn python_callable_is_static(node: Node<'_>, source: &str) -> bool {
1819 debug_assert!(
1820 node.kind() == "function_definition",
1821 "Python callable signature metadata is built for function_definition, not {}",
1822 node.kind()
1823 );
1824 python_function_has_decorator(node, source, "staticmethod")
1825 || python_function_has_decorator(node, source, "classmethod")
1826}
1827
1828fn python_parameter_label_nodes(parameters_node: Node<'_>) -> Vec<Node<'_>> {
1829 let mut labels = Vec::new();
1830 let mut cursor = parameters_node.walk();
1831 for child in parameters_node.named_children(&mut cursor) {
1832 if let Some(label_node) = python_parameter_label_node(child) {
1833 labels.push(label_node);
1834 }
1835 }
1836 labels
1837}
1838
1839#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1856pub enum PythonParameterSplat {
1857 Positional,
1859 Keyword,
1861}
1862
1863pub fn python_parameter_splat(parameter: Node<'_>) -> Option<PythonParameterSplat> {
1864 let splat = match parameter.kind() {
1865 kind @ ("list_splat_pattern" | "dictionary_splat_pattern") => kind,
1866 _ => {
1867 let mut cursor = parameter.walk();
1868 parameter
1869 .named_children(&mut cursor)
1870 .map(|child| child.kind())
1871 .find(|kind| matches!(*kind, "list_splat_pattern" | "dictionary_splat_pattern"))?
1872 }
1873 };
1874 match splat {
1875 "list_splat_pattern" => Some(PythonParameterSplat::Positional),
1876 "dictionary_splat_pattern" => Some(PythonParameterSplat::Keyword),
1877 _ => unreachable!("the splat kind was matched above"),
1878 }
1879}
1880
1881pub fn python_parameter_label_node(node: Node<'_>) -> Option<Node<'_>> {
1882 match node.kind() {
1883 "identifier" => Some(node),
1884 "typed_parameter"
1885 | "typed_default_parameter"
1886 | "default_parameter"
1887 | "list_splat_pattern"
1888 | "dictionary_splat_pattern"
1889 | "keyword_separator" => node.child_by_field_name("name").or_else(|| {
1890 let mut cursor = node.walk();
1891 node.named_children(&mut cursor)
1892 .find_map(python_parameter_label_node)
1893 }),
1894 _ => None,
1895 }
1896}
1897
1898fn python_is_property_mutator(node: Node<'_>, source: &str) -> bool {
1899 python_header_with_decorators(node, source)
1900 .lines()
1901 .map(str::trim)
1902 .filter(|line| line.starts_with('@'))
1903 .any(|decorator| decorator.ends_with(".setter") || decorator.ends_with(".deleter"))
1904}
1905
1906pub fn python_expanded_comment_start(source: &str, start_byte: usize) -> usize {
1907 let line_starts = compute_line_starts(source);
1908 let line_index = find_line_index_for_offset(&line_starts, start_byte);
1909
1910 let mut comment_start = start_byte;
1911 for line_idx in (0..line_index).rev() {
1912 let line_start = line_starts[line_idx];
1913 let line_end = line_starts
1914 .get(line_idx + 1)
1915 .copied()
1916 .unwrap_or(source.len());
1917 let line = &source[line_start..line_end];
1918 let trimmed = line.trim_start();
1919
1920 if trimmed.trim().is_empty() {
1921 continue;
1922 }
1923
1924 if trimmed.starts_with('#') {
1925 comment_start = line_start;
1926 continue;
1927 }
1928
1929 break;
1930 }
1931
1932 comment_start
1933}
1934
1935fn python_header_with_decorators(node: Node<'_>, source: &str) -> String {
1936 let raw = py_node_text(node, source);
1937 let lines: Vec<_> = raw
1938 .lines()
1939 .map(str::trim_end)
1940 .filter(|line| !line.trim().is_empty())
1941 .collect();
1942 let mut relevant = Vec::new();
1943 for line in lines {
1944 let trimmed = line.trim_start();
1945 if trimmed.starts_with('@')
1946 || trimmed.starts_with("def ")
1947 || trimmed.starts_with("async def ")
1948 || trimmed.starts_with("class ")
1949 {
1950 relevant.push(trimmed.to_string());
1951 if trimmed.starts_with("def ")
1952 || trimmed.starts_with("async def ")
1953 || trimmed.starts_with("class ")
1954 {
1955 break;
1956 }
1957 }
1958 }
1959 relevant.join("\n")
1960}
1961
1962fn extract_python_supertypes(node: Node<'_>, source: &str) -> Vec<String> {
1981 let Some(superclasses) = node.child_by_field_name("superclasses") else {
1982 return Vec::new();
1983 };
1984 let mut result = Vec::new();
1985 let mut cursor = superclasses.walk();
1986 for child in superclasses.named_children(&mut cursor) {
1987 if child.kind() == "keyword_argument" {
1988 continue;
1989 }
1990 let named = python_base_origin_node(child);
1991 let text = py_node_text(named, source).trim();
1992 if !text.is_empty() {
1993 result.push(text.to_string());
1994 }
1995 }
1996 result
1997}
1998
1999pub fn python_base_origin_node<'tree>(base: Node<'tree>) -> Node<'tree> {
2002 if base.kind() != "subscript" {
2003 return base;
2004 }
2005 let Some(value) = base.child_by_field_name("value") else {
2006 return base;
2007 };
2008 if matches!(value.kind(), "identifier" | "attribute") {
2009 value
2010 } else {
2011 base
2012 }
2013}
2014
2015fn collect_assigned_name_nodes<'tree>(node: Node<'tree>, _source: &str) -> Vec<Node<'tree>> {
2016 let mut names = Vec::new();
2017 walk_named_tree_preorder(node, true, |node| {
2018 match node.kind() {
2019 "attribute" | "subscript" => WalkControl::SkipChildren,
2023 "identifier" => {
2024 names.push(node);
2025 WalkControl::Continue
2026 }
2027 _ => WalkControl::Continue,
2028 }
2029 });
2030 names
2031}
2032
2033fn collect_self_assigned_attributes<'tree>(
2034 node: Node<'tree>,
2035 source: &str,
2036 receiver_name: &str,
2037) -> Vec<(String, Node<'tree>)> {
2038 let mut attributes = Vec::new();
2039 collect_direct_self_assigned_attributes(node, source, receiver_name, &mut attributes);
2040 attributes
2041}
2042
2043fn collect_direct_self_assigned_attributes<'tree>(
2044 node: Node<'tree>,
2045 source: &str,
2046 receiver_name: &str,
2047 attributes: &mut Vec<(String, Node<'tree>)>,
2048) {
2049 match node.kind() {
2050 "attribute" => {
2051 let Some(object) = node.child_by_field_name("object") else {
2052 return;
2053 };
2054 if object.kind() != "identifier" || py_node_text(object, source).trim() != receiver_name
2055 {
2056 return;
2057 }
2058 let Some(attribute) = node.child_by_field_name("attribute") else {
2059 return;
2060 };
2061 let name = py_node_text(attribute, source).trim();
2062 if !name.is_empty() {
2063 attributes.push((name.to_string(), attribute));
2064 }
2065 }
2066 "pattern_list"
2071 | "tuple_pattern"
2072 | "list_pattern"
2073 | "tuple"
2074 | "list"
2075 | "parenthesized_expression" => {
2076 let mut cursor = node.walk();
2077 for child in node.named_children(&mut cursor) {
2078 collect_direct_self_assigned_attributes(child, source, receiver_name, attributes);
2079 }
2080 }
2081 _ => {}
2082 }
2083}
2084
2085fn python_instance_method_receiver_name(node: Node<'_>, source: &str) -> Option<String> {
2086 if python_function_has_decorator(node, source, "staticmethod")
2087 || python_function_has_decorator(node, source, "classmethod")
2088 {
2089 return None;
2090 }
2091 python_first_parameter_name(node, source)
2092}
2093
2094fn python_function_has_decorator(node: Node<'_>, source: &str, decorator_name: &str) -> bool {
2095 let Some(parent) = node.parent() else {
2096 return false;
2097 };
2098 if parent.kind() != "decorated_definition" {
2099 return false;
2100 }
2101 let mut cursor = parent.walk();
2102 parent
2103 .named_children(&mut cursor)
2104 .filter(|child| child.kind() == "decorator")
2105 .filter_map(|decorator| decorator.named_child(0))
2106 .filter_map(expression_name_node)
2107 .any(|name| py_node_text(name, source).trim() == decorator_name)
2108}
2109
2110pub fn python_first_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
2113 let parameters = node.child_by_field_name("parameters")?;
2114 let mut cursor = parameters.walk();
2115 parameters
2116 .named_children(&mut cursor)
2117 .find_map(|child| python_parameter_name(child, source))
2118}
2119
2120fn python_parameter_name(node: Node<'_>, source: &str) -> Option<String> {
2121 match node.kind() {
2122 "identifier" => Some(py_node_text(node, source).trim().to_string()),
2123 "typed_parameter"
2124 | "default_parameter"
2125 | "list_splat_pattern"
2126 | "dictionary_splat_pattern" => node
2127 .child_by_field_name("name")
2128 .or_else(|| {
2129 let mut cursor = node.walk();
2130 node.named_children(&mut cursor)
2131 .find(|child| child.kind() == "identifier")
2132 })
2133 .and_then(|name| python_parameter_name(name, source)),
2134 _ => None,
2135 }
2136 .filter(|name| !name.is_empty())
2137}
2138
2139pub fn collect_python_identifiers(node: Node<'_>, source: &str, identifiers: &mut HashSet<String>) {
2140 walk_named_tree_preorder(node, true, |node| {
2141 if node.kind() == "identifier" {
2142 let text = py_node_text(node, source).trim();
2143 if !text.is_empty() {
2144 identifiers.insert(text.to_string());
2145 }
2146 }
2147 WalkControl::Continue
2148 });
2149}
2150
2151pub fn parse_python_tree(source: &str) -> Option<Tree> {
2152 let mut parser = Parser::new();
2153 parser
2154 .set_language(&tree_sitter_python::LANGUAGE.into())
2155 .expect("failed to load python parser");
2156 parser.parse(source, None)
2157}
2158
2159#[cfg(test)]
2160mod primary_tests {
2161 use super::*;
2162 use brokk_bifrost_core::analyzer::python_facts::PythonAnnotationReferenceName;
2163
2164 fn parse(source: &str) -> ParsedFile {
2165 let file = ProjectFile::new(std::env::temp_dir(), "m4.py");
2166 let tree = parse_python_tree(source).expect("Python fixture tree");
2167 parse_python_file(&file, source, &tree)
2168 }
2169
2170 #[test]
2171 fn replacing_a_class_withdraws_descendant_metadata_links() {
2172 let parsed = parse(
2173 "class Replaced:\n def obsolete(self): pass\n class Nested:\n def stale(self): pass\n\nclass Kept:\n def stable(self): pass\n\nclass Replaced:\n def current(self): pass\n",
2174 );
2175 let mut names = parsed
2176 .source_declaration_metadata
2177 .iter()
2178 .map(|link| {
2179 assert!(
2180 parsed.signature_metadata[&link.unit]
2181 .get(link.metadata_ordinal)
2182 .is_some(),
2183 "the link must name retained callable metadata: {link:?}"
2184 );
2185 link.unit.short_name().to_string()
2186 })
2187 .collect::<Vec<_>>();
2188 names.sort();
2189 assert_eq!(names, ["Kept.stable", "Replaced.current"]);
2190 }
2191
2192 #[test]
2193 fn canonical_declarations_share_structural_name_identity_and_exact_ranges() {
2194 let source = "@decorate\nclass Item:\n value = 1\n value = 2\n def write(self):\n self.member = 1\n self.member = 2\n\nfirst, second = 1, 2\n";
2195 let parsed = parse(source);
2196 let facts = parsed
2197 .source_facts
2198 .as_ref()
2199 .expect("canonical Python facts");
2200 for (id, unit) in &parsed.source_declaration_units {
2201 let declaration = facts.occurrences.declaration(*id);
2202 let range = facts.occurrences.occurrence(declaration.occurrence).range;
2203 assert!(parsed.ranges[unit].contains(&range), "{unit:?}: {range:?}");
2204 let name = declaration.name.expect("source declaration name");
2205 assert!(
2206 facts
2207 .structural
2208 .nodes()
2209 .iter()
2210 .any(|node| node.occurrence == name),
2211 "shared name identity for {unit:?}"
2212 );
2213 }
2214 let fields = parsed
2215 .source_declaration_units
2216 .iter()
2217 .filter(|(_, unit)| unit.short_name() == "Item.value")
2218 .collect::<Vec<_>>();
2219 assert_eq!(fields.len(), 2);
2220 assert_ne!(fields[0].0, fields[1].0);
2221 assert_eq!(
2222 parsed
2223 .source_declaration_units
2224 .iter()
2225 .filter(|(_, unit)| unit.short_name() == "Item.member")
2226 .count(),
2227 1
2228 );
2229 let class = parsed
2230 .source_declaration_units
2231 .iter()
2232 .find(|(_, unit)| unit.short_name() == "Item")
2233 .expect("decorated class");
2234 let range = facts
2235 .occurrences
2236 .occurrence(facts.occurrences.declaration(class.0).occurrence)
2237 .range;
2238 assert_eq!(range.start_byte, 0);
2239 assert_eq!(&source[range.start_byte..range.start_byte + 9], "@decorate");
2240 }
2241
2242 #[test]
2243 fn imports_keep_scope_order_and_late_overload_capture_without_local_pollution() {
2244 let source = "@overload\ndef public(value: int):\n pass\nfrom typing import overload\n\ndef outer():\n from typing import overload as local_only\n@local_only\ndef ordinary(value: int):\n pass\nmatch 1:\n case 1:\n from .models import Item as Imported\n";
2245 let parsed = parse(source);
2246 let facts = parsed.source_facts.as_ref().expect("canonical imports");
2247 assert_eq!(facts.imports.len(), 3);
2248 assert!(
2249 facts.imports[0]
2250 .path
2251 .as_ref()
2252 .unwrap()
2253 .lexical_scopes
2254 .is_empty()
2255 );
2256 assert_eq!(
2257 facts.imports[1].path.as_ref().unwrap().lexical_scopes.len(),
2258 1
2259 );
2260 assert!(
2261 facts.imports[2]
2262 .path
2263 .as_ref()
2264 .unwrap()
2265 .lexical_scopes
2266 .is_empty()
2267 );
2268 for (id, import) in facts.generic_imports.iter().zip(&parsed.imports) {
2269 assert_eq!(
2270 &facts.imports[id.index()].import_info(&facts.occurrences),
2271 import
2272 );
2273 }
2274 let metadata = |name: &str| {
2275 parsed
2276 .signature_metadata
2277 .iter()
2278 .find(|(unit, _)| unit.short_name() == name)
2279 .unwrap()
2280 .1
2281 };
2282 assert!(metadata("public")[0].is_declaration_only());
2283 assert!(!metadata("ordinary")[0].is_declaration_only());
2284 }
2285
2286 #[test]
2287 fn aliased_import_leaves_keep_the_statement_and_exact_binder_tokens() {
2288 let source = "from pkg import alpha as beta, beta as alpha\n";
2289 let parsed = parse(source);
2290 assert_eq!(parsed.imports.len(), 2);
2291 for (import, name) in parsed.imports.iter().zip(["beta", "alpha"]) {
2292 assert_eq!(import.path.as_ref().unwrap().declaration_start_byte, 0);
2293 let span = import.binder_span.as_ref().expect("exact alias binder");
2294 assert_eq!(&source[span.start_byte..span.end_byte], name);
2295 }
2296 }
2297
2298 #[test]
2299 fn parameter_offsets_use_ast_tokens_instead_of_annotation_spelling() {
2300 let source = "@decorate(\"(first, second)\")\ndef call(first: Literal[\"second\"], second: int):\n pass\n";
2301 let parsed = parse(source);
2302 let metadata = parsed
2303 .signature_metadata
2304 .values()
2305 .flatten()
2306 .find(|metadata| metadata.parameters().len() == 2)
2307 .expect("parameter metadata");
2308 let second = &metadata.parameters()[1];
2309 assert_eq!(
2310 &metadata.label()[second.start_byte()..second.end_byte()],
2311 "second"
2312 );
2313 assert_eq!(
2314 second.start_byte(),
2315 metadata.label().rfind("second:").unwrap()
2316 );
2317 }
2318
2319 #[test]
2320 fn empty_and_recovered_python_publish_canonical_structural_facts() {
2321 for source in [
2322 "",
2323 "class Broken(:\n pass\ndef valid(value):\n return value\n",
2324 ] {
2325 let parsed = parse(source);
2326 let facts = parsed.source_facts.expect("complete producer publication");
2327 assert_eq!(facts.source_bytes, source.len());
2328 assert!(facts.python.is_some());
2329 assert_eq!(parsed.resolution_facts, Default::default());
2330 assert!(
2331 facts
2332 .occurrences
2333 .occurrences()
2334 .iter()
2335 .all(|occurrence| occurrence.range.end_byte <= source.len())
2336 );
2337 }
2338 }
2339
2340 #[test]
2341 fn unmounted_annotated_functions_keep_source_declarations_and_return_facts() {
2342 let source = "if outer:\n if inner:\n def hidden(value: int) -> Hidden:\n pass\n\ndef visible() -> Visible:\n pass\n";
2343 let parsed = parse(source);
2344 let facts = parsed
2345 .source_facts
2346 .as_ref()
2347 .expect("canonical Python facts");
2348 let python = facts.python.as_ref().expect("Python source facts");
2349 assert_eq!(python.callable_returns.len(), 2);
2350
2351 let fact_named = |name: &str| {
2352 python
2353 .callable_returns
2354 .iter()
2355 .find(|fact| {
2356 let declaration = facts.occurrences.declaration(fact.declaration);
2357 let Some(name_occurrence) = declaration.name else {
2358 return false;
2359 };
2360 let range = facts.occurrences.occurrence(name_occurrence).range;
2361 &source[range.start_byte..range.end_byte] == name
2362 })
2363 .expect("callable return fact")
2364 };
2365 let hidden = fact_named("hidden");
2366 let hidden_declaration = facts.occurrences.declaration(hidden.declaration);
2367 let hidden_range = facts
2368 .occurrences
2369 .occurrence(hidden_declaration.occurrence)
2370 .range;
2371 assert_eq!(
2372 &source[hidden_range.start_byte..hidden_range.start_byte + 3],
2373 "def"
2374 );
2375 let hidden_annotation = hidden.return_annotation.expect("hidden annotation");
2376 let hidden_annotation_range = facts.occurrences.occurrence(hidden_annotation).range;
2377 assert_eq!(
2378 &source[hidden_annotation_range.start_byte..hidden_annotation_range.end_byte],
2379 "Hidden"
2380 );
2381 assert!(
2382 parsed
2383 .source_declaration_units
2384 .iter()
2385 .all(|(_, unit)| unit.short_name() != "hidden")
2386 );
2387
2388 let visible = fact_named("visible");
2389 assert!(visible.return_annotation.is_some());
2390 assert!(visible.runtime_type.is_some());
2391 assert!(
2392 parsed
2393 .source_declaration_units
2394 .iter()
2395 .any(|(_, unit)| unit.short_name() == "visible")
2396 );
2397 let visible_metadata = parsed
2398 .signature_metadata
2399 .iter()
2400 .find(|(unit, _)| unit.short_name() == "visible")
2401 .and_then(|(_, metadata)| metadata.first())
2402 .expect("visible signature metadata");
2403 assert_eq!(visible_metadata.return_type_text(), Some("Visible"));
2404 assert_eq!(
2405 visible_metadata.return_type_identity(),
2406 visible.runtime_type.as_ref()
2407 );
2408 }
2409
2410 #[test]
2411 fn return_fact_preserves_unsupported_annotation_without_runtime_identity() {
2412 let source = "def unsupported() -> Left | Right:\n pass\n\ndef plain():\n pass\n";
2413 let parsed = parse(source);
2414 let facts = parsed
2415 .source_facts
2416 .as_ref()
2417 .expect("canonical Python facts");
2418 let python = facts.python.as_ref().expect("Python source facts");
2419 let by_name = |name: &str| {
2420 python
2421 .callable_returns
2422 .iter()
2423 .find(|fact| {
2424 let declaration = facts.occurrences.declaration(fact.declaration);
2425 let Some(name_occurrence) = declaration.name else {
2426 return false;
2427 };
2428 let range = facts.occurrences.occurrence(name_occurrence).range;
2429 &source[range.start_byte..range.end_byte] == name
2430 })
2431 .expect("callable return fact")
2432 };
2433 let unsupported = by_name("unsupported");
2434 assert!(unsupported.return_annotation.is_some());
2435 assert!(unsupported.runtime_type.is_none());
2436 let plain = by_name("plain");
2437 assert!(plain.return_annotation.is_none());
2438 assert!(plain.runtime_type.is_none());
2439 }
2440
2441 #[test]
2442 fn return_annotation_references_preserve_runtime_owners() {
2443 let source = "def generic() -> list[User]:\n pass\n\ndef nested() -> list[pkg.User]:\n pass\n\ndef direct() -> pkg.User:\n pass\n\ndef quoted() -> \"pkg.User\":\n pass\n\ndef unsupported() -> Left | Right:\n pass\n";
2444 let parsed = parse(source);
2445 let facts = parsed
2446 .source_facts
2447 .as_ref()
2448 .expect("canonical Python facts");
2449 let python = facts.python.as_ref().expect("Python source facts");
2450 let fact_named = |name: &str| {
2451 python
2452 .callable_returns
2453 .iter()
2454 .find(|fact| {
2455 let declaration = facts.occurrences.declaration(fact.declaration);
2456 let Some(name_occurrence) = declaration.name else {
2457 return false;
2458 };
2459 let range = facts.occurrences.occurrence(name_occurrence).range;
2460 &source[range.start_byte..range.end_byte] == name
2461 })
2462 .expect("callable return fact")
2463 };
2464 let generic = fact_named("generic");
2465 assert_eq!(
2466 generic
2467 .annotation_references
2468 .iter()
2469 .map(|reference| &reference.name)
2470 .collect::<Vec<_>>(),
2471 vec![&PythonAnnotationReferenceName::Lexical("list".to_string())]
2472 );
2473
2474 let nested = fact_named("nested");
2476 assert_eq!(nested.annotation_references.len(), 1);
2477 assert!(matches!(
2478 &nested.annotation_references[0].name,
2479 PythonAnnotationReferenceName::Lexical(name) if name == "list"
2480 ));
2481
2482 let direct = fact_named("direct");
2483 assert_eq!(direct.annotation_references.len(), 3);
2484 assert_eq!(direct.annotation_references[0].subtree_end, 3);
2485 assert_eq!(direct.annotation_references[0].lookup_depth, 1);
2486 assert!(matches!(
2487 &direct.annotation_references[0].name,
2488 PythonAnnotationReferenceName::Qualified(path)
2489 if path == &vec!["pkg".to_string(), "User".to_string()]
2490 ));
2491
2492 let quoted = fact_named("quoted");
2493 assert_eq!(quoted.annotation_references.len(), 1);
2494 assert!(matches!(
2495 "ed.annotation_references[0].name,
2496 PythonAnnotationReferenceName::Lexical(name) if name == "pkg.User"
2497 ));
2498 assert_eq!(quoted.annotation_references[0].lookup_depth, 2);
2499
2500 let unsupported = fact_named("unsupported");
2501 assert!(
2502 unsupported
2503 .annotation_references
2504 .iter()
2505 .all(|reference| !matches!(
2506 &reference.name,
2507 PythonAnnotationReferenceName::Qualified(_)
2508 ))
2509 );
2510 }
2511}
2512
2513fn python_chained_assignment_targets<'tree>(assignment: Node<'tree>) -> Vec<Node<'tree>> {
2520 let mut targets = Vec::new();
2521 let mut node = assignment;
2522 while let Some(left) = node.child_by_field_name("left") {
2523 targets.push(left);
2524 match node.child_by_field_name("right") {
2525 Some(right) if right.kind() == "assignment" => node = right,
2526 _ => break,
2527 }
2528 }
2529 targets
2530}
2531
2532#[cfg(test)]
2533mod supertype_tests {
2534 use super::extract_python_supertypes;
2535 use tree_sitter::{Node, Parser};
2536
2537 fn class_node<'tree>(tree: &'tree tree_sitter::Tree, source: &str) -> Node<'tree> {
2538 let mut cursor = tree.root_node().walk();
2539 tree.root_node()
2540 .named_children(&mut cursor)
2541 .find(|node| node.kind() == "class_definition")
2542 .unwrap_or_else(|| panic!("source declares a class: {source}"))
2543 }
2544
2545 fn parse(source: &str) -> tree_sitter::Tree {
2546 let mut parser = Parser::new();
2547 parser
2548 .set_language(&tree_sitter_python::LANGUAGE.into())
2549 .expect("the Python grammar loads");
2550 parser.parse(source, None).expect("the source parses")
2551 }
2552
2553 #[test]
2554 fn every_positional_base_contributes_a_spelling() {
2555 for (source, expected) in [
2556 ("class A(Base): pass\n", vec!["Base"]),
2557 ("class A(pkg.Base): pass\n", vec!["pkg.Base"]),
2558 ("class A(Base[int]): pass\n", vec!["Base"]),
2561 ("class A(pkg.Base[str, int]): pass\n", vec!["pkg.Base"]),
2562 (
2563 "class A(Mapping[str, Any], Base): pass\n",
2564 vec!["Mapping", "Base"],
2565 ),
2566 ("class A(Base, metaclass=Meta): pass\n", vec!["Base"]),
2568 ("class A(metaclass=Meta): pass\n", Vec::new()),
2569 (
2572 "class A(namedtuple(\"P\", \"x\")): pass\n",
2573 vec!["namedtuple(\"P\", \"x\")"],
2574 ),
2575 ("class A(*bases): pass\n", vec!["*bases"]),
2576 ("class A: pass\n", Vec::new()),
2577 ] {
2578 let tree = parse(source);
2579 let node = class_node(&tree, source);
2580 assert_eq!(
2581 extract_python_supertypes(node, source),
2582 expected,
2583 "supertypes of {source}"
2584 );
2585 }
2586 }
2587}
2588
2589#[cfg(test)]
2590mod callable_modifier_tests {
2591 use super::*;
2592
2593 #[test]
2616 fn callable_metadata_records_python_receiver_contracts_structurally() {
2617 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";
2618 let file = ProjectFile::new(std::env::temp_dir(), "widget.py");
2619 let tree = parse_python_tree(source).expect("parse the Python fixture");
2620 let parsed = parse_python_file(&file, source, &tree);
2621
2622 let entry = |fq_name: &str| {
2623 parsed
2624 .signature_metadata
2625 .iter()
2626 .find(|(unit, _)| unit.fq_name() == fq_name)
2627 .unwrap_or_else(|| {
2628 panic!(
2629 "missing Python declaration {fq_name}; recorded {:?}",
2630 parsed
2631 .signature_metadata
2632 .keys()
2633 .map(CodeUnit::fq_name)
2634 .collect::<Vec<_>>()
2635 )
2636 })
2637 };
2638 let modifiers = |fq_name: &str| {
2639 let (_, entries) = entry(fq_name);
2640 let metadata = entries
2641 .first()
2642 .unwrap_or_else(|| panic!("{fq_name} carries no signature metadata"));
2643 assert!(
2644 metadata.callable_modifiers_recorded(),
2645 "{fq_name} must record that the walk read its declaration shape"
2646 );
2647 (
2648 metadata.callable_is_static(),
2649 metadata.callable_is_constructor(),
2650 metadata.parameters().len(),
2651 )
2652 };
2653
2654 assert_eq!(modifiers("widget.free"), (false, false, 1));
2655 assert_eq!(
2656 modifiers("widget.Widget.__init__"),
2657 (false, false, 2),
2658 "`__init__` is an ordinary instance method, exactly as Ruby's `initialize`"
2659 );
2660 assert_eq!(modifiers("widget.Widget.render"), (false, false, 2));
2661 assert_eq!(
2662 modifiers("widget.Widget.build"),
2663 (true, false, 1),
2664 "`@staticmethod` binds no instance receiver"
2665 );
2666 assert_eq!(
2667 modifiers("widget.Widget.measure"),
2668 (true, false, 2),
2669 "`@classmethod` receives the class, not an instance"
2670 );
2671 assert_eq!(
2672 modifiers("widget.fetch_all"),
2673 (false, false, 1),
2674 "an async def is the same function_definition node"
2675 );
2676 assert_eq!(modifiers("widget.outer"), (false, false, 1));
2677
2678 let (property_unit, property_entries) = entry("widget.Widget.label");
2679 assert_eq!(
2680 property_unit.kind(),
2681 CodeUnitType::Field,
2682 "`@property` is read as an attribute, so it mints a Field and no key"
2683 );
2684 assert!(!property_unit.is_callable());
2685 assert!(
2686 property_entries
2687 .first()
2688 .expect("the property still carries signature metadata")
2689 .callable_modifiers_recorded(),
2690 "the shared metadata path records modifiers for every declaration it mints"
2691 );
2692
2693 let lambda_unit = parsed
2698 .declarations()
2699 .iter()
2700 .find(|unit| unit.fq_name() == "widget.assigned")
2701 .expect("the module walk still records the name-bound lambda's assignment");
2702 assert_eq!(
2703 lambda_unit.kind(),
2704 CodeUnitType::Field,
2705 "a name-bound lambda is recorded as a field, not as a callable declaration"
2706 );
2707 assert!(
2708 !parsed.signature_metadata.contains_key(lambda_unit),
2709 "a field assignment carries signature text but no callable signature metadata"
2710 );
2711 assert!(
2712 parsed
2713 .declarations()
2714 .iter()
2715 .all(|unit| unit.terminal_name() != "inner"),
2716 "a function nested in another function is a local binding, not a declaration"
2717 );
2718 }
2719}